@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
package/cli/index.js
CHANGED
|
@@ -15,7 +15,7 @@ const LAYERS = ['tokens', 'reset', 'base', 'a11y', 'layout', 'components', 'util
|
|
|
15
15
|
|
|
16
16
|
const LAYER_FILES = {
|
|
17
17
|
tokens: [
|
|
18
|
-
'tokens/color.css', 'tokens/spacing.css', 'tokens/typography.css',
|
|
18
|
+
'tokens/fonts.css', 'tokens/color.css', 'tokens/spacing.css', 'tokens/typography.css',
|
|
19
19
|
'tokens/radius.css', 'tokens/shadow.css', 'tokens/motion.css',
|
|
20
20
|
'tokens/z-index.css', 'tokens/aspect-ratio.css',
|
|
21
21
|
],
|
|
@@ -25,6 +25,8 @@ const LAYER_FILES = {
|
|
|
25
25
|
'a11y/sr-only.css', 'a11y/skip-link.css', 'a11y/reduced-motion.css',
|
|
26
26
|
'a11y/forced-colors.css', 'a11y/skeleton.css', 'a11y/preferences.css',
|
|
27
27
|
'a11y/focus-not-obscured.css', 'a11y/target-size.css', 'a11y/high-contrast-aaa.css',
|
|
28
|
+
'a11y/authentication.css', 'a11y/consistent-help.css', 'a11y/dragging-alternatives.css',
|
|
29
|
+
'a11y/focus-appearance.css',
|
|
28
30
|
],
|
|
29
31
|
security: ['a11y/security.css'],
|
|
30
32
|
layout: [
|
|
@@ -47,8 +49,10 @@ const LAYER_FILES = {
|
|
|
47
49
|
'utilities/position.css', 'utilities/animation.css', 'utilities/gradient.css',
|
|
48
50
|
'utilities/print.css', 'utilities/responsive.css', 'utilities/divide.css',
|
|
49
51
|
'utilities/scroll.css', 'utilities/color-mix.css', 'utilities/scroll-animation.css',
|
|
50
|
-
'utilities/view-transition.css',
|
|
51
|
-
'utilities/filter.css', 'utilities/
|
|
52
|
+
'utilities/view-transition.css', 'utilities/scope.css', 'utilities/anchor.css',
|
|
53
|
+
'utilities/filter.css', 'utilities/filter-effects.css', 'utilities/chart-animation.css',
|
|
54
|
+
'utilities/container-style.css', 'utilities/state.css',
|
|
55
|
+
'utilities/safe-area.css',
|
|
52
56
|
],
|
|
53
57
|
helpers: ['helpers/helpers.css'],
|
|
54
58
|
};
|
|
@@ -79,7 +83,7 @@ function hasFlag(flag, alias) {
|
|
|
79
83
|
|
|
80
84
|
function help() {
|
|
81
85
|
console.log(`
|
|
82
|
-
${C.bold('VelinStyle CLI')} v0.
|
|
86
|
+
${C.bold('VelinStyle CLI')} v0.9.0
|
|
83
87
|
|
|
84
88
|
${C.bold('Usage:')}
|
|
85
89
|
velinstyle init Create velinstyle.config.js
|
|
@@ -90,7 +94,14 @@ function help() {
|
|
|
90
94
|
velinstyle scan [path] Security & accessibility scanner
|
|
91
95
|
velinstyle prefix [path] Add missing velin- prefix (dry-run; use --write)
|
|
92
96
|
velinstyle blueprint [name] Print HTML blueprint (run: velinstyle blueprint list)
|
|
97
|
+
velinstyle scaffold "<prompt>" Generate layout HTML from a text description
|
|
98
|
+
velinstyle layout <sub> Responsive layout audit and fixes
|
|
99
|
+
velinstyle perf <sub> Performance audit (images, scripts) with --fix
|
|
93
100
|
velinstyle tokens build Generate CSS variables from tokens.json
|
|
101
|
+
velinstyle tokens validate Validate tokens.json schema
|
|
102
|
+
velinstyle docs generate Auto-generate Markdown API reference
|
|
103
|
+
velinstyle meta Build agent context (velin-agent.json, llms.txt)
|
|
104
|
+
velinstyle search index Build JSON search index for VelinSearch
|
|
94
105
|
|
|
95
106
|
${C.bold('Icons subcommands:')}
|
|
96
107
|
icons list Show available icon providers
|
|
@@ -107,14 +118,39 @@ function help() {
|
|
|
107
118
|
velinstyle blueprint list Print all blueprint ids
|
|
108
119
|
velinstyle blueprint <name> [--output, -o <file>]
|
|
109
120
|
|
|
121
|
+
${C.bold('Scaffold (0.8.0):')}
|
|
122
|
+
velinstyle scaffold "<prompt>" Compose blueprints from natural language
|
|
123
|
+
velinstyle scaffold list-intents Show supported intent keywords
|
|
124
|
+
velinstyle scaffold "<prompt>" -o out.html [--json]
|
|
125
|
+
|
|
126
|
+
${C.bold('Layout (0.8.0):')}
|
|
127
|
+
velinstyle layout audit [path] Report flex/grid/responsive issues
|
|
128
|
+
velinstyle layout suggest [path] Audit with fix suggestions
|
|
129
|
+
velinstyle layout fix [path] Apply safe fixes (--dry-run default, --write)
|
|
130
|
+
|
|
131
|
+
${C.bold('Performance (0.9.0):')}
|
|
132
|
+
velinstyle perf audit [path] Report CLS, lazy-load, script defer issues
|
|
133
|
+
velinstyle perf suggest [path] Same as audit with fix hints
|
|
134
|
+
velinstyle perf fix [path] Apply safe fixes (--write)
|
|
135
|
+
|
|
110
136
|
${C.bold('Tokens:')}
|
|
111
137
|
velinstyle tokens build [--input <path>] [--output, -o <file>]
|
|
138
|
+
velinstyle tokens validate [--input <path>]
|
|
139
|
+
|
|
140
|
+
${C.bold('Docs (0.9.0):')}
|
|
141
|
+
velinstyle docs generate [--scope all|components|tokens|utilities|cli|rules|a11y|meta] [--out docs/generated]
|
|
142
|
+
velinstyle meta [--out dist/velin-agent.json] [--llms-out dist/llms.txt] [--base-url URL]
|
|
143
|
+
velinstyle meta page <file.html> [--write]
|
|
144
|
+
|
|
145
|
+
${C.bold('Search:')}
|
|
146
|
+
velinstyle search index [--out dist/search-index.json] [--extra-html dir1,dir2]
|
|
112
147
|
|
|
113
148
|
${C.bold('Scan options:')}
|
|
114
149
|
--fix Auto-fix safe issues (writes files)
|
|
115
150
|
--fix-dry-run Show files that would be auto-fixed; no write
|
|
116
151
|
--fix-lang <code> Default lang for a11y/html-lang fix (default: de)
|
|
117
152
|
--severity <level> Minimum severity: error, warning, info
|
|
153
|
+
--only <category> Filter: security, pii, a11y, css, perf
|
|
118
154
|
|
|
119
155
|
${C.bold('Prefix options (velinstyle prefix):')}
|
|
120
156
|
--write Write files (default is dry-run)
|
|
@@ -457,15 +493,66 @@ async function tokensBuildCmd() {
|
|
|
457
493
|
}
|
|
458
494
|
}
|
|
459
495
|
|
|
496
|
+
async function tokensValidateCmd() {
|
|
497
|
+
const input = getArg('--input') || 'examples/tokens.sample.json';
|
|
498
|
+
const { validateTokensJson } = await import('./tokens-validate.js');
|
|
499
|
+
const result = validateTokensJson(resolve(input));
|
|
500
|
+
if (result.ok) {
|
|
501
|
+
console.log(C.green(`Valid: ${resolve(input)}`));
|
|
502
|
+
process.exit(0);
|
|
503
|
+
}
|
|
504
|
+
console.log(C.red(`Invalid: ${resolve(input)}`));
|
|
505
|
+
result.errors.forEach((e) => console.log(` - ${e}`));
|
|
506
|
+
process.exit(1);
|
|
507
|
+
}
|
|
508
|
+
|
|
460
509
|
async function tokensCmd() {
|
|
461
510
|
const sub = args[1];
|
|
462
511
|
if (sub === 'build') {
|
|
463
512
|
await tokensBuildCmd();
|
|
513
|
+
} else if (sub === 'validate') {
|
|
514
|
+
await tokensValidateCmd();
|
|
464
515
|
} else {
|
|
465
|
-
console.log('Usage: velinstyle tokens build [--input tokens.json] [--output
|
|
516
|
+
console.log('Usage: velinstyle tokens build|validate [--input tokens.json] [--output, -o <file>]');
|
|
466
517
|
}
|
|
467
518
|
}
|
|
468
519
|
|
|
520
|
+
async function perfCmd() {
|
|
521
|
+
const sub = args[1] || 'audit';
|
|
522
|
+
const rawTarget = args[2] && !args[2].startsWith('-') ? args[2] : '.';
|
|
523
|
+
const targetPath = resolve(rawTarget);
|
|
524
|
+
const asJson = hasFlag('--json');
|
|
525
|
+
const write = hasFlag('--write');
|
|
526
|
+
const dryRun = hasFlag('--dry-run') || !write;
|
|
527
|
+
|
|
528
|
+
const { auditPath, formatTextReport, fixPath } = await import('./perf-audit.js');
|
|
529
|
+
|
|
530
|
+
if (sub === 'fix') {
|
|
531
|
+
const result = fixPath(targetPath, { write, dryRun });
|
|
532
|
+
if (asJson) {
|
|
533
|
+
console.log(JSON.stringify(result, null, 2));
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
if (result.changes.length === 0) {
|
|
537
|
+
console.log(C.green('No safe performance fixes to apply.'));
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
for (const c of result.changes) {
|
|
541
|
+
console.log(`${dryRun ? '[dry-run] ' : ''}${c.file}`);
|
|
542
|
+
c.changes.forEach((ch) => console.log(C.dim(` - ${ch}`)));
|
|
543
|
+
}
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const { issues, files } = auditPath(targetPath);
|
|
548
|
+
if (asJson) {
|
|
549
|
+
console.log(JSON.stringify({ files, issues }, null, 2));
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
console.log(formatTextReport(issues, files));
|
|
553
|
+
process.exit(issues.some((x) => x.severity === 'error') ? 1 : 0);
|
|
554
|
+
}
|
|
555
|
+
|
|
469
556
|
async function prefixCmd() {
|
|
470
557
|
const rawTarget = args[1] && !args[1].startsWith('-') ? args[1] : '.';
|
|
471
558
|
const targetPath = resolve(rawTarget);
|
|
@@ -497,6 +584,142 @@ async function prefixCmd() {
|
|
|
497
584
|
}
|
|
498
585
|
}
|
|
499
586
|
|
|
587
|
+
async function scaffoldCmd() {
|
|
588
|
+
const sub = args[1];
|
|
589
|
+
const { scaffoldFromPrompt, listIntents } = await import('./scaffold.js');
|
|
590
|
+
if (sub === 'list-intents') {
|
|
591
|
+
console.log(`\n ${C.bold('Scaffold intents:')}\n`);
|
|
592
|
+
for (const i of listIntents()) {
|
|
593
|
+
console.log(` ${C.cyan(i.id)} — ${i.blueprints.join(' + ')}`);
|
|
594
|
+
console.log(C.dim(` keywords: ${i.keywords.slice(0, 5).join(', ')}…`));
|
|
595
|
+
}
|
|
596
|
+
console.log('');
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
const promptParts = [];
|
|
600
|
+
let i = 1;
|
|
601
|
+
while (i < args.length) {
|
|
602
|
+
const a = args[i];
|
|
603
|
+
if (a.startsWith('-')) break;
|
|
604
|
+
if (a !== 'scaffold') promptParts.push(a);
|
|
605
|
+
i += 1;
|
|
606
|
+
}
|
|
607
|
+
const prompt = promptParts.join(' ').trim() || sub;
|
|
608
|
+
if (!prompt || prompt === 'list-intents') {
|
|
609
|
+
console.log('Usage: velinstyle scaffold "<description>" [-o file.html] [--json]');
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
const r = scaffoldFromPrompt(prompt);
|
|
613
|
+
if (!r.ok) {
|
|
614
|
+
console.log(C.red(r.error));
|
|
615
|
+
process.exit(1);
|
|
616
|
+
}
|
|
617
|
+
const out = getArg('--output', '-o');
|
|
618
|
+
const asJson = hasFlag('--json');
|
|
619
|
+
if (asJson) {
|
|
620
|
+
console.log(JSON.stringify(r, null, 2));
|
|
621
|
+
} else if (out) {
|
|
622
|
+
writeFileSync(resolve(out), r.html, 'utf-8');
|
|
623
|
+
console.log(C.green(`Wrote ${resolve(out)} (intent: ${r.intent}, ${r.confidence})`));
|
|
624
|
+
if (r.responsiveHints?.length) {
|
|
625
|
+
console.log(C.yellow(` ${r.responsiveHints.length} layout hint(s) — run: velinstyle layout suggest ${out}`));
|
|
626
|
+
}
|
|
627
|
+
} else {
|
|
628
|
+
console.log(r.html);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
async function layoutCmd() {
|
|
633
|
+
const sub = args[1] || 'audit';
|
|
634
|
+
const rawTarget = args[2] && !args[2].startsWith('-') ? args[2] : '.';
|
|
635
|
+
const targetPath = resolve(rawTarget);
|
|
636
|
+
const asJson = hasFlag('--json');
|
|
637
|
+
const write = hasFlag('--write');
|
|
638
|
+
const dryRun = hasFlag('--dry-run') || !write;
|
|
639
|
+
|
|
640
|
+
const { auditPath, suggestFromIssues, formatTextReport, fixPath } = await import('./layout-audit.js');
|
|
641
|
+
|
|
642
|
+
if (sub === 'fix') {
|
|
643
|
+
const result = fixPath(targetPath, { write, dryRun });
|
|
644
|
+
if (!result.ok) {
|
|
645
|
+
console.log(C.red(result.error || 'Fix failed'));
|
|
646
|
+
process.exit(1);
|
|
647
|
+
}
|
|
648
|
+
if (asJson) {
|
|
649
|
+
console.log(JSON.stringify(result, null, 2));
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
if (result.changes.length === 0) {
|
|
653
|
+
console.log(C.green('No safe fixes to apply.'));
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
for (const c of result.changes) {
|
|
657
|
+
console.log(`${dryRun ? '[dry-run] ' : ''}${c.file}`);
|
|
658
|
+
c.changes.forEach((ch) => console.log(C.dim(` - ${ch}`)));
|
|
659
|
+
}
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const { issues, files } = auditPath(targetPath);
|
|
664
|
+
const outIssues = sub === 'suggest' ? suggestFromIssues(issues) : issues;
|
|
665
|
+
|
|
666
|
+
if (asJson) {
|
|
667
|
+
console.log(JSON.stringify({ files, issues: outIssues }, null, 2));
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
console.log(formatTextReport(outIssues, files));
|
|
672
|
+
if (sub === 'suggest' && issues.length) {
|
|
673
|
+
console.log(C.bold('Suggested fixes:'));
|
|
674
|
+
for (const i of outIssues) {
|
|
675
|
+
console.log(` [${i.id}] ${i.fix}`);
|
|
676
|
+
if (i.responsive?.mobile) {
|
|
677
|
+
console.log(C.dim(` mobile: ${i.responsive.mobile}`));
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
console.log('');
|
|
681
|
+
}
|
|
682
|
+
process.exit(issues.some((x) => x.severity === 'error') ? 1 : 0);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
async function docsCmd() {
|
|
686
|
+
const sub = args[1];
|
|
687
|
+
if (sub === 'generate') {
|
|
688
|
+
const scope = getArg('--scope') || 'all';
|
|
689
|
+
const out = getArg('--out') || join(PKG_ROOT, 'docs', 'generated');
|
|
690
|
+
const { generateDocs } = await import('./docs-generate.js');
|
|
691
|
+
const result = await generateDocs({ scope, outDir: resolve(out), searchIndex: true });
|
|
692
|
+
if (!result.ok) {
|
|
693
|
+
console.log(C.red(result.error));
|
|
694
|
+
process.exit(1);
|
|
695
|
+
}
|
|
696
|
+
console.log(C.green(`Generated ${result.written} file(s) → ${resolve(out)}`));
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
console.log('Usage: velinstyle docs generate [--scope all|components|tokens|utilities|cli|rules|a11y|meta] [--out <dir>]');
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
async function metaCmd() {
|
|
703
|
+
const { metaMain } = await import('./meta.js');
|
|
704
|
+
await metaMain(process.argv.slice(2));
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
async function searchCmd() {
|
|
708
|
+
const sub = args[1];
|
|
709
|
+
if (sub === 'index') {
|
|
710
|
+
const out = getArg('--out') || join(PKG_ROOT, 'dist', 'search-index.json');
|
|
711
|
+
const extra = getArg('--extra-html');
|
|
712
|
+
const { buildSearchIndex } = await import('./search-index.js');
|
|
713
|
+
const result = buildSearchIndex({
|
|
714
|
+
outFile: resolve(out),
|
|
715
|
+
extraHtmlDirs: extra ? extra.split(',').map((p) => resolve(p.trim())) : [],
|
|
716
|
+
});
|
|
717
|
+
console.log(C.green(`Search index: ${result.count} entries → ${result.outFile}`));
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
console.log('Usage: velinstyle search index [--out <file>] [--extra-html dir1,dir2]');
|
|
721
|
+
}
|
|
722
|
+
|
|
500
723
|
async function scanCmd() {
|
|
501
724
|
const targetPath = args[1] && !args[1].startsWith('-') ? resolve(args[1]) : resolve('.');
|
|
502
725
|
const format = getArg('--format') || 'text';
|
|
@@ -508,6 +731,7 @@ async function scanCmd() {
|
|
|
508
731
|
fixLang = 'de';
|
|
509
732
|
}
|
|
510
733
|
const severity = getArg('--severity') || 'warning';
|
|
734
|
+
const only = getArg('--only');
|
|
511
735
|
|
|
512
736
|
const { scan } = await import('./scanner.js');
|
|
513
737
|
const exitCode = scan(targetPath, {
|
|
@@ -516,6 +740,7 @@ async function scanCmd() {
|
|
|
516
740
|
fixDryRun,
|
|
517
741
|
fixLang: fixLang || undefined,
|
|
518
742
|
format,
|
|
743
|
+
only,
|
|
519
744
|
});
|
|
520
745
|
process.exit(exitCode);
|
|
521
746
|
}
|
|
@@ -532,6 +757,12 @@ switch (command) {
|
|
|
532
757
|
case 'tokens': await tokensCmd(); break;
|
|
533
758
|
case 'scan': scanCmd(); break;
|
|
534
759
|
case 'prefix': await prefixCmd(); break;
|
|
760
|
+
case 'scaffold': await scaffoldCmd(); break;
|
|
761
|
+
case 'layout': await layoutCmd(); break;
|
|
762
|
+
case 'perf': await perfCmd(); break;
|
|
763
|
+
case 'docs': await docsCmd(); break;
|
|
764
|
+
case 'meta': await metaCmd(); break;
|
|
765
|
+
case 'search': await searchCmd(); break;
|
|
535
766
|
case '--help': case '-h': help(); break;
|
|
536
767
|
default: help(); break;
|
|
537
768
|
}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static layout audit for VelinStyle HTML — flex/grid, containers, responsive display.
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync, statSync } from 'fs';
|
|
5
|
+
import { join, extname, relative } from 'path';
|
|
6
|
+
|
|
7
|
+
const DEFAULT_IGNORE = ['node_modules', 'dist', '.git', '.next', '.nuxt', 'vendor', 'build'];
|
|
8
|
+
|
|
9
|
+
export function walkHtmlFiles(dir, ignore = DEFAULT_IGNORE) {
|
|
10
|
+
const results = [];
|
|
11
|
+
if (!existsSync(dir)) return results;
|
|
12
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
13
|
+
if (ignore.includes(entry.name)) continue;
|
|
14
|
+
const full = join(dir, entry.name);
|
|
15
|
+
if (entry.isDirectory()) {
|
|
16
|
+
results.push(...walkHtmlFiles(full, ignore));
|
|
17
|
+
} else if (extname(entry.name).toLowerCase() === '.html') {
|
|
18
|
+
results.push(full);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return results;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function lineOf(content, index) {
|
|
25
|
+
return content.slice(0, index).split('\n').length;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function extractClassTokens(html) {
|
|
29
|
+
const tokens = new Set();
|
|
30
|
+
const re = /class\s*=\s*["']([^"']+)["']/gi;
|
|
31
|
+
let m;
|
|
32
|
+
while ((m = re.exec(html)) !== null) {
|
|
33
|
+
for (const c of m[1].split(/\s+/)) {
|
|
34
|
+
if (c) tokens.add(c);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return tokens;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function hasAnyClass(tokens, patterns) {
|
|
41
|
+
return patterns.some((p) => {
|
|
42
|
+
if (typeof p === 'string') return tokens.has(p);
|
|
43
|
+
return [...tokens].some((t) => p.test(t));
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** @typedef {{ id: string, severity: string, message: string, line?: number, file?: string, suggestions?: object, responsive?: object }} LayoutIssue */
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {string} html
|
|
51
|
+
* @param {{ file?: string }} meta
|
|
52
|
+
* @returns {LayoutIssue[]}
|
|
53
|
+
*/
|
|
54
|
+
export function auditHtml(html, meta = {}) {
|
|
55
|
+
const issues = [];
|
|
56
|
+
const file = meta.file || '';
|
|
57
|
+
const classes = extractClassTokens(html);
|
|
58
|
+
const classStr = html;
|
|
59
|
+
|
|
60
|
+
// velin-col-* without velin-row in nearby context (heuristic: same file has col but no row)
|
|
61
|
+
const hasCol = /\bvelin-(?:sm-|md-|lg-|xl-)?col(?:-\d+)?\b/.test(classStr);
|
|
62
|
+
const hasRow = /\bvelin-row\b/.test(classStr);
|
|
63
|
+
if (hasCol && !hasRow) {
|
|
64
|
+
const idx = classStr.search(/\bvelin-(?:sm-|md-|lg-|xl-)?col/);
|
|
65
|
+
issues.push({
|
|
66
|
+
id: 'grid-missing-row',
|
|
67
|
+
severity: 'warning',
|
|
68
|
+
message: 'Column classes (velin-col-*) found without a parent velin-row.',
|
|
69
|
+
line: idx >= 0 ? lineOf(html, idx) : undefined,
|
|
70
|
+
file,
|
|
71
|
+
suggestions: { addClasses: ['velin-row', 'velin-g-4'] },
|
|
72
|
+
responsive: {
|
|
73
|
+
mobile: 'Stack columns: velin-flex velin-flex--col on small screens or use velin-grid.',
|
|
74
|
+
tablet: 'velin-row with velin-md-col-* at 48rem+.',
|
|
75
|
+
desktop: 'velin-row + velin-col-* for 12-column layout.',
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// flex without wrap and many flex children (heuristic: 6+ velin-btn or 8+ class on flex container)
|
|
81
|
+
const flexNoWrap =
|
|
82
|
+
/\bvelin-flex\b/.test(classStr) &&
|
|
83
|
+
!/\bvelin-flex--wrap\b/.test(classStr) &&
|
|
84
|
+
(classStr.match(/<a\b/gi)?.length || 0) + (classStr.match(/<button\b/gi)?.length || 0) >= 4;
|
|
85
|
+
if (flexNoWrap) {
|
|
86
|
+
issues.push({
|
|
87
|
+
id: 'flex-no-wrap-overflow',
|
|
88
|
+
severity: 'warning',
|
|
89
|
+
message: 'Flex layout without velin-flex--wrap may overflow on narrow viewports.',
|
|
90
|
+
file,
|
|
91
|
+
suggestions: { addClasses: ['velin-flex--wrap'], alt: ['velin-overflow-x-auto'] },
|
|
92
|
+
responsive: {
|
|
93
|
+
mobile: 'Add velin-flex--wrap or velin-flex--col for stacked mobile nav.',
|
|
94
|
+
tablet: 'velin-flex--wrap with velin-gap-4.',
|
|
95
|
+
desktop: 'Keep row layout with wrap for many items.',
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// main or section without container
|
|
101
|
+
const hasMainOrSection = /<(?:main|section)\b/i.test(html);
|
|
102
|
+
const hasContainer = /\bvelin-container\b/.test(classStr);
|
|
103
|
+
if (hasMainOrSection && !hasContainer) {
|
|
104
|
+
issues.push({
|
|
105
|
+
id: 'missing-container',
|
|
106
|
+
severity: 'info',
|
|
107
|
+
message: 'Page sections without velin-container may span edge-to-edge unintentionally.',
|
|
108
|
+
file,
|
|
109
|
+
suggestions: { wrapWith: 'velin-container', alt: ['velin-container--fluid'] },
|
|
110
|
+
responsive: {
|
|
111
|
+
mobile: 'velin-container keeps readable padding on phones.',
|
|
112
|
+
tablet: 'Same container max-width scales at 48rem / 62rem breakpoints.',
|
|
113
|
+
desktop: 'Use velin-container--wide for marketing pages if needed.',
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// velin-hidden without responsive show (velin-md-block etc.)
|
|
119
|
+
if (hasAnyClass(classes, ['velin-hidden']) && !hasAnyClass(classes, [/^velin-(?:sm|md|lg)-(?:block|flex|grid|inline)/])) {
|
|
120
|
+
const idx = classStr.indexOf('velin-hidden');
|
|
121
|
+
issues.push({
|
|
122
|
+
id: 'mobile-hidden-only',
|
|
123
|
+
severity: 'warning',
|
|
124
|
+
message: 'velin-hidden without a matching velin-md-block / velin-md-flex (element may stay hidden on all breakpoints).',
|
|
125
|
+
line: idx >= 0 ? lineOf(html, idx) : undefined,
|
|
126
|
+
file,
|
|
127
|
+
suggestions: { pairWith: ['velin-hidden', 'velin-md-flex'], example: 'velin-hidden velin-md-flex' },
|
|
128
|
+
responsive: {
|
|
129
|
+
mobile: 'Hidden on default (mobile-first).',
|
|
130
|
+
tablet: 'Show from md (48rem) with velin-md-flex or velin-md-block.',
|
|
131
|
+
desktop: 'Adjust with velin-lg-* if needed.',
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// desktop-only nav without mobile alternative hint
|
|
137
|
+
const desktopOnlyNav =
|
|
138
|
+
/\bvelin-desktop-only\b/.test(classStr) &&
|
|
139
|
+
/<nav\b/i.test(html) &&
|
|
140
|
+
!/\bvelin-mobile-only\b/.test(classStr) &&
|
|
141
|
+
!/bottom-nav|velin-bottom-nav/i.test(classStr);
|
|
142
|
+
if (desktopOnlyNav) {
|
|
143
|
+
issues.push({
|
|
144
|
+
id: 'desktop-nav-no-mobile',
|
|
145
|
+
severity: 'info',
|
|
146
|
+
message: 'Desktop-only navigation detected; consider velin-mobile-only + bottom-nav-mobile blueprint for phones.',
|
|
147
|
+
file,
|
|
148
|
+
suggestions: { blueprint: 'bottom-nav-mobile', addClasses: ['velin-mobile-only'] },
|
|
149
|
+
responsive: {
|
|
150
|
+
mobile: 'Use blueprint bottom-nav-mobile or velin-mobile-only block.',
|
|
151
|
+
tablet: 'velin-md-flex for nav from 48rem.',
|
|
152
|
+
desktop: 'velin-desktop-only for wide layouts.',
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 100vw
|
|
158
|
+
if (/100vw|width:\s*100vw/i.test(html)) {
|
|
159
|
+
issues.push({
|
|
160
|
+
id: 'viewport-width',
|
|
161
|
+
severity: 'warning',
|
|
162
|
+
message: '100vw can cause horizontal scroll; prefer velin-w-full inside velin-container.',
|
|
163
|
+
file,
|
|
164
|
+
suggestions: { replaceWith: 'velin-w-full', wrapWith: 'velin-container' },
|
|
165
|
+
responsive: {
|
|
166
|
+
mobile: 'Avoid 100vw; use container + full width children.',
|
|
167
|
+
tablet: '—',
|
|
168
|
+
desktop: '—',
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// sticky + overflow hidden on same element or parent snippet
|
|
174
|
+
if (
|
|
175
|
+
(/position:\s*sticky|velin-position-sticky/i.test(html)) &&
|
|
176
|
+
(/overflow:\s*hidden|velin-overflow-hidden/i.test(html))
|
|
177
|
+
) {
|
|
178
|
+
issues.push({
|
|
179
|
+
id: 'sticky-overflow-parent',
|
|
180
|
+
severity: 'warning',
|
|
181
|
+
message: 'Sticky positioning may not work when an ancestor uses overflow: hidden.',
|
|
182
|
+
file,
|
|
183
|
+
suggestions: { doc: 'Remove overflow:hidden from sticky ancestors or use velin-position-sticky on a different wrapper.' },
|
|
184
|
+
responsive: { mobile: 'Test sticky headers on iOS Safari.', tablet: '—', desktop: '—' },
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// table without responsive wrapper
|
|
189
|
+
if (/<table\b/i.test(html) && !/\btable-responsive\b|velin-table-responsive|overflow-x-auto|velin-overflow-x-auto/i.test(html)) {
|
|
190
|
+
issues.push({
|
|
191
|
+
id: 'table-not-responsive',
|
|
192
|
+
severity: 'info',
|
|
193
|
+
message: 'Tables should scroll horizontally on small screens (blueprint table-responsive or velin-overflow-x-auto).',
|
|
194
|
+
file,
|
|
195
|
+
suggestions: { blueprint: 'table-responsive', wrapWith: 'velin-overflow-x-auto' },
|
|
196
|
+
responsive: {
|
|
197
|
+
mobile: 'Wrap table in scroll container.',
|
|
198
|
+
tablet: 'Full table visible from md if columns fit.',
|
|
199
|
+
desktop: '—',
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return issues;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* @param {string} targetPath file or directory
|
|
209
|
+
* @returns {{ issues: LayoutIssue[], files: string[] }}
|
|
210
|
+
*/
|
|
211
|
+
export function auditPath(targetPath) {
|
|
212
|
+
let files = [];
|
|
213
|
+
if (!existsSync(targetPath)) {
|
|
214
|
+
return { issues: [], files: [] };
|
|
215
|
+
}
|
|
216
|
+
const st = statSync(targetPath);
|
|
217
|
+
if (st.isFile()) {
|
|
218
|
+
files = extname(targetPath).toLowerCase() === '.html' ? [targetPath] : [];
|
|
219
|
+
} else {
|
|
220
|
+
files = walkHtmlFiles(targetPath);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const issues = [];
|
|
224
|
+
for (const file of files) {
|
|
225
|
+
const html = readFileSync(file, 'utf-8');
|
|
226
|
+
issues.push(...auditHtml(html, { file }));
|
|
227
|
+
}
|
|
228
|
+
return { issues, files };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* @param {LayoutIssue[]} issues
|
|
233
|
+
*/
|
|
234
|
+
export function suggestFromIssues(issues) {
|
|
235
|
+
return issues.map((i) => ({
|
|
236
|
+
...i,
|
|
237
|
+
fix:
|
|
238
|
+
i.suggestions?.addClasses?.length ?
|
|
239
|
+
`Add classes: ${i.suggestions.addClasses.join(', ')}`
|
|
240
|
+
: i.suggestions?.pairWith ?
|
|
241
|
+
`Use: ${i.suggestions.pairWith.join(' ')}`
|
|
242
|
+
: i.suggestions?.wrapWith ?
|
|
243
|
+
`Wrap content in <div class="${i.suggestions.wrapWith}">…</div>`
|
|
244
|
+
: i.suggestions?.blueprint ?
|
|
245
|
+
`Consider blueprint: ${i.suggestions.blueprint}`
|
|
246
|
+
: i.suggestions?.doc || 'See responsive-layout guide.',
|
|
247
|
+
}));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Whitelisted safe fixes on HTML string.
|
|
252
|
+
* @returns {{ html: string, changes: string[] }}
|
|
253
|
+
*/
|
|
254
|
+
export function applySafeFixes(html) {
|
|
255
|
+
const changes = [];
|
|
256
|
+
let out = html;
|
|
257
|
+
|
|
258
|
+
// Add velin-flex--wrap to velin-flex that lacks wrap/nowrap
|
|
259
|
+
out = out.replace(
|
|
260
|
+
/class\s*=\s*["']([^"']*\bvelin-flex\b[^"']*)["']/gi,
|
|
261
|
+
(match, cls) => {
|
|
262
|
+
if (/\bvelin-flex--(?:wrap|nowrap)\b/.test(cls)) return match;
|
|
263
|
+
changes.push('Added velin-flex--wrap to flex container');
|
|
264
|
+
return `class="${cls} velin-flex--wrap"`;
|
|
265
|
+
},
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// velin-hidden alone on same element -> add velin-md-block (conservative: only if exactly velin-hidden)
|
|
269
|
+
out = out.replace(
|
|
270
|
+
/class\s*=\s*["']velin-hidden["']/gi,
|
|
271
|
+
() => {
|
|
272
|
+
changes.push('Paired velin-hidden with velin-md-block');
|
|
273
|
+
return 'class="velin-hidden velin-md-block"';
|
|
274
|
+
},
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
// Wrap bare <main> children - skip (too invasive)
|
|
278
|
+
|
|
279
|
+
return { html: out, changes };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* @param {string} targetPath
|
|
284
|
+
* @param {{ write?: boolean, dryRun?: boolean }} opts
|
|
285
|
+
*/
|
|
286
|
+
export function fixPath(targetPath, opts = {}) {
|
|
287
|
+
const { issues, files } = auditPath(targetPath);
|
|
288
|
+
if (files.length === 0) {
|
|
289
|
+
return { ok: false, error: 'No HTML files found.', changes: [] };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const allChanges = [];
|
|
293
|
+
for (const file of files) {
|
|
294
|
+
const html = readFileSync(file, 'utf-8');
|
|
295
|
+
const { html: next, changes } = applySafeFixes(html);
|
|
296
|
+
if (changes.length) {
|
|
297
|
+
allChanges.push({ file, changes });
|
|
298
|
+
if (opts.write && !opts.dryRun) {
|
|
299
|
+
writeFileSync(file, next, 'utf-8');
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return { ok: true, issues, changes: allChanges, dryRun: !!opts.dryRun && !opts.write };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function formatTextReport(issues, files) {
|
|
308
|
+
const lines = [];
|
|
309
|
+
lines.push(`\nLayout audit: ${files.length} file(s), ${issues.length} issue(s)\n`);
|
|
310
|
+
if (issues.length === 0) {
|
|
311
|
+
lines.push(' No layout issues detected.\n');
|
|
312
|
+
return lines.join('\n');
|
|
313
|
+
}
|
|
314
|
+
for (const i of issues) {
|
|
315
|
+
const loc = i.file ? ` ${relative(process.cwd(), i.file)}` : '';
|
|
316
|
+
const ln = i.line ? `:${i.line}` : '';
|
|
317
|
+
lines.push(` [${i.severity.toUpperCase()}] ${i.id}${loc}${ln}`);
|
|
318
|
+
lines.push(` ${i.message}`);
|
|
319
|
+
if (i.suggestions?.addClasses) {
|
|
320
|
+
lines.push(` → Add: ${i.suggestions.addClasses.join(', ')}`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
lines.push('');
|
|
324
|
+
return lines.join('\n');
|
|
325
|
+
}
|