@palate/cli 0.0.0-stage → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +151 -2
- package/bin/palate.mjs +894 -0
- package/kb/anatomy/data-table.md +24 -0
- package/kb/anatomy/empty-state.md +16 -0
- package/kb/anatomy/filter-toolbar.md +19 -0
- package/kb/anatomy/meter.md +16 -0
- package/kb/anatomy/product-window.md +18 -0
- package/kb/anatomy/settings-page.md +18 -0
- package/kb/anatomy/status-indicator.md +17 -0
- package/kb/anatomy/toast-undo.md +16 -0
- package/kb/bench/briefs.json +618 -0
- package/kb/craft/SOURCES.md +48 -0
- package/kb/craft/creativity.md +55 -0
- package/kb/craft/details.md +75 -0
- package/kb/craft/landing-demos.md +46 -0
- package/kb/craft/mistakes.md +73 -0
- package/kb/craft/motion.md +53 -0
- package/kb/craft/principles.md +68 -0
- package/kb/index/discovery.json +1 -0
- package/kb/inspiration/index.json +79 -0
- package/kb/moves/animated-comparison-table.md +58 -0
- package/kb/moves/before-after-slider.md +58 -0
- package/kb/moves/bento-live-tiles.md +57 -0
- package/kb/moves/calm-motion-system.md +63 -0
- package/kb/moves/canvas-grain.md +58 -0
- package/kb/moves/changelog-as-magazine.md +56 -0
- package/kb/moves/code-drawn-diagram.md +56 -0
- package/kb/moves/colophon.md +54 -0
- package/kb/moves/command-palette-site-nav.md +58 -0
- package/kb/moves/confetti-free-celebration.md +58 -0
- package/kb/moves/console-easter-egg.md +57 -0
- package/kb/moves/copy-confirmation.md +57 -0
- package/kb/moves/cursor-reactive-light.md +57 -0
- package/kb/moves/dark-band-contrast-section.md +55 -0
- package/kb/moves/designed-dark-mode.md +58 -0
- package/kb/moves/duotone-photography.md +55 -0
- package/kb/moves/editorial-drop-cap.md +58 -0
- package/kb/moves/editorial-grid-pull-quotes.md +55 -0
- package/kb/moves/engraving-colour-lens.md +64 -0
- package/kb/moves/explorable-control.md +57 -0
- package/kb/moves/feature-films-on-painted-stages.md +58 -0
- package/kb/moves/first-win-onboarding.md +59 -0
- package/kb/moves/footer-live-status.md +56 -0
- package/kb/moves/footer-rewards-the-end.md +58 -0
- package/kb/moves/generative-brand-pattern.md +55 -0
- package/kb/moves/gentle-error-nudge.md +59 -0
- package/kb/moves/halftone-dot-imagery.md +55 -0
- package/kb/moves/hand-drawn-annotations.md +55 -0
- package/kb/moves/hero-paints-itself-in.md +65 -0
- package/kb/moves/honest-progress-narration.md +58 -0
- package/kb/moves/honest-testimonial-wall.md +57 -0
- package/kb/moves/inline-sparklines-in-copy.md +57 -0
- package/kb/moves/inspect-our-taste.md +58 -0
- package/kb/moves/interactive-pricing-calculator.md +59 -0
- package/kb/moves/isometric-illustration-system.md +56 -0
- package/kb/moves/kinetic-variable-type.md +57 -0
- package/kb/moves/konami-secret.md +59 -0
- package/kb/moves/layered-paper-depth.md +56 -0
- package/kb/moves/lightweight-3d-object-hero.md +59 -0
- package/kb/moves/line-engraving-from-image.md +63 -0
- package/kb/moves/link-preview-peek.md +57 -0
- package/kb/moves/live-data-ticker.md +61 -0
- package/kb/moves/made-with-proof.md +51 -0
- package/kb/moves/magnetic-buttons.md +55 -0
- package/kb/moves/marquee-done-right.md +58 -0
- package/kb/moves/moves.yaml +450 -0
- package/kb/moves/museum-painting-backdrop.md +59 -0
- package/kb/moves/one-colour-brand-world.md +56 -0
- package/kb/moves/playful-404.md +58 -0
- package/kb/moves/pointer-highlight-word.md +56 -0
- package/kb/moves/product-in-context.md +56 -0
- package/kb/moves/real-world-data.md +58 -0
- package/kb/moves/referral-reward-reveal.md +59 -0
- package/kb/moves/riso-print-texture.md +57 -0
- package/kb/moves/screenshot-detail-crops.md +56 -0
- package/kb/moves/scroll-progress-chapters.md +57 -0
- package/kb/moves/scroll-scrubbed-product-film.md +62 -0
- package/kb/moves/self-playing-product-demo.md +61 -0
- package/kb/moves/shape-continuity-morph.md +58 -0
- package/kb/moves/sliding-nav-indicator.md +58 -0
- package/kb/moves/sticky-scroll-story.md +56 -0
- package/kb/moves/story-led-long-scroll.md +56 -0
- package/kb/moves/tactile-button.md +63 -0
- package/kb/moves/technical-drawing-lines.md +57 -0
- package/kb/moves/time-of-day-theming.md +56 -0
- package/kb/moves/tiny-game-easter-egg.md +60 -0
- package/kb/moves/type-decode-reveal.md +57 -0
- package/kb/moves/typographic-numeral-hero.md +58 -0
- package/kb/moves/view-transitions-between-pages.md +57 -0
- package/kb/moves/waitlist-position-live.md +60 -0
- package/kb/patterns/app-team-members.md +27 -0
- package/kb/patterns/chat-agent-approvals.md +31 -0
- package/kb/patterns/commerce-product-page.md +34 -0
- package/kb/patterns/dashboard-entity-table.md +37 -0
- package/kb/patterns/dashboard-home-today.md +24 -0
- package/kb/patterns/docs-api-reference.md +31 -0
- package/kb/patterns/marketing-product-led-landing.md +30 -0
- package/kb/patterns/mobile-daily-checklist.md +26 -0
- package/kb/references/index.json +1 -0
- package/kb/references/thumbs/004c5e9072.jpg +0 -0
- package/kb/references/thumbs/008915eb21.jpg +0 -0
- package/kb/references/thumbs/0098cf66c2.jpg +0 -0
- package/kb/references/thumbs/012e61b31f.jpg +0 -0
- package/kb/references/thumbs/014d4e46fc.jpg +0 -0
- package/kb/references/thumbs/01e54f394a.jpg +0 -0
- package/kb/references/thumbs/01e99a579c.jpg +0 -0
- package/kb/references/thumbs/01fd014575.jpg +0 -0
- package/kb/references/thumbs/022778ca72.jpg +0 -0
- package/kb/references/thumbs/02717bf1e1.jpg +0 -0
- package/kb/references/thumbs/02a5cfdbe9.jpg +0 -0
- package/kb/references/thumbs/02c9823f8d.jpg +0 -0
- package/kb/references/thumbs/02f3974613.jpg +0 -0
- package/kb/references/thumbs/032427f1ce.jpg +0 -0
- package/kb/references/thumbs/03ba677d07.jpg +0 -0
- package/kb/references/thumbs/04f2476cfe.jpg +0 -0
- package/kb/references/thumbs/0563e7c52e.jpg +0 -0
- package/kb/references/thumbs/05fa6b8eef.jpg +0 -0
- package/kb/references/thumbs/065bce1517.jpg +0 -0
- package/kb/references/thumbs/06c017f113.jpg +0 -0
- package/kb/references/thumbs/0770e2d8d3.jpg +0 -0
- package/kb/references/thumbs/078a26c4cb.jpg +0 -0
- package/kb/references/thumbs/08679b6e84.jpg +0 -0
- package/kb/references/thumbs/088c45a34c.jpg +0 -0
- package/kb/references/thumbs/089e9fd4e4.jpg +0 -0
- package/kb/references/thumbs/08f773231e.jpg +0 -0
- package/kb/references/thumbs/093ca3e2e3.jpg +0 -0
- package/kb/references/thumbs/0963320f74.jpg +0 -0
- package/kb/references/thumbs/09fc1ded85.jpg +0 -0
- package/kb/references/thumbs/0a368f3963.jpg +0 -0
- package/kb/references/thumbs/0a9e0e5201.jpg +0 -0
- package/kb/references/thumbs/0aace67ee7.jpg +0 -0
- package/kb/references/thumbs/0b61ecf793.jpg +0 -0
- package/kb/references/thumbs/0bde18ae09.jpg +0 -0
- package/kb/references/thumbs/0bf7ee8143.jpg +0 -0
- package/kb/references/thumbs/0ca20a13e0.jpg +0 -0
- package/kb/references/thumbs/0cdc0198c4.jpg +0 -0
- package/kb/references/thumbs/0cf9790b28.jpg +0 -0
- package/kb/references/thumbs/0d0202904b.jpg +0 -0
- package/kb/references/thumbs/0d053aba18.jpg +0 -0
- package/kb/references/thumbs/0d0c2c13d2.jpg +0 -0
- package/kb/references/thumbs/0d30be99b0.jpg +0 -0
- package/kb/references/thumbs/0d503da1b7.jpg +0 -0
- package/kb/references/thumbs/0ddad7b740.jpg +0 -0
- package/kb/references/thumbs/0e0a148101.jpg +0 -0
- package/kb/references/thumbs/0e401b6b75.jpg +0 -0
- package/kb/references/thumbs/0e8f32ba0f.jpg +0 -0
- package/kb/references/thumbs/0ef1966a7d.jpg +0 -0
- package/kb/references/thumbs/0efd416d11.jpg +0 -0
- package/kb/references/thumbs/0f3af263ae.jpg +0 -0
- package/kb/references/thumbs/0f9644c3a5.jpg +0 -0
- package/kb/references/thumbs/1020552737.jpg +0 -0
- package/kb/references/thumbs/1037626587.jpg +0 -0
- package/kb/references/thumbs/10512b7e94.jpg +0 -0
- package/kb/references/thumbs/106dc4a7b5.jpg +0 -0
- package/kb/references/thumbs/108943f6f7.jpg +0 -0
- package/kb/references/thumbs/10a5551d51.jpg +0 -0
- package/kb/references/thumbs/10dd01076c.jpg +0 -0
- package/kb/references/thumbs/11b06e39da.jpg +0 -0
- package/kb/references/thumbs/11baa1ec83.jpg +0 -0
- package/kb/references/thumbs/11bbe12167.jpg +0 -0
- package/kb/references/thumbs/11ca37feb8.jpg +0 -0
- package/kb/references/thumbs/11f14d3f12.jpg +0 -0
- package/kb/references/thumbs/124530ceb5.jpg +0 -0
- package/kb/references/thumbs/1276402336.jpg +0 -0
- package/kb/references/thumbs/1280e5c9d6.jpg +0 -0
- package/kb/references/thumbs/12cac0d172.jpg +0 -0
- package/kb/references/thumbs/138be6623a.jpg +0 -0
- package/kb/references/thumbs/13a0c544d1.jpg +0 -0
- package/kb/references/thumbs/13a32cb94c.jpg +0 -0
- package/kb/references/thumbs/144035d641.jpg +0 -0
- package/kb/references/thumbs/1457e41924.jpg +0 -0
- package/kb/references/thumbs/14972e3a86.jpg +0 -0
- package/kb/references/thumbs/15a47f2e4c.jpg +0 -0
- package/kb/references/thumbs/15aa15d22e.jpg +0 -0
- package/kb/references/thumbs/160dcd24b7.jpg +0 -0
- package/kb/references/thumbs/1610978529.jpg +0 -0
- package/kb/references/thumbs/16db30ba37.jpg +0 -0
- package/kb/references/thumbs/16f19ae4f3.jpg +0 -0
- package/kb/references/thumbs/16f758b03f.jpg +0 -0
- package/kb/references/thumbs/1748e5b751.jpg +0 -0
- package/kb/references/thumbs/175d0b0e69.jpg +0 -0
- package/kb/references/thumbs/178f571a36.jpg +0 -0
- package/kb/references/thumbs/17c43dc025.jpg +0 -0
- package/kb/references/thumbs/17c7f05b50.jpg +0 -0
- package/kb/references/thumbs/17f97e61dd.jpg +0 -0
- package/kb/references/thumbs/1804e17365.jpg +0 -0
- package/kb/references/thumbs/195c162ed1.jpg +0 -0
- package/kb/references/thumbs/19e3699c29.jpg +0 -0
- package/kb/references/thumbs/1a48e0a4d5.jpg +0 -0
- package/kb/references/thumbs/1a6cda265e.jpg +0 -0
- package/kb/references/thumbs/1a7e012091.jpg +0 -0
- package/kb/references/thumbs/1a9173a172.jpg +0 -0
- package/kb/references/thumbs/1b34a6b4d2.jpg +0 -0
- package/kb/references/thumbs/1b5284f3b2.jpg +0 -0
- package/kb/references/thumbs/1b93fbd6d3.jpg +0 -0
- package/kb/references/thumbs/1c6728fef6.jpg +0 -0
- package/kb/references/thumbs/1c6f101618.jpg +0 -0
- package/kb/references/thumbs/1cf7011a94.jpg +0 -0
- package/kb/references/thumbs/1d1769e539.jpg +0 -0
- package/kb/references/thumbs/1d1fd23199.jpg +0 -0
- package/kb/references/thumbs/1d7b6602ac.jpg +0 -0
- package/kb/references/thumbs/1dbbc61b9e.jpg +0 -0
- package/kb/references/thumbs/1dbdd6d21d.jpg +0 -0
- package/kb/references/thumbs/1dd873744b.jpg +0 -0
- package/kb/references/thumbs/1df4dd7cf9.jpg +0 -0
- package/kb/references/thumbs/1ece8c35b8.jpg +0 -0
- package/kb/references/thumbs/1f0e5b9aa4.jpg +0 -0
- package/kb/references/thumbs/1f1419651b.jpg +0 -0
- package/kb/references/thumbs/1f2a6da92d.jpg +0 -0
- package/kb/references/thumbs/1f651f33c0.jpg +0 -0
- package/kb/references/thumbs/1f8b697d18.jpg +0 -0
- package/kb/references/thumbs/1fee3824cd.jpg +0 -0
- package/kb/references/thumbs/1ff7757853.jpg +0 -0
- package/kb/references/thumbs/2043182f01.jpg +0 -0
- package/kb/references/thumbs/2059dabcd5.jpg +0 -0
- package/kb/references/thumbs/209d9dbb95.jpg +0 -0
- package/kb/references/thumbs/20d8923554.jpg +0 -0
- package/kb/references/thumbs/210dd3b671.jpg +0 -0
- package/kb/references/thumbs/2120719116.jpg +0 -0
- package/kb/references/thumbs/21a7384429.jpg +0 -0
- package/kb/references/thumbs/225f187391.jpg +0 -0
- package/kb/references/thumbs/230caf7dd2.jpg +0 -0
- package/kb/references/thumbs/235afad355.jpg +0 -0
- package/kb/references/thumbs/2378066687.jpg +0 -0
- package/kb/references/thumbs/241faf68a1.jpg +0 -0
- package/kb/references/thumbs/24d9f1a9b9.jpg +0 -0
- package/kb/references/thumbs/2505c8b9a0.jpg +0 -0
- package/kb/references/thumbs/25593638ca.jpg +0 -0
- package/kb/references/thumbs/25dd454f18.jpg +0 -0
- package/kb/references/thumbs/2602e123e2.jpg +0 -0
- package/kb/references/thumbs/26095c0b11.jpg +0 -0
- package/kb/references/thumbs/26260d6238.jpg +0 -0
- package/kb/references/thumbs/26bba21836.jpg +0 -0
- package/kb/references/thumbs/26bde3520b.jpg +0 -0
- package/kb/references/thumbs/272e22067c.jpg +0 -0
- package/kb/references/thumbs/27421b3226.jpg +0 -0
- package/kb/references/thumbs/2769ebca41.jpg +0 -0
- package/kb/references/thumbs/27e2f6e8b0.jpg +0 -0
- package/kb/references/thumbs/28432ba373.jpg +0 -0
- package/kb/references/thumbs/2846626f19.jpg +0 -0
- package/kb/references/thumbs/28750e821e.jpg +0 -0
- package/kb/references/thumbs/289481230f.jpg +0 -0
- package/kb/references/thumbs/2897967408.jpg +0 -0
- package/kb/references/thumbs/28c0a5fa17.jpg +0 -0
- package/kb/references/thumbs/28c181fba5.jpg +0 -0
- package/kb/references/thumbs/29806f539f.jpg +0 -0
- package/kb/references/thumbs/2997f56ee7.jpg +0 -0
- package/kb/references/thumbs/29b34a10fb.jpg +0 -0
- package/kb/references/thumbs/29bd78cdb4.jpg +0 -0
- package/kb/references/thumbs/29c8c6c22a.jpg +0 -0
- package/kb/references/thumbs/2ac23f7fae.jpg +0 -0
- package/kb/references/thumbs/2b76e13f79.jpg +0 -0
- package/kb/references/thumbs/2b84226479.jpg +0 -0
- package/kb/references/thumbs/2bf2dbf737.jpg +0 -0
- package/kb/references/thumbs/2bf89abbf6.jpg +0 -0
- package/kb/references/thumbs/2c562cd534.jpg +0 -0
- package/kb/references/thumbs/2cfe9d48c4.jpg +0 -0
- package/kb/references/thumbs/2d49f19b27.jpg +0 -0
- package/kb/references/thumbs/2dc8670da7.jpg +0 -0
- package/kb/references/thumbs/2de947c469.jpg +0 -0
- package/kb/references/thumbs/2e075ca79f.jpg +0 -0
- package/kb/references/thumbs/2eb0820e6b.jpg +0 -0
- package/kb/references/thumbs/2ec56a9dc0.jpg +0 -0
- package/kb/references/thumbs/30bb4ad607.jpg +0 -0
- package/kb/references/thumbs/30d19b8444.jpg +0 -0
- package/kb/references/thumbs/3173c71d73.jpg +0 -0
- package/kb/references/thumbs/31e59e4518.jpg +0 -0
- package/kb/references/thumbs/322b8960e8.jpg +0 -0
- package/kb/references/thumbs/325c9be6f4.jpg +0 -0
- package/kb/references/thumbs/3273338982.jpg +0 -0
- package/kb/references/thumbs/32f8387f38.jpg +0 -0
- package/kb/references/thumbs/332f6b74c3.jpg +0 -0
- package/kb/references/thumbs/334b146b6a.jpg +0 -0
- package/kb/references/thumbs/339abd60e4.jpg +0 -0
- package/kb/references/thumbs/33e88ec295.jpg +0 -0
- package/kb/references/thumbs/340931b5d4.jpg +0 -0
- package/kb/references/thumbs/34165a93bd.jpg +0 -0
- package/kb/references/thumbs/34d0575dd7.jpg +0 -0
- package/kb/references/thumbs/3549e75f0a.jpg +0 -0
- package/kb/references/thumbs/35827f1561.jpg +0 -0
- package/kb/references/thumbs/37c50280ec.jpg +0 -0
- package/kb/references/thumbs/37e66ecf11.jpg +0 -0
- package/kb/references/thumbs/3841a04fdc.jpg +0 -0
- package/kb/references/thumbs/38490c088f.jpg +0 -0
- package/kb/references/thumbs/3850319e9f.jpg +0 -0
- package/kb/references/thumbs/3856a64166.jpg +0 -0
- package/kb/references/thumbs/3881f211c2.jpg +0 -0
- package/kb/references/thumbs/38c7dde2f3.jpg +0 -0
- package/kb/references/thumbs/38ce9e486c.jpg +0 -0
- package/kb/references/thumbs/390ea8e01b.jpg +0 -0
- package/kb/references/thumbs/391b7fc27c.jpg +0 -0
- package/kb/references/thumbs/395f7214dc.jpg +0 -0
- package/kb/references/thumbs/39e81a5db2.jpg +0 -0
- package/kb/references/thumbs/3a67c8d655.jpg +0 -0
- package/kb/references/thumbs/3a89c25e3b.jpg +0 -0
- package/kb/references/thumbs/3aa0837542.jpg +0 -0
- package/kb/references/thumbs/3aae18aa8a.jpg +0 -0
- package/kb/references/thumbs/3bdbe767db.jpg +0 -0
- package/kb/references/thumbs/3c0616fc7f.jpg +0 -0
- package/kb/references/thumbs/3c18e9db1d.jpg +0 -0
- package/kb/references/thumbs/3c5a14076c.jpg +0 -0
- package/kb/references/thumbs/3ca9ec15c6.jpg +0 -0
- package/kb/references/thumbs/3d7756fd35.jpg +0 -0
- package/kb/references/thumbs/3dd45bd33d.jpg +0 -0
- package/kb/references/thumbs/3de72d5414.jpg +0 -0
- package/kb/references/thumbs/3dede597af.jpg +0 -0
- package/kb/references/thumbs/3ea948619e.jpg +0 -0
- package/kb/references/thumbs/402c12054c.jpg +0 -0
- package/kb/references/thumbs/403ab4af90.jpg +0 -0
- package/kb/references/thumbs/40773a8c63.jpg +0 -0
- package/kb/references/thumbs/407d9583db.jpg +0 -0
- package/kb/references/thumbs/415dc1d84c.jpg +0 -0
- package/kb/references/thumbs/420b676a4f.jpg +0 -0
- package/kb/references/thumbs/42a1ee85eb.jpg +0 -0
- package/kb/references/thumbs/43059221aa.jpg +0 -0
- package/kb/references/thumbs/432fa0d22c.jpg +0 -0
- package/kb/references/thumbs/43826f20e3.jpg +0 -0
- package/kb/references/thumbs/43ac55c98a.jpg +0 -0
- package/kb/references/thumbs/43d098f3c3.jpg +0 -0
- package/kb/references/thumbs/4449d80c2c.jpg +0 -0
- package/kb/references/thumbs/44625727dc.jpg +0 -0
- package/kb/references/thumbs/4482bb87a1.jpg +0 -0
- package/kb/references/thumbs/44b078acdc.jpg +0 -0
- package/kb/references/thumbs/44d0305850.jpg +0 -0
- package/kb/references/thumbs/456bb6b59e.jpg +0 -0
- package/kb/references/thumbs/45df73ce48.jpg +0 -0
- package/kb/references/thumbs/45edb45d87.jpg +0 -0
- package/kb/references/thumbs/4671d57cd5.jpg +0 -0
- package/kb/references/thumbs/46cc108518.jpg +0 -0
- package/kb/references/thumbs/470022e614.jpg +0 -0
- package/kb/references/thumbs/4719a669ce.jpg +0 -0
- package/kb/references/thumbs/47404f5ad4.jpg +0 -0
- package/kb/references/thumbs/479d3eb977.jpg +0 -0
- package/kb/references/thumbs/479ed92ceb.jpg +0 -0
- package/kb/references/thumbs/47baed50c7.jpg +0 -0
- package/kb/references/thumbs/47fdcfd7b0.jpg +0 -0
- package/kb/references/thumbs/48fd5e94c5.jpg +0 -0
- package/kb/references/thumbs/493807917b.jpg +0 -0
- package/kb/references/thumbs/49a1c67293.jpg +0 -0
- package/kb/references/thumbs/49e2d7fafe.jpg +0 -0
- package/kb/references/thumbs/49f540fcb6.jpg +0 -0
- package/kb/references/thumbs/4a67ec4028.jpg +0 -0
- package/kb/references/thumbs/4a93275b04.jpg +0 -0
- package/kb/references/thumbs/4aae6f52da.jpg +0 -0
- package/kb/references/thumbs/4b1bae66bd.jpg +0 -0
- package/kb/references/thumbs/4b75da15a9.jpg +0 -0
- package/kb/references/thumbs/4b77789b88.jpg +0 -0
- package/kb/references/thumbs/4bc2f3f3a4.jpg +0 -0
- package/kb/references/thumbs/4c48f27b88.jpg +0 -0
- package/kb/references/thumbs/4c52bd8bfb.jpg +0 -0
- package/kb/references/thumbs/4c9b4acc60.jpg +0 -0
- package/kb/references/thumbs/4cbf0752d4.jpg +0 -0
- package/kb/references/thumbs/4cd2672517.jpg +0 -0
- package/kb/references/thumbs/4dd22a8181.jpg +0 -0
- package/kb/references/thumbs/4ee3447314.jpg +0 -0
- package/kb/references/thumbs/4ef29c8144.jpg +0 -0
- package/kb/references/thumbs/4f82fadfd6.jpg +0 -0
- package/kb/references/thumbs/4fc9b643d8.jpg +0 -0
- package/kb/references/thumbs/50549524b0.jpg +0 -0
- package/kb/references/thumbs/509a615883.jpg +0 -0
- package/kb/references/thumbs/50c03ef855.jpg +0 -0
- package/kb/references/thumbs/50cf9acad6.jpg +0 -0
- package/kb/references/thumbs/51131b025c.jpg +0 -0
- package/kb/references/thumbs/51da1124f0.jpg +0 -0
- package/kb/references/thumbs/524d5e7288.jpg +0 -0
- package/kb/references/thumbs/5251247c81.jpg +0 -0
- package/kb/references/thumbs/526b91b7ec.jpg +0 -0
- package/kb/references/thumbs/52f02f6c2e.jpg +0 -0
- package/kb/references/thumbs/530ab530dc.jpg +0 -0
- package/kb/references/thumbs/5335ee4404.jpg +0 -0
- package/kb/references/thumbs/5385b90939.jpg +0 -0
- package/kb/references/thumbs/53c54654cc.jpg +0 -0
- package/kb/references/thumbs/5420794c6c.jpg +0 -0
- package/kb/references/thumbs/542a9dce8b.jpg +0 -0
- package/kb/references/thumbs/5571530a5f.jpg +0 -0
- package/kb/references/thumbs/56cc68d804.jpg +0 -0
- package/kb/references/thumbs/577b7f7c44.jpg +0 -0
- package/kb/references/thumbs/57adf8f1bb.jpg +0 -0
- package/kb/references/thumbs/57c5f5e3a6.jpg +0 -0
- package/kb/references/thumbs/57ffe4e839.jpg +0 -0
- package/kb/references/thumbs/5817dfc7d6.jpg +0 -0
- package/kb/references/thumbs/581a182a7a.jpg +0 -0
- package/kb/references/thumbs/58807679c8.jpg +0 -0
- package/kb/references/thumbs/589a1d9111.jpg +0 -0
- package/kb/references/thumbs/59167391ab.jpg +0 -0
- package/kb/references/thumbs/5a70420166.jpg +0 -0
- package/kb/references/thumbs/5b10df1b8d.jpg +0 -0
- package/kb/references/thumbs/5b7a712fdb.jpg +0 -0
- package/kb/references/thumbs/5bc46b92ad.jpg +0 -0
- package/kb/references/thumbs/5c0546a120.jpg +0 -0
- package/kb/references/thumbs/5c2b6125a8.jpg +0 -0
- package/kb/references/thumbs/5c536b9e42.jpg +0 -0
- package/kb/references/thumbs/5c8e441d30.jpg +0 -0
- package/kb/references/thumbs/5cb6f1baa5.jpg +0 -0
- package/kb/references/thumbs/5ce6c1f21e.jpg +0 -0
- package/kb/references/thumbs/5cef052c0a.jpg +0 -0
- package/kb/references/thumbs/5d08438209.jpg +0 -0
- package/kb/references/thumbs/5d0f9af9b0.jpg +0 -0
- package/kb/references/thumbs/5d300ed332.jpg +0 -0
- package/kb/references/thumbs/5d708a4794.jpg +0 -0
- package/kb/references/thumbs/5de8826a98.jpg +0 -0
- package/kb/references/thumbs/5df7bff22c.jpg +0 -0
- package/kb/references/thumbs/5e087d0f00.jpg +0 -0
- package/kb/references/thumbs/5e44e70ca0.jpg +0 -0
- package/kb/references/thumbs/5eff5d80ce.jpg +0 -0
- package/kb/references/thumbs/5f015c96b1.jpg +0 -0
- package/kb/references/thumbs/5f3289b7c4.jpg +0 -0
- package/kb/references/thumbs/5f830c40eb.jpg +0 -0
- package/kb/references/thumbs/60c4c3527b.jpg +0 -0
- package/kb/references/thumbs/60e9e594dd.jpg +0 -0
- package/kb/references/thumbs/615421d7be.jpg +0 -0
- package/kb/references/thumbs/6176e4004f.jpg +0 -0
- package/kb/references/thumbs/61f0fe5e06.jpg +0 -0
- package/kb/references/thumbs/6213785db4.jpg +0 -0
- package/kb/references/thumbs/62a670ae09.jpg +0 -0
- package/kb/references/thumbs/63cc12981b.jpg +0 -0
- package/kb/references/thumbs/641f15cc71.jpg +0 -0
- package/kb/references/thumbs/642ab2081d.jpg +0 -0
- package/kb/references/thumbs/644a59bf6e.jpg +0 -0
- package/kb/references/thumbs/644da8a8e3.jpg +0 -0
- package/kb/references/thumbs/6458986897.jpg +0 -0
- package/kb/references/thumbs/64cfbd7b93.jpg +0 -0
- package/kb/references/thumbs/661a57686a.jpg +0 -0
- package/kb/references/thumbs/66355a813a.jpg +0 -0
- package/kb/references/thumbs/664396dd44.jpg +0 -0
- package/kb/references/thumbs/66d832d65f.jpg +0 -0
- package/kb/references/thumbs/66de9e9833.jpg +0 -0
- package/kb/references/thumbs/6750964fd9.jpg +0 -0
- package/kb/references/thumbs/67c7164832.jpg +0 -0
- package/kb/references/thumbs/681600b6cd.jpg +0 -0
- package/kb/references/thumbs/682e75afe4.jpg +0 -0
- package/kb/references/thumbs/69274bc952.jpg +0 -0
- package/kb/references/thumbs/696ec08c52.jpg +0 -0
- package/kb/references/thumbs/69760111cc.jpg +0 -0
- package/kb/references/thumbs/697e32664f.jpg +0 -0
- package/kb/references/thumbs/69df92544b.jpg +0 -0
- package/kb/references/thumbs/69e48c5229.jpg +0 -0
- package/kb/references/thumbs/6a5fb75de1.jpg +0 -0
- package/kb/references/thumbs/6a7389e472.jpg +0 -0
- package/kb/references/thumbs/6a9b041e02.jpg +0 -0
- package/kb/references/thumbs/6ab2fa93ee.jpg +0 -0
- package/kb/references/thumbs/6ae3400319.jpg +0 -0
- package/kb/references/thumbs/6b21344bf3.jpg +0 -0
- package/kb/references/thumbs/6bd1b67a8a.jpg +0 -0
- package/kb/references/thumbs/6c99156c4f.jpg +0 -0
- package/kb/references/thumbs/6cc9b65384.jpg +0 -0
- package/kb/references/thumbs/6d15830a73.jpg +0 -0
- package/kb/references/thumbs/6d4c33d180.jpg +0 -0
- package/kb/references/thumbs/6e36815799.jpg +0 -0
- package/kb/references/thumbs/6eddb9cbfc.jpg +0 -0
- package/kb/references/thumbs/6f97968a7c.jpg +0 -0
- package/kb/references/thumbs/6fa0716a7d.jpg +0 -0
- package/kb/references/thumbs/6facd57184.jpg +0 -0
- package/kb/references/thumbs/6fd57b785a.jpg +0 -0
- package/kb/references/thumbs/700b3cb185.jpg +0 -0
- package/kb/references/thumbs/71197443fa.jpg +0 -0
- package/kb/references/thumbs/72082bc9ee.jpg +0 -0
- package/kb/references/thumbs/72453db479.jpg +0 -0
- package/kb/references/thumbs/72cd64d500.jpg +0 -0
- package/kb/references/thumbs/72d1e75283.jpg +0 -0
- package/kb/references/thumbs/72e13f7f5b.jpg +0 -0
- package/kb/references/thumbs/7459f8e644.jpg +0 -0
- package/kb/references/thumbs/7470d55c79.jpg +0 -0
- package/kb/references/thumbs/74c468f02a.jpg +0 -0
- package/kb/references/thumbs/74fed6eb77.jpg +0 -0
- package/kb/references/thumbs/756e05080d.jpg +0 -0
- package/kb/references/thumbs/75df7e5f49.jpg +0 -0
- package/kb/references/thumbs/76226da6ef.jpg +0 -0
- package/kb/references/thumbs/76a34aa056.jpg +0 -0
- package/kb/references/thumbs/76c7076589.jpg +0 -0
- package/kb/references/thumbs/76cccc396c.jpg +0 -0
- package/kb/references/thumbs/774654789f.jpg +0 -0
- package/kb/references/thumbs/7746cc494d.jpg +0 -0
- package/kb/references/thumbs/7766e36aa3.jpg +0 -0
- package/kb/references/thumbs/78049331ef.jpg +0 -0
- package/kb/references/thumbs/78808a0ca5.jpg +0 -0
- package/kb/references/thumbs/7890b0a310.jpg +0 -0
- package/kb/references/thumbs/78911975e6.jpg +0 -0
- package/kb/references/thumbs/78d289d5a0.jpg +0 -0
- package/kb/references/thumbs/794126267e.jpg +0 -0
- package/kb/references/thumbs/7976eb9025.jpg +0 -0
- package/kb/references/thumbs/798519ec7c.jpg +0 -0
- package/kb/references/thumbs/7a6cc5eecc.jpg +0 -0
- package/kb/references/thumbs/7ac8a7cd4a.jpg +0 -0
- package/kb/references/thumbs/7b1341d61d.jpg +0 -0
- package/kb/references/thumbs/7b9c4031e3.jpg +0 -0
- package/kb/references/thumbs/7ba5ad3779.jpg +0 -0
- package/kb/references/thumbs/7be6eb2bd0.jpg +0 -0
- package/kb/references/thumbs/7c30864f5d.jpg +0 -0
- package/kb/references/thumbs/7c379b5e17.jpg +0 -0
- package/kb/references/thumbs/7c4050eb02.jpg +0 -0
- package/kb/references/thumbs/7ca47b3221.jpg +0 -0
- package/kb/references/thumbs/7d43516bd7.jpg +0 -0
- package/kb/references/thumbs/7da9dbb98a.jpg +0 -0
- package/kb/references/thumbs/7ddda8f57c.jpg +0 -0
- package/kb/references/thumbs/7ece6d215c.jpg +0 -0
- package/kb/references/thumbs/7eebf029de.jpg +0 -0
- package/kb/references/thumbs/7f45483c15.jpg +0 -0
- package/kb/references/thumbs/7fde06eaa5.jpg +0 -0
- package/kb/references/thumbs/80407b97e9.jpg +0 -0
- package/kb/references/thumbs/80f0a6cf4b.jpg +0 -0
- package/kb/references/thumbs/8118b451b8.jpg +0 -0
- package/kb/references/thumbs/816baed4a8.jpg +0 -0
- package/kb/references/thumbs/817d2d15d9.jpg +0 -0
- package/kb/references/thumbs/8260cb430d.jpg +0 -0
- package/kb/references/thumbs/827b562899.jpg +0 -0
- package/kb/references/thumbs/8293cf1e92.jpg +0 -0
- package/kb/references/thumbs/8294653169.jpg +0 -0
- package/kb/references/thumbs/83061f871f.jpg +0 -0
- package/kb/references/thumbs/833df778ca.jpg +0 -0
- package/kb/references/thumbs/83d3acaafc.jpg +0 -0
- package/kb/references/thumbs/844c6efeff.jpg +0 -0
- package/kb/references/thumbs/84563d482a.jpg +0 -0
- package/kb/references/thumbs/848082dd94.jpg +0 -0
- package/kb/references/thumbs/8494b1da94.jpg +0 -0
- package/kb/references/thumbs/84b71014be.jpg +0 -0
- package/kb/references/thumbs/84be4a5f3c.jpg +0 -0
- package/kb/references/thumbs/84fd0fd25a.jpg +0 -0
- package/kb/references/thumbs/853e488e22.jpg +0 -0
- package/kb/references/thumbs/858c91585d.jpg +0 -0
- package/kb/references/thumbs/85903e8fe9.jpg +0 -0
- package/kb/references/thumbs/86597d43fd.jpg +0 -0
- package/kb/references/thumbs/866eb99500.jpg +0 -0
- package/kb/references/thumbs/86ac050340.jpg +0 -0
- package/kb/references/thumbs/86afec7b64.jpg +0 -0
- package/kb/references/thumbs/87238127f5.jpg +0 -0
- package/kb/references/thumbs/87321df792.jpg +0 -0
- package/kb/references/thumbs/87460f70cd.jpg +0 -0
- package/kb/references/thumbs/8770dfeaec.jpg +0 -0
- package/kb/references/thumbs/8798451a8c.jpg +0 -0
- package/kb/references/thumbs/87da1f34a3.jpg +0 -0
- package/kb/references/thumbs/882cdf8c05.jpg +0 -0
- package/kb/references/thumbs/8839f4106f.jpg +0 -0
- package/kb/references/thumbs/8844455212.jpg +0 -0
- package/kb/references/thumbs/88abba0497.jpg +0 -0
- package/kb/references/thumbs/88b372435d.jpg +0 -0
- package/kb/references/thumbs/88bd4f35fa.jpg +0 -0
- package/kb/references/thumbs/88c71a075e.jpg +0 -0
- package/kb/references/thumbs/892e5f048e.jpg +0 -0
- package/kb/references/thumbs/893cc8e920.jpg +0 -0
- package/kb/references/thumbs/896c198814.jpg +0 -0
- package/kb/references/thumbs/8a8e22f756.jpg +0 -0
- package/kb/references/thumbs/8a9d8a35cb.jpg +0 -0
- package/kb/references/thumbs/8ab1d67fec.jpg +0 -0
- package/kb/references/thumbs/8ab201fe12.jpg +0 -0
- package/kb/references/thumbs/8ac372c102.jpg +0 -0
- package/kb/references/thumbs/8b15736d62.jpg +0 -0
- package/kb/references/thumbs/8b1d8d6a05.jpg +0 -0
- package/kb/references/thumbs/8bb6050e20.jpg +0 -0
- package/kb/references/thumbs/8c6c9d1577.jpg +0 -0
- package/kb/references/thumbs/8d337851e0.jpg +0 -0
- package/kb/references/thumbs/8d44b33cc5.jpg +0 -0
- package/kb/references/thumbs/8d4536ff3b.jpg +0 -0
- package/kb/references/thumbs/8d9b99454a.jpg +0 -0
- package/kb/references/thumbs/8dda2303ff.jpg +0 -0
- package/kb/references/thumbs/8dec722d62.jpg +0 -0
- package/kb/references/thumbs/8e0ea4ca74.jpg +0 -0
- package/kb/references/thumbs/8ebba84d69.jpg +0 -0
- package/kb/references/thumbs/8ef09140c0.jpg +0 -0
- package/kb/references/thumbs/8f2928a1b0.jpg +0 -0
- package/kb/references/thumbs/8f88f9ddba.jpg +0 -0
- package/kb/references/thumbs/8fa4451009.jpg +0 -0
- package/kb/references/thumbs/90109b0e02.jpg +0 -0
- package/kb/references/thumbs/90e854e5c9.jpg +0 -0
- package/kb/references/thumbs/914e79f893.jpg +0 -0
- package/kb/references/thumbs/9161369197.jpg +0 -0
- package/kb/references/thumbs/9165c87d6f.jpg +0 -0
- package/kb/references/thumbs/9193f6558a.jpg +0 -0
- package/kb/references/thumbs/91ac7df877.jpg +0 -0
- package/kb/references/thumbs/9216bd4931.jpg +0 -0
- package/kb/references/thumbs/922ae8a26a.jpg +0 -0
- package/kb/references/thumbs/92a70e6867.jpg +0 -0
- package/kb/references/thumbs/92c77ece22.jpg +0 -0
- package/kb/references/thumbs/92e5914fbf.jpg +0 -0
- package/kb/references/thumbs/92ee36419a.jpg +0 -0
- package/kb/references/thumbs/94153a60b0.jpg +0 -0
- package/kb/references/thumbs/9449666640.jpg +0 -0
- package/kb/references/thumbs/944be6a646.jpg +0 -0
- package/kb/references/thumbs/945831476e.jpg +0 -0
- package/kb/references/thumbs/94c1af82f4.jpg +0 -0
- package/kb/references/thumbs/9599d6d589.jpg +0 -0
- package/kb/references/thumbs/960710f4cf.jpg +0 -0
- package/kb/references/thumbs/9608f12d64.jpg +0 -0
- package/kb/references/thumbs/962dabfaa0.jpg +0 -0
- package/kb/references/thumbs/965a3a95c6.jpg +0 -0
- package/kb/references/thumbs/9672ed5ae4.jpg +0 -0
- package/kb/references/thumbs/96891ae9c1.jpg +0 -0
- package/kb/references/thumbs/96e1907c99.jpg +0 -0
- package/kb/references/thumbs/96f25b6578.jpg +0 -0
- package/kb/references/thumbs/97211df9ae.jpg +0 -0
- package/kb/references/thumbs/9769d763a5.jpg +0 -0
- package/kb/references/thumbs/9792bd6d05.jpg +0 -0
- package/kb/references/thumbs/97d2837e3e.jpg +0 -0
- package/kb/references/thumbs/97d90d1d85.jpg +0 -0
- package/kb/references/thumbs/97fcd5774a.jpg +0 -0
- package/kb/references/thumbs/980f68564e.jpg +0 -0
- package/kb/references/thumbs/98511fb393.jpg +0 -0
- package/kb/references/thumbs/98f8fa694d.jpg +0 -0
- package/kb/references/thumbs/9a42c6bb97.jpg +0 -0
- package/kb/references/thumbs/9ab3600a06.jpg +0 -0
- package/kb/references/thumbs/9ab48d9130.jpg +0 -0
- package/kb/references/thumbs/9ad519197d.jpg +0 -0
- package/kb/references/thumbs/9b1382c657.jpg +0 -0
- package/kb/references/thumbs/9b6cc1e077.jpg +0 -0
- package/kb/references/thumbs/9beede8480.jpg +0 -0
- package/kb/references/thumbs/9bf8976952.jpg +0 -0
- package/kb/references/thumbs/9cace7b75a.jpg +0 -0
- package/kb/references/thumbs/9cdcc5745e.jpg +0 -0
- package/kb/references/thumbs/9d0b269074.jpg +0 -0
- package/kb/references/thumbs/9d41788b48.jpg +0 -0
- package/kb/references/thumbs/9d475bd6af.jpg +0 -0
- package/kb/references/thumbs/9e6bc543d0.jpg +0 -0
- package/kb/references/thumbs/9ed0e0a206.jpg +0 -0
- package/kb/references/thumbs/9ef538de13.jpg +0 -0
- package/kb/references/thumbs/9efc373ab4.jpg +0 -0
- package/kb/references/thumbs/9f4a939fa5.jpg +0 -0
- package/kb/references/thumbs/9f8d12d7b9.jpg +0 -0
- package/kb/references/thumbs/9fd5e3efa7.jpg +0 -0
- package/kb/references/thumbs/a0a9b9c572.jpg +0 -0
- package/kb/references/thumbs/a29ca9b185.jpg +0 -0
- package/kb/references/thumbs/a2a42884af.jpg +0 -0
- package/kb/references/thumbs/a2f5064e34.jpg +0 -0
- package/kb/references/thumbs/a3112ce672.jpg +0 -0
- package/kb/references/thumbs/a3437736d1.jpg +0 -0
- package/kb/references/thumbs/a372667e76.jpg +0 -0
- package/kb/references/thumbs/a39bae94e9.jpg +0 -0
- package/kb/references/thumbs/a3fc75d94c.jpg +0 -0
- package/kb/references/thumbs/a440487631.jpg +0 -0
- package/kb/references/thumbs/a4993d5fb5.jpg +0 -0
- package/kb/references/thumbs/a500476a86.jpg +0 -0
- package/kb/references/thumbs/a55506bf46.jpg +0 -0
- package/kb/references/thumbs/a59a5f9843.jpg +0 -0
- package/kb/references/thumbs/a5bce0eede.jpg +0 -0
- package/kb/references/thumbs/a5d921e1f0.jpg +0 -0
- package/kb/references/thumbs/a5e19d4637.jpg +0 -0
- package/kb/references/thumbs/a5f92b97d5.jpg +0 -0
- package/kb/references/thumbs/a6562c210a.jpg +0 -0
- package/kb/references/thumbs/a6f61a8d09.jpg +0 -0
- package/kb/references/thumbs/a719fdf663.jpg +0 -0
- package/kb/references/thumbs/a724e5442f.jpg +0 -0
- package/kb/references/thumbs/a852b4319f.jpg +0 -0
- package/kb/references/thumbs/a896c08448.jpg +0 -0
- package/kb/references/thumbs/a8d190a4b8.jpg +0 -0
- package/kb/references/thumbs/a8dcdd57d7.jpg +0 -0
- package/kb/references/thumbs/a951347e18.jpg +0 -0
- package/kb/references/thumbs/a9a1a0bf7d.jpg +0 -0
- package/kb/references/thumbs/a9b72986a8.jpg +0 -0
- package/kb/references/thumbs/aa11555ba8.jpg +0 -0
- package/kb/references/thumbs/aa4aab9093.jpg +0 -0
- package/kb/references/thumbs/aa89b96658.jpg +0 -0
- package/kb/references/thumbs/aaeb3739b6.jpg +0 -0
- package/kb/references/thumbs/ab25a608ec.jpg +0 -0
- package/kb/references/thumbs/ab34b62268.jpg +0 -0
- package/kb/references/thumbs/ab3b0217bb.jpg +0 -0
- package/kb/references/thumbs/ac70c04afb.jpg +0 -0
- package/kb/references/thumbs/ace0879117.jpg +0 -0
- package/kb/references/thumbs/ad0515ae55.jpg +0 -0
- package/kb/references/thumbs/ad66b25b47.jpg +0 -0
- package/kb/references/thumbs/ad68229d2f.jpg +0 -0
- package/kb/references/thumbs/add6480693.jpg +0 -0
- package/kb/references/thumbs/ae5b8c3b8c.jpg +0 -0
- package/kb/references/thumbs/ae81247b47.jpg +0 -0
- package/kb/references/thumbs/af8aa54379.jpg +0 -0
- package/kb/references/thumbs/afa80dedd4.jpg +0 -0
- package/kb/references/thumbs/afe04d730f.jpg +0 -0
- package/kb/references/thumbs/b09a7dc315.jpg +0 -0
- package/kb/references/thumbs/b0c02629f6.jpg +0 -0
- package/kb/references/thumbs/b1cb4f91de.jpg +0 -0
- package/kb/references/thumbs/b1d4c13c92.jpg +0 -0
- package/kb/references/thumbs/b20eef86db.jpg +0 -0
- package/kb/references/thumbs/b3136ccdde.jpg +0 -0
- package/kb/references/thumbs/b3f5e8d7dd.jpg +0 -0
- package/kb/references/thumbs/b44adb81c9.jpg +0 -0
- package/kb/references/thumbs/b457a5894b.jpg +0 -0
- package/kb/references/thumbs/b461281e0f.jpg +0 -0
- package/kb/references/thumbs/b4942644f0.jpg +0 -0
- package/kb/references/thumbs/b49f8eccad.jpg +0 -0
- package/kb/references/thumbs/b56c69be23.jpg +0 -0
- package/kb/references/thumbs/b59786b33c.jpg +0 -0
- package/kb/references/thumbs/b5d368ce5a.jpg +0 -0
- package/kb/references/thumbs/b5eb442406.jpg +0 -0
- package/kb/references/thumbs/b7fdabf4ab.jpg +0 -0
- package/kb/references/thumbs/b88ee8d235.jpg +0 -0
- package/kb/references/thumbs/b8a4e5be8a.jpg +0 -0
- package/kb/references/thumbs/b8c85b661c.jpg +0 -0
- package/kb/references/thumbs/ba1b8e7435.jpg +0 -0
- package/kb/references/thumbs/ba9a593eed.jpg +0 -0
- package/kb/references/thumbs/baa7afc201.jpg +0 -0
- package/kb/references/thumbs/baf74398f7.jpg +0 -0
- package/kb/references/thumbs/bb3e947c10.jpg +0 -0
- package/kb/references/thumbs/bb4c1bb7c1.jpg +0 -0
- package/kb/references/thumbs/bbaa25cc8a.jpg +0 -0
- package/kb/references/thumbs/bc7e7f88cc.jpg +0 -0
- package/kb/references/thumbs/bc9d5b58f5.jpg +0 -0
- package/kb/references/thumbs/bcc99a51f7.jpg +0 -0
- package/kb/references/thumbs/bce13dd1c1.jpg +0 -0
- package/kb/references/thumbs/bd079a6130.jpg +0 -0
- package/kb/references/thumbs/bd2d70dda7.jpg +0 -0
- package/kb/references/thumbs/bd5895c5f2.jpg +0 -0
- package/kb/references/thumbs/bde8e31d62.jpg +0 -0
- package/kb/references/thumbs/be084e78f3.jpg +0 -0
- package/kb/references/thumbs/be94f170c8.jpg +0 -0
- package/kb/references/thumbs/bed33654f7.jpg +0 -0
- package/kb/references/thumbs/beeb9a0c18.jpg +0 -0
- package/kb/references/thumbs/bef42094dc.jpg +0 -0
- package/kb/references/thumbs/bf625496fa.jpg +0 -0
- package/kb/references/thumbs/bf771f1398.jpg +0 -0
- package/kb/references/thumbs/c0ddefa3d9.jpg +0 -0
- package/kb/references/thumbs/c0f01e536e.jpg +0 -0
- package/kb/references/thumbs/c1d592b678.jpg +0 -0
- package/kb/references/thumbs/c1e83e1b3e.jpg +0 -0
- package/kb/references/thumbs/c2168f8096.jpg +0 -0
- package/kb/references/thumbs/c249cd6b17.jpg +0 -0
- package/kb/references/thumbs/c2e3d4feed.jpg +0 -0
- package/kb/references/thumbs/c3cec01df1.jpg +0 -0
- package/kb/references/thumbs/c47bedb42d.jpg +0 -0
- package/kb/references/thumbs/c4b03b4680.jpg +0 -0
- package/kb/references/thumbs/c4e6d18ec3.jpg +0 -0
- package/kb/references/thumbs/c5477e2877.jpg +0 -0
- package/kb/references/thumbs/c566794674.jpg +0 -0
- package/kb/references/thumbs/c5b945afbb.jpg +0 -0
- package/kb/references/thumbs/c5e4659497.jpg +0 -0
- package/kb/references/thumbs/c622786702.jpg +0 -0
- package/kb/references/thumbs/c6250649ca.jpg +0 -0
- package/kb/references/thumbs/c6261a9526.jpg +0 -0
- package/kb/references/thumbs/c62e609076.jpg +0 -0
- package/kb/references/thumbs/c6602d3e85.jpg +0 -0
- package/kb/references/thumbs/c6c1a16e40.jpg +0 -0
- package/kb/references/thumbs/c77f932157.jpg +0 -0
- package/kb/references/thumbs/c79788f01e.jpg +0 -0
- package/kb/references/thumbs/c7a511ad1e.jpg +0 -0
- package/kb/references/thumbs/c7acfbc3c8.jpg +0 -0
- package/kb/references/thumbs/c7bb6d1bb8.jpg +0 -0
- package/kb/references/thumbs/c829eadcb9.jpg +0 -0
- package/kb/references/thumbs/c891fd9d85.jpg +0 -0
- package/kb/references/thumbs/c89f812ee3.jpg +0 -0
- package/kb/references/thumbs/c8a85b9f61.jpg +0 -0
- package/kb/references/thumbs/c8fdf85c9a.jpg +0 -0
- package/kb/references/thumbs/c961ac1031.jpg +0 -0
- package/kb/references/thumbs/c9fd3f88d8.jpg +0 -0
- package/kb/references/thumbs/ca0d6399c3.jpg +0 -0
- package/kb/references/thumbs/ca316c83e4.jpg +0 -0
- package/kb/references/thumbs/ca3b93aa0d.jpg +0 -0
- package/kb/references/thumbs/cb0596a85b.jpg +0 -0
- package/kb/references/thumbs/cb95a5c3ef.jpg +0 -0
- package/kb/references/thumbs/cbe238b656.jpg +0 -0
- package/kb/references/thumbs/cbf530e608.jpg +0 -0
- package/kb/references/thumbs/cc173494ec.jpg +0 -0
- package/kb/references/thumbs/ccb664bddf.jpg +0 -0
- package/kb/references/thumbs/ccf69d36cd.jpg +0 -0
- package/kb/references/thumbs/ccf759db42.jpg +0 -0
- package/kb/references/thumbs/cd2a285807.jpg +0 -0
- package/kb/references/thumbs/cd4a24a8ca.jpg +0 -0
- package/kb/references/thumbs/cdb3fe8938.jpg +0 -0
- package/kb/references/thumbs/cdce1b9bc6.jpg +0 -0
- package/kb/references/thumbs/cde3aa761a.jpg +0 -0
- package/kb/references/thumbs/ce9d5cd9c2.jpg +0 -0
- package/kb/references/thumbs/ceb01f8a34.jpg +0 -0
- package/kb/references/thumbs/cee15c98d0.jpg +0 -0
- package/kb/references/thumbs/cf6944ddae.jpg +0 -0
- package/kb/references/thumbs/d15dd21cd8.jpg +0 -0
- package/kb/references/thumbs/d1633bf2ed.jpg +0 -0
- package/kb/references/thumbs/d16a838000.jpg +0 -0
- package/kb/references/thumbs/d1cf87e9f0.jpg +0 -0
- package/kb/references/thumbs/d32511d913.jpg +0 -0
- package/kb/references/thumbs/d327e9dedd.jpg +0 -0
- package/kb/references/thumbs/d35bf6c85e.jpg +0 -0
- package/kb/references/thumbs/d37afc9d61.jpg +0 -0
- package/kb/references/thumbs/d3b66655c7.jpg +0 -0
- package/kb/references/thumbs/d40b4172b6.jpg +0 -0
- package/kb/references/thumbs/d4c40d1824.jpg +0 -0
- package/kb/references/thumbs/d4cb55a4b6.jpg +0 -0
- package/kb/references/thumbs/d4f0c30f7e.jpg +0 -0
- package/kb/references/thumbs/d51cce3ae5.jpg +0 -0
- package/kb/references/thumbs/d52948f9ba.jpg +0 -0
- package/kb/references/thumbs/d62bd3cba4.jpg +0 -0
- package/kb/references/thumbs/d668a21c92.jpg +0 -0
- package/kb/references/thumbs/d685072c52.jpg +0 -0
- package/kb/references/thumbs/d6d7b96093.jpg +0 -0
- package/kb/references/thumbs/d7cd874b21.jpg +0 -0
- package/kb/references/thumbs/d7e111736a.jpg +0 -0
- package/kb/references/thumbs/d88325f832.jpg +0 -0
- package/kb/references/thumbs/d9272ded76.jpg +0 -0
- package/kb/references/thumbs/d962e7c654.jpg +0 -0
- package/kb/references/thumbs/d97f000ee2.jpg +0 -0
- package/kb/references/thumbs/d9c1ae2543.jpg +0 -0
- package/kb/references/thumbs/d9ccb3cac8.jpg +0 -0
- package/kb/references/thumbs/da99019680.jpg +0 -0
- package/kb/references/thumbs/da9b8bc615.jpg +0 -0
- package/kb/references/thumbs/daafbee2c1.jpg +0 -0
- package/kb/references/thumbs/db0f11f143.jpg +0 -0
- package/kb/references/thumbs/db5dd367f5.jpg +0 -0
- package/kb/references/thumbs/db69677866.jpg +0 -0
- package/kb/references/thumbs/db87c1956e.jpg +0 -0
- package/kb/references/thumbs/dbb86d8bda.jpg +0 -0
- package/kb/references/thumbs/dbd3a8ff2a.jpg +0 -0
- package/kb/references/thumbs/dbdf4b2794.jpg +0 -0
- package/kb/references/thumbs/dd4c19b4b1.jpg +0 -0
- package/kb/references/thumbs/dd98d9f6fa.jpg +0 -0
- package/kb/references/thumbs/ddb00e3b6a.jpg +0 -0
- package/kb/references/thumbs/dde3541a5c.jpg +0 -0
- package/kb/references/thumbs/de2a4da502.jpg +0 -0
- package/kb/references/thumbs/de4c387a66.jpg +0 -0
- package/kb/references/thumbs/de5618fa82.jpg +0 -0
- package/kb/references/thumbs/de6bec2538.jpg +0 -0
- package/kb/references/thumbs/dea13cbb1f.jpg +0 -0
- package/kb/references/thumbs/deff9202aa.jpg +0 -0
- package/kb/references/thumbs/df12835f8d.jpg +0 -0
- package/kb/references/thumbs/df57acbb3d.jpg +0 -0
- package/kb/references/thumbs/df8265009e.jpg +0 -0
- package/kb/references/thumbs/dfc99a1e8e.jpg +0 -0
- package/kb/references/thumbs/dfc9fde460.jpg +0 -0
- package/kb/references/thumbs/e038348eee.jpg +0 -0
- package/kb/references/thumbs/e04c2ae1d5.jpg +0 -0
- package/kb/references/thumbs/e070345c43.jpg +0 -0
- package/kb/references/thumbs/e0a0c84d47.jpg +0 -0
- package/kb/references/thumbs/e0af1e6b9e.jpg +0 -0
- package/kb/references/thumbs/e1025942c4.jpg +0 -0
- package/kb/references/thumbs/e11b6f5b82.jpg +0 -0
- package/kb/references/thumbs/e1553de3fc.jpg +0 -0
- package/kb/references/thumbs/e162ebe7cd.jpg +0 -0
- package/kb/references/thumbs/e1f1e531d8.jpg +0 -0
- package/kb/references/thumbs/e20842e5c9.jpg +0 -0
- package/kb/references/thumbs/e24e7d949b.jpg +0 -0
- package/kb/references/thumbs/e2a3e5d10a.jpg +0 -0
- package/kb/references/thumbs/e2de02615a.jpg +0 -0
- package/kb/references/thumbs/e305dad7dd.jpg +0 -0
- package/kb/references/thumbs/e52e3ef97f.jpg +0 -0
- package/kb/references/thumbs/e570e92d06.jpg +0 -0
- package/kb/references/thumbs/e583932fb9.jpg +0 -0
- package/kb/references/thumbs/e5da3c038f.jpg +0 -0
- package/kb/references/thumbs/e657634af3.jpg +0 -0
- package/kb/references/thumbs/e67946e3e1.jpg +0 -0
- package/kb/references/thumbs/e69e0f3639.jpg +0 -0
- package/kb/references/thumbs/e6ebe66031.jpg +0 -0
- package/kb/references/thumbs/e7db9563c9.jpg +0 -0
- package/kb/references/thumbs/e84a4c3b7b.jpg +0 -0
- package/kb/references/thumbs/e84cf52c27.jpg +0 -0
- package/kb/references/thumbs/e8707cf6e8.jpg +0 -0
- package/kb/references/thumbs/e888890b40.jpg +0 -0
- package/kb/references/thumbs/e8a502b418.jpg +0 -0
- package/kb/references/thumbs/e8ba68371d.jpg +0 -0
- package/kb/references/thumbs/e9098f54cc.jpg +0 -0
- package/kb/references/thumbs/e9144deae9.jpg +0 -0
- package/kb/references/thumbs/e93e98d757.jpg +0 -0
- package/kb/references/thumbs/e9762dd435.jpg +0 -0
- package/kb/references/thumbs/e9db4544c8.jpg +0 -0
- package/kb/references/thumbs/e9ddffeacd.jpg +0 -0
- package/kb/references/thumbs/e9e2cf1d7e.jpg +0 -0
- package/kb/references/thumbs/ea1e23e623.jpg +0 -0
- package/kb/references/thumbs/ea41200f5e.jpg +0 -0
- package/kb/references/thumbs/eaf4916e1a.jpg +0 -0
- package/kb/references/thumbs/eb2c505fc8.jpg +0 -0
- package/kb/references/thumbs/eb47943693.jpg +0 -0
- package/kb/references/thumbs/eb4ffd6625.jpg +0 -0
- package/kb/references/thumbs/eb82ac0a4d.jpg +0 -0
- package/kb/references/thumbs/ebeb345ade.jpg +0 -0
- package/kb/references/thumbs/ec4e2aba0a.jpg +0 -0
- package/kb/references/thumbs/ed683bc9f7.jpg +0 -0
- package/kb/references/thumbs/ed9e236a84.jpg +0 -0
- package/kb/references/thumbs/ee45e1caea.jpg +0 -0
- package/kb/references/thumbs/ee50bbf2e7.jpg +0 -0
- package/kb/references/thumbs/eed6ad3b7b.jpg +0 -0
- package/kb/references/thumbs/eee247fd70.jpg +0 -0
- package/kb/references/thumbs/ef87b6cb99.jpg +0 -0
- package/kb/references/thumbs/ef87dccf04.jpg +0 -0
- package/kb/references/thumbs/ef941bba1e.jpg +0 -0
- package/kb/references/thumbs/effc14fb2c.jpg +0 -0
- package/kb/references/thumbs/f04aa5a191.jpg +0 -0
- package/kb/references/thumbs/f13f12485f.jpg +0 -0
- package/kb/references/thumbs/f16a5c0fd4.jpg +0 -0
- package/kb/references/thumbs/f1ba7a7a8f.jpg +0 -0
- package/kb/references/thumbs/f1bc4c9c19.jpg +0 -0
- package/kb/references/thumbs/f1ee827878.jpg +0 -0
- package/kb/references/thumbs/f21736aeff.jpg +0 -0
- package/kb/references/thumbs/f22bc41213.jpg +0 -0
- package/kb/references/thumbs/f266aaffcc.jpg +0 -0
- package/kb/references/thumbs/f2c0ed0d65.jpg +0 -0
- package/kb/references/thumbs/f32ac710fd.jpg +0 -0
- package/kb/references/thumbs/f32b697ff6.jpg +0 -0
- package/kb/references/thumbs/f39c883904.jpg +0 -0
- package/kb/references/thumbs/f3b9d55e6e.jpg +0 -0
- package/kb/references/thumbs/f3c1763e14.jpg +0 -0
- package/kb/references/thumbs/f40853fd43.jpg +0 -0
- package/kb/references/thumbs/f4fd07dc4d.jpg +0 -0
- package/kb/references/thumbs/f50b756ee5.jpg +0 -0
- package/kb/references/thumbs/f51272e9b3.jpg +0 -0
- package/kb/references/thumbs/f538895eee.jpg +0 -0
- package/kb/references/thumbs/f5d28081b5.jpg +0 -0
- package/kb/references/thumbs/f6046c9501.jpg +0 -0
- package/kb/references/thumbs/f62e4af316.jpg +0 -0
- package/kb/references/thumbs/f6f6bffeb8.jpg +0 -0
- package/kb/references/thumbs/f736e00af6.jpg +0 -0
- package/kb/references/thumbs/f7932da47d.jpg +0 -0
- package/kb/references/thumbs/f879e118ca.jpg +0 -0
- package/kb/references/thumbs/f88257215d.jpg +0 -0
- package/kb/references/thumbs/f88c45a6d7.jpg +0 -0
- package/kb/references/thumbs/f89a7b646e.jpg +0 -0
- package/kb/references/thumbs/f8a1e7a274.jpg +0 -0
- package/kb/references/thumbs/f9183cf2b6.jpg +0 -0
- package/kb/references/thumbs/f995de5ce9.jpg +0 -0
- package/kb/references/thumbs/f9ada2c3e1.jpg +0 -0
- package/kb/references/thumbs/f9d4616f89.jpg +0 -0
- package/kb/references/thumbs/f9f4307bb0.jpg +0 -0
- package/kb/references/thumbs/fa070122c7.jpg +0 -0
- package/kb/references/thumbs/fa29cc1369.jpg +0 -0
- package/kb/references/thumbs/fa791a4dab.jpg +0 -0
- package/kb/references/thumbs/fa8060509e.jpg +0 -0
- package/kb/references/thumbs/fafeed4003.jpg +0 -0
- package/kb/references/thumbs/fb1c024f38.jpg +0 -0
- package/kb/references/thumbs/fb77aa28b3.jpg +0 -0
- package/kb/references/thumbs/fb9f7f1966.jpg +0 -0
- package/kb/references/thumbs/fba8a8c2da.jpg +0 -0
- package/kb/references/thumbs/fce2754c36.jpg +0 -0
- package/kb/references/thumbs/fce2fb83df.jpg +0 -0
- package/kb/references/thumbs/fd4c51bb47.jpg +0 -0
- package/kb/references/thumbs/fd8c269817.jpg +0 -0
- package/kb/references/thumbs/fdb7b250ca.jpg +0 -0
- package/kb/references/thumbs/fe18088de8.jpg +0 -0
- package/kb/references/thumbs/fe4f742ba7.jpg +0 -0
- package/kb/references/thumbs/fe64742ce7.jpg +0 -0
- package/kb/references/thumbs/feb5d62161.jpg +0 -0
- package/kb/references/thumbs/fed21b3e98.jpg +0 -0
- package/kb/references/thumbs/fed2205951.jpg +0 -0
- package/kb/references/thumbs/ffdacf1715.jpg +0 -0
- package/kb/sections/_buttons.md +93 -0
- package/kb/sections/_finish.md +325 -0
- package/kb/sections/about-founder-note.md +85 -0
- package/kb/sections/about-team-roster.md +86 -0
- package/kb/sections/alternating-rows-live-tiles.md +110 -0
- package/kb/sections/annotated-product-shot.md +105 -0
- package/kb/sections/asymmetric-bento-dominant.md +114 -0
- package/kb/sections/before-after-features.md +104 -0
- package/kb/sections/before-after-repair.md +107 -0
- package/kb/sections/bento-lead-tile.md +112 -0
- package/kb/sections/book-a-slot-honest.md +116 -0
- package/kb/sections/capability-prompt-ledger.md +68 -0
- package/kb/sections/case-study-band.md +113 -0
- package/kb/sections/centred-stack-hero.md +107 -0
- package/kb/sections/closing-demo-final-state.md +81 -0
- package/kb/sections/closing-full-bleed-line-and-form.md +86 -0
- package/kb/sections/closing-result-previewed.md +88 -0
- package/kb/sections/closing-three-real-steps.md +87 -0
- package/kb/sections/command-reference-table.md +70 -0
- package/kb/sections/comparison-old-way-bars.md +88 -0
- package/kb/sections/comparison-same-task-two-ways.md +88 -0
- package/kb/sections/day-of-the-place.md +102 -0
- package/kb/sections/docs-reading-rail.md +88 -0
- package/kb/sections/editorial-columns-hero.md +107 -0
- package/kb/sections/editorial-essay-pull-quote.md +112 -0
- package/kb/sections/events-calendar-list.md +107 -0
- package/kb/sections/explorable-control-demo.md +102 -0
- package/kb/sections/faq-open-answers-editorial.md +85 -0
- package/kb/sections/faq-ruled-accordion-aside.md +87 -0
- package/kb/sections/footer-closing-scene.md +84 -0
- package/kb/sections/footer-editorial-sitemap.md +85 -0
- package/kb/sections/footer-minimal-credits.md +80 -0
- package/kb/sections/full-bleed-image-band.md +103 -0
- package/kb/sections/full-bleed-image-hero.md +105 -0
- package/kb/sections/gallery-first-hero.md +109 -0
- package/kb/sections/horizontal-scroll-strip.md +101 -0
- package/kb/sections/how-i-work-own-voice.md +111 -0
- package/kb/sections/how-it-works-in-place.md +109 -0
- package/kb/sections/impact-numbers-sourced.md +109 -0
- package/kb/sections/install-steps-per-agent.md +77 -0
- package/kb/sections/masonry-real-work.md +109 -0
- package/kb/sections/numbered-editorial-columns.md +105 -0
- package/kb/sections/object-first-hero.md +106 -0
- package/kb/sections/object-timeline.md +105 -0
- package/kb/sections/one-huge-number.md +100 -0
- package/kb/sections/one-person-story.md +103 -0
- package/kb/sections/order-ahead-ticket.md +119 -0
- package/kb/sections/press-strip-quoted.md +106 -0
- package/kb/sections/pricing-comparison-table-sticky.md +94 -0
- package/kb/sections/pricing-free-pro-upgrade-moment.md +85 -0
- package/kb/sections/pricing-plan-beside-product.md +88 -0
- package/kb/sections/pricing-plans-ledger.md +94 -0
- package/kb/sections/pricing-usage-calculator.md +93 -0
- package/kb/sections/printed-menu-card.md +110 -0
- package/kb/sections/process-happening.md +102 -0
- package/kb/sections/project-index-ruled.md +106 -0
- package/kb/sections/proof-logo-cloud-with-reasons.md +82 -0
- package/kb/sections/proof-measured-numbers.md +88 -0
- package/kb/sections/proof-output-wall.md +84 -0
- package/kb/sections/proof-single-honest-quote.md +81 -0
- package/kb/sections/resources-searchable-list.md +110 -0
- package/kb/sections/rooms-and-rates.md +107 -0
- package/kb/sections/ruled-facts-table.md +108 -0
- package/kb/sections/ruled-list-pinned-visual.md +107 -0
- package/kb/sections/scroll-scrubbed-product-demo.md +103 -0
- package/kb/sections/self-playing-run-demo.md +104 -0
- package/kb/sections/services-price-ledger.md +106 -0
- package/kb/sections/signup-form-with-next-steps.md +86 -0
- package/kb/sections/signup-waitlist-place-in-line.md +90 -0
- package/kb/sections/spec-sheet-features.md +108 -0
- package/kb/sections/split-hero-live-object.md +108 -0
- package/kb/sections/sticky-feature-narrative.md +104 -0
- package/kb/sections/todays-board-hero.md +112 -0
- package/kb/sections/touchable-tool-hero.md +111 -0
- package/kb/sections/typographic-statement-hero.md +99 -0
- package/kb/sections/visit-hours-map.md +110 -0
- package/kb/sections/ways-to-help-honest.md +115 -0
- package/kb/sources/design-reference.md +991 -0
- package/kb/sources/product-and-docs.md +37 -0
- package/kb/systems/1win/DESIGN.md +463 -0
- package/kb/systems/LICENSE-awesome-design-md +21 -0
- package/kb/systems/README.md +7 -0
- package/kb/systems/airbnb/DESIGN.md +545 -0
- package/kb/systems/airtable/DESIGN.md +554 -0
- package/kb/systems/aljazeera/DESIGN.md +594 -0
- package/kb/systems/apple/DESIGN.md +562 -0
- package/kb/systems/binance/DESIGN.md +634 -0
- package/kb/systems/bmw/DESIGN.md +544 -0
- package/kb/systems/bmw-m/DESIGN.md +503 -0
- package/kb/systems/bugatti/DESIGN.md +454 -0
- package/kb/systems/cal/DESIGN.md +542 -0
- package/kb/systems/categories.json +87 -0
- package/kb/systems/claude/DESIGN.md +589 -0
- package/kb/systems/clay/DESIGN.md +541 -0
- package/kb/systems/clickhouse/DESIGN.md +544 -0
- package/kb/systems/cohere/DESIGN.md +451 -0
- package/kb/systems/coinbase/DESIGN.md +570 -0
- package/kb/systems/composio/DESIGN.md +506 -0
- package/kb/systems/cursor/DESIGN.md +537 -0
- package/kb/systems/dell-1996/DESIGN.md +632 -0
- package/kb/systems/dji/DESIGN.md +882 -0
- package/kb/systems/elevenlabs/DESIGN.md +504 -0
- package/kb/systems/expo/DESIGN.md +526 -0
- package/kb/systems/ferrari/DESIGN.md +531 -0
- package/kb/systems/figma/DESIGN.md +578 -0
- package/kb/systems/framer/DESIGN.md +544 -0
- package/kb/systems/hashicorp/DESIGN.md +575 -0
- package/kb/systems/hp/DESIGN.md +670 -0
- package/kb/systems/ibm/DESIGN.md +550 -0
- package/kb/systems/intercom/DESIGN.md +546 -0
- package/kb/systems/jobright/DESIGN.md +525 -0
- package/kb/systems/kraken/DESIGN.md +125 -0
- package/kb/systems/lamborghini/DESIGN.md +288 -0
- package/kb/systems/linear.app/DESIGN.md +548 -0
- package/kb/systems/lovable/DESIGN.md +298 -0
- package/kb/systems/mastercard/DESIGN.md +365 -0
- package/kb/systems/mercury/DESIGN.md +568 -0
- package/kb/systems/meta/DESIGN.md +683 -0
- package/kb/systems/minimax/DESIGN.md +746 -0
- package/kb/systems/mintlify/DESIGN.md +852 -0
- package/kb/systems/miro/DESIGN.md +825 -0
- package/kb/systems/mistral.ai/DESIGN.md +773 -0
- package/kb/systems/mongodb/DESIGN.md +767 -0
- package/kb/systems/mydealz/DESIGN.md +551 -0
- package/kb/systems/nerdwallet/DESIGN.md +722 -0
- package/kb/systems/nike/DESIGN.md +575 -0
- package/kb/systems/nintendo-2001/DESIGN.md +649 -0
- package/kb/systems/notion/DESIGN.md +821 -0
- package/kb/systems/nvidia/DESIGN.md +640 -0
- package/kb/systems/ollama/DESIGN.md +539 -0
- package/kb/systems/opencode.ai/DESIGN.md +521 -0
- package/kb/systems/oracle/DESIGN.md +778 -0
- package/kb/systems/oura/DESIGN.md +616 -0
- package/kb/systems/pinterest/DESIGN.md +597 -0
- package/kb/systems/playstation/DESIGN.md +661 -0
- package/kb/systems/posthog/DESIGN.md +690 -0
- package/kb/systems/raycast/DESIGN.md +669 -0
- package/kb/systems/renault/DESIGN.md +589 -0
- package/kb/systems/replicate/DESIGN.md +616 -0
- package/kb/systems/resend/DESIGN.md +585 -0
- package/kb/systems/revolut/DESIGN.md +636 -0
- package/kb/systems/runwayml/DESIGN.md +244 -0
- package/kb/systems/sanity/DESIGN.md +357 -0
- package/kb/systems/sap/DESIGN.md +639 -0
- package/kb/systems/sentry/DESIGN.md +551 -0
- package/kb/systems/shopify/DESIGN.md +516 -0
- package/kb/systems/slack/DESIGN.md +482 -0
- package/kb/systems/spacex/DESIGN.md +363 -0
- package/kb/systems/spotify/DESIGN.md +246 -0
- package/kb/systems/starbucks/DESIGN.md +580 -0
- package/kb/systems/stripe/DESIGN.md +487 -0
- package/kb/systems/supabase/DESIGN.md +462 -0
- package/kb/systems/superhuman/DESIGN.md +448 -0
- package/kb/systems/tesla/DESIGN.md +286 -0
- package/kb/systems/theverge/DESIGN.md +339 -0
- package/kb/systems/together.ai/DESIGN.md +633 -0
- package/kb/systems/uber/DESIGN.md +636 -0
- package/kb/systems/vercel/DESIGN.md +736 -0
- package/kb/systems/vodafone/DESIGN.md +538 -0
- package/kb/systems/voltagent/DESIGN.md +521 -0
- package/kb/systems/warp/DESIGN.md +526 -0
- package/kb/systems/webflow/DESIGN.md +588 -0
- package/kb/systems/wired/DESIGN.md +497 -0
- package/kb/systems/wise/DESIGN.md +544 -0
- package/kb/systems/x.ai/DESIGN.md +465 -0
- package/kb/systems/zapier/DESIGN.md +537 -0
- package/kb/taste/garvit-by-job.md +46 -0
- package/kb/taste/garvit-portfolio.md +94 -0
- package/kb/universal/color-roles.md +26 -0
- package/kb/universal/copy.md +16 -0
- package/kb/universal/details.md +31 -0
- package/kb/universal/first-screen.md +30 -0
- package/kb/universal/fit-the-codebase.md +29 -0
- package/kb/universal/layout-width.md +28 -0
- package/kb/universal/motion-feedback.md +23 -0
- package/kb/universal/signature.md +29 -0
- package/kb/universal/states.md +17 -0
- package/kb/universal/type-app.md +33 -0
- package/kb/universal/type-marketing.md +28 -0
- package/package.json +36 -4
- package/scripts/pack-assets.mjs +48 -0
- package/skill/palate/SKILL.md +261 -0
- package/src/auth-recheck.mjs +4 -0
- package/src/auth.mjs +416 -0
- package/src/capture.mjs +182 -0
- package/src/catalog/classify.mjs +96 -0
- package/src/catalog/curated.mjs +128 -0
- package/src/catalog/index-build.mjs +67 -0
- package/src/catalog/literal.mjs +146 -0
- package/src/catalog/store.mjs +40 -0
- package/src/catalog/vibrant.mjs +126 -0
- package/src/check.mjs +93 -0
- package/src/concept.mjs +157 -0
- package/src/context.mjs +226 -0
- package/src/copy.mjs +188 -0
- package/src/devserver.mjs +135 -0
- package/src/fontgenre.mjs +44 -0
- package/src/guard-agents.mjs +213 -0
- package/src/guard.mjs +377 -0
- package/src/kb.mjs +91 -0
- package/src/look.mjs +312 -0
- package/src/loop/ambition.mjs +620 -0
- package/src/loop/animate.mjs +70 -0
- package/src/loop/artdirection.mjs +264 -0
- package/src/loop/bench.mjs +184 -0
- package/src/loop/benchpick.mjs +155 -0
- package/src/loop/blueprint.mjs +620 -0
- package/src/loop/board.html +180 -0
- package/src/loop/build.mjs +393 -0
- package/src/loop/buttons.mjs +413 -0
- package/src/loop/common.mjs +206 -0
- package/src/loop/cost.mjs +63 -0
- package/src/loop/craft.mjs +152 -0
- package/src/loop/craftchecks.mjs +148 -0
- package/src/loop/critique.mjs +393 -0
- package/src/loop/details.mjs +225 -0
- package/src/loop/direction.html +168 -0
- package/src/loop/direction.mjs +256 -0
- package/src/loop/directions.json +8 -0
- package/src/loop/explore.mjs +219 -0
- package/src/loop/fail.mjs +110 -0
- package/src/loop/fix.mjs +194 -0
- package/src/loop/floor.mjs +677 -0
- package/src/loop/forget.mjs +52 -0
- package/src/loop/gate.mjs +1053 -0
- package/src/loop/grid.html +123 -0
- package/src/loop/growth.mjs +118 -0
- package/src/loop/icons.mjs +109 -0
- package/src/loop/imagery.mjs +1280 -0
- package/src/loop/judge-api.mjs +245 -0
- package/src/loop/judge.mjs +167 -0
- package/src/loop/layouts.mjs +492 -0
- package/src/loop/library.mjs +82 -0
- package/src/loop/lookplan.mjs +182 -0
- package/src/loop/lookvec.mjs +645 -0
- package/src/loop/match.mjs +923 -0
- package/src/loop/matched.mjs +26 -0
- package/src/loop/myrefs.mjs +151 -0
- package/src/loop/pick.mjs +213 -0
- package/src/loop/picker.html +118 -0
- package/src/loop/plan.mjs +289 -0
- package/src/loop/polish.mjs +39 -0
- package/src/loop/research-learn.mjs +41 -0
- package/src/loop/research.mjs +1130 -0
- package/src/loop/rulereview.mjs +146 -0
- package/src/loop/rules-board.html +199 -0
- package/src/loop/rules.mjs +939 -0
- package/src/loop/similar.mjs +276 -0
- package/src/loop/states-source.mjs +258 -0
- package/src/loop/states.mjs +1547 -0
- package/src/loop/stats.mjs +78 -0
- package/src/loop/taste.mjs +81 -0
- package/src/loop/timeline.mjs +149 -0
- package/src/loop/variants.mjs +228 -0
- package/src/loop/vary.mjs +249 -0
- package/src/mcp/server.mjs +1246 -0
- package/src/motion.mjs +207 -0
- package/src/moves.mjs +242 -0
- package/src/novelty.mjs +116 -0
- package/src/page-ambition.js +181 -0
- package/src/page-craft.js +724 -0
- package/src/page-details.js +415 -0
- package/src/page-devices.js +365 -0
- package/src/page-fingerprint.js +281 -0
- package/src/page-gate.js +1307 -0
- package/src/page-look.js +698 -0
- package/src/page-metrics.js +78 -0
- package/src/page-principles.js +1167 -0
- package/src/page-rules.js +308 -0
- package/src/page-sections.js +211 -0
- package/src/page-states.js +703 -0
- package/src/paths.mjs +13 -0
- package/src/png.mjs +107 -0
- package/src/point/auto.mjs +224 -0
- package/src/point/locate.mjs +114 -0
- package/src/point/overlay.js +1516 -0
- package/src/point/server.mjs +1122 -0
- package/src/point/trail.mjs +219 -0
- package/src/pool.mjs +80 -0
- package/src/principles.mjs +279 -0
- package/src/refs.mjs +299 -0
- package/src/robots.mjs +145 -0
- package/src/sections.mjs +210 -0
- package/src/shoot.mjs +235 -0
- package/src/systems/apply.mjs +355 -0
- package/src/systems/load.mjs +251 -0
- package/src/systems/pick.mjs +213 -0
- package/templates/vite-react-tw/index.html +12 -0
- package/templates/vite-react-tw/package.json +25 -0
- package/templates/vite-react-tw/src/App.tsx +3 -0
- package/templates/vite-react-tw/src/index.css +1 -0
- package/templates/vite-react-tw/src/main.tsx +10 -0
- package/templates/vite-react-tw/tsconfig.app.json +26 -0
- package/templates/vite-react-tw/tsconfig.json +7 -0
- package/templates/vite-react-tw/tsconfig.node.json +23 -0
- package/templates/vite-react-tw/vite.config.ts +8 -0
|
@@ -0,0 +1,1167 @@
|
|
|
1
|
+
// Runs inside the page (read as text and evaluated, optionally with the sections from page-sections.js).
|
|
2
|
+
// Measures the core design principles a reviewer judges by eye: alignment, visual hierarchy, spacing and
|
|
3
|
+
// proximity, sizing and type, consistency, and density. Every problem names its offenders: a CSS path, a
|
|
4
|
+
// short label, a text snippet and a box in page coordinates. Thresholds live in principles.mjs, except the
|
|
5
|
+
// few needed to decide what counts as an offender here.
|
|
6
|
+
(sectionsIn) => {
|
|
7
|
+
const vw = innerWidth;
|
|
8
|
+
const vh = innerHeight;
|
|
9
|
+
const sx = scrollX;
|
|
10
|
+
const sy = scrollY;
|
|
11
|
+
const pageH = document.documentElement.scrollHeight;
|
|
12
|
+
const body = document.body;
|
|
13
|
+
const clean = (s) => (s ?? "").replace(/\s+/g, " ").trim();
|
|
14
|
+
const r1 = (v) => Math.round(v * 10) / 10;
|
|
15
|
+
const median = (a) => {
|
|
16
|
+
if (!a.length) return 0;
|
|
17
|
+
const s = [...a].sort((x, y) => x - y);
|
|
18
|
+
const m = s.length >> 1;
|
|
19
|
+
return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
|
|
20
|
+
};
|
|
21
|
+
const tally = (arr, key) => {
|
|
22
|
+
const m = new Map();
|
|
23
|
+
for (const x of arr) {
|
|
24
|
+
const k = key(x);
|
|
25
|
+
if (k == null) continue;
|
|
26
|
+
m.set(k, [...(m.get(k) ?? []), x]);
|
|
27
|
+
}
|
|
28
|
+
return [...m.entries()].sort((a, b) => b[1].length - a[1].length);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// ---- cached element facts ----
|
|
32
|
+
const CS = new Map();
|
|
33
|
+
const cs = (el) => {
|
|
34
|
+
let c = CS.get(el);
|
|
35
|
+
if (!c) CS.set(el, (c = getComputedStyle(el)));
|
|
36
|
+
return c;
|
|
37
|
+
};
|
|
38
|
+
const BOX = new Map();
|
|
39
|
+
const box = (el) => {
|
|
40
|
+
let b = BOX.get(el);
|
|
41
|
+
if (!b) {
|
|
42
|
+
const r = el.getBoundingClientRect();
|
|
43
|
+
b = { x: r.left + sx, y: r.top + sy, w: r.width, h: r.height, r: r.right + sx, b: r.bottom + sy };
|
|
44
|
+
BOX.set(el, b);
|
|
45
|
+
}
|
|
46
|
+
return b;
|
|
47
|
+
};
|
|
48
|
+
const VIS = new Map();
|
|
49
|
+
const visible = (el) => {
|
|
50
|
+
let v = VIS.get(el);
|
|
51
|
+
if (v !== undefined) return v;
|
|
52
|
+
const b = box(el);
|
|
53
|
+
v = b.w >= 1 && b.h >= 1 && b.r > 0 && b.x < vw;
|
|
54
|
+
if (v) v = el.checkVisibility ? el.checkVisibility({ opacityProperty: true, visibilityProperty: true }) : cs(el).display !== "none" && cs(el).visibility !== "hidden";
|
|
55
|
+
// Screen-reader-only content (clipped to nothing) is not seen.
|
|
56
|
+
if (v && ((b.w <= 1 && b.h <= 1) || /^rect\(0(px)?,? 0(px)?/.test(cs(el).clip) || /inset\(50%/.test(cs(el).clipPath))) v = false;
|
|
57
|
+
VIS.set(el, v);
|
|
58
|
+
return v;
|
|
59
|
+
};
|
|
60
|
+
const selector = (el) => {
|
|
61
|
+
const parts = [];
|
|
62
|
+
for (let e = el; e && e.nodeType === 1 && e !== body; e = e.parentElement) {
|
|
63
|
+
if (e.id && /^[A-Za-z][\w-]*$/.test(e.id) && document.querySelectorAll(`#${CSS.escape(e.id)}`).length === 1) {
|
|
64
|
+
parts.unshift(`#${e.id}`);
|
|
65
|
+
return parts.join(" > ");
|
|
66
|
+
}
|
|
67
|
+
let i = 1;
|
|
68
|
+
for (let s = e.previousElementSibling; s; s = s.previousElementSibling) if (s.tagName === e.tagName) i++;
|
|
69
|
+
parts.unshift(`${e.tagName.toLowerCase()}:nth-of-type(${i})`);
|
|
70
|
+
}
|
|
71
|
+
return ["body", ...parts].join(" > ");
|
|
72
|
+
};
|
|
73
|
+
const short = (el) => {
|
|
74
|
+
const tag = el.tagName.toLowerCase();
|
|
75
|
+
if (el.id && /^[A-Za-z][\w-]*$/.test(el.id)) return `${tag}#${el.id}`;
|
|
76
|
+
const cls = [...el.classList].filter((c) => c.length < 28 && !/[:[\]/]/.test(c)).slice(0, 2);
|
|
77
|
+
return tag + cls.map((c) => `.${c}`).join("");
|
|
78
|
+
};
|
|
79
|
+
const snippet = (el) => clean(el.innerText || el.textContent || el.getAttribute?.("aria-label") || el.getAttribute?.("alt") || el.getAttribute?.("placeholder") || "").slice(0, 60);
|
|
80
|
+
const bx = (b) => ({ x: Math.round(b.x), y: Math.round(b.y), w: Math.round(b.w), h: Math.round(b.h) });
|
|
81
|
+
const off = (el, b = box(el), text) => ({ selector: selector(el), label: short(el), text: text ?? snippet(el), box: bx(b) });
|
|
82
|
+
|
|
83
|
+
// ---- colour ----
|
|
84
|
+
const cv = document.createElement("canvas");
|
|
85
|
+
cv.width = cv.height = 1;
|
|
86
|
+
const cx = cv.getContext("2d", { willReadFrequently: true });
|
|
87
|
+
const COL = new Map();
|
|
88
|
+
const rgba = (c) => {
|
|
89
|
+
if (!c) return [0, 0, 0, 0];
|
|
90
|
+
let v = COL.get(c);
|
|
91
|
+
if (v) return v;
|
|
92
|
+
const m = c.match(/^rgba?\(([^)]+)\)$/);
|
|
93
|
+
if (m) {
|
|
94
|
+
const p = m[1].split(/[\s,/]+/).filter(Boolean).map(parseFloat);
|
|
95
|
+
v = [p[0], p[1], p[2], p[3] ?? 1];
|
|
96
|
+
} else {
|
|
97
|
+
cx.clearRect(0, 0, 1, 1);
|
|
98
|
+
cx.fillStyle = "rgba(0,0,0,0)";
|
|
99
|
+
cx.fillStyle = c;
|
|
100
|
+
cx.fillRect(0, 0, 1, 1);
|
|
101
|
+
const d = cx.getImageData(0, 0, 1, 1).data;
|
|
102
|
+
v = [d[0], d[1], d[2], d[3] / 255];
|
|
103
|
+
}
|
|
104
|
+
COL.set(c, v);
|
|
105
|
+
return v;
|
|
106
|
+
};
|
|
107
|
+
const hex = (v) => "#" + v.slice(0, 3).map((x) => Math.round(x).toString(16).padStart(2, "0")).join("");
|
|
108
|
+
const blend = (fg, bg) => [0, 1, 2].map((i) => fg[i] * fg[3] + bg[i] * (1 - fg[3])).concat(1);
|
|
109
|
+
const lum = (v) => {
|
|
110
|
+
const f = (x) => ((x /= 255) <= 0.03928 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4);
|
|
111
|
+
return 0.2126 * f(v[0]) + 0.7152 * f(v[1]) + 0.0722 * f(v[2]);
|
|
112
|
+
};
|
|
113
|
+
const contrast = (a, b) => {
|
|
114
|
+
const [x, y] = [lum(a), lum(b)].sort((p, q) => q - p);
|
|
115
|
+
return (x + 0.05) / (y + 0.05);
|
|
116
|
+
};
|
|
117
|
+
const dist = (a, b) => Math.max(Math.abs(a[0] - b[0]), Math.abs(a[1] - b[1]), Math.abs(a[2] - b[2]));
|
|
118
|
+
const hsl = (v) => {
|
|
119
|
+
const [r, g, b] = v.slice(0, 3).map((x) => x / 255);
|
|
120
|
+
const max = Math.max(r, g, b);
|
|
121
|
+
const min = Math.min(r, g, b);
|
|
122
|
+
const l = (max + min) / 2;
|
|
123
|
+
const d = max - min;
|
|
124
|
+
if (d < 0.0001) return { h: 0, s: 0, l };
|
|
125
|
+
const s = d / (1 - Math.abs(2 * l - 1));
|
|
126
|
+
const h = max === r ? ((g - b) / d) % 6 : max === g ? (b - r) / d + 2 : (r - g) / d + 4;
|
|
127
|
+
return { h: (h * 60 + 360) % 360, s, l };
|
|
128
|
+
};
|
|
129
|
+
const rootBg = (() => {
|
|
130
|
+
for (const e of [body, document.documentElement]) {
|
|
131
|
+
const v = rgba(cs(e).backgroundColor);
|
|
132
|
+
if (v[3] > 0.5) return v;
|
|
133
|
+
}
|
|
134
|
+
return [255, 255, 255, 1];
|
|
135
|
+
})();
|
|
136
|
+
const firstStop = (img) => {
|
|
137
|
+
const m = img.match(/(?:rgba?|hsla?|oklch|oklab|lab|lch|color)\([^()]*\)|#[0-9a-f]{3,8}\b/i);
|
|
138
|
+
return m ? rgba(m[0]) : null;
|
|
139
|
+
};
|
|
140
|
+
// The colour behind an element: null when it sits on a picture (unknown).
|
|
141
|
+
const BG = new Map();
|
|
142
|
+
const bgOf = (el) => {
|
|
143
|
+
if (!el || el === document.documentElement) return rootBg;
|
|
144
|
+
if (BG.has(el)) return BG.get(el);
|
|
145
|
+
const c = cs(el);
|
|
146
|
+
let v;
|
|
147
|
+
const own = rgba(c.backgroundColor);
|
|
148
|
+
if (own[3] > 0.5) v = own;
|
|
149
|
+
else if (/url\(/.test(c.backgroundImage)) v = null;
|
|
150
|
+
else if (/gradient/.test(c.backgroundImage) && firstStop(c.backgroundImage)?.[3] > 0.5) v = firstStop(c.backgroundImage);
|
|
151
|
+
else {
|
|
152
|
+
const up = bgOf(el.parentElement);
|
|
153
|
+
v = up && own[3] > 0.02 ? blend(own, up) : up;
|
|
154
|
+
}
|
|
155
|
+
BG.set(el, v);
|
|
156
|
+
return v;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// ---- the elements ----
|
|
160
|
+
const SKIP = /^(SCRIPT|STYLE|NOSCRIPT|TEMPLATE|LINK|META|TITLE|BR|WBR|OPTION|SOURCE|TRACK)$/;
|
|
161
|
+
const all = [];
|
|
162
|
+
for (const el of body.querySelectorAll("*")) {
|
|
163
|
+
if (SKIP.test(el.tagName) || (el.ownerSVGElement && el.tagName !== "svg") || !visible(el)) continue;
|
|
164
|
+
if (el.closest("svg") && el.closest("svg") !== el) continue;
|
|
165
|
+
all.push(el);
|
|
166
|
+
}
|
|
167
|
+
const allSet = new Set(all);
|
|
168
|
+
const hidden = (el) => !!el.closest('[aria-hidden="true"], [inert]');
|
|
169
|
+
const sides = ["Top", "Right", "Bottom", "Left"];
|
|
170
|
+
const SURF = new Map();
|
|
171
|
+
const surface = (el) => {
|
|
172
|
+
if (SURF.has(el)) return SURF.get(el);
|
|
173
|
+
const c = cs(el);
|
|
174
|
+
const b = box(el);
|
|
175
|
+
let s = null;
|
|
176
|
+
if (b.w >= 24 && b.h >= 16 && !["svg", "IMG", "VIDEO", "CANVAS", "INPUT", "TEXTAREA", "SELECT"].includes(el.tagName)) {
|
|
177
|
+
const bg = rgba(c.backgroundColor);
|
|
178
|
+
const behind = bgOf(el.parentElement);
|
|
179
|
+
const filled = bg[3] > 0.08 && (!behind || dist(blend(bg, behind), behind) > 3);
|
|
180
|
+
const bw = sides.map((k) => (c[`border${k}Style`] !== "none" && rgba(c[`border${k}Color`])[3] > 0.05 ? parseFloat(c[`border${k}Width`]) : 0));
|
|
181
|
+
const bordered = bw.every((w) => w > 0);
|
|
182
|
+
const shadow = c.boxShadow !== "none";
|
|
183
|
+
const gradient = /gradient/.test(c.backgroundImage);
|
|
184
|
+
if (filled || bordered || shadow || gradient) s = { filled, bordered, shadow, gradient, bg: filled ? hex(behind ? blend(bg, behind) : bg) : null, border: bordered ? `${bw[0]}px ${hex(rgba(c.borderTopColor))}` : null, radius: parseFloat(c.borderTopLeftRadius) || 0 };
|
|
185
|
+
}
|
|
186
|
+
SURF.set(el, s);
|
|
187
|
+
return s;
|
|
188
|
+
};
|
|
189
|
+
const CONTROL = 'button, [role=button], input[type=submit], input[type=button], a[href], [role=tab], [role=menuitem]';
|
|
190
|
+
const isControl = (el) => {
|
|
191
|
+
if (!el.matches(CONTROL)) return false;
|
|
192
|
+
const b = box(el);
|
|
193
|
+
return b.h >= 14 && b.h <= 88 && b.w < 520 && clean(el.innerText).length <= 48;
|
|
194
|
+
};
|
|
195
|
+
const buttonLike = (el) => isControl(el) && (el.matches("button, [role=button], input[type=submit], input[type=button]") ? !!surface(el) || box(el).h >= 28 : !!surface(el));
|
|
196
|
+
const controls = all.filter((el) => isControl(el) && !hidden(el) && !el.parentElement.closest(CONTROL));
|
|
197
|
+
const inputs = all.filter((el) => el.matches('input:not([type=hidden]):not([type=checkbox]):not([type=radio]):not([type=submit]):not([type=button]), select, textarea') && !hidden(el));
|
|
198
|
+
const media = all.filter((el) => {
|
|
199
|
+
if (hidden(el) && !el.matches("img, video")) return false;
|
|
200
|
+
const b = box(el);
|
|
201
|
+
if (el.matches("img, video, canvas, iframe, svg")) return b.w >= 24 && b.h >= 24;
|
|
202
|
+
return b.w >= 80 && b.h >= 80 && /url\(/.test(cs(el).backgroundImage);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Text blocks: the nearest non-inline box around each run of text, with its real ink (from ranges).
|
|
206
|
+
const INLINE = new Set(["inline", "contents", "ruby"]);
|
|
207
|
+
const blockOf = (el) => {
|
|
208
|
+
let b = el;
|
|
209
|
+
while (b && b !== body && INLINE.has(cs(b).display)) b = b.parentElement;
|
|
210
|
+
return b;
|
|
211
|
+
};
|
|
212
|
+
const groups = new Map();
|
|
213
|
+
const tw = document.createTreeWalker(body, NodeFilter.SHOW_TEXT);
|
|
214
|
+
for (let n = tw.nextNode(); n; n = tw.nextNode()) {
|
|
215
|
+
if (!n.textContent.trim()) continue;
|
|
216
|
+
const p = n.parentElement;
|
|
217
|
+
// aria-hidden text still shows (split-letter headings hide their spans from screen readers), so it counts.
|
|
218
|
+
if (!p || SKIP.test(p.tagName) || p.closest("svg, textarea, select") || !visible(p)) continue;
|
|
219
|
+
const blk = blockOf(p);
|
|
220
|
+
if (!blk || blk === body) continue;
|
|
221
|
+
if (!groups.has(blk)) groups.set(blk, []);
|
|
222
|
+
groups.get(blk).push(n);
|
|
223
|
+
}
|
|
224
|
+
const range = document.createRange();
|
|
225
|
+
const texts = [];
|
|
226
|
+
for (const [el, nodes] of groups) {
|
|
227
|
+
const rects = [];
|
|
228
|
+
let longest = nodes[0];
|
|
229
|
+
for (const n of nodes) {
|
|
230
|
+
range.selectNodeContents(n);
|
|
231
|
+
for (const r of range.getClientRects()) if (r.width > 0.5 && r.height > 0.5) rects.push(r);
|
|
232
|
+
if (n.textContent.trim().length > longest.textContent.trim().length) longest = n;
|
|
233
|
+
}
|
|
234
|
+
if (!rects.length) continue;
|
|
235
|
+
const st = longest.parentElement;
|
|
236
|
+
const c = cs(st);
|
|
237
|
+
const size = parseFloat(c.fontSize);
|
|
238
|
+
// Lines: rects grouped by their vertical centre.
|
|
239
|
+
const lines = [];
|
|
240
|
+
for (const r of [...rects].sort((a, b) => a.top + a.height / 2 - (b.top + b.height / 2))) {
|
|
241
|
+
const cy = r.top + r.height / 2;
|
|
242
|
+
const ln = lines.at(-1);
|
|
243
|
+
if (ln && Math.abs(cy - ln.cy) < size * 0.6) {
|
|
244
|
+
ln.l = Math.min(ln.l, r.left);
|
|
245
|
+
ln.r = Math.max(ln.r, r.right);
|
|
246
|
+
ln.t = Math.min(ln.t, r.top);
|
|
247
|
+
ln.b = Math.max(ln.b, r.bottom);
|
|
248
|
+
} else lines.push({ cy, l: r.left, r: r.right, t: r.top, b: r.bottom });
|
|
249
|
+
}
|
|
250
|
+
const ink = { x: Math.min(...lines.map((l) => l.l)) + sx, y: Math.min(...lines.map((l) => l.t)) + sy };
|
|
251
|
+
ink.r = Math.max(...lines.map((l) => l.r)) + sx;
|
|
252
|
+
ink.b = Math.max(...lines.map((l) => l.b)) + sy;
|
|
253
|
+
ink.w = ink.r - ink.x;
|
|
254
|
+
ink.h = ink.b - ink.y;
|
|
255
|
+
if (ink.r <= 0 || ink.x >= vw) continue;
|
|
256
|
+
const text = clean(nodes.map((n) => n.textContent).join(" "));
|
|
257
|
+
const letters = (text.match(/\p{L}/gu) ?? []).length;
|
|
258
|
+
const upperText = letters >= 4 && !/\p{Ll}/u.test(text) && /\p{Lu}/u.test(text);
|
|
259
|
+
const color = rgba(c.color);
|
|
260
|
+
const bg = bgOf(st);
|
|
261
|
+
const lh = c.lineHeight === "normal" ? size * 1.2 : parseFloat(c.lineHeight);
|
|
262
|
+
const ls = c.letterSpacing === "normal" ? 0 : parseFloat(c.letterSpacing) || 0;
|
|
263
|
+
const hEl = el.closest("h1, h2, h3, h4, h5, h6, [role=heading]");
|
|
264
|
+
const level = hEl ? Number(hEl.tagName[1]) || Number(hEl.getAttribute("aria-level")) || 2 : 0;
|
|
265
|
+
const ctl = el.closest(CONTROL);
|
|
266
|
+
const inkW = rects.reduce((s, r) => s + r.width, 0);
|
|
267
|
+
texts.push({
|
|
268
|
+
el,
|
|
269
|
+
st,
|
|
270
|
+
nodes,
|
|
271
|
+
ink,
|
|
272
|
+
lines: lines.map((l) => ({ l: l.l + sx, r: l.r + sx, t: l.t + sy, b: l.b + sy })),
|
|
273
|
+
nLines: lines.length,
|
|
274
|
+
text,
|
|
275
|
+
chars: text.length,
|
|
276
|
+
words: text.split(" ").filter(Boolean).length,
|
|
277
|
+
charW: inkW / Math.max(1, text.length),
|
|
278
|
+
size,
|
|
279
|
+
weight: Number(c.fontWeight) || 400,
|
|
280
|
+
family: c.fontFamily.split(",")[0].replace(/["']/g, "").trim(),
|
|
281
|
+
color,
|
|
282
|
+
contrast: bg ? contrast(blend(color, bg), bg) : null,
|
|
283
|
+
lh,
|
|
284
|
+
ls,
|
|
285
|
+
upper: c.textTransform === "uppercase" || upperText,
|
|
286
|
+
align: cs(el).textAlign,
|
|
287
|
+
level,
|
|
288
|
+
quote: /^[“"„«]/.test(text) || !!el.closest("blockquote, q"),
|
|
289
|
+
control: ctl && isControl(ctl) ? ctl : null,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// A heading set as several block lines (one span per line) is one heading.
|
|
294
|
+
const byHeading = new Map();
|
|
295
|
+
for (const t of texts) {
|
|
296
|
+
const h = t.level ? t.el.closest("h1, h2, h3, h4, h5, h6, [role=heading]") : null;
|
|
297
|
+
if (h && h !== t.el) byHeading.set(h, [...(byHeading.get(h) ?? []), t]);
|
|
298
|
+
}
|
|
299
|
+
for (const [h, parts] of byHeading) {
|
|
300
|
+
if (parts.length < 2) continue;
|
|
301
|
+
const first = parts[0];
|
|
302
|
+
const lines = parts.flatMap((p) => p.lines).sort((a, b) => a.t - b.t);
|
|
303
|
+
const ink = { x: Math.min(...parts.map((p) => p.ink.x)), y: Math.min(...parts.map((p) => p.ink.y)), r: Math.max(...parts.map((p) => p.ink.r)), b: Math.max(...parts.map((p) => p.ink.b)) };
|
|
304
|
+
ink.w = ink.r - ink.x;
|
|
305
|
+
ink.h = ink.b - ink.y;
|
|
306
|
+
const text = parts.map((p) => p.text).join(" ");
|
|
307
|
+
const merged = { ...first, el: h, nodes: parts.flatMap((p) => p.nodes), ink, lines, nLines: lines.length, text, chars: text.length, words: text.split(" ").filter(Boolean).length, align: cs(h).textAlign };
|
|
308
|
+
for (const p of parts) texts.splice(texts.indexOf(p), 1);
|
|
309
|
+
texts.push(merged);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Centred or left-led: compare the ink with the nearest box that is wider than it.
|
|
313
|
+
for (const t of texts) {
|
|
314
|
+
t.centred = false;
|
|
315
|
+
t.leftLed = false;
|
|
316
|
+
let cont = t.el;
|
|
317
|
+
for (let k = 0; cont && k < 4 && box(cont).w <= t.ink.w + 24; k++) cont = cont.parentElement;
|
|
318
|
+
if (!cont) continue;
|
|
319
|
+
const b = box(cont);
|
|
320
|
+
const c = cs(cont);
|
|
321
|
+
const left = b.x + parseFloat(c.paddingLeft) + parseFloat(c.borderLeftWidth);
|
|
322
|
+
const right = b.r - parseFloat(c.paddingRight) - parseFloat(c.borderRightWidth);
|
|
323
|
+
const inkMid = (t.ink.x + t.ink.r) / 2;
|
|
324
|
+
// Wrapped text is centred only by text-align; a single line can also be centred by its box.
|
|
325
|
+
if (t.align === "center" || (t.nLines === 1 && Math.abs(inkMid - (left + right) / 2) <= 1.5 && right - left > t.ink.w + 24)) t.centred = true;
|
|
326
|
+
else if (Math.abs(t.ink.x - left) <= 1.5 || ["left", "start", "justify"].includes(t.align)) t.leftLed = !["right", "end"].includes(t.align);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ---- product mockups ----
|
|
330
|
+
// A framed region full of small text (a fake app window, a booking widget, an issue list) is a picture of
|
|
331
|
+
// the product, not the page's own typography, buttons or grid. It is judged as one picture; what is inside
|
|
332
|
+
// it is left out of the page-level checks.
|
|
333
|
+
const tally2 = new Map();
|
|
334
|
+
for (const t of texts) {
|
|
335
|
+
for (let e = t.el, k = 0; e && e !== body && k < 40; e = e.parentElement, k++) {
|
|
336
|
+
const c = tally2.get(e) ?? { n: 0, small: 0, big: 0 };
|
|
337
|
+
c.n++;
|
|
338
|
+
// Scaled-down UI (transform: scale) renders smaller than its font-size says.
|
|
339
|
+
const scale = Math.min(1, t.ink.h / Math.max(1, t.nLines) / Math.max(1, t.lh));
|
|
340
|
+
if (t.size * scale <= 13.5) c.small++;
|
|
341
|
+
if (t.size >= 28) c.big++;
|
|
342
|
+
tally2.set(e, c);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const CHROME = "footer, header, nav, [role=contentinfo], [role=banner], [role=navigation]";
|
|
346
|
+
const mockCands = [...tally2.entries()]
|
|
347
|
+
.filter(([el, c]) => {
|
|
348
|
+
if (c.big || !((c.n >= 12 && c.small >= c.n * 0.6) || (c.n >= 8 && c.small >= c.n * 0.75))) return false;
|
|
349
|
+
const b = box(el);
|
|
350
|
+
const bleeds = b.x < -1 || b.r > vw + 1;
|
|
351
|
+
if (b.w < 280 || b.h < 180 || (b.w > vw * 0.98 && !bleeds) || el.closest(CHROME)) return false;
|
|
352
|
+
if (bleeds) return true;
|
|
353
|
+
// Framed, or so full of small text that it can only be a picture of an app.
|
|
354
|
+
return !!surface(el) || !!el.closest("figure, [aria-hidden=true], [inert]") || el.matches("figure") || (c.n >= 25 && c.small >= c.n * 0.8);
|
|
355
|
+
})
|
|
356
|
+
.map(([el]) => el);
|
|
357
|
+
const mocks = mockCands.filter((m) => !mockCands.some((o) => o !== m && o.contains(m)));
|
|
358
|
+
const mockSet = new Set(mocks);
|
|
359
|
+
const inMock = (el) => {
|
|
360
|
+
if (!mocks.length) return false;
|
|
361
|
+
for (let e = el.parentElement; e && e !== body; e = e.parentElement) if (mockSet.has(e)) return true;
|
|
362
|
+
return false;
|
|
363
|
+
};
|
|
364
|
+
const prune = (arr, get = (x) => x) => {
|
|
365
|
+
let j = 0;
|
|
366
|
+
for (const x of arr) if (!inMock(get(x))) arr[j++] = x;
|
|
367
|
+
arr.length = j;
|
|
368
|
+
};
|
|
369
|
+
if (mocks.length) {
|
|
370
|
+
prune(all);
|
|
371
|
+
prune(texts, (t) => t.el);
|
|
372
|
+
prune(controls);
|
|
373
|
+
prune(inputs);
|
|
374
|
+
prune(media);
|
|
375
|
+
allSet.clear();
|
|
376
|
+
for (const el of all) allSet.add(el);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ---- sections ----
|
|
380
|
+
const sections = (sectionsIn && sectionsIn.length ? sectionsIn : [{ index: 1, name: "Page", tag: "body", selector: "body", box: { x: 0, y: 0, width: vw, height: pageH } }]).map((s) => ({
|
|
381
|
+
...s,
|
|
382
|
+
chrome: ["header", "nav", "footer"].includes(s.tag) || /^(Header|Footer|Navigation)$/.test(s.name),
|
|
383
|
+
el: document.querySelector(s.selector),
|
|
384
|
+
}));
|
|
385
|
+
const sectionAt = (b) => {
|
|
386
|
+
const cy = b.y + b.h / 2;
|
|
387
|
+
return sections.find((s) => cy >= s.box.y && cy <= s.box.y + s.box.height)?.index ?? 0;
|
|
388
|
+
};
|
|
389
|
+
const nameOf = (i) => sections.find((s) => s.index === i)?.name ?? "page";
|
|
390
|
+
for (const t of texts) t.section = sectionAt(t.ink);
|
|
391
|
+
|
|
392
|
+
// ---- leaves: what the eye sees as items (text, controls, inputs, pictures) ----
|
|
393
|
+
const leaves = [];
|
|
394
|
+
for (const t of texts) if (!t.control) leaves.push({ kind: "text", el: t.el, b: t.ink, t });
|
|
395
|
+
// A control with no surface (a text link, a ghost button) is seen as its text, not its padded box.
|
|
396
|
+
const inkOf = (el) => {
|
|
397
|
+
const ts = texts.filter((t) => t.control === el || el.contains(t.el));
|
|
398
|
+
if (!ts.length) return box(el);
|
|
399
|
+
const x = Math.min(...ts.map((t) => t.ink.x));
|
|
400
|
+
const y = Math.min(...ts.map((t) => t.ink.y));
|
|
401
|
+
const r = Math.max(...ts.map((t) => t.ink.r));
|
|
402
|
+
const b = Math.max(...ts.map((t) => t.ink.b));
|
|
403
|
+
const icons = [...el.querySelectorAll("svg, img")].map(box).filter((m) => m.w > 0);
|
|
404
|
+
const xs = [x, ...icons.map((m) => m.x)];
|
|
405
|
+
const rs = [r, ...icons.map((m) => m.r)];
|
|
406
|
+
return { x: Math.min(...xs), y, r: Math.max(...rs), b, w: Math.max(...rs) - Math.min(...xs), h: b - y };
|
|
407
|
+
};
|
|
408
|
+
for (const el of controls) leaves.push({ kind: "control", el, b: surface(el) ? box(el) : inkOf(el) });
|
|
409
|
+
for (const el of inputs) leaves.push({ kind: "input", el, b: box(el) });
|
|
410
|
+
for (const el of media) if (!el.closest(CONTROL) || !isControl(el.closest(CONTROL))) leaves.push({ kind: "media", el, b: box(el) });
|
|
411
|
+
for (const el of mocks) leaves.push({ kind: "media", el, b: box(el), mock: true });
|
|
412
|
+
for (const l of leaves) l.section = sectionAt(l.b);
|
|
413
|
+
const cards = all.filter((el) => {
|
|
414
|
+
const s = surface(el);
|
|
415
|
+
const b = box(el);
|
|
416
|
+
return s && b.w >= 80 && b.h >= 40 && b.w < vw * 0.96 && !isControl(el) && !el.matches("input, select, textarea") && !hidden(el);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// ================= 1. ALIGNMENT =================
|
|
420
|
+
const cluster = (vals, tol = 1) => {
|
|
421
|
+
const s = [...vals].sort((a, b) => a.x - b.x);
|
|
422
|
+
const out = [];
|
|
423
|
+
for (const v of s) {
|
|
424
|
+
const c = out.at(-1);
|
|
425
|
+
if (c && v.x - c.x0 <= tol) c.m.push(v);
|
|
426
|
+
else out.push({ x0: v.x, m: [v] });
|
|
427
|
+
}
|
|
428
|
+
for (const c of out) {
|
|
429
|
+
c.x = r1(median(c.m.map((v) => v.x)));
|
|
430
|
+
// Members nested in another member count once (a card and the text flush inside it).
|
|
431
|
+
c.outer = c.m.filter((a) => !c.m.some((b) => b !== a && b.el !== a.el && b.el.contains(a.el)));
|
|
432
|
+
c.n = new Set(c.outer.map((v) => v.el)).size;
|
|
433
|
+
}
|
|
434
|
+
return out;
|
|
435
|
+
};
|
|
436
|
+
const related = (a, b) => a === b || a.contains(b) || b.contains(a);
|
|
437
|
+
// A control or picture in a centred group (a centred button pair) has no meaningful left edge.
|
|
438
|
+
const CEN = new Map();
|
|
439
|
+
const centredGroup = (el) => {
|
|
440
|
+
if (CEN.has(el)) return CEN.get(el);
|
|
441
|
+
let res = false;
|
|
442
|
+
let p = el.parentElement;
|
|
443
|
+
for (let k = 0; p && p !== body && k < 3 && box(p).w <= box(el).w + 24; k++) p = p.parentElement;
|
|
444
|
+
if (p && p !== body) {
|
|
445
|
+
const c = cs(p);
|
|
446
|
+
const ks = [...p.children].filter((k) => allSet.has(k));
|
|
447
|
+
if (ks.length) {
|
|
448
|
+
const x0 = Math.min(...ks.map((k) => box(k).x));
|
|
449
|
+
const x1 = Math.max(...ks.map((k) => box(k).r));
|
|
450
|
+
const pl = box(p).x + parseFloat(c.paddingLeft);
|
|
451
|
+
const pr = box(p).r - parseFloat(c.paddingRight);
|
|
452
|
+
res = c.textAlign === "center" || c.justifyContent === "center" || (Math.abs((x0 + x1) / 2 - (pl + pr) / 2) <= 2 && x1 - x0 < pr - pl - 24);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
CEN.set(el, res);
|
|
456
|
+
return res;
|
|
457
|
+
};
|
|
458
|
+
const edgeItems = (sec, side) => {
|
|
459
|
+
const out = [];
|
|
460
|
+
for (const l of leaves) {
|
|
461
|
+
if (sec && l.section !== sec) continue;
|
|
462
|
+
if (l.kind !== "text" && centredGroup(l.el)) continue;
|
|
463
|
+
if (l.kind === "text" && (l.t.centred || !(side === "left" ? l.t.leftLed : ["right", "end"].includes(l.t.align)))) continue;
|
|
464
|
+
const x = side === "left" ? l.b.x : l.b.r;
|
|
465
|
+
if (x <= 1 || x >= vw - 1 || l.b.r > vw + 1 || l.b.x < -1) continue;
|
|
466
|
+
out.push({ x, el: l.el, b: l.b, kind: l.kind });
|
|
467
|
+
}
|
|
468
|
+
for (const el of cards) {
|
|
469
|
+
const b = box(el);
|
|
470
|
+
if (sec && sectionAt(b) !== sec) continue;
|
|
471
|
+
const x = side === "left" ? b.x : b.r;
|
|
472
|
+
if (x > 1 && x < vw - 1) out.push({ x, el, b, kind: "box" });
|
|
473
|
+
}
|
|
474
|
+
return out;
|
|
475
|
+
};
|
|
476
|
+
const nearMisses = [];
|
|
477
|
+
const seenMiss = new Set();
|
|
478
|
+
const alignSections = [];
|
|
479
|
+
const cardSet = new Set(cards);
|
|
480
|
+
const frameOf = (el) => {
|
|
481
|
+
for (let e = el.parentElement; e && e !== body; e = e.parentElement) if (cardSet.has(e)) return e;
|
|
482
|
+
return null;
|
|
483
|
+
};
|
|
484
|
+
const findMisses = (clusters, sec, side, scope) => {
|
|
485
|
+
const strong = clusters.filter((c) => c.n >= 3);
|
|
486
|
+
// Content inset inside a frame that itself sits on a shared edge follows the frame's padding, not the page grid.
|
|
487
|
+
const framedOnLine = (el) => {
|
|
488
|
+
const f = frameOf(el);
|
|
489
|
+
if (!f) return false;
|
|
490
|
+
const fx = side === "left" ? box(f).x : box(f).r;
|
|
491
|
+
return clusters.some((c) => c.n >= 2 && Math.abs(c.x - fx) <= 1) || fx <= 1 || fx >= vw - 1;
|
|
492
|
+
};
|
|
493
|
+
const page = scope === "page";
|
|
494
|
+
for (const L of strong) {
|
|
495
|
+
// Two competing lines a few px apart (each well used) are a split grid, not one line and its strays.
|
|
496
|
+
const near = clusters.filter((c) => c !== L && Math.abs(c.x - L.x) > 1 && Math.abs(c.x - L.x) <= 6);
|
|
497
|
+
if (near.reduce((a, c) => a + c.n, 0) >= L.n * 0.6) continue;
|
|
498
|
+
for (const c of near) {
|
|
499
|
+
const d = c.x - L.x;
|
|
500
|
+
if (c.n >= L.n || (page && Math.abs(d) < 2.5)) continue;
|
|
501
|
+
for (const v of c.outer) {
|
|
502
|
+
if (L.m.some((w) => related(w.el, v.el)) || framedOnLine(v.el)) continue;
|
|
503
|
+
// Large type nudged left by its side bearing is optical alignment, done on purpose.
|
|
504
|
+
const t = v.kind === "text" ? leaves.find((l) => l.el === v.el)?.t : null;
|
|
505
|
+
if (t && t.size >= 28 && side === "left" && d < 0 && -d <= t.size * 0.08) continue;
|
|
506
|
+
// Only items near the line in reading order: a line member close by vertically (same or next section on the page).
|
|
507
|
+
const sv = sectionAt(v.b);
|
|
508
|
+
if (!L.m.some((w) => Math.abs(w.b.y - v.b.y) < vh * (page ? 1 : 1.5) && (!page || Math.abs(sectionAt(w.b) - sv) <= 1))) continue;
|
|
509
|
+
const key = `${side}:${selector(v.el)}`;
|
|
510
|
+
if (seenMiss.has(key)) continue;
|
|
511
|
+
seenMiss.add(key);
|
|
512
|
+
const ref = L.m.find((w) => !related(w.el, v.el));
|
|
513
|
+
nearMisses.push({ side, scope, section: sec, offBy: r1(d), line: L.x, lineCount: L.n, x: r1(v.x), offender: off(v.el, v.b), onLine: off(ref.el, ref.b) });
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
for (const s of sections) {
|
|
519
|
+
const left = cluster(edgeItems(s.index, "left"));
|
|
520
|
+
const right = cluster(edgeItems(s.index, "right"));
|
|
521
|
+
findMisses(left, s.index, "left", "section");
|
|
522
|
+
findMisses(right, s.index, "right", "section");
|
|
523
|
+
const lines = left.filter((c) => c.n >= 2);
|
|
524
|
+
alignSections.push({ section: s.index, name: s.name, leftLines: lines.length, leftEdges: left.length, lines: lines.sort((a, b) => b.n - a.n).slice(0, 6).map((c) => ({ x: c.x, items: c.n })), rightLines: right.filter((c) => c.n >= 2).length });
|
|
525
|
+
}
|
|
526
|
+
// Page lines: edges shared by 3+ sections (the content column, the gutter).
|
|
527
|
+
const pageLeft = cluster(edgeItems(0, "left"));
|
|
528
|
+
for (const c of pageLeft) c.sections = new Set(c.m.map((v) => sectionAt(v.b))).size;
|
|
529
|
+
const pageLines = pageLeft.filter((c) => c.sections >= 3).sort((a, b) => b.sections - a.sections || b.n - a.n);
|
|
530
|
+
findMisses(pageLeft.map((c) => ({ ...c, n: c.sections >= 3 ? c.n : Math.min(c.n, 2) })), 0, "left", "page");
|
|
531
|
+
|
|
532
|
+
// Centred and left-aligned text stacked in the same column of one section.
|
|
533
|
+
const mixed = [];
|
|
534
|
+
const inCard = (el, sec) => cards.some((c) => c.contains(el) && box(c).w < (sec?.box.width ?? vw) * 0.8);
|
|
535
|
+
for (const s of sections) {
|
|
536
|
+
const ts = texts.filter((t) => t.section === s.index && !t.control && t.size >= 13 && t.chars >= 3 && !inCard(t.el, s) && (t.centred || t.leftLed));
|
|
537
|
+
const pairs = [];
|
|
538
|
+
for (const a of ts.filter((t) => t.centred && (t.nLines > 1 || t.ink.w < box(t.el).w - 24 || t.align === "center"))) {
|
|
539
|
+
for (const b of ts.filter((t) => t.leftLed && !t.centred)) {
|
|
540
|
+
const overlap = Math.min(a.ink.r, b.ink.r) - Math.max(a.ink.x, b.ink.x);
|
|
541
|
+
const gap = Math.max(a.ink.y, b.ink.y) - Math.min(a.ink.b, b.ink.b);
|
|
542
|
+
const stack = (x) => [x.el.parentElement, x.el.parentElement?.parentElement];
|
|
543
|
+
const sameStack = stack(a).some((p) => p && stack(b).includes(p));
|
|
544
|
+
if (overlap > 0 && sameStack && gap < 120 && gap > -4 && b.ink.w > 120) pairs.push({ centred: off(a.el, a.ink, a.text.slice(0, 60)), left: off(b.el, b.ink, b.text.slice(0, 60)) });
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (pairs.length) mixed.push({ section: s.index, name: s.name, pairs: pairs.slice(0, 3), count: pairs.length });
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Grid rows whose items (or the titles inside them) start at different heights.
|
|
551
|
+
const kidsOf = (p) => [...p.children].flatMap((k) => (cs(k).display === "contents" ? kidsOf(k) : [k]));
|
|
552
|
+
const ragged = [];
|
|
553
|
+
const rowParents = new Set();
|
|
554
|
+
for (const p of all) {
|
|
555
|
+
const ks = kidsOf(p).filter((k) => allSet.has(k));
|
|
556
|
+
if (ks.length < 2) continue;
|
|
557
|
+
const rows = [];
|
|
558
|
+
for (const k of ks) {
|
|
559
|
+
const b = box(k);
|
|
560
|
+
const row = rows.find((r) => {
|
|
561
|
+
const f = box(r[0]);
|
|
562
|
+
return Math.min(f.b, b.b) - Math.max(f.y, b.y) > Math.min(f.h, b.h) * 0.5 && !r.some((o) => Math.min(box(o).r, b.r) - Math.max(box(o).x, b.x) > 2);
|
|
563
|
+
});
|
|
564
|
+
if (row) row.push(k);
|
|
565
|
+
else rows.push([k]);
|
|
566
|
+
}
|
|
567
|
+
for (const row of rows) {
|
|
568
|
+
if (row.length < 2) continue;
|
|
569
|
+
const ws = row.map((k) => box(k).w);
|
|
570
|
+
if (Math.min(...ws) < 120 || Math.max(...ws) / Math.min(...ws) > 1.25) continue;
|
|
571
|
+
const tx = row.map((k) => texts.filter((t) => !t.control && k.contains(t.el)).sort((a, b) => a.ink.y - b.ink.y));
|
|
572
|
+
if (tx.some((t) => t.length < 2)) continue;
|
|
573
|
+
// Only rows of like items (cards, columns of one kind): same tag, same title size, similar amount of text.
|
|
574
|
+
const counts = tx.map((t) => t.length);
|
|
575
|
+
if (new Set(row.map((k) => k.tagName)).size > 1 || !tx.every((t) => t[0].size === tx[0][0].size) || Math.max(...counts) > Math.min(...counts) * 2 + 1) continue;
|
|
576
|
+
if ([...rowParents].some((q) => q.contains(p) && q !== p && row.every((k) => q.contains(k)))) continue;
|
|
577
|
+
rowParents.add(p);
|
|
578
|
+
const tops = row.map((k) => box(k).y);
|
|
579
|
+
const first = row.map((k, i) => tx[i][0].ink.y - box(k).y);
|
|
580
|
+
const firstSame = tx.every((t) => t[0].size === tx[0][0].size);
|
|
581
|
+
const second = row.map((k, i) => tx[i][1].ink.y - box(k).y);
|
|
582
|
+
const surfaced = row.every((k) => surface(k));
|
|
583
|
+
const heights = row.map((k) => box(k).h);
|
|
584
|
+
const spread = (a) => r1(Math.max(...a) - Math.min(...a));
|
|
585
|
+
const issues = [];
|
|
586
|
+
if (surfaced && spread(tops) > 2) issues.push({ kind: "item tops", spread: spread(tops) });
|
|
587
|
+
if (firstSame && spread(first) > 3) issues.push({ kind: "first lines", spread: spread(first), why: row.some((k, i) => k.querySelector("img, svg") && !row[(i + 1) % row.length].querySelector("img, svg")) ? "some items have an icon or picture above the text, others do not" : null });
|
|
588
|
+
else if (firstSame && spread(second) > 3) issues.push({ kind: "second lines", spread: spread(second), why: new Set(tx.map((t) => t[0].nLines)).size > 1 ? `the titles wrap to ${[...new Set(tx.map((t) => t[0].nLines))].join(" and ")} lines` : null });
|
|
589
|
+
if (surfaced && spread(heights) > 4 && cs(p).alignItems !== "stretch") issues.push({ kind: "card heights", spread: spread(heights) });
|
|
590
|
+
if (issues.length) ragged.push({ section: sectionAt(box(p)), container: off(p, box(p), ""), items: row.length, issues, offenders: row.slice(0, 4).map((k, i) => off(k, box(k), tx[i][0].text.slice(0, 40))) });
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Two different blocks side by side (a picture and its label, a demo and its copy): their visible edges should line
|
|
595
|
+
// up (tops, bottoms or centres) or clearly not. A near miss, a few px off, reads as a mistake. Seen on palate's own
|
|
596
|
+
// site: a painting and its placard 8px apart at the bottom, the placard 80px taller at the top.
|
|
597
|
+
const visualOf = (k) => {
|
|
598
|
+
if (surface(k)) return box(k);
|
|
599
|
+
const kb = box(k);
|
|
600
|
+
for (const d of k.querySelectorAll(":scope > *, :scope > * > *, :scope > * > * > *")) if (surface(d) && box(d).w >= kb.w * 0.85) return box(d);
|
|
601
|
+
return null;
|
|
602
|
+
};
|
|
603
|
+
for (const p of all) {
|
|
604
|
+
const ks = kidsOf(p).filter((k) => allSet.has(k));
|
|
605
|
+
if (ks.length !== 2) continue;
|
|
606
|
+
const [a, b] = ks.map((k) => box(k));
|
|
607
|
+
const beside = Math.min(a.b, b.b) - Math.max(a.y, b.y) > Math.min(a.h, b.h) * 0.5 && (a.r <= b.x + 2 || b.r <= a.x + 2);
|
|
608
|
+
if (!beside) continue;
|
|
609
|
+
const va = visualOf(ks[0]), vb = visualOf(ks[1]);
|
|
610
|
+
if (!va || !vb || Math.min(va.w, vb.w) < 160 || Math.min(va.h, vb.h) < 120) continue;
|
|
611
|
+
const dTop = Math.abs(va.y - vb.y), dBot = Math.abs(va.y + va.h - (vb.y + vb.h)), dMid = Math.abs(va.y + va.h / 2 - (vb.y + vb.h / 2));
|
|
612
|
+
const best = Math.min(dTop, dBot, dMid);
|
|
613
|
+
if (best <= 2 || best > 24) continue;
|
|
614
|
+
const edge = best === dTop ? "top" : best === dBot ? "bottom" : "centre";
|
|
615
|
+
ragged.push({ section: sectionAt(box(p)), container: off(p, box(p), ""), items: 2, issues: [{ kind: "pair edges", spread: r1(best), why: edge }], offenders: ks.map((k) => off(k, box(k), (k.innerText || "").trim().slice(0, 40))) });
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// ================= 2. HIERARCHY =================
|
|
619
|
+
const wf = (w) => Math.sqrt(w / 400);
|
|
620
|
+
const cf = (c) => (c == null ? 0.8 : Math.max(0.2, Math.min(1, Math.log(c) / Math.log(10))));
|
|
621
|
+
const ctlInfo = (el) => {
|
|
622
|
+
const c = cs(el);
|
|
623
|
+
const s = surface(el);
|
|
624
|
+
const bg = rgba(c.backgroundColor);
|
|
625
|
+
const behind = bgOf(el.parentElement) ?? rootBg;
|
|
626
|
+
const filled = !!(s && (s.filled || s.gradient));
|
|
627
|
+
const fill = filled ? (s.gradient && !s.filled ? firstStop(c.backgroundImage) ?? bg : blend(bg, behind)) : null;
|
|
628
|
+
const fillContrast = fill ? contrast(fill, behind) : 1;
|
|
629
|
+
const t = texts.find((t) => el.contains(t.el)) ?? null;
|
|
630
|
+
const size = t?.size ?? parseFloat(c.fontSize);
|
|
631
|
+
const weight = t?.weight ?? (Number(c.fontWeight) || 400);
|
|
632
|
+
const b = box(el);
|
|
633
|
+
const prominence = Math.sqrt(b.w * b.h) * (filled ? 1 + Math.min(1, Math.log(fillContrast) / Math.log(8)) * 1.2 : s?.bordered ? 1.15 : 0.8);
|
|
634
|
+
return { filled, bordered: !!s?.bordered, fill: fill ? hex(fill) : null, fillContrast: r1(fillContrast), size, weight, prominence: r1(prominence), score: size * wf(weight) * (t ? cf(t.contrast) : 0.8) * (filled ? 1 + 0.5 * Math.min(1, Math.log(fillContrast) / Math.log(8)) : 1), text: clean(el.innerText).slice(0, 40) };
|
|
635
|
+
};
|
|
636
|
+
const CTL = new Map(controls.filter(buttonLike).map((el) => [el, ctlInfo(el)]));
|
|
637
|
+
const scoreText = (t) => t.size * wf(t.weight) * cf(t.contrast);
|
|
638
|
+
// Single glyphs and text on small tiles (keycaps, badges, avatars) are parts of an illustration.
|
|
639
|
+
const glyph = (t) => {
|
|
640
|
+
if (!/[\p{L}\p{N}]{2,}|\p{N}/u.test(t.text)) return true;
|
|
641
|
+
if (t.level) return false;
|
|
642
|
+
for (let e = t.el; e && e !== body; e = e.parentElement) {
|
|
643
|
+
if (!surface(e)) continue;
|
|
644
|
+
const b = box(e);
|
|
645
|
+
return b.w <= 160 && b.h <= 160 && !isControl(e);
|
|
646
|
+
}
|
|
647
|
+
return false;
|
|
648
|
+
};
|
|
649
|
+
const bodySize = (() => {
|
|
650
|
+
const m = tally(texts.filter((t) => !t.control && t.chars >= 20), (t) => t.size);
|
|
651
|
+
let best = null;
|
|
652
|
+
let most = 0;
|
|
653
|
+
for (const [s, ts] of m) {
|
|
654
|
+
const n = ts.reduce((a, t) => a + t.chars, 0);
|
|
655
|
+
if (n > most) [best, most] = [s, n];
|
|
656
|
+
}
|
|
657
|
+
return best ?? 16;
|
|
658
|
+
})();
|
|
659
|
+
const scopes = [];
|
|
660
|
+
const scopeOf = (name, section, inScope) => {
|
|
661
|
+
const items = [];
|
|
662
|
+
for (const t of texts) if (!t.control && !glyph(t) && inScope(t.ink)) items.push({ kind: t.level ? `h${t.level}` : "text", score: scoreText(t), el: t.el, b: t.ink, t });
|
|
663
|
+
for (const [el, info] of CTL) if (inScope(box(el)) && !el.closest(CHROME)) items.push({ kind: info.filled ? "button (filled)" : "button", score: info.score, el, b: box(el), info });
|
|
664
|
+
if (!items.length) return null;
|
|
665
|
+
items.sort((a, b) => b.score - a.score);
|
|
666
|
+
const top = items[0];
|
|
667
|
+
const pics = media.filter((m) => inScope(box(m)) && box(m).w * box(m).h > vw * vh * 0.08);
|
|
668
|
+
const heading =
|
|
669
|
+
items.find((i) => i.t && i.t.level === 1) ??
|
|
670
|
+
(section ? items.filter((i) => i.t && i.t.level && i.t.level <= 3).sort((a, b) => a.b.y - b.b.y)[0] : null) ??
|
|
671
|
+
null;
|
|
672
|
+
const res = { scope: name, section, top: items.slice(0, 5).map((i) => ({ kind: i.kind, score: r1(i.score), size: i.t?.size ?? i.info?.size, weight: i.t?.weight ?? i.info?.weight, ...off(i.el, i.b, (i.t?.text ?? i.info?.text ?? "").slice(0, 60)) })) };
|
|
673
|
+
if (pics.length) res.largestPicture = off(pics.sort((a, b) => box(b).w * box(b).h - box(a).w * box(a).h)[0]);
|
|
674
|
+
if (heading) {
|
|
675
|
+
res.heading = { kind: heading.kind, size: heading.t.size, weight: heading.t.weight, score: r1(heading.score), ...off(heading.el, heading.b, heading.t.text.slice(0, 60)) };
|
|
676
|
+
res.headingRank = items.indexOf(heading) + 1;
|
|
677
|
+
res.outranking = items.filter((i) => i.score > heading.score * 1.02 && i !== heading).slice(0, 3).map((i) => ({ kind: i.kind, score: r1(i.score), ...off(i.el, i.b, (i.t?.text ?? i.info?.text ?? "").slice(0, 60)) }));
|
|
678
|
+
}
|
|
679
|
+
const lead = heading ?? top;
|
|
680
|
+
// Focal points: distinct things within 15% of the top weight (one row of equal titles counts as one).
|
|
681
|
+
const close = items.filter((i) => i.score >= top.score * 0.85);
|
|
682
|
+
// One style repeated (a row of stats, card titles) is one focal point; the heading is its own.
|
|
683
|
+
const kinds = tally(close, (i) => (i === heading ? "H" : `${i.kind}|${i.t?.size ?? i.info?.size}|${i.t?.weight ?? i.info?.weight}`));
|
|
684
|
+
res.focalPoints = kinds.length;
|
|
685
|
+
if (heading) res.peers = items.filter((i) => i !== heading && i.t && i.score >= heading.score * 0.95).slice(0, 4).map((i) => ({ kind: i.kind, score: r1(i.score), ...off(i.el, i.b, (i.t?.text ?? i.info?.text ?? "").slice(0, 60)) }));
|
|
686
|
+
res.competing = close.slice(1, 5).map((i) => ({ kind: i.kind, score: r1(i.score), ...off(i.el, i.b, (i.t?.text ?? i.info?.text ?? "").slice(0, 60)) }));
|
|
687
|
+
// Steps: headline : subhead : body.
|
|
688
|
+
if (lead.t) {
|
|
689
|
+
const h = lead.t;
|
|
690
|
+
const below = texts
|
|
691
|
+
.filter((t) => !t.control && t !== h && inScope(t.ink) && t.ink.y >= h.ink.b - 2 && t.ink.y - h.ink.b < 160 && Math.min(t.ink.r, h.ink.r) - Math.max(t.ink.x, h.ink.x) > 0)
|
|
692
|
+
.sort((a, b) => a.ink.y - b.ink.y)[0];
|
|
693
|
+
const scopeBody = (() => {
|
|
694
|
+
const m = tally(texts.filter((t) => !t.control && inScope(t.ink) && t.chars >= 20 && t.size < h.size), (t) => t.size);
|
|
695
|
+
let best = null;
|
|
696
|
+
let most = 0;
|
|
697
|
+
for (const [s, ts] of m) {
|
|
698
|
+
const n = ts.reduce((a, t) => a + t.chars, 0);
|
|
699
|
+
if (n > most) [best, most] = [s, n];
|
|
700
|
+
}
|
|
701
|
+
return best ?? bodySize;
|
|
702
|
+
})();
|
|
703
|
+
const sub = below && below.size > scopeBody * 1.05 && below.size < h.size ? below : null;
|
|
704
|
+
const next = below ?? null;
|
|
705
|
+
res.steps = {
|
|
706
|
+
headline: h.size,
|
|
707
|
+
headlineWeight: h.weight,
|
|
708
|
+
subhead: sub ? sub.size : null,
|
|
709
|
+
body: scopeBody,
|
|
710
|
+
next: next ? { size: next.size, weight: next.weight, ...off(next.el, next.ink, next.text.slice(0, 60)) } : null,
|
|
711
|
+
ratios: sub ? [r1(h.size / sub.size), r1(sub.size / scopeBody)] : [r1(h.size / scopeBody)],
|
|
712
|
+
headlineToNext: next ? Math.round((h.size / next.size) * 100) / 100 : null,
|
|
713
|
+
scoreRatio: next ? Math.round((scoreText(h) / scoreText(next)) * 100) / 100 : null,
|
|
714
|
+
weightGap: next ? h.weight - next.weight : null,
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
// Buttons side by side: is one clearly the primary?
|
|
718
|
+
const toggle = (el) => el.matches("[aria-pressed], [aria-selected], [role=tab], [role=radio], [aria-checked]") || !!el.closest("[role=tablist], [role=radiogroup], [role=group][aria-label]");
|
|
719
|
+
const btns = [...CTL.entries()].filter(([el]) => inScope(box(el)) && box(el).h >= 28 && !toggle(el) && !el.closest("header, nav, [role=banner], [role=navigation], footer"));
|
|
720
|
+
const groupsB = tally(btns, ([el]) => {
|
|
721
|
+
let p = el.parentElement;
|
|
722
|
+
for (let k = 0; k < 3 && p && btns.filter(([o]) => p.contains(o)).length < 2; k++) p = p.parentElement;
|
|
723
|
+
return p && btns.filter(([o]) => p.contains(o)).length >= 2 ? selector(p) : null;
|
|
724
|
+
}).filter(([, g]) => g.length >= 2);
|
|
725
|
+
res.buttonGroups = groupsB.map(([, g]) => {
|
|
726
|
+
const sorted = [...g].sort((a, b) => b[1].prominence - a[1].prominence);
|
|
727
|
+
const labels = g.map(([, i]) => i.text.toLowerCase());
|
|
728
|
+
const sameStyle = new Set(g.map(([, i]) => `${i.filled}|${i.fill}|${i.bordered}`)).size === 1;
|
|
729
|
+
// A set of parallel choices (platforms, filters, chips, icon links) has no primary by design.
|
|
730
|
+
const choiceSet = labels.some((l) => !l) || (g.length >= 3 && sameStyle) || new Set(labels.map((l) => l.split(" ")[0])).size === 1;
|
|
731
|
+
return {
|
|
732
|
+
choiceSet,
|
|
733
|
+
clearPrimary: sorted[0][1].prominence > sorted[1][1].prominence * 1.12,
|
|
734
|
+
equalStyle: sorted[0][1].filled === sorted[1][1].filled && sorted[0][1].fill === sorted[1][1].fill,
|
|
735
|
+
primaryFirst: g[0][0] === sorted[0][0],
|
|
736
|
+
buttons: sorted.map(([el, info]) => ({ prominence: info.prominence, filled: info.filled, fill: info.fill, fillContrast: info.fillContrast, ...off(el, box(el), info.text) })),
|
|
737
|
+
};
|
|
738
|
+
});
|
|
739
|
+
return res;
|
|
740
|
+
};
|
|
741
|
+
const fs = scopeOf("first screen", 0, (b) => b.y < sy + vh && b.y + b.h > sy);
|
|
742
|
+
if (fs) {
|
|
743
|
+
// The strongest control anywhere on the first screen (nav included) against the hero's primary.
|
|
744
|
+
const ctlsFirst = [...CTL.entries()].filter(([el]) => box(el).y < sy + vh).sort((a, b) => b[1].prominence - a[1].prominence);
|
|
745
|
+
fs.controls = ctlsFirst.slice(0, 4).map(([el, info]) => ({ prominence: info.prominence, filled: info.filled, fill: info.fill, inNav: !!el.closest("header, nav, [role=banner], [role=navigation]"), ...off(el, box(el), info.text) }));
|
|
746
|
+
scopes.push(fs);
|
|
747
|
+
}
|
|
748
|
+
for (const s of sections) {
|
|
749
|
+
if (s.chrome) continue;
|
|
750
|
+
const sc = scopeOf(s.name, s.index, (b) => sectionAt(b) === s.index);
|
|
751
|
+
if (sc) scopes.push(sc);
|
|
752
|
+
}
|
|
753
|
+
// One per <h1> element: a highlighted word inside the headline is a separate text run, not a second <h1>.
|
|
754
|
+
const h1s = [...new Map(texts.filter((t) => t.level === 1 && !t.control).map((t) => [t.el.closest("h1, [role=heading]") ?? t.el, t])).values()];
|
|
755
|
+
|
|
756
|
+
// ================= 3. SPACING =================
|
|
757
|
+
const spaceVals = [];
|
|
758
|
+
const PROPS = ["paddingTop", "paddingBottom", "paddingLeft", "paddingRight", "marginTop", "marginBottom", "rowGap", "columnGap"];
|
|
759
|
+
for (const el of all) {
|
|
760
|
+
const c = cs(el);
|
|
761
|
+
if (INLINE.has(c.display) || el.tagName === "svg") continue;
|
|
762
|
+
for (const p of PROPS) {
|
|
763
|
+
if ((p === "rowGap" || p === "columnGap") && !/flex|grid/.test(c.display)) continue;
|
|
764
|
+
const v = parseFloat(c[p]);
|
|
765
|
+
if (!(v >= 3) || v > 400) continue;
|
|
766
|
+
spaceVals.push({ v, p, el });
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
const onGrid = (v, u) => Math.abs(v / u - Math.round(v / u)) * u <= 0.26;
|
|
770
|
+
const offScale = (v) => (v >= 12 ? !onGrid(v, 4) : !onGrid(v, 2));
|
|
771
|
+
const fluid = (v) => Math.abs(v - Math.round(v)) > 0.05 && Math.abs(v * 2 - Math.round(v * 2)) > 0.05;
|
|
772
|
+
const share = (u) => (spaceVals.length ? spaceVals.filter((s) => onGrid(s.v, u)).length / spaceVals.length : 0);
|
|
773
|
+
const spacing = {
|
|
774
|
+
values: spaceVals.length,
|
|
775
|
+
distinct: new Set(spaceVals.map((s) => r1(s.v))).size,
|
|
776
|
+
on8: Math.round(share(8) * 100),
|
|
777
|
+
on4: Math.round(share(4) * 100),
|
|
778
|
+
on2: Math.round(share(2) * 100),
|
|
779
|
+
unit: share(8) >= 0.7 ? 8 : share(4) >= 0.7 ? 4 : share(2) >= 0.8 ? 2 : null,
|
|
780
|
+
offScaleShare: spaceVals.length ? Math.round((spaceVals.filter((s) => offScale(s.v) && !fluid(s.v)).length / spaceVals.length) * 100) : 0,
|
|
781
|
+
offScale: tally(spaceVals.filter((s) => offScale(s.v) && !fluid(s.v)), (s) => r1(s.v))
|
|
782
|
+
.slice(0, 8)
|
|
783
|
+
.map(([v, xs]) => ({ value: v, count: xs.length, examples: xs.slice(0, 2).map((s) => ({ prop: s.p, ...off(s.el, box(s.el), snippet(s.el).slice(0, 40)) })) })),
|
|
784
|
+
fluid: [...new Set(spaceVals.filter((s) => fluid(s.v)).map((s) => r1(s.v)))].slice(0, 8),
|
|
785
|
+
common: tally(spaceVals, (s) => Math.round(s.v)).slice(0, 12).map(([v, xs]) => `${v}×${xs.length}`),
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
// Proximity: a heading or label should sit closer to what it introduces than to what came before.
|
|
789
|
+
const containerOf = (el, sec) => {
|
|
790
|
+
for (let e = el.parentElement; e && e !== body; e = e.parentElement) {
|
|
791
|
+
if (surface(e) && box(e).w < vw * 0.96) return e;
|
|
792
|
+
if (sec?.el && e === sec.el) return e;
|
|
793
|
+
}
|
|
794
|
+
return sec?.el ?? body;
|
|
795
|
+
};
|
|
796
|
+
const proximity = [];
|
|
797
|
+
const hOverlap = (a, b) => Math.min(a.r, b.r) - Math.max(a.x, b.x) > Math.min(a.w, b.w) * 0.3;
|
|
798
|
+
const leafSet = leaves.filter((l) => !(l.kind === "media" && l.b.w < 40));
|
|
799
|
+
for (const t of texts) {
|
|
800
|
+
if (t.control || t.words > 12) continue;
|
|
801
|
+
const sec = sections.find((s) => s.index === t.section);
|
|
802
|
+
if (!sec || sec.chrome) continue;
|
|
803
|
+
const isLabel = t.el.matches("label, legend, dt, h1, h2, h3, h4, h5, h6") || t.level;
|
|
804
|
+
const cont = containerOf(t.el, sec);
|
|
805
|
+
const cb = box(cont);
|
|
806
|
+
const inC = (l) => l.el !== t.el && cont.contains(l.el) && !l.el.contains(t.el) && !t.el.contains(l.el) && l.b.y >= cb.y - 1 && l.b.b <= cb.b + 1;
|
|
807
|
+
const below = leafSet.filter((l) => inC(l) && l.b.y >= t.ink.b - 1 && hOverlap(l.b, t.ink)).sort((a, b) => a.b.y - b.b.y)[0];
|
|
808
|
+
if (!below) continue;
|
|
809
|
+
const nextT = below.t;
|
|
810
|
+
// A peer in a list (the next item has the same style) is not introducing it.
|
|
811
|
+
if (nextT && nextT.size === t.size && nextT.weight === t.weight && dist(nextT.color, t.color) < 8) continue;
|
|
812
|
+
// A label with its value beside it (dt | dd, label | input on one row) introduces that, not what is below.
|
|
813
|
+
if (leafSet.some((l) => inC(l) && l.b.x >= t.ink.r - 2 && Math.min(l.b.b, t.ink.b) - Math.max(l.b.y, t.ink.y) > Math.min(l.b.h, t.ink.h) * 0.5)) continue;
|
|
814
|
+
const fieldLabel = below.kind === "input" && (t.el.matches("label, legend") || !!t.el.closest("label") || (t.words <= 4 && t.size <= 16));
|
|
815
|
+
const headingLike = isLabel || fieldLabel || (nextT && t.words <= 8 && (t.size >= nextT.size * 1.15 || (t.weight >= 600 && nextT.weight < 600)));
|
|
816
|
+
if (!headingLike) continue;
|
|
817
|
+
let top = t.ink;
|
|
818
|
+
let above = leafSet.filter((l) => inC(l) && l.b.b <= top.y + 1 && hOverlap(l.b, t.ink)).sort((a, b) => b.b.b - a.b.b)[0];
|
|
819
|
+
// A kicker label above the heading belongs to it: measure from the kicker instead.
|
|
820
|
+
// So does an icon or small picture directly above it.
|
|
821
|
+
if (above && ((above.t && above.t.words <= 6 && above.t.size < t.size) || (above.kind === "control" && above.b.h <= 40) || (above.kind === "media" && above.b.w <= 240 && above.b.h <= 64)) && top.y - above.b.b < 32) {
|
|
822
|
+
top = above.b;
|
|
823
|
+
above = leafSet.filter((l) => inC(l) && l.b.b <= top.y + 1 && hOverlap(l.b, t.ink) && l !== above).sort((a, b) => b.b.b - a.b.b)[0];
|
|
824
|
+
}
|
|
825
|
+
if (!above) continue;
|
|
826
|
+
// A subline right under a heading belongs to the heading.
|
|
827
|
+
if (above.t?.level && top.y - above.b.b < t.size) continue;
|
|
828
|
+
// Something between (a rule, a divider, a box edge) separates the groups already.
|
|
829
|
+
const gapAbove = top.y - above.b.b;
|
|
830
|
+
const gapBelow = below.b.y - t.ink.b;
|
|
831
|
+
// A line glued to the one above (a heading's second line in another colour) is part of that block.
|
|
832
|
+
if (gapBelow < 6 || gapBelow > 96 || gapAbove > gapBelow || gapAbove < 6) continue;
|
|
833
|
+
const rule = all.some((el) => {
|
|
834
|
+
const b = box(el);
|
|
835
|
+
const c = cs(el);
|
|
836
|
+
return b.y >= above.b.b - 1 && b.b <= top.y + 1 && b.w > 40 && (b.h <= 3 && rgba(c.backgroundColor)[3] > 0.1 || (parseFloat(c.borderTopWidth) > 0 && c.borderTopStyle !== "none" && b.y > above.b.b - 2 && b.y < top.y));
|
|
837
|
+
});
|
|
838
|
+
if (rule) continue;
|
|
839
|
+
proximity.push({ section: t.section, gapAbove: r1(gapAbove), gapBelow: r1(gapBelow), offender: off(t.el, t.ink, t.text.slice(0, 50)), above: off(above.el, above.b), below: off(below.el, below.b) });
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Section padding: where the content starts and ends inside each section, and the gap between sections.
|
|
843
|
+
const content = sections.filter((s) => !s.chrome && s.box.height >= 120);
|
|
844
|
+
const padRows = content.map((s) => {
|
|
845
|
+
const ls = leaves.filter((l) => l.section === s.index && !(l.kind === "media" && l.b.w >= s.box.width * 0.95 && l.b.h >= s.box.height * 0.9));
|
|
846
|
+
if (!ls.length) return null;
|
|
847
|
+
const top = Math.min(...ls.map((l) => l.b.y));
|
|
848
|
+
const bot = Math.max(...ls.map((l) => l.b.b));
|
|
849
|
+
return { section: s.index, name: s.name, top: Math.round(top - s.box.y), bottom: Math.round(s.box.y + s.box.height - bot), contentTop: top, contentBottom: bot, ownBg: !!(s.el && surface(s.el)?.filled) };
|
|
850
|
+
}).filter(Boolean);
|
|
851
|
+
for (let i = 1; i < padRows.length; i++) padRows[i].gapFromPrevious = Math.round(padRows[i].contentTop - padRows[i - 1].contentBottom);
|
|
852
|
+
const sectionPadding = { sections: padRows.map(({ contentTop, contentBottom, ...r }) => r) };
|
|
853
|
+
|
|
854
|
+
// ================= 4. SIZING AND TYPE =================
|
|
855
|
+
const body2 = texts.filter((t) => !t.control);
|
|
856
|
+
const measure = body2
|
|
857
|
+
.filter((t) => t.nLines >= 3 && t.size >= 13 && t.size <= 24 && t.chars >= 120 && !t.level)
|
|
858
|
+
.map((t) => {
|
|
859
|
+
const maxW = Math.max(...t.lines.map((l) => l.r - l.l));
|
|
860
|
+
return { cpl: Math.round(maxW / Math.max(1, t.charW)), size: t.size, lines: t.nLines, ...off(t.el, t.ink, t.text.slice(0, 50)) };
|
|
861
|
+
});
|
|
862
|
+
const lineHeight = body2
|
|
863
|
+
.filter((t) => t.nLines >= 2 && t.chars >= 40)
|
|
864
|
+
.map((t) => ({ ratio: Math.round((t.lh / t.size) * 100) / 100, size: t.size, kind: t.size >= 32 ? "display" : t.level || t.weight >= 600 || t.size > 20 ? "heading" : "body", lines: t.nLines, chars: t.chars, ...off(t.el, t.ink, t.text.slice(0, 50)) }));
|
|
865
|
+
const weights = tally(body2, (t) => t.weight).map(([w, ts]) => ({ weight: w, blocks: ts.length, chars: ts.reduce((a, t) => a + t.chars, 0) }));
|
|
866
|
+
const looseDisplay = body2.filter((t) => t.size >= 28 && !t.upper && t.ls / t.size > 0.01).map((t) => ({ size: t.size, tracking: r1((t.ls / t.size) * 100) / 100, ...off(t.el, t.ink, t.text.slice(0, 50)) }));
|
|
867
|
+
const tightDisplay = body2.filter((t) => t.size >= 40 && t.ls === 0).map((t) => ({ size: t.size, family: t.family, ...off(t.el, t.ink, t.text.slice(0, 50)) }));
|
|
868
|
+
const capsNoTracking = texts
|
|
869
|
+
.filter((t) => t.upper && t.size <= 20 && (t.words >= 2 || t.chars >= 6) && t.ls / t.size < 0.03)
|
|
870
|
+
.map((t) => ({ size: t.size, tracking: Math.round((t.ls / t.size) * 1000) / 1000, ...off(t.el, t.ink, t.text.slice(0, 40)) }));
|
|
871
|
+
// Type scale: sizes weighted by use, the ratio that fits most of them, and sizes too close to tell apart.
|
|
872
|
+
const sizeTally = tally(texts, (t) => Math.round(t.size * 2) / 2).map(([s, ts]) => ({ size: s, blocks: ts.length }));
|
|
873
|
+
const sizes = sizeTally.map((s) => s.size).sort((a, b) => a - b);
|
|
874
|
+
const RATIOS = [1.067, 1.125, 1.2, 1.25, 1.333, 1.414, 1.5, 1.618];
|
|
875
|
+
const fits = (r, base) => sizes.filter((s) => {
|
|
876
|
+
const n = Math.log(s / base) / Math.log(r);
|
|
877
|
+
return Math.abs(s - base * r ** Math.round(n)) <= Math.max(1, s * 0.04);
|
|
878
|
+
});
|
|
879
|
+
let bestScale = null;
|
|
880
|
+
for (const r of RATIOS) {
|
|
881
|
+
const f = fits(r, bodySize);
|
|
882
|
+
if (!bestScale || f.length > bestScale.fit.length) bestScale = { ratio: r, fit: f };
|
|
883
|
+
}
|
|
884
|
+
const closeSizes = [];
|
|
885
|
+
for (let i = 1; i < sizes.length; i++) {
|
|
886
|
+
const a = sizes[i - 1];
|
|
887
|
+
const b = sizes[i];
|
|
888
|
+
const na = sizeTally.find((s) => s.size === a).blocks;
|
|
889
|
+
const nb = sizeTally.find((s) => s.size === b).blocks;
|
|
890
|
+
if (b / a < 1.08 && na >= 2 && nb >= 2 && a >= 12) {
|
|
891
|
+
const ex = (s) => texts.find((t) => Math.round(t.size * 2) / 2 === s);
|
|
892
|
+
closeSizes.push({ sizes: [a, b], blocks: [na, nb], examples: [off(ex(a).el, ex(a).ink, ex(a).text.slice(0, 30)), off(ex(b).el, ex(b).ink, ex(b).text.slice(0, 30))] });
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
const headingLike = (t) => !t.control && !t.quote && (t.level >= 1 && t.level <= 4 || (t.size >= Math.max(22, bodySize * 1.35) && t.weight >= 500));
|
|
896
|
+
const longHeadings = body2.filter((t) => !t.quote && t.level >= 1 && t.level <= 3 && t.words > 12).map((t) => ({ words: t.words, size: t.size, ...off(t.el, t.ink, t.text.slice(0, 80)) }));
|
|
897
|
+
// Widows: a heading whose last line holds one word.
|
|
898
|
+
const widows = [];
|
|
899
|
+
for (const t of body2.filter((t) => headingLike(t) && t.nLines >= 2 && t.words >= 3)) {
|
|
900
|
+
const lastTop = t.lines.at(-1).t;
|
|
901
|
+
let lastWords = 0;
|
|
902
|
+
let lastWord = "";
|
|
903
|
+
for (const n of t.nodes) {
|
|
904
|
+
const s = n.textContent;
|
|
905
|
+
for (const m of s.matchAll(/\S+/g)) {
|
|
906
|
+
range.setStart(n, m.index);
|
|
907
|
+
range.setEnd(n, m.index + m[0].length);
|
|
908
|
+
const r = range.getBoundingClientRect();
|
|
909
|
+
if (r.width > 0 && r.top + sy + r.height / 2 > lastTop + 1) {
|
|
910
|
+
lastWords++;
|
|
911
|
+
lastWord = m[0];
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
if (lastWords === 1 && lastWord.length < 20) widows.push({ word: lastWord, lines: t.nLines, wrap: cs(t.el).textWrap || cs(t.el).textWrapStyle || null, size: t.size, ...off(t.el, t.ink, t.text.slice(0, 60)) });
|
|
916
|
+
}
|
|
917
|
+
const families = tally(texts, (t) => t.family).map(([f, ts]) => ({ family: f, blocks: ts.length }));
|
|
918
|
+
|
|
919
|
+
// ================= 5. CONSISTENCY =================
|
|
920
|
+
const radiusKey = (el) => {
|
|
921
|
+
const b = box(el);
|
|
922
|
+
const r = parseFloat(cs(el).borderTopLeftRadius) || 0;
|
|
923
|
+
if (r >= Math.min(b.h, b.w) / 2 - 1 && r > 0) return "pill/circle";
|
|
924
|
+
return String(Math.round(r));
|
|
925
|
+
};
|
|
926
|
+
const fieldSurface = (el) => {
|
|
927
|
+
const c = cs(el);
|
|
928
|
+
return rgba(c.backgroundColor)[3] > 0.05 || (parseFloat(c.borderTopWidth) > 0 && c.borderTopStyle !== "none" && rgba(c.borderTopColor)[3] > 0.05) || c.boxShadow !== "none";
|
|
929
|
+
};
|
|
930
|
+
const rounded = all.filter((el) => (el.matches("input, select, textarea") ? fieldSurface(el) : surface(el) || el.matches("img, video")) && box(el).w >= 16 && !hidden(el));
|
|
931
|
+
const nestedRadii = [];
|
|
932
|
+
const derived = new Set();
|
|
933
|
+
for (const el of rounded) {
|
|
934
|
+
const r = parseFloat(cs(el).borderTopLeftRadius) || 0;
|
|
935
|
+
if (r < 4) continue;
|
|
936
|
+
const b = box(el);
|
|
937
|
+
if (r >= Math.min(b.h, b.w) / 2 - 1) continue;
|
|
938
|
+
let p = el.parentElement;
|
|
939
|
+
while (p && p !== body && !(surface(p) && (parseFloat(cs(p).borderTopLeftRadius) || 0) > 0)) p = p.parentElement;
|
|
940
|
+
if (!p || p === body) continue;
|
|
941
|
+
const pb = box(p);
|
|
942
|
+
const R = parseFloat(cs(p).borderTopLeftRadius) || 0;
|
|
943
|
+
if (R >= Math.min(pb.h, pb.w) / 2 - 1) continue;
|
|
944
|
+
const insetX = b.x - pb.x;
|
|
945
|
+
const insetY = b.y - pb.y;
|
|
946
|
+
// Only corners that sit near each other: the inner box shares the outer box's top-left corner region.
|
|
947
|
+
if (insetX < 0 || insetY < 0 || insetX > 32 || insetY > 32 || Math.abs(insetX - insetY) > 6) continue;
|
|
948
|
+
const inset = Math.min(insetX, insetY);
|
|
949
|
+
const ideal = Math.max(0, R - inset);
|
|
950
|
+
if (Math.abs(r - ideal) <= 1.5 && inset < R) derived.add(el);
|
|
951
|
+
// A picture counts only when it fills the frame (a screenshot in a card), not an icon sitting inside it.
|
|
952
|
+
if (el.matches("img, video") && b.w < pb.w * 0.5) continue;
|
|
953
|
+
if (inset >= 2 && inset < R * (el.matches("img, video") ? 1 : 1.5) && r > ideal + 3 && r >= R * 0.6) nestedRadii.push({ outer: Math.round(R), inner: Math.round(r), inset: Math.round(inset), ideal: Math.round(ideal), inner_: off(el), outer_: off(p, pb, "") });
|
|
954
|
+
}
|
|
955
|
+
// Radii derived concentrically from a parent (outer − inset) are not separate choices.
|
|
956
|
+
const radii = tally(rounded.filter((el) => !derived.has(el)), radiusKey).map(([r, els]) => ({ radius: r, count: els.length, example: off(els[0]) }));
|
|
957
|
+
const concentric = tally([...derived], radiusKey).map(([r, els]) => ({ radius: r, count: els.length }));
|
|
958
|
+
const borders = tally(all.filter((el) => surface(el)?.bordered && !hidden(el)), (el) => surface(el).border).map(([k, els]) => ({ border: k, count: els.length, example: off(els[0]) }));
|
|
959
|
+
const dividers = tally(all.filter((el) => {
|
|
960
|
+
const c = cs(el);
|
|
961
|
+
const n = sides.filter((k) => c[`border${k}Style`] !== "none" && parseFloat(c[`border${k}Width`]) > 0 && rgba(c[`border${k}Color`])[3] > 0.05).length;
|
|
962
|
+
return n >= 1 && n <= 2;
|
|
963
|
+
}), (el) => {
|
|
964
|
+
const c = cs(el);
|
|
965
|
+
const k = sides.find((k) => c[`border${k}Style`] !== "none" && parseFloat(c[`border${k}Width`]) > 0);
|
|
966
|
+
return `${parseFloat(c[`border${k}Width`])}px ${hex(rgba(c[`border${k}Color`]))}`;
|
|
967
|
+
}).map(([k, els]) => ({ border: k, count: els.length }));
|
|
968
|
+
const shadowKey = (el) => {
|
|
969
|
+
const layers = cs(el).boxShadow.replace(/(?:rgba?|oklch|oklab|hsla?|lab|lch|color)\([^)]*\)/g, (m) => {
|
|
970
|
+
const v = rgba(m);
|
|
971
|
+
return v[3] < 0.01 ? "transparent" : hex(v) + (v[3] < 1 ? `/${r1(v[3])}` : "");
|
|
972
|
+
}).split(/,(?![^(]*\))/).map((x) => x.trim()).filter((x) => !/transparent/.test(x) && !/^(\S+ )?0px 0px 0px( 0px)?( \S+)?$/.test(x.replace(/inset ?/, "")));
|
|
973
|
+
return layers.length ? layers.join(", ") : null;
|
|
974
|
+
};
|
|
975
|
+
const shadows = tally(all.filter((el) => cs(el).boxShadow !== "none" && !hidden(el)), shadowKey).map(([k, els]) => ({ shadow: k.slice(0, 120), count: els.length, example: off(els[0]) }));
|
|
976
|
+
|
|
977
|
+
// Colours: text, fills and borders, with the area each covers; the accent and how much of the page it takes.
|
|
978
|
+
const colArea = new Map();
|
|
979
|
+
const addCol = (v, area, role) => {
|
|
980
|
+
if (!v || v[3] < 0.05 || area <= 0) return;
|
|
981
|
+
const k = hex(v);
|
|
982
|
+
const e = colArea.get(k) ?? { hex: k, v, area: 0, roles: new Set() };
|
|
983
|
+
e.area += area;
|
|
984
|
+
e.roles.add(role);
|
|
985
|
+
colArea.set(k, e);
|
|
986
|
+
};
|
|
987
|
+
for (const t of texts) addCol(t.color[3] < 1 && bgOf(t.st) ? blend(t.color, bgOf(t.st)) : t.color, t.ink.w * t.ink.h * 0.35, "text");
|
|
988
|
+
for (const el of all) {
|
|
989
|
+
const s = surface(el);
|
|
990
|
+
const b = box(el);
|
|
991
|
+
if (s?.filled) {
|
|
992
|
+
const area = b.w * b.h - [...el.children].filter((k) => surface(k)?.filled).reduce((a, k) => a + box(k).w * box(k).h, 0);
|
|
993
|
+
addCol(rgba(s.bg), Math.max(0, area), "fill");
|
|
994
|
+
}
|
|
995
|
+
if (s?.bordered) addCol(rgba(cs(el).borderTopColor), 2 * (b.w + b.h) * parseFloat(cs(el).borderTopWidth), "border");
|
|
996
|
+
}
|
|
997
|
+
addCol(rootBg, Math.max(0, vw * pageH - [...colArea.values()].filter((c) => c.roles.has("fill")).reduce((a, c) => a + c.area, 0)), "page");
|
|
998
|
+
const palette = [...colArea.values()].sort((a, b) => b.area - a.area);
|
|
999
|
+
const total = palette.reduce((a, c) => a + c.area, 0) || 1;
|
|
1000
|
+
const chromatic = palette.filter((c) => {
|
|
1001
|
+
const h = hsl(c.v);
|
|
1002
|
+
return h.s > 0.25 && h.l > 0.12 && h.l < 0.92;
|
|
1003
|
+
});
|
|
1004
|
+
const hueFamilies = tally(chromatic, (c) => Math.round(hsl(c.v).h / 30) % 12).map(([h, cs_]) => ({ hue: h * 30, colors: cs_.map((c) => c.hex), share: Math.round((cs_.reduce((a, c) => a + c.area, 0) / total) * 1000) / 10 })).sort((a, b) => b.share - a.share);
|
|
1005
|
+
const nearDupColors = [];
|
|
1006
|
+
for (let i = 0; i < palette.length; i++)
|
|
1007
|
+
for (let j = i + 1; j < palette.length; j++) {
|
|
1008
|
+
const a = palette[i];
|
|
1009
|
+
const b = palette[j];
|
|
1010
|
+
if (dist(a.v, b.v) > 0 && dist(a.v, b.v) <= 5 && a.area / total > 0.0005 && b.area / total > 0.0005) nearDupColors.push([a.hex, b.hex]);
|
|
1011
|
+
}
|
|
1012
|
+
const colors = {
|
|
1013
|
+
distinct: palette.length,
|
|
1014
|
+
text: new Set(texts.map((t) => hex(t.color))).size,
|
|
1015
|
+
top: palette.slice(0, 10).map((c) => ({ hex: c.hex, share: Math.round((c.area / total) * 1000) / 10, roles: [...c.roles] })),
|
|
1016
|
+
accent: hueFamilies[0] ? { hue: hueFamilies[0].hue, colors: hueFamilies[0].colors.slice(0, 4), share: hueFamilies[0].share } : null,
|
|
1017
|
+
hueFamilies: hueFamilies.filter((h) => h.share >= 0.3).slice(0, 6),
|
|
1018
|
+
nearDuplicates: nearDupColors.slice(0, 8),
|
|
1019
|
+
};
|
|
1020
|
+
|
|
1021
|
+
// The same component styled several ways.
|
|
1022
|
+
const sig = (parts) => parts.join(" · ");
|
|
1023
|
+
const variants = (els, key) => tally(els, key).map(([k, xs]) => ({ style: k, count: xs.length, example: off(xs[0]) }));
|
|
1024
|
+
// Plain (no fill, no border) buttons read as links, not as button styles.
|
|
1025
|
+
const btnEls = [...CTL.entries()].filter(([el, i]) => i.filled || i.bordered).map(([el]) => el);
|
|
1026
|
+
const buttons = variants(btnEls, (el) => {
|
|
1027
|
+
const c = cs(el);
|
|
1028
|
+
const i = CTL.get(el);
|
|
1029
|
+
const s = surface(el);
|
|
1030
|
+
return sig([i.filled ? `filled ${i.fill}` : s?.bordered ? `outline ${s.border}` : "plain", `h${Math.round(box(el).h / 2) * 2}`, `r${radiusKey(el)}`, `${Math.round(i.size)}px/${i.weight}`, `px${Math.round(parseFloat(c.paddingLeft))}`]);
|
|
1031
|
+
});
|
|
1032
|
+
const cardEls = cards.filter((el) => texts.filter((t) => el.contains(t.el)).length >= 2 && box(el).w >= 160 && box(el).h >= 80 && box(el).w < vw * 0.9);
|
|
1033
|
+
const cardVariants = variants(cardEls, (el) => {
|
|
1034
|
+
const s = surface(el);
|
|
1035
|
+
const c = cs(el);
|
|
1036
|
+
return sig([s.bg ? `bg ${s.bg}` : "no fill", s.border ? `border ${s.border}` : "no border", s.shadow ? "shadow" : "flat", `r${radiusKey(el)}`, `pad ${Math.round(parseFloat(c.paddingTop))}/${Math.round(parseFloat(c.paddingLeft))}`]);
|
|
1037
|
+
});
|
|
1038
|
+
const inputVariants = variants(inputs, (el) => {
|
|
1039
|
+
const c = cs(el);
|
|
1040
|
+
const bw = c.borderTopStyle !== "none" && rgba(c.borderTopColor)[3] > 0.05 ? parseFloat(c.borderTopWidth) : 0;
|
|
1041
|
+
const bg = rgba(c.backgroundColor);
|
|
1042
|
+
return sig([`h${Math.round(box(el).h)}`, fieldSurface(el) ? `r${radiusKey(el)}` : "r-", bw ? `border ${bw}px ${hex(rgba(c.borderTopColor))}` : "no border", bg[3] > 0.05 ? `bg ${hex(bg)}` : "no fill", `${parseFloat(c.fontSize)}px`]);
|
|
1043
|
+
});
|
|
1044
|
+
// Variants that differ by a hair are usually accidents (12 vs 14px radius, 40 vs 42px tall).
|
|
1045
|
+
const nearVariants = (vs) => {
|
|
1046
|
+
const out = [];
|
|
1047
|
+
const parse = (s) => s.split(" · ");
|
|
1048
|
+
for (let i = 0; i < vs.length; i++)
|
|
1049
|
+
for (let j = i + 1; j < vs.length; j++) {
|
|
1050
|
+
const a = parse(vs[i].style);
|
|
1051
|
+
const b = parse(vs[j].style);
|
|
1052
|
+
const diff = a.map((x, k) => (x === b[k] ? null : [x, b[k]])).filter(Boolean);
|
|
1053
|
+
if (diff.length === 1) {
|
|
1054
|
+
const [x, y] = diff[0];
|
|
1055
|
+
const nx = parseFloat(x.replace(/^[^\d-]+/, ""));
|
|
1056
|
+
const ny = parseFloat(y.replace(/^[^\d-]+/, ""));
|
|
1057
|
+
if (Number.isFinite(nx) && Number.isFinite(ny) && Math.abs(nx - ny) > 0 && Math.abs(nx - ny) <= Math.max(4, Math.max(nx, ny) * 0.15)) out.push({ a: vs[i].style, b: vs[j].style, differs: `${x} vs ${y}`, examples: [vs[i].example, vs[j].example] });
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
return out;
|
|
1061
|
+
};
|
|
1062
|
+
|
|
1063
|
+
// ================= 6. DENSITY =================
|
|
1064
|
+
const density = sections.map((s) => {
|
|
1065
|
+
const area = s.box.width * s.box.height || 1;
|
|
1066
|
+
const ls = leaves.filter((l) => l.section === s.index);
|
|
1067
|
+
const clip = (b) => Math.max(0, Math.min(b.r, s.box.x + s.box.width) - Math.max(b.x, s.box.x)) * Math.max(0, Math.min(b.b, s.box.y + s.box.height) - Math.max(b.y, s.box.y));
|
|
1068
|
+
// Pictures and controls overlap (a screenshot inside a frame), so count their union on a 10px raster.
|
|
1069
|
+
const CELL = 10;
|
|
1070
|
+
const cols = Math.ceil(s.box.width / CELL);
|
|
1071
|
+
const rows = Math.ceil(s.box.height / CELL);
|
|
1072
|
+
const grid = new Uint8Array(Math.max(1, cols * rows));
|
|
1073
|
+
const paint = (b, v) => {
|
|
1074
|
+
const x0 = Math.max(0, Math.floor((b.x - s.box.x) / CELL));
|
|
1075
|
+
const x1 = Math.min(cols, Math.ceil((b.r - s.box.x) / CELL));
|
|
1076
|
+
const y0 = Math.max(0, Math.floor((b.y - s.box.y) / CELL));
|
|
1077
|
+
const y1 = Math.min(rows, Math.ceil((b.b - s.box.y) / CELL));
|
|
1078
|
+
let n = 0;
|
|
1079
|
+
for (let y = y0; y < y1; y++) for (let x = x0; x < x1; x++) if (!grid[y * cols + x]) {
|
|
1080
|
+
grid[y * cols + x] = v;
|
|
1081
|
+
n++;
|
|
1082
|
+
}
|
|
1083
|
+
return n * CELL * CELL;
|
|
1084
|
+
};
|
|
1085
|
+
let mediaInk = 0;
|
|
1086
|
+
let ctlInk = 0;
|
|
1087
|
+
let textInk = 0;
|
|
1088
|
+
for (const l of ls) if (l.kind === "media" && clip(l.b) > 0) mediaInk += paint(l.b, 1);
|
|
1089
|
+
for (const l of ls) if ((l.kind === "control" || l.kind === "input") && clip(l.b) > 0) ctlInk += paint(l.b, 2);
|
|
1090
|
+
for (const l of ls) if (l.kind === "text") for (const ln of l.t.lines) textInk += paint({ x: ln.l, r: ln.r, y: ln.t, b: ln.b }, 3) * 0.6;
|
|
1091
|
+
// The tallest band with nothing in it (inside the content, not the padding).
|
|
1092
|
+
const spans = ls.map((l) => [l.b.y, l.b.b]).sort((a, b) => a[0] - b[0]);
|
|
1093
|
+
let hole = 0;
|
|
1094
|
+
let holeAt = null;
|
|
1095
|
+
let reach = spans[0]?.[1] ?? 0;
|
|
1096
|
+
for (const [a, b] of spans.slice(1)) {
|
|
1097
|
+
if (a - reach > hole) {
|
|
1098
|
+
hole = a - reach;
|
|
1099
|
+
holeAt = Math.round(reach);
|
|
1100
|
+
}
|
|
1101
|
+
reach = Math.max(reach, b);
|
|
1102
|
+
}
|
|
1103
|
+
return {
|
|
1104
|
+
section: s.index,
|
|
1105
|
+
name: s.name,
|
|
1106
|
+
chrome: s.chrome,
|
|
1107
|
+
height: Math.round(s.box.height),
|
|
1108
|
+
ink: Math.min(100, Math.round(((textInk + mediaInk + ctlInk) / area) * 100)),
|
|
1109
|
+
textInk: Math.round((textInk / area) * 100),
|
|
1110
|
+
mediaInk: Math.round((mediaInk / area) * 100),
|
|
1111
|
+
ctlInk: Math.round((ctlInk / area) * 100),
|
|
1112
|
+
words: texts.filter((t) => t.section === s.index).reduce((a, t) => a + t.words, 0),
|
|
1113
|
+
items: ls.length,
|
|
1114
|
+
emptiestRun: Math.round(hole),
|
|
1115
|
+
emptiestAt: holeAt,
|
|
1116
|
+
};
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
return {
|
|
1120
|
+
viewport: { width: vw, height: vh },
|
|
1121
|
+
pageHeight: pageH,
|
|
1122
|
+
bodySize,
|
|
1123
|
+
sections: sections.map((s) => ({ index: s.index, name: s.name, tag: s.tag, chrome: s.chrome, box: s.box })),
|
|
1124
|
+
counts: { texts: texts.length, controls: controls.length, inputs: inputs.length, media: media.length, cards: cards.length },
|
|
1125
|
+
mockups: mocks.slice(0, 8).map((m) => ({ texts: tally2.get(m).n, ...off(m, box(m), snippet(m).slice(0, 40)) })),
|
|
1126
|
+
alignment: {
|
|
1127
|
+
pageLines: pageLines.slice(0, 5).map((c) => ({ x: c.x, items: c.n, sections: c.sections })),
|
|
1128
|
+
sections: alignSections,
|
|
1129
|
+
nearMisses: nearMisses.sort((a, b) => b.lineCount - a.lineCount).slice(0, 24),
|
|
1130
|
+
mixed,
|
|
1131
|
+
ragged: ragged.slice(0, 12),
|
|
1132
|
+
},
|
|
1133
|
+
hierarchy: { h1Count: h1s.length, h1: h1s.slice(0, 3).map((t) => off(t.el, t.ink, t.text.slice(0, 60))), scopes },
|
|
1134
|
+
spacing: { ...spacing, proximity: proximity.slice(0, 16), sectionPadding },
|
|
1135
|
+
type: {
|
|
1136
|
+
bodySize,
|
|
1137
|
+
sizes: sizeTally.sort((a, b) => b.size - a.size),
|
|
1138
|
+
scale: bestScale ? { ratio: bestScale.ratio, fit: bestScale.fit.length, of: sizes.length, outside: sizes.filter((s) => !bestScale.fit.includes(s)) } : null,
|
|
1139
|
+
closeSizes,
|
|
1140
|
+
families,
|
|
1141
|
+
weights,
|
|
1142
|
+
measure,
|
|
1143
|
+
lineHeight,
|
|
1144
|
+
looseDisplay,
|
|
1145
|
+
tightDisplay: tightDisplay.slice(0, 4),
|
|
1146
|
+
capsNoTracking: capsNoTracking.slice(0, 12),
|
|
1147
|
+
longHeadings,
|
|
1148
|
+
widows,
|
|
1149
|
+
},
|
|
1150
|
+
consistency: {
|
|
1151
|
+
radii,
|
|
1152
|
+
concentric,
|
|
1153
|
+
nestedRadii: nestedRadii.slice(0, 10),
|
|
1154
|
+
borders,
|
|
1155
|
+
dividers,
|
|
1156
|
+
shadows,
|
|
1157
|
+
colors,
|
|
1158
|
+
variants: {
|
|
1159
|
+
buttons,
|
|
1160
|
+
cards: cardVariants,
|
|
1161
|
+
inputs: inputVariants,
|
|
1162
|
+
near: { buttons: nearVariants(buttons), cards: nearVariants(cardVariants), inputs: nearVariants(inputVariants) },
|
|
1163
|
+
},
|
|
1164
|
+
},
|
|
1165
|
+
density,
|
|
1166
|
+
};
|
|
1167
|
+
};
|