@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,1130 @@
|
|
|
1
|
+
// `palate research "<job>" [--route /] [--urls a,b,c] [--inspire d,e]`: what a designer spends hours on before a new
|
|
2
|
+
// screen, done before any build. palate
|
|
3
|
+
// 1. understands the job (product, audience, industry, mood, a working metaphor, and whether the project already
|
|
4
|
+
// has a brand to work inside or starts from scratch) and writes the web searches for competitors and category
|
|
5
|
+
// leaders (palate can't search; the calling agent runs them and calls back with the URLs it found),
|
|
6
|
+
// 2. looks at those pages live, plus a few well-crafted ones from palate's own short list (kb/inspiration),
|
|
7
|
+
// politely: robots.txt for every page, one page at a time per host a few seconds apart, at most 12 per run,
|
|
8
|
+
// never logging in, never submitting anything, skipping bot walls and CAPTCHAs,
|
|
9
|
+
// 3. ranks them by craft (measured principles), the user's taste (rules, profile, past research verdicts) and
|
|
10
|
+
// diversity (no five near-identical heroes), keeping the best 4–6,
|
|
11
|
+
// 4. distils them into abstract notes: what makes each work, the moves it uses (moves library ids), one thing
|
|
12
|
+
// this product could learn, and what the competitors all do (the cliché to avoid).
|
|
13
|
+
// Everything stays in the project: <project>/.palate/research/<name>/ (gitignored) holds the screenshots (the user's
|
|
14
|
+
// private moodboard), research.md, contact.png and research.json. Nothing is uploaded; palate ships no third-party
|
|
15
|
+
// screenshots, code, copy, images or fonts. `palate direction "<job>" --from-research <name>` turns it into 3 art
|
|
16
|
+
// directions; the chosen one carries into palate build, and novelty.mjs compares the build with every reference.
|
|
17
|
+
import fs from "node:fs";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
import { PALATE_HOME } from "../catalog/store.mjs";
|
|
21
|
+
import { MEASURE } from "../capture.mjs";
|
|
22
|
+
import { fontClass } from "../catalog/classify.mjs";
|
|
23
|
+
import { brandOf } from "../context.mjs";
|
|
24
|
+
import { MOTION_INIT, contactSheet, scrollThrough } from "../look.mjs";
|
|
25
|
+
import { acquireBrowser } from "../pool.mjs";
|
|
26
|
+
import { measurePrinciples, principleFindings, principleFacts } from "../principles.mjs";
|
|
27
|
+
import { BOT_WALL, UA_SUFFIX, mayLook, politeRun, publicUrl } from "../robots.mjs";
|
|
28
|
+
import { normalizeRoute } from "../shoot.mjs";
|
|
29
|
+
import { TASTE_DIR, readJson } from "./common.mjs";
|
|
30
|
+
import { costLine, estimate, logCost } from "./cost.mjs";
|
|
31
|
+
import { ARCH_NAME, DUPLICATE, SIMILAR, compare, layoutRecord, screenOf } from "./layouts.mjs";
|
|
32
|
+
import { activeRules, appliesTo, checkRules, kindTags, measureRules, projectOf } from "./rules.mjs";
|
|
33
|
+
import { KB_ROOT } from "../paths.mjs";
|
|
34
|
+
|
|
35
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
36
|
+
const KB_DIR = process.env.PALATE_KB ?? KB_ROOT;
|
|
37
|
+
export const INDEX_FILE = path.join(KB_DIR, "inspiration", "index.json");
|
|
38
|
+
// The user's own growing list: pages their research looked at and kept (links and palate's tags only).
|
|
39
|
+
export const LOCAL_INDEX = path.join(PALATE_HOME, "research", "inspiration.json");
|
|
40
|
+
// What the user said about research references and which ones fed a kept build: palate's learning log.
|
|
41
|
+
export const RESEARCH_LOG = path.join(TASTE_DIR, "research.jsonl");
|
|
42
|
+
export const MAX_CAPTURES = 12;
|
|
43
|
+
const script = (f) => fs.readFileSync(path.join(here, "..", f), "utf8").trim().replace(/;\s*$/, "");
|
|
44
|
+
const slash = (p) => String(p).replace(/\\/g, "/");
|
|
45
|
+
const uniq = (a) => [...new Set(a.filter(Boolean))];
|
|
46
|
+
const readLines = (f) => (fs.existsSync(f) ? fs.readFileSync(f, "utf8").split("\n").filter(Boolean).map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean) : []);
|
|
47
|
+
const read = (f) => {
|
|
48
|
+
try {
|
|
49
|
+
return fs.readFileSync(f, "utf8");
|
|
50
|
+
} catch {
|
|
51
|
+
return "";
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// The rule every brief built from research carries, word for word.
|
|
56
|
+
export const LICENCE_RULE = "Use the ideas, never the expression: no copied code, copy, images, illustrations, logos or paid fonts; nothing that looks like one specific site.";
|
|
57
|
+
|
|
58
|
+
// The moves library (kb/moves/*.md, src/moves.mjs). If it isn't there, palate's older move list stands in.
|
|
59
|
+
const Moves = await import("../moves.mjs").catch(() => null);
|
|
60
|
+
function fallbackMoves() {
|
|
61
|
+
try {
|
|
62
|
+
const yaml = read(path.join(KB_DIR, "moves", "moves.yaml"));
|
|
63
|
+
return [...yaml.matchAll(/- id: ([\w-]+)\n\s+family: (\w+)[\s\S]*?signals: \[([^\]]*)\][\s\S]*?cost: (\w+)/g)].map((m) => ({ id: m[1], name: m[1].replace(/-/g, " "), kind: m[2], fits: m[3].split(",").map((x) => x.trim()), signature: ["composition", "motion", "interaction"].includes(m[2]) && m[4] !== "low" }));
|
|
64
|
+
} catch {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export function matchMovesSafe(q = {}) {
|
|
69
|
+
if (Moves?.matchMoves && Moves.loadMoves().length) return Moves.matchMoves(q);
|
|
70
|
+
const tags = (q.tags ?? []).map((t) => String(t).toLowerCase());
|
|
71
|
+
return fallbackMoves()
|
|
72
|
+
.filter((m) => q.signature == null || m.signature === Boolean(q.signature))
|
|
73
|
+
.filter((m) => !(q.exclude ?? []).includes(m.id))
|
|
74
|
+
.map((m) => ({ ...m, score: m.fits.filter((f) => tags.includes(f)).length, reason: "palate concept move (the moves library isn't installed)" }))
|
|
75
|
+
.filter((m) => !tags.length || m.score > 0)
|
|
76
|
+
.sort((a, b) => b.score - a.score)
|
|
77
|
+
.slice(0, q.limit ?? 8);
|
|
78
|
+
}
|
|
79
|
+
export const moveBriefSafe = (ids) => (Moves?.moveBrief ? Moves.moveBrief(ids) : ids.map((id) => `### ${id}\n(the moves library isn't installed: palate concept describes this move)`).join("\n\n"));
|
|
80
|
+
export const moveExists = (id) => (Moves?.loadMoves ? Moves.loadMoves().some((m) => m.id === id) : fallbackMoves().some((m) => m.id === id));
|
|
81
|
+
|
|
82
|
+
// ---- 1. understand
|
|
83
|
+
// Order matters: the first match leads (it picks the competitors). Specific categories come before generic words,
|
|
84
|
+
// and businesses that aren't software (a bakery, an inn, a nonprofit, a studio) have their own categories, so a
|
|
85
|
+
// bakery isn't read as "commerce" or a tool library as "dev-tools".
|
|
86
|
+
const INDUSTRY = [
|
|
87
|
+
[/\bshared (inbox|mailbox)|team inbox|group inbox|collaborative inbox\b/, ["shared-inbox", "email", "collaboration"]],
|
|
88
|
+
[/\b(observability|distributed tracing|tracing|traces|apm|telemetry|opentelemetry|monitoring|log management|error tracking|incident response)\b/, ["observability", "dev-tools"]],
|
|
89
|
+
[/\b(ci|ci\/cd|continuous integration|build pipelines?|monorepos?|schema migrations?|database migrations?|migration cli|feature flags?|docs-as-code|api documentation)\b/, ["dev-tools"]],
|
|
90
|
+
[/\b(bakery|bakeries|bakehouse|patisserie|boulangerie|bread|pastry|pastries|croissants?)\b/, ["bakery", "food", "hospitality"]],
|
|
91
|
+
[/\b(restaurants?|bistro|brasserie|trattoria|caf[eé]s?|coffee (shop|house|roasters?)|diner|eatery|wood-fired|tasting menu|wine bar|cocktail bar|brewery|pub)\b/, ["restaurant", "food", "hospitality"]],
|
|
92
|
+
[/\b(inns?|hotels?|hostels?|b&b|bed and breakfast|guest ?house|lodges?|resorts?)\b/, ["lodging", "hospitality", "travel"]],
|
|
93
|
+
[/\b(tour operators?|guided tours|walking tours|travel|(rail|train|walking|cycling|small-group) journeys|trips|holidays|itinerar(y|ies))\b/, ["travel"]],
|
|
94
|
+
[/\b(architects?|architecture|architectural)\b/, ["architecture", "portfolio"]],
|
|
95
|
+
[/\b(photograph(er|ers|y)|portraits?|weddings?)\b/, ["photography", "portfolio"]],
|
|
96
|
+
[/\b(type foundry|typefaces?|font licen[cs]es?)\b/, ["type-foundry", "creative"]],
|
|
97
|
+
[/\b(risograph|print studio|printmaking|letterpress|screen ?print(ing)?)\b/, ["print", "creative"]],
|
|
98
|
+
[/\b(sound design|music studio|original music|composers?|recording studio|audio post)\b/, ["music", "creative"]],
|
|
99
|
+
[/\b(non-?profits?|charit(y|ies)|volunteers?|volunteer programme|foundation|mutual aid|tool library|civic|ballots?|voters?|elections?|nonpartisan)\b/, ["nonprofit", "civic"]],
|
|
100
|
+
[/\b(river|water quality|conservation|climate|environment(al)?|wildlife)\b/, ["environment"]],
|
|
101
|
+
[/\b(classes|workshops?|courses?|coding clubs?|after-school|literacy|tutoring|reading and writing|language exchange|teach(ing|ers)?)\b/, ["education"]],
|
|
102
|
+
[/\b(pottery|ceramics?|bookbinding|hand-bookbinding|woodworking|weaving)\b/, ["craft"]],
|
|
103
|
+
[/\b(florists?|flowers?|bouquets?)\b/, ["florist", "local-business"]],
|
|
104
|
+
[/\b(repair shop|bike (shop|repair)|bicycles?|alterations|tailor(ing)?|clothing repair|mending|cobbler|barbers?|salon)\b/, ["local-business"]],
|
|
105
|
+
[/\b(meal[- ]planning|recipes?|groceries|grocery|cooking|fridge|pantry)\b/, ["food"]],
|
|
106
|
+
[/\b(football|basketball|soccer|sports?|pickup games?|running club)\b/, ["sports"]],
|
|
107
|
+
[/\b(budget(s|ing)?|personal finance|savings)\b/, ["finance"]],
|
|
108
|
+
[/\b(inbox|e-?mails?|mail)\b/, ["email"]],
|
|
109
|
+
[/\b(support|help ?desk|tickets?|customer service)\b/, ["support", "customer-service"]],
|
|
110
|
+
[/\b(design studios?|agenc(y|ies)|creative|designers?|studios?|illustrators?|architects?)\b/, ["creative"]],
|
|
111
|
+
[/\b(developers?|api|sdk|code|devops|deploy(ment)?s?|cli)\b/, ["dev-tools"]],
|
|
112
|
+
[/\b(bank(ing)?|finance|payments?|invoic(e|es|ing)|accounting|expenses?|payroll)\b/, ["finance"]],
|
|
113
|
+
[/\b(notes?|writing|docs|documents|knowledge base|wiki)\b/, ["notes", "writing", "docs"]],
|
|
114
|
+
[/\b(tasks?|to-?dos?|projects?|kanban|issues?|roadmaps?)\b/, ["productivity", "project-management"]],
|
|
115
|
+
[/\b(calendar|scheduling|bookings?|appointments?)\b/, ["scheduling"]],
|
|
116
|
+
[/\b(analytics|metrics|insights dashboard)\b/, ["analytics"]],
|
|
117
|
+
[/\b(crm|sales|pipeline|leads)\b/, ["crm"]],
|
|
118
|
+
[/\b(shop|store|e-?commerce|retail|checkout)\b/, ["commerce"]],
|
|
119
|
+
[/\b(health|wellness|meditation|sleep|fitness|therapy)\b/, ["wellness"]],
|
|
120
|
+
[/\b(ai|agents?|llms?|assistant|copilot)\b/, ["ai"]],
|
|
121
|
+
[/\b(security|passwords?|auth(entication)?|privacy)\b/, ["security"]],
|
|
122
|
+
[/\b(chat|messaging|team communication)\b/, ["collaboration"]],
|
|
123
|
+
[/\b(video|podcasts?|audio|music)\b/, ["video"]],
|
|
124
|
+
[/\b(research|interviews|user testing)\b/, ["research"]],
|
|
125
|
+
];
|
|
126
|
+
// [words, index moods, moves-library tags]
|
|
127
|
+
const MOOD = [
|
|
128
|
+
[/\b(calm|calmer|calmly|quiet|peaceful|serene|unhurried|gentle|soft|slow|zen|relaxed)\b/, ["calm", "quiet", "minimal"], ["calm", "quiet"]],
|
|
129
|
+
[/\b(craft(ed)?|handmade|careful|thoughtful|design studios?|designers?|artisan)\b/, ["crafted", "editorial"], ["crafted", "editorial"]],
|
|
130
|
+
[/\b(playful|fun|whimsical|delightful|cheeky)\b/, ["playful"], ["playful"]],
|
|
131
|
+
[/\b(bold|loud|confident|striking|punchy)\b/, ["bold"], ["bold"]],
|
|
132
|
+
[/\b(premium|luxury|high-end|elegant|refined)\b/, ["premium"], ["premium"]],
|
|
133
|
+
[/\b(technical|developers?|engineers?)\b/, ["technical", "precise"], ["technical", "developer"]],
|
|
134
|
+
[/\b(warm|friendly|human|personal|small (teams?|studios?|business(es)?))\b/, ["warm", "friendly", "human"], ["warm", "friendly"]],
|
|
135
|
+
[/\b(editorial|magazine|story|stories|journal)\b/, ["editorial"], ["editorial"]],
|
|
136
|
+
[/\b(fast|speed|quick|instant|snappy)\b/, ["fast", "precise"], ["energetic"]],
|
|
137
|
+
[/\b(simple|minimal|clean|focused)\b/, ["minimal"], ["minimal"]],
|
|
138
|
+
[/\b(trust(ed|worthy)?|secure|reliable|safe)\b/, [], ["trustworthy"]],
|
|
139
|
+
[/\b(nature|ocean|sea|tide|forest|garden|river)\b/, ["calm"], ["nature"]],
|
|
140
|
+
];
|
|
141
|
+
const SAAS = new Set(["shared-inbox", "email", "support", "dev-tools", "observability", "finance", "notes", "productivity", "project-management", "scheduling", "analytics", "crm", "ai", "security", "collaboration", "research"]);
|
|
142
|
+
// Categories that are a business, a place or a practice, not software: their searches look for comparable sites
|
|
143
|
+
// ("best bakery websites"), never "<x> software" or "<famous SaaS> alternatives".
|
|
144
|
+
const BUSINESS = new Set(["bakery", "restaurant", "food", "hospitality", "lodging", "travel", "architecture", "photography", "portfolio", "type-foundry", "print", "music", "nonprofit", "civic", "environment", "education", "craft", "florist", "local-business"]);
|
|
145
|
+
// The category's head noun says whether the product is software ("…app", "…CLI", "…service").
|
|
146
|
+
const SOFTWARE_HEAD = /\b(apps?|software|saas|platform|tools?|service|cli|sdk|api|generator|extension|plugin|bot|engine|editor|dashboard|inbox|client|tracker|planner|tracing|monitoring|analytics|crm)$/i;
|
|
147
|
+
// Index entries that aren't software sites (a retailer, a magazine, a hardware maker): the only craft picks a
|
|
148
|
+
// business gets, so a bakery isn't shown three note-taking apps.
|
|
149
|
+
const SOFTWARE_SITE = new Set([...SAAS, "payments", "banking", "infrastructure", "design-tools", "presentation", "browser", "tasks", "reading", "video", "wellness"]);
|
|
150
|
+
const NON_SOFTWARE_SITE = new Set(["retail", "publishing", "hardware", ...BUSINESS]);
|
|
151
|
+
export const softwareSite = (s) => (s.industry ?? []).some((i) => SOFTWARE_SITE.has(i)) || !(s.industry ?? []).some((i) => NON_SOFTWARE_SITE.has(i));
|
|
152
|
+
// When the job names no mood, a guess from the category (the blueprint decides the real mood).
|
|
153
|
+
const CATEGORY_MOOD = [
|
|
154
|
+
[["bakery", "restaurant", "food", "florist", "local-business", "craft"], ["warm", "crafted"]],
|
|
155
|
+
[["lodging", "travel", "environment"], ["calm", "warm"]],
|
|
156
|
+
[["nonprofit", "civic", "education"], ["warm", "friendly"]],
|
|
157
|
+
[["architecture", "photography"], ["editorial", "quiet"]],
|
|
158
|
+
[["type-foundry", "print", "music", "creative"], ["crafted", "editorial"]],
|
|
159
|
+
];
|
|
160
|
+
// Images a product name can carry: palate's own short phrases, used as a first working metaphor.
|
|
161
|
+
const IMAGES = {
|
|
162
|
+
tidepool: "a tide pool: a small, still pool the tide leaves behind, where everything that matters is visible at a glance while the sea keeps moving",
|
|
163
|
+
tide: "the tide: things arrive in waves and go out again on a rhythm",
|
|
164
|
+
pool: "a still pool: clear enough to see to the bottom",
|
|
165
|
+
harbor: "a harbour: what comes in gets tied up and looked after",
|
|
166
|
+
harbour: "a harbour: what comes in gets tied up and looked after",
|
|
167
|
+
lantern: "a lantern: one warm light that shows the next step",
|
|
168
|
+
atlas: "an atlas: the whole territory, folded so you can find your place",
|
|
169
|
+
ledger: "a ledger: every line accounted for, in order",
|
|
170
|
+
garden: "a garden: tended a little every day, it grows",
|
|
171
|
+
orbit: "an orbit: things come round again, on time",
|
|
172
|
+
field: "a field: open space with room to see far",
|
|
173
|
+
nest: "a nest: small, built by hand, safe",
|
|
174
|
+
hive: "a hive: many hands, one shared order",
|
|
175
|
+
compass: "a compass: one needle that always points the way",
|
|
176
|
+
anchor: "an anchor: holds steady while things move around it",
|
|
177
|
+
beacon: "a beacon: seen from far, it says where to go",
|
|
178
|
+
loom: "a loom: many threads woven into one cloth",
|
|
179
|
+
river: "a river: a steady current that carries things along",
|
|
180
|
+
stream: "a stream: a light, steady flow",
|
|
181
|
+
meadow: "a meadow: open, quiet, unhurried",
|
|
182
|
+
stone: "a stone: smooth, solid, simple",
|
|
183
|
+
paper: "paper: a clean sheet, ready to be written on",
|
|
184
|
+
ink: "ink: one mark, made to last",
|
|
185
|
+
slate: "a slate: wiped clean at the end of the day",
|
|
186
|
+
cove: "a cove: a sheltered bay away from the open sea",
|
|
187
|
+
shore: "the shore: where the waves come in and settle",
|
|
188
|
+
reef: "a reef: a living structure built up piece by piece",
|
|
189
|
+
lighthouse: "a lighthouse: a steady signal in rough weather",
|
|
190
|
+
hearth: "a hearth: the warm centre of the house",
|
|
191
|
+
shelf: "a shelf: everything in its place, in reach",
|
|
192
|
+
folio: "a folio: work gathered and bound with care",
|
|
193
|
+
pebble: "a pebble: small, smooth, worn calm by the water",
|
|
194
|
+
post: "a post room: every letter sorted and delivered",
|
|
195
|
+
desk: "a clean desk: only what today needs",
|
|
196
|
+
studio: "a studio: a working room where things get made",
|
|
197
|
+
bloom: "a bloom: something opens at the right moment",
|
|
198
|
+
moss: "moss: soft, slow, quietly covering everything",
|
|
199
|
+
};
|
|
200
|
+
const INDUSTRY_METAPHOR = {
|
|
201
|
+
"shared-inbox": "a well-run front desk: everything that arrives is seen, owned and answered",
|
|
202
|
+
email: "a quiet post room: every letter sorted, nothing shouting",
|
|
203
|
+
support: "a good concierge: you're known, and it's handled",
|
|
204
|
+
"dev-tools": "a workbench: the right tool in reach, nothing in the way",
|
|
205
|
+
finance: "a ledger: every line accounted for",
|
|
206
|
+
notes: "a commonplace book: ideas kept in one well-bound place",
|
|
207
|
+
productivity: "a clean desk: only what today needs",
|
|
208
|
+
"project-management": "a map of the work: where everything is and where it's going",
|
|
209
|
+
scheduling: "a well-kept diary",
|
|
210
|
+
analytics: "an instrument panel: the few readings that matter",
|
|
211
|
+
wellness: "a deep breath",
|
|
212
|
+
commerce: "a shop window: the thing itself, well lit",
|
|
213
|
+
creative: "a studio wall: work pinned up where everyone can see it",
|
|
214
|
+
};
|
|
215
|
+
const MOOD_METAPHOR = { calm: "slack water: the still moment between tides", warm: "a kitchen table where the team sits down together", crafted: "a well-made tool, worn in", playful: "a toy box", premium: "a quiet hotel lobby", technical: "an engineering drawing", editorial: "a printed journal" };
|
|
216
|
+
|
|
217
|
+
function splitName(name) {
|
|
218
|
+
const s = String(name ?? "").toLowerCase().replace(/[^a-z]/g, "");
|
|
219
|
+
if (IMAGES[s]) return [s];
|
|
220
|
+
for (let i = 3; i < s.length - 2; i++) if (IMAGES[s.slice(0, i)] && IMAGES[s.slice(i)]) return [s.slice(0, i), s.slice(i)];
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
export function metaphorFor({ product, industry = [], mood = [] } = {}) {
|
|
224
|
+
const parts = splitName(product);
|
|
225
|
+
const whole = parts.length === 1 ? IMAGES[parts[0]] : parts.length === 2 ? `${IMAGES[parts[0]].split(":")[0]} meets ${IMAGES[parts[1]]}` : null;
|
|
226
|
+
const fromIndustry = industry.map((i) => INDUSTRY_METAPHOR[i]).find(Boolean);
|
|
227
|
+
const fromMood = mood.map((m) => MOOD_METAPHOR[m]).find(Boolean);
|
|
228
|
+
const all = uniq([whole, fromIndustry, fromMood, "the product's own core object, shown plainly"]);
|
|
229
|
+
return { primary: all[0], alternates: all.slice(1, 3), from: whole ? `the name ${product}` : fromIndustry ? "the category" : "the mood" };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const TEMPLATE_README = /This template provides|Vite \+|bootstrapped with|create-react-app|Getting Started with|npm run dev/i;
|
|
233
|
+
const GENERIC_NAME = /^(app|web|site|demo|template|starter|my-?app|vite|next|react|project)([-_].*)?$|[-_](demo|app|site|web|starter)$/i;
|
|
234
|
+
// Never inside a hyphenated word: "small-group rail journeys" keeps its "small".
|
|
235
|
+
const MOOD_WORDS = /(?<![\w-])(calm|calmer|quiet|simple|modern|beautiful|fast|friendly|playful|bold|premium|minimal|clean|gentle|soft|new|better|smart|powerful|lightweight|focused|tiny|small|little)(?![\w-])/gi;
|
|
236
|
+
// A product name: capitalised words, possessives and apostrophes (Mabel's, Mabel’s), "&" / "and" / "of" between
|
|
237
|
+
// them (Ember & Oak, House of Bread), up to five words (North Arc Architects).
|
|
238
|
+
const NAME = "[A-Z0-9][\\w.'’-]*(?:(?:\\s+(?:&|and|\\+|of|the|de|du|la|le)\\s+|\\s+)[A-Z0-9][\\w.'’-]*){0,4}";
|
|
239
|
+
// People an audience is made of: "for kids aged 9–14" is an audience, "for weekend pickup" is not.
|
|
240
|
+
const PEOPLE = /\b(teams?|people|kids|children|adults|famil(y|ies)|couples|developers|engineers|designers|studios|students|parents|owners|founders|freelancers|nomads|backpackers|travell?ers|members|neighbou?rs|volunteers|businesses|companies|startups|agencies|makers|artists|writers|readers|musicians|athletes|players|seniors|patients|customers|shops|restaurants|creators|marketers|managers|clinics)\b/i;
|
|
241
|
+
const RELATIVE = /\s+(?:that|which|who|whose|where|with|so that|to help)\s+/i;
|
|
242
|
+
// "a neighbourhood bakery that takes pre-orders for weekend pickup" → category "neighbourhood bakery", does
|
|
243
|
+
// "takes pre-orders for weekend pickup", no audience. "a social hostel in Lisbon for backpackers" → category
|
|
244
|
+
// "social hostel", place "Lisbon", audience "backpackers". "distributed tracing for small engineering teams" →
|
|
245
|
+
// category "distributed tracing", audience "small engineering teams".
|
|
246
|
+
export function describe(description) {
|
|
247
|
+
const d = String(description ?? "").replace(/^(an?|the)\s+/i, "").trim();
|
|
248
|
+
if (!d) return { category: null, head: null, audience: null, place: null, does: null };
|
|
249
|
+
const cut = d.search(RELATIVE);
|
|
250
|
+
let head = cut >= 0 ? d.slice(0, cut) : d;
|
|
251
|
+
const does = cut >= 0 ? d.slice(cut).trim() : null;
|
|
252
|
+
let audience = null;
|
|
253
|
+
const forAt = head.match(/^(.*?)\s+for\s+(.+)$/i);
|
|
254
|
+
// "an app for organising pickup games": a purpose, not an audience; it stays in the category.
|
|
255
|
+
if (forAt && !/^[a-z]+ing\b/i.test(forAt[2])) [head, audience] = [forAt[1], forAt[2]];
|
|
256
|
+
if (!audience && does) audience = does.match(/\bfor\s+((?:small |large |busy |remote |independent |local )?[a-z0-9][a-z0-9 '’–-]{2,60}?)(?=[.,;)]|$)/i)?.[1] ?? null;
|
|
257
|
+
if (audience && !forAt?.[2]?.startsWith(audience) && !PEOPLE.test(audience)) audience = null;
|
|
258
|
+
if (audience) audience = audience.split(RELATIVE)[0].trim();
|
|
259
|
+
const at = head.match(/\s+(?:in|on the|across|near|around)\s+((?:the\s+)?[A-Z][^,;]*)$/);
|
|
260
|
+
const place = at ? at[1].trim() : (does?.match(/\b(?:in|across|on the)\s+((?:the\s+)?[A-Z][\w'’-]+(?:\s+[A-Z][\w'’-]+)*)/)?.[1] ?? null);
|
|
261
|
+
if (at) head = head.slice(0, at.index);
|
|
262
|
+
const forPurpose = head.match(/^(.*?)\s+for\s+[a-z]+ing\b/i)?.[1];
|
|
263
|
+
const category =
|
|
264
|
+
head
|
|
265
|
+
.replace(MOOD_WORDS, "")
|
|
266
|
+
.replace(/\s+(built|made|designed)$/i, "")
|
|
267
|
+
.replace(/^\d+[-–]\w+\s+/, "")
|
|
268
|
+
.replace(/\s+/g, " ")
|
|
269
|
+
.trim() || null;
|
|
270
|
+
return { category, head: (forPurpose ?? category ?? "").replace(MOOD_WORDS, "").replace(/\s+(built|made|designed)$/i, "").trim() || null, audience: audience || null, place, does };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function understand(dir, job, { route } = {}) {
|
|
274
|
+
const text = String(job ?? "").trim();
|
|
275
|
+
if (!text) throw new Error('Usage: palate research "<the job, in the user\'s words>" [--route /] [--urls a,b,c]');
|
|
276
|
+
// Stored in research.json and carried into the direction: a Git Bash path is refused here, never stored.
|
|
277
|
+
if (route) route = normalizeRoute(route);
|
|
278
|
+
const pkg = (() => {
|
|
279
|
+
try {
|
|
280
|
+
return JSON.parse(read(path.join(dir, "package.json")) || "{}");
|
|
281
|
+
} catch {
|
|
282
|
+
return {};
|
|
283
|
+
}
|
|
284
|
+
})();
|
|
285
|
+
const readmeRaw = read(path.join(dir, "README.md")).slice(0, 4000);
|
|
286
|
+
const readme = TEMPLATE_README.test(readmeRaw) ? "" : readmeRaw;
|
|
287
|
+
// "for Tidepool, a calm shared inbox for small design studios" · "Tidepool, a calm…" · "called Tidepool" ·
|
|
288
|
+
// "for Mabel's Bakehouse, a bakery…" · "for Ember & Oak, a restaurant…" · "for North Arc Architects, a…"
|
|
289
|
+
const named = text.match(new RegExp(`\\b(?:for|called|named|of)\\s+(${NAME})(?=\\s*(?:,|\\(|:|—|–|-\\s|$))`)) ?? text.match(new RegExp(`(?:^|[.!?:—–]\\s*)(${NAME}),\\s+(?:an?|the)\\s`)) ?? text.match(/\b([A-Z][a-z]+[A-Za-z0-9]*),\s+(?:an?|the)\s/);
|
|
290
|
+
const readmeName = readme.match(/^#\s+([A-Z][\w .'’&-]{1,40})$/m)?.[1]?.trim();
|
|
291
|
+
const product = named?.[1] ?? readmeName ?? (pkg.name && !GENERIC_NAME.test(pkg.name) ? pkg.name : null);
|
|
292
|
+
const description = (product && text.match(new RegExp(`${product.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*[,(:—–-]\\s*((?:an?|the)\\s[^.;()]+|[a-z][^.;()]+)`))?.[1]?.trim()) || pkg.description || null;
|
|
293
|
+
const parts = describe(description);
|
|
294
|
+
const forPart = description ? parts.audience : text.match(/\bfor ((?:small |large |busy |remote |independent )?[a-z][a-z -]{2,40}?)(?=[.,;)]|$)/i)?.[1]?.trim();
|
|
295
|
+
const audience = forPart && forPart !== product ? forPart : null;
|
|
296
|
+
const category = parts.category;
|
|
297
|
+
// The job names the category; the README only when the job doesn't (its words are noisy: a tracing tool's README
|
|
298
|
+
// mentions checkout and invoices, which made it "finance, commerce").
|
|
299
|
+
const fromJob = INDUSTRY.filter(([re]) => re.test(`${text} ${description ?? ""}`.toLowerCase()));
|
|
300
|
+
const industry = uniq((fromJob.length ? fromJob : INDUSTRY.filter(([re]) => re.test(readme.toLowerCase()))).flatMap(([, t]) => t));
|
|
301
|
+
const business = industry.some((i) => BUSINESS.has(i));
|
|
302
|
+
const software = SOFTWARE_HEAD.test(parts.head ?? "") || (industry.some((i) => SAAS.has(i)) && !business);
|
|
303
|
+
const moodHits = MOOD.filter(([re]) => re.test(`${text} ${description ?? ""}`.toLowerCase()));
|
|
304
|
+
let mood = uniq(moodHits.flatMap(([, m]) => m));
|
|
305
|
+
// No mood words in the job: a placeholder for the moves library only. The blueprint decides the real mood
|
|
306
|
+
// from the audience and product (moodGuessed tells it this one was guessed).
|
|
307
|
+
const moodGuessed = !mood.length;
|
|
308
|
+
if (!mood.length) mood = software && industry.includes("dev-tools") ? ["technical", "precise"] : industry.map((i) => CATEGORY_MOOD.find(([of]) => of.includes(i))?.[1]).find(Boolean) ?? ["crafted"];
|
|
309
|
+
const tags = uniq([...moodHits.flatMap(([, , t]) => t), ...(software && industry.some((i) => SAAS.has(i)) ? ["saas", "b2b"] : []), ...(industry.includes("email") || industry.includes("productivity") ? ["productivity", "daily-tool"] : []), ...(industry.includes("ai") ? ["ai"] : []), ...(industry.includes("finance") ? ["finance"] : []), ...(industry.includes("dev-tools") ? ["developer"] : [])]);
|
|
310
|
+
const screen = (() => {
|
|
311
|
+
const s = screenOf("", `${route ?? ""} ${text}`);
|
|
312
|
+
return s === "other" && (route === "/" || /\bhome ?page|landing\b/i.test(text)) ? "landing" : s;
|
|
313
|
+
})();
|
|
314
|
+
const kind = { landing: "landing", hero: "landing", pricing: "pricing", features: "features", docs: "docs" }[screen] ?? "landing";
|
|
315
|
+
const brand = brandOf(dir, text);
|
|
316
|
+
const u = { moodGuessed, job: text, product, description, audience, category: category || (industry[0] ?? "").replace(/-/g, " ") || null, industry, software, place: parts.place, does: parts.does, mood, tags, screen, kind, route: route ?? null, metaphor: metaphorFor({ product, industry, mood }), brand: { mode: brand.mode, rebrand: brand.rebrand, line: brand.line, brandColour: brand.brandColour, fonts: brand.fonts, customFonts: brand.customFonts, radii: brand.radii, components: brand.components, colours: brand.colours.slice(0, 24), tokenFiles: brand.tokenFiles } };
|
|
317
|
+
u.queries = queriesFor(u);
|
|
318
|
+
return u;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ---- the shipped list, plus what this user's research added
|
|
322
|
+
export function loadIndex() {
|
|
323
|
+
const shipped = readJson(INDEX_FILE, { sites: [], browse: [] });
|
|
324
|
+
const local = readJson(LOCAL_INDEX, { sites: [] });
|
|
325
|
+
const key = (s) => s.url.replace(/^https?:\/\/(www\.)?/, "").replace(/\/$/, "");
|
|
326
|
+
const by = new Map();
|
|
327
|
+
for (const s of [...(shipped.sites ?? []), ...(local.sites ?? []).map((s) => ({ ...s, local: true }))]) if (!by.has(key(s))) by.set(key(s), s);
|
|
328
|
+
return { sites: [...by.values()], browse: shipped.browse ?? [] };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Index entries for this job: competitors (same category) and well-crafted pages that share its mood.
|
|
332
|
+
export function matchIndex(u, { competitors = 4, craft = 3, exclude = [] } = {}) {
|
|
333
|
+
const { sites } = loadIndex();
|
|
334
|
+
const host = (url) => url.replace(/^https?:\/\/(www\.)?/, "").replace(/\/.*$/, "");
|
|
335
|
+
const skip = new Set(exclude.map(host));
|
|
336
|
+
const lead = u.industry[0];
|
|
337
|
+
const scored = sites
|
|
338
|
+
.filter((s) => !skip.has(host(s.url)))
|
|
339
|
+
.map((s) => {
|
|
340
|
+
const ind = (s.industry ?? []).filter((i) => u.industry.includes(i)).length;
|
|
341
|
+
const mood = (s.mood ?? []).filter((m) => u.mood.includes(m)).length;
|
|
342
|
+
const kind = (s.kind ?? []).includes(u.kind) ? 1 : 0;
|
|
343
|
+
return { ...s, competitor: Boolean(lead && (s.industry ?? []).includes(lead)), match: { industry: ind, mood, kind }, score: 3 * ind + 2 * mood + kind + ((s.industry ?? []).includes(lead) ? 2 : 0) };
|
|
344
|
+
});
|
|
345
|
+
const comp = scored.filter((s) => s.competitor).sort((a, b) => b.score - a.score || b.match.mood - a.match.mood).slice(0, competitors);
|
|
346
|
+
// A business (a bakery, an inn, a studio) gets craft picks that aren't software sites, or none: a note-taking
|
|
347
|
+
// app's landing page teaches a bakery nothing.
|
|
348
|
+
const crafted = scored.filter((s) => !s.competitor && s.match.mood > 0 && (u.software !== false || !softwareSite(s))).sort((a, b) => b.match.mood - a.match.mood || b.score - a.score).slice(0, craft);
|
|
349
|
+
return { competitors: comp, craft: crafted };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// "neighbourhood bakery" → "bakery"; "two-person risograph print studio" → "print studio"; "documentary wedding
|
|
353
|
+
// and portrait photographer" → "wedding and portrait photographer".
|
|
354
|
+
const BROAD = new Set(["dev-tools", "productivity", "creative", "collaboration", "ai", "commerce", "finance", "food", "education", "local-business"]);
|
|
355
|
+
const GENERIC_LAST =/^(shop|studio|practice|programme|program|operator|guide|library|workshop|foundry|service|company|agency|club|centre|center|school|house|inn|hostel|hotel|bar|kitchen|market|collective|co-op|cooperative)$/i;
|
|
356
|
+
export function headNoun(cat) {
|
|
357
|
+
const w = String(cat ?? "").trim().split(/\s+/).filter(Boolean);
|
|
358
|
+
if (w.length >= 4 && /^(and|&)$/i.test(w.at(-3))) return w.slice(-4).join(" ");
|
|
359
|
+
if (w.length >= 2 && GENERIC_LAST.test(w.at(-1))) return w.slice(-2).join(" ");
|
|
360
|
+
return w.at(-1) ?? "";
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// The searches the calling agent runs with its own web search (palate can't search).
|
|
364
|
+
export function queriesFor(u) {
|
|
365
|
+
const cat = u.category || u.industry[0]?.replace(/-/g, " ") || "product";
|
|
366
|
+
// "<leader> alternatives" only when the leader is in the product's own category: a broad bucket (dev-tools,
|
|
367
|
+
// finance) would make Linear the alternative to a CI service, or Stripe to a budgeting app.
|
|
368
|
+
const leaders = BROAD.has(u.industry[0]) ? [] : matchIndex(u, { competitors: 2, craft: 0 }).competitors.map((c) => c.name);
|
|
369
|
+
const year = new Date().getFullYear();
|
|
370
|
+
const screenPage = u.screen !== "other" && u.screen !== "landing" ? `${u.screen} page` : null;
|
|
371
|
+
// A business, not software: comparable businesses' own sites ("best bakery websites", "bakery Lisbon"), the
|
|
372
|
+
// category's leaders by name when palate knows them, never "<x> software" or "<famous SaaS> alternatives".
|
|
373
|
+
if (u.software === false) {
|
|
374
|
+
const noun = headNoun(cat);
|
|
375
|
+
const does = u.does ? u.does.split(/\s+/).slice(0, 12).join(" ").replace(/(\s+(the|a|an|and|of|on|in|your|their|for|to|with))+$/i, "") : null;
|
|
376
|
+
return uniq([
|
|
377
|
+
`best ${noun} websites${u.place ? ` ${u.place}` : ""}`,
|
|
378
|
+
u.place ? `${cat} ${u.place}` : null,
|
|
379
|
+
`${cat} website design`,
|
|
380
|
+
`independent ${noun.replace(/^independent\s+/i, "")} websites`,
|
|
381
|
+
does ? `${noun} ${does}` : null,
|
|
382
|
+
...leaders.map((l) => `sites like ${l}`),
|
|
383
|
+
u.audience ? `${noun} for ${u.audience}` : null,
|
|
384
|
+
]).slice(0, 6);
|
|
385
|
+
}
|
|
386
|
+
return uniq([
|
|
387
|
+
`${cat}${/\b(apps?|software|service|platform|cli|tools?|sdk|api|generator)\b/i.test(cat) ? "" : " software"}${u.audience ? ` for ${u.audience}` : ""}`,
|
|
388
|
+
/\bapps?$/i.test(cat) ? `best ${cat.replace(/s?$/, "s")} ${year}` : /\bapps?\b/i.test(cat) ? `best ${cat} ${year}` : `best ${cat} tools ${year}`,
|
|
389
|
+
...leaders.map((l) => `${l} alternatives`),
|
|
390
|
+
leaders.length ? null : `${cat} startups ${year}`,
|
|
391
|
+
`${u.mood[0] ?? "well designed"} ${cat}${/\bapps?\b/i.test(cat) ? "" : " app"}`,
|
|
392
|
+
screenPage ? `${leaders[0] ?? cat} ${screenPage}` : null,
|
|
393
|
+
u.brand?.mode === "brand" && screenPage ? `${cat} ${screenPage} examples` : null,
|
|
394
|
+
]).slice(0, 6);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ---- 2. look
|
|
398
|
+
const sectionsScript = script("page-sections.js");
|
|
399
|
+
const devicesScript = script("page-devices.js");
|
|
400
|
+
const fingerprintScript = script("page-fingerprint.js");
|
|
401
|
+
const REJECT = ["Reject all", "Reject All", "Reject", "Decline", "Decline all", "Only necessary", "Necessary only", "Use necessary cookies only", "Allow necessary only", "Deny"];
|
|
402
|
+
// Cookie banners: palate declines (never accepts); when there is no decline button, it hides the banner from the
|
|
403
|
+
// screenshot without answering it.
|
|
404
|
+
async function quietBanners(page) {
|
|
405
|
+
for (const label of REJECT) {
|
|
406
|
+
const b = page.getByRole("button", { name: label, exact: true }).first();
|
|
407
|
+
if (await b.isVisible().catch(() => false)) {
|
|
408
|
+
await b.click({ timeout: 2000 }).catch(() => {});
|
|
409
|
+
await page.waitForTimeout(400);
|
|
410
|
+
return "declined";
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return page
|
|
414
|
+
.evaluate(() => {
|
|
415
|
+
let n = 0;
|
|
416
|
+
for (const el of document.querySelectorAll('[id*="cookie" i],[class*="cookie" i],[id*="consent" i],[class*="consent" i],[aria-label*="cookie" i],#onetrust-banner-sdk,#CybotCookiebotDialog,.cc-window')) {
|
|
417
|
+
const p = getComputedStyle(el).position;
|
|
418
|
+
if (p === "fixed" || p === "sticky") {
|
|
419
|
+
el.style.setProperty("display", "none", "important");
|
|
420
|
+
n++;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return n ? "hidden" : "none";
|
|
424
|
+
})
|
|
425
|
+
.catch(() => "none");
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const slugOf = (url) => {
|
|
429
|
+
const u = new URL(url);
|
|
430
|
+
return (u.hostname.replace(/^www\./, "") + u.pathname).replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/g, "").slice(0, 60);
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
// One page, live: screenshots (first screen + full page) and every measurement palate takes of its own work.
|
|
434
|
+
export async function lookAt(browser, target, outDir, { dir, robotsCache, allowLocal = false, fetchImpl, rules = [] } = {}) {
|
|
435
|
+
const t0 = Date.now();
|
|
436
|
+
const gate = await mayLook(target.url, { allowLocal, cache: robotsCache, fetchImpl });
|
|
437
|
+
if (!gate.ok) return { ...target, status: "skipped", why: gate.why, ms: Date.now() - t0 };
|
|
438
|
+
const ua = `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${browser.version?.() ?? "130.0.0.0"} Safari/537.36 ${UA_SUFFIX}`;
|
|
439
|
+
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, userAgent: ua, acceptDownloads: false, serviceWorkers: "block", locale: "en-US" });
|
|
440
|
+
try {
|
|
441
|
+
const page = await ctx.newPage();
|
|
442
|
+
await page.addInitScript(MOTION_INIT);
|
|
443
|
+
let res;
|
|
444
|
+
try {
|
|
445
|
+
res = await page.goto(gate.url, { waitUntil: "domcontentloaded", timeout: 45_000 });
|
|
446
|
+
} catch (e) {
|
|
447
|
+
return { ...target, status: "skipped", why: `did not load (${String(e.message).split("\n")[0].slice(0, 80)})`, ms: Date.now() - t0 };
|
|
448
|
+
}
|
|
449
|
+
const status = res?.status() ?? 0;
|
|
450
|
+
if (status >= 400) return { ...target, status: "skipped", why: `answered ${status}${status === 403 || status === 429 ? " (blocks bots): skipped, not worked around" : ""}`, ms: Date.now() - t0 };
|
|
451
|
+
await page.waitForLoadState("networkidle", { timeout: 12_000 }).catch(() => {});
|
|
452
|
+
await page.waitForTimeout(900);
|
|
453
|
+
const finalUrl = page.url();
|
|
454
|
+
if (/\/(login|log-in|signin|sign-in|sso|auth)\b/i.test(new URL(finalUrl).pathname)) return { ...target, status: "skipped", why: "redirected to a sign-in page; palate never logs in", ms: Date.now() - t0 };
|
|
455
|
+
const wall = await page.evaluate(() => ({ title: document.title, text: (document.body?.innerText ?? "").slice(0, 2000), len: (document.body?.innerText ?? "").length, frames: [...document.querySelectorAll("iframe")].map((f) => f.src).join(" ") })).catch(() => ({ title: "", text: "", len: 0, frames: "" }));
|
|
456
|
+
if (BOT_WALL.test(wall.title) || (wall.len < 1500 && BOT_WALL.test(`${wall.text} ${wall.frames}`))) return { ...target, status: "skipped", why: "a bot check or CAPTCHA: skipped, never worked around", ms: Date.now() - t0 };
|
|
457
|
+
if (wall.len < 80) return { ...target, status: "skipped", why: "the page rendered almost no text", ms: Date.now() - t0 };
|
|
458
|
+
const banner = await quietBanners(page);
|
|
459
|
+
const slug = slugOf(finalUrl);
|
|
460
|
+
const fold = path.join(outDir, `${slug}-fold.png`);
|
|
461
|
+
await page.screenshot({ path: fold });
|
|
462
|
+
await scrollThrough(page, { pause: 160 }).catch(() => {});
|
|
463
|
+
const height = await page.evaluate(() => document.documentElement.scrollHeight).catch(() => 0);
|
|
464
|
+
const full = path.join(outDir, `${slug}-full.jpg`);
|
|
465
|
+
await page.screenshot({ path: full, fullPage: height <= 12_000, type: "jpeg", quality: 60, ...(height > 12_000 ? { clip: { x: 0, y: 0, width: 1440, height: 12_000 }, fullPage: true } : {}) }).catch(() => {});
|
|
466
|
+
const measures = await page.evaluate(MEASURE).catch(() => null);
|
|
467
|
+
const fp = await page.evaluate(`(${fingerprintScript})()`).catch(() => null);
|
|
468
|
+
const sections = await page.evaluate(`(${sectionsScript})()`).catch(() => []);
|
|
469
|
+
const devices = await page.evaluate(`(${devicesScript})(${JSON.stringify(sections)})`).catch(() => null);
|
|
470
|
+
const principles = await measurePrinciples(page, { sections }).catch(() => null);
|
|
471
|
+
const facts = rules.length ? (await measureRules(page, rules).catch(() => null))?.facts ?? null : null;
|
|
472
|
+
const layout = fp ? layoutRecord(dir ?? process.cwd(), "/", fp, { file: fold, kind: "marketing", source: "reference" }) : null;
|
|
473
|
+
const findings = principles ? principleFindings(principles) : [];
|
|
474
|
+
return {
|
|
475
|
+
...target,
|
|
476
|
+
url: finalUrl,
|
|
477
|
+
host: new URL(finalUrl).host,
|
|
478
|
+
status: "ok",
|
|
479
|
+
title: wall.title.slice(0, 120),
|
|
480
|
+
banner,
|
|
481
|
+
files: { fold, full: fs.existsSync(full) ? full : null },
|
|
482
|
+
measures,
|
|
483
|
+
fingerprint: fp ? { skeleton: fp.skeleton, cliches: fp.cliches, firstScreen: fp.firstScreen, style: fp.style } : null,
|
|
484
|
+
sections: sections.map((s) => s.name).slice(0, 24),
|
|
485
|
+
devices: devices ? { kickers: devices.kickers.length, kickerSections: uniq(devices.kickers.map((k) => k.section)).length, media: devices.media.map((m) => ({ kind: m.kind, section: m.section, framed: Boolean(m.framed) })), logoRows: devices.logoRows.map((r) => ({ section: r.section, count: r.count })), repeated: devices.repeated.map((r) => r.device), motion: { entrances: (devices.motion.entrances ?? 0) + (devices.motion.scriptedAnimations ?? 0) + devices.motion.cssAnimations, transitions: devices.motion.transitions } } : null,
|
|
486
|
+
principles: principles ? { facts: principleFacts(principles), counts: { high: findings.filter((f) => f.severity === "high").length, medium: findings.filter((f) => f.severity === "medium").length, low: findings.filter((f) => f.severity === "low").length }, findings: findings.slice(0, 6).map((f) => ({ id: f.id, area: f.area, severity: f.severity, what: f.what.slice(0, 200) })), spacing: { unit: principles.spacing?.unit ?? null, on8: principles.spacing?.on8 ?? null, offScale: principles.spacing?.offScaleShare ?? null }, type: { sizes: principles.type?.sizes?.length ?? null, scale: principles.type?.scale ?? null, families: (principles.type?.families ?? []).map((f) => f.family), weights: (principles.type?.weights ?? []).map((w) => w.weight) }, colours: { distinct: principles.consistency?.colors?.distinct ?? null, accent: principles.consistency?.colors?.accent ? { color: principles.consistency.colors.accent.colors?.[0], share: principles.consistency.colors.accent.share } : null }, density: (principles.density ?? []).map((d) => d.ink) } : null,
|
|
487
|
+
ruleFacts: facts,
|
|
488
|
+
layout: layout ? { shape: layout.shape, skeleton: layout.skeleton, firstScreen: layout.firstScreen, style: layout.style, moves: layout.moves, thumb: layout.thumb } : null,
|
|
489
|
+
height,
|
|
490
|
+
ms: Date.now() - t0,
|
|
491
|
+
};
|
|
492
|
+
} finally {
|
|
493
|
+
await ctx.close().catch(() => {});
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ---- 3. rank
|
|
498
|
+
const median = (a) => {
|
|
499
|
+
const s = [...a].filter((x) => Number.isFinite(x)).sort((x, y) => x - y);
|
|
500
|
+
return s.length ? s[s.length >> 1] : null;
|
|
501
|
+
};
|
|
502
|
+
const hexLum = (h) => {
|
|
503
|
+
const m = String(h ?? "").match(/^#?([0-9a-f]{6})$/i);
|
|
504
|
+
if (!m) return null;
|
|
505
|
+
const [r, g, b] = [0, 2, 4].map((i) => parseInt(m[1].slice(i, i + 2), 16));
|
|
506
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
// What a reference does, as named traits: what palate notices, what competitors share, what a product can learn.
|
|
510
|
+
const TRAITS = {
|
|
511
|
+
"live-product": { label: "the product shown live (video, canvas or real UI), not a still screenshot", lesson: "show the product working, not a still picture of it", tags: ["saas", "technical"] },
|
|
512
|
+
"serif-display": { label: "a serif display face", lesson: "a serif display face gives the voice a human, editorial register", tags: ["editorial", "crafted"] },
|
|
513
|
+
"modest-headline": { label: "a modest headline that lets the content lead", lesson: "a smaller headline makes room for the product to be the first thing seen", tags: ["calm", "minimal"] },
|
|
514
|
+
"huge-headline": { label: "a very large headline", lesson: "one oversized line can carry the whole first screen", tags: ["bold"] },
|
|
515
|
+
"light-display": { label: "a light-weight display", lesson: "a light display weight reads as quiet confidence", tags: ["calm", "premium"] },
|
|
516
|
+
airy: { label: "airy sections with lots of space", lesson: "generous space between few elements makes the page feel unhurried", tags: ["calm", "quiet", "minimal"] },
|
|
517
|
+
dense: { label: "dense, information-rich sections", lesson: "showing a lot at once can read as honesty and depth", tags: ["data-rich", "technical"] },
|
|
518
|
+
"one-accent": { label: "one accent colour used sparingly", lesson: "one accent, used rarely, makes every use of it mean something", tags: ["calm", "minimal", "premium"] },
|
|
519
|
+
"strict-rhythm": { label: "a strict spacing rhythm", lesson: "a strict spacing unit makes the whole page feel considered", tags: ["crafted", "technical"] },
|
|
520
|
+
"left-led": { label: "a left-aligned first screen", lesson: "a left-led first screen reads like a document, not an ad", tags: ["editorial"] },
|
|
521
|
+
"centred-hero": { label: "a centred hero stack", lesson: "a centred stack is clear but common", tags: [] },
|
|
522
|
+
"split-hero": { label: "a split hero (words left, product right)", lesson: "a split first screen pairs the claim with its proof", tags: ["saas"] },
|
|
523
|
+
"framed-shot": { label: "a framed product screenshot near the top", lesson: "a framed screenshot shows the product at a glance", tags: [] },
|
|
524
|
+
"logo-row": { label: "a customer logo row near the top", lesson: "early logos borrow trust", tags: ["trustworthy"] },
|
|
525
|
+
kickers: { label: "kicker labels above most headings", lesson: "small labels can organise a long page", tags: [] },
|
|
526
|
+
"equal-grid": { label: "a grid of equal feature cards", lesson: "a feature grid is scannable", tags: [] },
|
|
527
|
+
glow: { label: "a glow or gradient behind the hero", lesson: "a glow adds atmosphere", tags: [] },
|
|
528
|
+
dark: { label: "a dark theme", lesson: "a dark page reads as focused and premium", tags: ["cinematic", "premium"] },
|
|
529
|
+
pill: { label: "pill-shaped buttons", lesson: "pill buttons feel soft and friendly", tags: ["friendly"] },
|
|
530
|
+
moving: { label: "things fade or slide in on load and scroll", lesson: "motion that explains the product holds attention", tags: ["energetic"] },
|
|
531
|
+
still: { label: "a still page (nothing moves on load or scroll)", lesson: "a still page lets the words and type do the work", tags: ["calm", "quiet"] },
|
|
532
|
+
"long-page": { label: "a long page", lesson: "a long page can argue the case section by section", tags: ["editorial"] },
|
|
533
|
+
"short-page": { label: "a short page", lesson: "a short page says it once and stops", tags: ["minimal"] },
|
|
534
|
+
};
|
|
535
|
+
// The order a "could learn" pick prefers: distinctive, transferable ideas first.
|
|
536
|
+
const LEARN_ORDER = ["live-product", "serif-display", "modest-headline", "one-accent", "airy", "strict-rhythm", "left-led", "light-display", "still", "short-page", "huge-headline", "dense", "dark", "split-hero", "moving", "long-page", "pill"];
|
|
537
|
+
|
|
538
|
+
export function traitsOf(r) {
|
|
539
|
+
const out = [];
|
|
540
|
+
const m = r.measures ?? {};
|
|
541
|
+
const h = m.headline ?? {};
|
|
542
|
+
const p = r.principles ?? {};
|
|
543
|
+
const d = r.devices ?? {};
|
|
544
|
+
const shape = r.layout?.shape;
|
|
545
|
+
const sk = r.layout?.skeleton ?? r.fingerprint?.skeleton ?? [];
|
|
546
|
+
if (d.media?.some((x) => /^live/.test(x.kind))) out.push("live-product");
|
|
547
|
+
if (fontClass(h.family) === "serif" || /serif/i.test(h.family ?? "") && !/sans/i.test(h.family ?? "")) out.push("serif-display");
|
|
548
|
+
if (h.size && h.size <= 44) out.push("modest-headline");
|
|
549
|
+
if (h.size >= 72) out.push("huge-headline");
|
|
550
|
+
if (h.weight && Number(h.weight) <= 350) out.push("light-display");
|
|
551
|
+
const ink = median(p.density ?? []);
|
|
552
|
+
if (ink != null && ink < 22) out.push("airy");
|
|
553
|
+
if (ink != null && ink >= 45) out.push("dense");
|
|
554
|
+
if (p.colours?.accent && p.colours.accent.share != null && p.colours.accent.share < 3) out.push("one-accent");
|
|
555
|
+
if (p.spacing?.unit && p.spacing.offScale != null && p.spacing.offScale < 15) out.push("strict-rhythm");
|
|
556
|
+
if (shape === "centred") out.push("centred-hero");
|
|
557
|
+
if (shape === "split" || shape === "asymmetric") out.push("split-hero");
|
|
558
|
+
if (shape === "left-stack" || (sk[1] ?? sk[0] ?? "").startsWith("L")) out.push("left-led");
|
|
559
|
+
if (d.media?.some((x) => x.kind === "static picture" && x.framed && x.section <= 2)) out.push("framed-shot");
|
|
560
|
+
if (d.logoRows?.some((x) => x.section <= 3)) out.push("logo-row");
|
|
561
|
+
if ((d.kickerSections ?? 0) >= 3) out.push("kickers");
|
|
562
|
+
if (sk.some((t) => /g3/.test(t))) out.push("equal-grid");
|
|
563
|
+
if ((r.fingerprint?.cliches ?? []).includes("glow")) out.push("glow");
|
|
564
|
+
const bg = hexLum(r.layout?.style?.bg);
|
|
565
|
+
if (bg != null && bg < 90) out.push("dark");
|
|
566
|
+
if ((m.radii ?? []).includes("pill")) out.push("pill");
|
|
567
|
+
if (d.motion) out.push(d.motion.entrances >= 3 ? "moving" : d.motion.entrances === 0 ? "still" : null);
|
|
568
|
+
if (r.height > 9000) out.push("long-page");
|
|
569
|
+
if (r.height && r.height < 3600) out.push("short-page");
|
|
570
|
+
return uniq(out);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// 0–10, from the principles pass: fewer and milder findings, a clear spacing unit and a type scale score higher.
|
|
574
|
+
export function craftScore(r) {
|
|
575
|
+
const p = r.principles;
|
|
576
|
+
if (!p) return 5;
|
|
577
|
+
const c = p.counts ?? {};
|
|
578
|
+
let s = 10 - Math.min(6, 1.5 * (c.high ?? 0) + 0.6 * (c.medium ?? 0) + 0.15 * (c.low ?? 0));
|
|
579
|
+
if (p.spacing?.unit && (p.spacing.offScale ?? 100) < 15) s += 0.5;
|
|
580
|
+
if (p.type?.scale?.fit && p.type.scale.of && p.type.scale.fit / p.type.scale.of >= 0.7) s += 0.5;
|
|
581
|
+
if ((p.type?.sizes ?? 0) > 10) s -= 0.5;
|
|
582
|
+
if ((p.type?.families ?? []).length > 2) s -= 0.3 * ((p.type.families.length ?? 2) - 2);
|
|
583
|
+
return Math.max(0, Math.min(10, +s.toFixed(1)));
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// What this user said about research references before (likes and dislikes, by trait), and the directions they
|
|
587
|
+
// chose for this kind of screen: palate's history for ranking and auto-pick.
|
|
588
|
+
export function tasteHistory(screen) {
|
|
589
|
+
const traits = {};
|
|
590
|
+
const verdicts = readLines(RESEARCH_LOG).filter((x) => x.kind === "ref-verdict");
|
|
591
|
+
for (const v of verdicts.filter((v) => !screen || !v.screen || v.screen === screen)) for (const t of v.traits ?? []) traits[t] = (traits[t] ?? 0) + (v.verdict === "like" ? 1 : -1);
|
|
592
|
+
const chosen = readLines(path.join(TASTE_DIR, "directions.jsonl")).filter((d) => d.chosen && (!screen || d.screen === screen));
|
|
593
|
+
const moves = {};
|
|
594
|
+
for (const d of chosen) if (d.signature?.move) moves[d.signature.move] = (moves[d.signature.move] ?? 0) + (d.auto && !d.kept ? 0.5 : 1);
|
|
595
|
+
return { traits, moves, chosen: chosen.length, verdicts: verdicts.length };
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// The user's taste for one reference: their rules (a failing rule costs), their profile's wins and losses, past
|
|
599
|
+
// research verdicts on the same traits, and how well it matches the job's mood.
|
|
600
|
+
export function tasteScore(r, u, { rules = [], profile = "", history = tasteHistory(u.screen) } = {}) {
|
|
601
|
+
let s = 0.5;
|
|
602
|
+
const notes = [];
|
|
603
|
+
if (r.ruleFacts && rules.length) {
|
|
604
|
+
const fails = checkRules(rules, r.ruleFacts).filter((x) => x.status === "fail");
|
|
605
|
+
if (fails.length) {
|
|
606
|
+
s -= 0.15 * fails.length;
|
|
607
|
+
notes.push(`fails your rule${fails.length > 1 ? "s" : ""} ${fails.map((f) => f.id).join(", ")}`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
const traits = traitsOf(r);
|
|
611
|
+
const loses = (profile.match(/\*\*Loses:?\*\*[^\n]*/gi) ?? []).join(" ").toLowerCase();
|
|
612
|
+
const wins = (profile.match(/\*\*Wins:?\*\*[^\n]*/gi) ?? []).join(" ").toLowerCase();
|
|
613
|
+
const KEY = { "framed-shot": /screenshot|frame/, kickers: /kicker|label/, "equal-grid": /equal (tiles|cards)|grid of equal/, glow: /glow|gradient/, "centred-hero": /centred hero|centered hero/, dark: /dark/, dense: /dense|density/, airy: /white ?space|airy|calm/, "live-product": /live|real (ui|product)|demo/ };
|
|
614
|
+
for (const t of traits) {
|
|
615
|
+
const re = KEY[t];
|
|
616
|
+
if (re && re.test(loses)) {
|
|
617
|
+
s -= 0.06;
|
|
618
|
+
notes.push(`your profile tends to reject ${TRAITS[t].label}`);
|
|
619
|
+
} else if (re && re.test(wins)) {
|
|
620
|
+
s += 0.04;
|
|
621
|
+
notes.push(`your profile tends to keep ${TRAITS[t].label}`);
|
|
622
|
+
}
|
|
623
|
+
const past = history.traits[t];
|
|
624
|
+
if (past) {
|
|
625
|
+
s += Math.max(-0.2, Math.min(0.2, 0.07 * past));
|
|
626
|
+
notes.push(`you ${past > 0 ? "liked" : "disliked"} ${TRAITS[t].label} in earlier research (${past > 0 ? "+" : ""}${past})`);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
const moodHit = (r.mood ?? []).filter((m) => u.mood.includes(m)).length;
|
|
630
|
+
if (moodHit) s += 0.05 * moodHit;
|
|
631
|
+
return { score: Math.max(0, Math.min(1, +s.toFixed(2))), notes, traits };
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Best 4–6, diverse: at least two competitors (for the cliché list) and the craft picks, never two near-identical.
|
|
635
|
+
export function rankRefs(refs, u, { keep, rules = [], profile = "", history } = {}) {
|
|
636
|
+
const ok = refs.filter((r) => r.status === "ok");
|
|
637
|
+
const want = keep ?? Math.min(6, Math.max(4, Math.ceil(ok.length * 0.6)));
|
|
638
|
+
for (const r of ok) {
|
|
639
|
+
r.craft = r.craft ?? craftScore(r);
|
|
640
|
+
const t = tasteScore(r, u, { rules, profile, history });
|
|
641
|
+
r.taste = t.score;
|
|
642
|
+
r.tasteNotes = t.notes;
|
|
643
|
+
r.traits = t.traits;
|
|
644
|
+
r.score = +(0.6 * (r.craft / 10) + 0.4 * r.taste + (r.role === "competitor" ? 0.03 : 0)).toFixed(3);
|
|
645
|
+
}
|
|
646
|
+
const kept = [];
|
|
647
|
+
const dropped = [];
|
|
648
|
+
const sim = (a, b) => (a.layout && b.layout ? compare(a.layout, b.layout).total : 0);
|
|
649
|
+
const take = (pool, n) => {
|
|
650
|
+
for (let i = 0; i < n; i++) {
|
|
651
|
+
const cands = pool.filter((r) => !kept.includes(r) && !dropped.some((d) => d.ref === r));
|
|
652
|
+
if (!cands.length || kept.length >= want) return;
|
|
653
|
+
let best = null;
|
|
654
|
+
for (const r of cands) {
|
|
655
|
+
const near = kept.map((k) => ({ k, s: sim(r, k) })).sort((a, b) => b.s - a.s)[0];
|
|
656
|
+
if (near && near.s >= DUPLICATE) {
|
|
657
|
+
dropped.push({ ref: r, why: `near-duplicate of ${near.k.name ?? near.k.host} (${Math.round(near.s * 100)}%)` });
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
const v = r.score - 0.5 * (near?.s ?? 0);
|
|
661
|
+
if (!best || v > best.v) best = { r, v, near };
|
|
662
|
+
}
|
|
663
|
+
if (best) {
|
|
664
|
+
best.r.diversity = best.near ? +(1 - best.near.s).toFixed(2) : 1;
|
|
665
|
+
kept.push(best.r);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
const byScore = (a, b) => b.score - a.score;
|
|
670
|
+
// The pages the agent found (--urls) or the user gave (--inspire) are the research: every one that loaded is
|
|
671
|
+
// kept, first; palate's own picks only fill the slots left and never push one of them out.
|
|
672
|
+
const theirs = ok.filter((r) => r.source === "found" || r.source === "given").sort(byScore);
|
|
673
|
+
for (const r of theirs) {
|
|
674
|
+
const near = kept.map((k) => ({ k, s: sim(r, k) })).sort((a, b) => b.s - a.s)[0];
|
|
675
|
+
r.diversity = near ? +(1 - near.s).toFixed(2) : 1;
|
|
676
|
+
kept.push(r);
|
|
677
|
+
}
|
|
678
|
+
const mine = ok.filter((r) => !theirs.includes(r));
|
|
679
|
+
const has = (comp) => kept.filter((r) => (r.role === "competitor") === comp).length;
|
|
680
|
+
take(mine.filter((r) => r.role === "competitor").sort(byScore), Math.max(0, 2 - has(true)));
|
|
681
|
+
take(mine.filter((r) => r.role !== "competitor").sort(byScore), Math.max(0, 2 - has(false)));
|
|
682
|
+
take([...mine].sort(byScore), want);
|
|
683
|
+
for (const r of ok) if (!kept.includes(r) && !dropped.some((d) => d.ref === r)) dropped.push({ ref: r, why: `lower score (${r.score}) than the kept ${kept.length}` });
|
|
684
|
+
return { kept, dropped };
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// ---- 4. distil
|
|
688
|
+
// Only template-ish habits count as a cliché; a shared virtue (a strict rhythm, one accent) is not one.
|
|
689
|
+
const CLICHE_ELIGIBLE = new Set(["centred-hero", "split-hero", "framed-shot", "logo-row", "kickers", "equal-grid", "glow", "pill", "huge-headline", "dark", "moving", "serif-display"]);
|
|
690
|
+
const SYSTEM_FONT = /^(ui-|system-ui|-apple-system|blinkmacsystemfont|sans-serif|serif|monospace|arial|helvetica|segoe ui|roboto$)/i;
|
|
691
|
+
const fontName = (f) => String(f ?? "").replace(/[-_ ](regular|medium|bold|book|light|\d{2,3}\w*)$/i, "").trim();
|
|
692
|
+
// What the competitors all do: traits shared by at least 60% of them (at least two pages).
|
|
693
|
+
export function clichesOf(refs) {
|
|
694
|
+
const comp = refs.filter((r) => r.status === "ok" && r.role === "competitor");
|
|
695
|
+
const pool = comp.length >= 2 ? comp : refs.filter((r) => r.status === "ok");
|
|
696
|
+
if (pool.length < 2) return [];
|
|
697
|
+
const count = {};
|
|
698
|
+
for (const r of pool) for (const t of (r.traits ?? traitsOf(r)).filter((x) => CLICHE_ELIGIBLE.has(x))) count[t] = (count[t] ?? 0) + 1;
|
|
699
|
+
const shared = Object.entries(count).filter(([, n]) => n >= Math.max(2, Math.ceil(pool.length * 0.6)));
|
|
700
|
+
// Fonts too: the same family on most of them is a cliché of its own.
|
|
701
|
+
const fonts = {};
|
|
702
|
+
for (const r of pool) for (const f of (r.measures?.fonts ?? []).filter((x) => !SYSTEM_FONT.test(x)).slice(0, 1).map(fontName)) fonts[f] = (fonts[f] ?? 0) + 1;
|
|
703
|
+
const sharedFonts = Object.entries(fonts).filter(([, n]) => n >= Math.max(2, Math.ceil(pool.length * 0.6)));
|
|
704
|
+
return [
|
|
705
|
+
...shared.map(([t, n]) => ({ id: t, label: TRAITS[t]?.label ?? t, of: `${n} of ${pool.length}` })),
|
|
706
|
+
...sharedFonts.map(([f, n]) => ({ id: `font:${f}`, label: `${f} as the main typeface`, of: `${n} of ${pool.length}` })),
|
|
707
|
+
];
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const weightWord = (w) => (Number(w) <= 300 ? "light" : Number(w) <= 450 ? "regular" : Number(w) <= 650 ? "semibold" : "heavy");
|
|
711
|
+
// What makes it work, drafted from the measurements (the calling agent refines it from the screenshot).
|
|
712
|
+
export function whatWorks(r) {
|
|
713
|
+
const out = [];
|
|
714
|
+
const m = r.measures ?? {};
|
|
715
|
+
const h = m.headline ?? {};
|
|
716
|
+
const p = r.principles ?? {};
|
|
717
|
+
if (h.size && m.bodySize) out.push(`A ${Math.round(h.size)}px ${weightWord(h.weight)} ${h.family || "?"} headline over ${m.bodySize}px body (a ${(h.size / m.bodySize).toFixed(1)}× step): one clear voice on the first screen.`);
|
|
718
|
+
if (p.colours?.distinct != null) out.push(`${p.colours.distinct} colours in all${p.colours.accent ? `, one accent (${p.colours.accent.color}) on about ${p.colours.accent.share < 0.1 ? "<0.1" : p.colours.accent.share}% of the page` : ", no accent hue: contrast and type carry it"}.`);
|
|
719
|
+
const ink = median(p.density ?? []);
|
|
720
|
+
if (p.spacing) out.push(`${p.spacing.unit ? `A ${p.spacing.unit}px spacing unit (${100 - (p.spacing.offScale ?? 0)}% on scale)` : "No strict spacing unit"}; sections are ${ink == null ? "?" : ink < 22 ? "airy" : ink >= 45 ? "dense" : "balanced"}${ink != null ? ` (median ${ink}% ink)` : ""}.`);
|
|
721
|
+
if (r.layout?.shape) out.push(`${(ARCH_NAME[r.layout.shape] ?? r.layout.shape).replace(/^./, (c) => c.toUpperCase())} first screen; ${r.layout.skeleton.length} bands (${r.layout.skeleton.slice(0, 7).join(" · ")}).`);
|
|
722
|
+
const live = r.devices?.media?.filter((x) => /^live/.test(x.kind)).length ?? 0;
|
|
723
|
+
const pics = r.devices?.media?.filter((x) => x.kind === "static picture").length ?? 0;
|
|
724
|
+
if (r.devices) out.push(`The product is shown ${live ? `live in ${live} place(s)` : pics ? `as ${pics} still picture(s)` : "in words, not pictures"}; ${r.devices.motion?.entrances ? `${r.devices.motion.entrances} things animate on load or scroll` : "nothing animates on load or scroll"}.`);
|
|
725
|
+
const c = p.counts;
|
|
726
|
+
if (c) out.push(c.high + c.medium <= 2 ? "Clean alignment and hierarchy: the principles pass found almost nothing." : `The principles pass found ${c.high} high and ${c.medium} medium issues (${uniq((p.findings ?? []).map((f) => f.area)).slice(0, 3).join(", ")}): good, not flawless.`);
|
|
727
|
+
return out;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function learnFrom(r, kept, cliches, product, used = new Set()) {
|
|
731
|
+
const shared = new Set([...cliches.map((c) => c.id), ...used]);
|
|
732
|
+
const others = kept.filter((k) => k !== r);
|
|
733
|
+
const own = (r.traits ?? []).filter((t) => !shared.has(t) && TRAITS[t]);
|
|
734
|
+
const pick = LEARN_ORDER.filter((t) => own.includes(t)).sort((a, b) => others.filter((o) => (o.traits ?? []).includes(a)).length - others.filter((o) => (o.traits ?? []).includes(b)).length)[0];
|
|
735
|
+
return pick ? { trait: pick, line: `${product ?? "This product"} could learn: ${TRAITS[pick].lesson}.` } : { trait: null, line: `${product ?? "This product"} could learn: (palate found nothing measurable that sets it apart; say what from the screenshot).` };
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function movesFor(r, u) {
|
|
739
|
+
const tags = uniq([...(r.traits ?? []).flatMap((t) => TRAITS[t]?.tags ?? []), ...(r.mood ?? []).filter((m) => ["calm", "crafted", "editorial", "playful", "bold", "technical", "minimal", "premium", "warm", "friendly", "quiet"].includes(m))]);
|
|
740
|
+
return matchMovesSafe({ tags, limit: 3 }).map((m) => ({ id: m.id, name: m.name, why: m.reason }));
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// ---- the whole run
|
|
744
|
+
const nameOf = (u) => [u.product, u.screen !== "other" ? u.screen : null].filter(Boolean).join(" ").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "research";
|
|
745
|
+
export const researchRoot = (dir) => path.join(dir, ".palate", "research");
|
|
746
|
+
export function loadResearch(dir, name) {
|
|
747
|
+
const root = researchRoot(dir);
|
|
748
|
+
if (!fs.existsSync(root)) return null;
|
|
749
|
+
const names = fs.readdirSync(root).filter((n) => fs.existsSync(path.join(root, n, "research.json")));
|
|
750
|
+
const pick = name && name !== "latest" ? names.find((n) => n === name) : names.map((n) => ({ n, t: fs.statSync(path.join(root, n, "research.json")).mtimeMs })).sort((a, b) => b.t - a.t)[0]?.n;
|
|
751
|
+
return pick ? { ...readJson(path.join(root, pick, "research.json"), {}), dir: path.join(root, pick) } : null;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function targetsFor(u, { urls = [], inspire = [], index = true } = {}) {
|
|
755
|
+
const out = [];
|
|
756
|
+
const seen = new Set();
|
|
757
|
+
const push = (url, extra) => {
|
|
758
|
+
const pub = publicUrl(url, { allowLocal: true });
|
|
759
|
+
if (!pub.ok) return out.push({ url, status: "skipped", why: pub.why, ...extra });
|
|
760
|
+
const key = pub.url.replace(/^https?:\/\/(www\.)?/, "").replace(/\/$/, "");
|
|
761
|
+
if (seen.has(key)) return;
|
|
762
|
+
seen.add(key);
|
|
763
|
+
out.push({ url: pub.url, host: pub.host, ...extra });
|
|
764
|
+
};
|
|
765
|
+
const fromIndex = matchIndex(u, { competitors: 6, craft: 6 });
|
|
766
|
+
const indexed = new Map([...fromIndex.competitors, ...fromIndex.craft].map((s) => [s.url.replace(/^https?:\/\/(www\.)?/, "").replace(/\/$/, ""), s]));
|
|
767
|
+
const meta = (url) => indexed.get(url.replace(/^https?:\/\/(www\.)?/, "").replace(/\/$/, "")) ?? loadIndex().sites.find((s) => s.url.replace(/^https?:\/\/(www\.)?/, "").replace(/\/$/, "") === url.replace(/^https?:\/\/(www\.)?/, "").replace(/\/$/, "")) ?? null;
|
|
768
|
+
for (const url of urls) {
|
|
769
|
+
const s = meta(publicUrl(url, { allowLocal: true }).url ?? url);
|
|
770
|
+
push(url, { role: s && !s.industry?.includes(u.industry[0]) ? "craft" : "competitor", source: "found", name: s?.name, mood: s?.mood, industry: s?.industry });
|
|
771
|
+
}
|
|
772
|
+
for (const url of inspire) {
|
|
773
|
+
const s = meta(publicUrl(url, { allowLocal: true }).url ?? url);
|
|
774
|
+
push(url, { role: "craft", source: "given", name: s?.name, mood: s?.mood, industry: s?.industry });
|
|
775
|
+
}
|
|
776
|
+
if (index) {
|
|
777
|
+
const comps = out.filter((t) => t.role === "competitor").length;
|
|
778
|
+
for (const s of fromIndex.competitors.slice(0, Math.max(0, 4 - comps))) push(s.url, { role: "competitor", source: "palate list", name: s.name, mood: s.mood, industry: s.industry, note: s.note });
|
|
779
|
+
const crafts = out.filter((t) => t.role === "craft").length;
|
|
780
|
+
for (const s of fromIndex.craft.slice(0, Math.max(0, 3 - crafts))) push(s.url, { role: "craft", source: "palate list", name: s.name, mood: s.mood, industry: s.industry, note: s.note });
|
|
781
|
+
}
|
|
782
|
+
const live = out.filter((t) => t.status !== "skipped");
|
|
783
|
+
return { targets: live.slice(0, MAX_CAPTURES), over: live.slice(MAX_CAPTURES), invalid: out.filter((t) => t.status === "skipped") };
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const hostName = (r) => r.name ?? r.host?.replace(/^www\./, "") ?? r.url;
|
|
787
|
+
|
|
788
|
+
// urls: null → step 1 only (the understanding and the searches to run). urls: [] → look at palate's own picks only.
|
|
789
|
+
export async function research(dir, job, { route, urls = null, inspire = [], name, keep, allowLocal = false, fetchImpl, index = true, browser: given, log = true } = {}) {
|
|
790
|
+
const t0 = Date.now();
|
|
791
|
+
if (route) route = normalizeRoute(route);
|
|
792
|
+
const u = understand(dir, job, { route });
|
|
793
|
+
name ??= nameOf(u);
|
|
794
|
+
const session = path.join(researchRoot(dir), name);
|
|
795
|
+
fs.mkdirSync(session, { recursive: true });
|
|
796
|
+
fs.writeFileSync(path.join(dir, ".palate", ".gitignore"), "*\n");
|
|
797
|
+
const picks = matchIndex(u);
|
|
798
|
+
if (urls == null) {
|
|
799
|
+
const data = { name, stage: "understood", created: new Date().toISOString(), understanding: u, picks: { competitors: picks.competitors.map((s) => s.url), craft: picks.craft.map((s) => s.url) } };
|
|
800
|
+
fs.writeFileSync(path.join(session, "research.json"), JSON.stringify(data, null, 2));
|
|
801
|
+
return { data, session, text: understoodText(u, data, session, picks) };
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const { targets, over, invalid } = targetsFor(u, { urls, inspire, index });
|
|
805
|
+
const rules = activeRules().filter((r) => appliesTo(r, kindTags({ screen: u.screen, kind: "marketing", route }), { route, project: projectOf(dir) }));
|
|
806
|
+
const profile = read(path.join(TASTE_DIR, "profile.md"));
|
|
807
|
+
const robotsCache = new Map();
|
|
808
|
+
const shots = path.join(session, "shots");
|
|
809
|
+
fs.mkdirSync(shots, { recursive: true });
|
|
810
|
+
const own = given ? null : await acquireBrowser();
|
|
811
|
+
const browser = given ?? own.browser;
|
|
812
|
+
let refs;
|
|
813
|
+
try {
|
|
814
|
+
refs = await politeRun(targets.map((t) => ({ host: t.host, run: () => lookAt(browser, t, shots, { dir, robotsCache, allowLocal, fetchImpl, rules }) })));
|
|
815
|
+
refs = refs.map((r, i) => (r?.error ? { ...targets[i], status: "skipped", why: r.error.slice(0, 120) } : r));
|
|
816
|
+
const history = tasteHistory(u.screen);
|
|
817
|
+
const { kept, dropped } = rankRefs(refs, u, { keep, rules, profile, history });
|
|
818
|
+
const cliches = clichesOf(refs);
|
|
819
|
+
kept.forEach((r, i) => {
|
|
820
|
+
r.id = `R${i + 1}`;
|
|
821
|
+
r.works = whatWorks(r);
|
|
822
|
+
r.learn = learnFrom(r, kept, cliches, u.product, new Set(kept.slice(0, i).map((k) => k.learn?.trait)));
|
|
823
|
+
r.moves = movesFor(r, u);
|
|
824
|
+
});
|
|
825
|
+
const contact = kept.length ? path.join(session, "contact.png") : null;
|
|
826
|
+
if (contact) await contactSheet(browser, [{ label: `Kept for ${u.product ?? u.job.slice(0, 40)} (${kept.length} of ${refs.filter((r) => r.status === "ok").length} looked at) · private moodboard, never shipped`, cells: kept.map((r) => ({ caption: `${r.id} ${hostName(r)} · ${r.role} · craft ${r.craft}`, file: r.files.fold })) }], contact, { cell: 360, maxCell: 240 });
|
|
827
|
+
const data = {
|
|
828
|
+
name,
|
|
829
|
+
stage: "done",
|
|
830
|
+
created: new Date().toISOString(),
|
|
831
|
+
ms: Date.now() - t0,
|
|
832
|
+
understanding: u,
|
|
833
|
+
looked: refs.length,
|
|
834
|
+
kept: kept.map(slim),
|
|
835
|
+
cliches,
|
|
836
|
+
dropped: [...dropped.map((d) => ({ url: d.ref.url, name: hostName(d.ref), why: d.why, layout: d.ref.layout })), ...refs.filter((r) => r.status !== "ok").map((r) => ({ url: r.url, name: hostName(r), why: r.why })), ...over.map((r) => ({ url: r.url, why: `over the ${MAX_CAPTURES}-page limit for one run` })), ...invalid.map((r) => ({ url: r.url, why: r.why }))],
|
|
837
|
+
fonts: uniq(refs.filter((r) => r.status === "ok").flatMap((r) => (r.measures?.fonts ?? []).filter((x) => !SYSTEM_FONT.test(x)).slice(0, 2).map(fontName))),
|
|
838
|
+
contact,
|
|
839
|
+
history: { verdicts: history.verdicts, chosen: history.chosen },
|
|
840
|
+
};
|
|
841
|
+
const md = researchMarkdown(data, session);
|
|
842
|
+
fs.writeFileSync(path.join(session, "research.md"), md);
|
|
843
|
+
fs.writeFileSync(path.join(session, "research.json"), JSON.stringify(data, null, 1));
|
|
844
|
+
rememberSites(data);
|
|
845
|
+
const cost = estimate({ mode: "research", briefs: [md] });
|
|
846
|
+
if (log) logCost(dir, name, "research", cost, { looked: refs.length, kept: kept.length, ms: data.ms });
|
|
847
|
+
return { data, session, text: doneText(data, session, cost) };
|
|
848
|
+
} finally {
|
|
849
|
+
if (own) await own.release();
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const slim = (r) => ({ id: r.id, url: r.url, name: hostName(r), role: r.role, source: r.source, title: r.title, craft: r.craft, taste: r.taste, tasteNotes: r.tasteNotes, score: r.score, diversity: r.diversity, traits: r.traits, works: r.works, learn: r.learn, moves: r.moves, measures: r.measures ? { headline: r.measures.headline, fonts: r.measures.fonts, typeScale: r.measures.typeScale, bodySize: r.measures.bodySize, weights: r.measures.weights, radii: r.measures.radii, contentWidth: r.measures.contentWidth } : null, palette: r.layout?.style ? { bg: r.layout.style.bg, text: r.layout.style.text, accent: r.layout.style.accent } : null, principles: r.principles ? { facts: r.principles.facts, counts: r.principles.counts } : null, devices: r.devices, sections: r.sections, files: r.files, layout: r.layout, ms: r.ms, mood: r.mood, banner: r.banner });
|
|
854
|
+
|
|
855
|
+
// Pages this user's research kept join their own list (links and palate's tags only), so the list grows.
|
|
856
|
+
function rememberSites(data) {
|
|
857
|
+
try {
|
|
858
|
+
const local = readJson(LOCAL_INDEX, { sites: [] });
|
|
859
|
+
const has = new Set(local.sites.map((s) => s.url));
|
|
860
|
+
const shipped = new Set(loadIndex().sites.filter((s) => !s.local).map((s) => s.url.replace(/\/$/, "")));
|
|
861
|
+
for (const r of data.kept) {
|
|
862
|
+
const url = r.url.replace(/\/$/, "");
|
|
863
|
+
if (has.has(url) || shipped.has(url)) continue;
|
|
864
|
+
local.sites.push({ url, name: r.name, kind: [data.understanding.kind], mood: data.understanding.mood.slice(0, 3), industry: r.role === "competitor" ? data.understanding.industry.slice(0, 2) : [], note: `Kept by your research for ${data.understanding.product ?? data.understanding.job.slice(0, 40)} (craft ${r.craft}).`, added: new Date().toISOString().slice(0, 10) });
|
|
865
|
+
}
|
|
866
|
+
fs.mkdirSync(path.dirname(LOCAL_INDEX), { recursive: true });
|
|
867
|
+
fs.writeFileSync(LOCAL_INDEX, JSON.stringify(local, null, 1));
|
|
868
|
+
} catch {}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// ---- texts
|
|
872
|
+
function understoodText(u, data, session, picks) {
|
|
873
|
+
return [
|
|
874
|
+
`# Research: ${u.product ?? u.job}`,
|
|
875
|
+
"",
|
|
876
|
+
understandingLines(u).join("\n"),
|
|
877
|
+
"",
|
|
878
|
+
"## Search (you, with your own web search tool: palate can't search)",
|
|
879
|
+
...u.queries.map((q) => `- ${q}`),
|
|
880
|
+
u.software === false
|
|
881
|
+
? `From the results take 4–8 **homepages of real ${u.category ?? "businesses"} sites** (each business's own site), the best-made ones you can find. Not listicles, directories, review sites, marketplaces or design galleries.`
|
|
882
|
+
: "From the results take 4–8 **official product homepages** (the vendor's own site) of competitors and category leaders. Not listicles, review sites, app stores or design galleries.",
|
|
883
|
+
"",
|
|
884
|
+
"## palate's own picks (kb/inspiration, added automatically)",
|
|
885
|
+
...picks.competitors.map((s) => `- competitor: ${s.name} ${s.url}: ${s.note}`),
|
|
886
|
+
...picks.craft.map((s) => `- craft: ${s.name} ${s.url} (${s.mood.join(", ")}): ${s.note}`),
|
|
887
|
+
"",
|
|
888
|
+
"## Next (no need to ask the user)",
|
|
889
|
+
`Call palate_research again with the same job and urls: [the homepages you found] (CLI: palate research "<job>" --urls a,b,c). palate looks at them and its own picks live (robots.txt respected, a few seconds apart per site, at most ${MAX_CAPTURES} pages), ranks them and writes the research. To skip the search: urls: [] (palate's own picks only).`,
|
|
890
|
+
`State: ${slash(path.join(session, "research.json"))}`,
|
|
891
|
+
].join("\n");
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
export function understandingLines(u) {
|
|
895
|
+
return [
|
|
896
|
+
`- Product: ${u.product ?? "(not named)"}${u.description ? `, ${u.description}` : ""}`,
|
|
897
|
+
`- Audience: ${u.audience ?? "(not named)"} · industry: ${u.industry.join(", ") || "?"} · screen: ${u.screen}`,
|
|
898
|
+
`- Mood: ${u.mood.join(", ")}${u.moodGuessed ? " (a guess: the job names no mood; the blueprint decides it from the audience and the product)" : ""} · moves tags: ${u.tags.join(", ") || "-"}`,
|
|
899
|
+
`- Working metaphor (${u.metaphor.from}): ${u.metaphor.primary}${u.metaphor.alternates.length ? `. Also: ${u.metaphor.alternates.join("; ")}` : ""}`,
|
|
900
|
+
`- ${u.brand.mode === "brand" ? "Brand detected" : "No brand yet"}: ${u.brand.line}`,
|
|
901
|
+
];
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function researchMarkdown(d, session) {
|
|
905
|
+
const u = d.understanding;
|
|
906
|
+
const lines = [
|
|
907
|
+
`# Research: ${u.product ?? u.job}`,
|
|
908
|
+
"",
|
|
909
|
+
`> ${u.job}`,
|
|
910
|
+
"",
|
|
911
|
+
...understandingLines(u),
|
|
912
|
+
`- Looked at ${d.looked} public page(s) live on ${d.created.slice(0, 10)} (${Math.round(d.ms / 1000)}s), kept ${d.kept.length}. robots.txt respected, one page at a time per site, nothing logged into or submitted. Screenshots are this project's private moodboard (gitignored); nothing was uploaded.`,
|
|
913
|
+
d.contact ? `- Contact sheet: ${slash(d.contact)}` : null,
|
|
914
|
+
"",
|
|
915
|
+
`## The kept references`,
|
|
916
|
+
"palate drafted the notes below from measurements. **Open the contact sheet and each first-screen screenshot and refine them in plain words**: what makes it work, and the one thing worth learning.",
|
|
917
|
+
"",
|
|
918
|
+
];
|
|
919
|
+
for (const r of d.kept) {
|
|
920
|
+
lines.push(
|
|
921
|
+
`### ${r.id} ${r.name} (${r.url}) · ${r.role}${r.source === "palate list" ? ", from palate's list" : ""}`,
|
|
922
|
+
`craft ${r.craft}/10 · taste ${r.taste} · diversity ${r.diversity ?? 1}${r.tasteNotes?.length ? ` · ${r.tasteNotes.join("; ")}` : ""}`,
|
|
923
|
+
"",
|
|
924
|
+
"**What makes it work** (drafted from measurements)",
|
|
925
|
+
...r.works.map((w) => `- ${w}`),
|
|
926
|
+
"",
|
|
927
|
+
`**Moves**: ${r.moves.length ? r.moves.map((m) => `${m.id} (${m.name})`).join(", ") : "none matched in the moves library"}`,
|
|
928
|
+
`**Traits**: ${(r.traits ?? []).map((t) => TRAITS[t]?.label ?? t).join("; ") || "-"}`,
|
|
929
|
+
`**${r.learn.line.replace(/:.*/, "")}**:${r.learn.line.replace(/^[^:]*:/, "")}`,
|
|
930
|
+
`Screenshots: ${slash(r.files.fold)}${r.files.full ? ` · ${slash(r.files.full)}` : ""}`,
|
|
931
|
+
"",
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
lines.push("## What the competitors all do (the cliché to avoid)", ...(d.cliches.length ? d.cliches.map((c) => `- ${c.label} (${c.of})`) : ["- Too few competitors looked at to call anything a cliché."]), "");
|
|
935
|
+
// A cliché is an execution; a truth is what makes the category itself. Avoiding a cliché must not throw away
|
|
936
|
+
// the truth under it (Kiln, Oct 27: avoiding terracotta-on-beige lost the warmth of clay altogether).
|
|
937
|
+
lines.push("## Category truths (keep, even when every competitor uses them)", "- (fill in from the screenshots: the materials, objects, rituals and words that make this category itself. For pottery: clay, glaze, the kiln, hands, the shelf of finished pieces. Avoid the cliché's execution, keep its truth.)", "");
|
|
938
|
+
if (d.fonts.length) lines.push("## Their fonts (measured; theirs, never load them)", `- ${d.fonts.join(", ")}`, "");
|
|
939
|
+
if (d.dropped.length) lines.push("## Left out", ...d.dropped.map((x) => `- ${x.name ?? x.url}: ${x.why}`), "");
|
|
940
|
+
lines.push("## The rule for everything built from this", `- ${LICENCE_RULE}`, "", "## Next", `palate_direction with the job and from_research "${d.name}" (CLI: palate direction "<job>" --from-research ${d.name}): 3 art directions, each with a metaphor and one signature moment.`);
|
|
941
|
+
return lines.filter((l) => l != null).join("\n");
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function doneText(d, session, cost) {
|
|
945
|
+
const md = path.join(session, "research.md");
|
|
946
|
+
return [
|
|
947
|
+
`# Research done: ${d.understanding.product ?? d.understanding.job.slice(0, 60)}`,
|
|
948
|
+
"",
|
|
949
|
+
`Looked at ${d.looked} page(s) in ${Math.round(d.ms / 1000)}s; kept ${d.kept.length}: ${d.kept.map((r) => `${r.id} ${r.name} (${r.role}, craft ${r.craft})`).join(", ")}.`,
|
|
950
|
+
d.cliches.length ? `What the competitors all do: ${d.cliches.map((c) => c.label).join("; ")}.` : null,
|
|
951
|
+
d.understanding.brand.line,
|
|
952
|
+
d.dropped.length ? `Left out ${d.dropped.length}: ${d.dropped.slice(0, 5).map((x) => `${x.name ?? x.url} (${x.why})`).join("; ")}${d.dropped.length > 5 ? "…" : ""}.` : null,
|
|
953
|
+
"",
|
|
954
|
+
`Research: ${slash(md)}`,
|
|
955
|
+
d.contact ? `Contact sheet: ${slash(d.contact)}` : null,
|
|
956
|
+
costLine(cost),
|
|
957
|
+
"",
|
|
958
|
+
"Now, without asking the user: open the contact sheet and the first-screen screenshots with your image tool, refine each reference's notes in research.md in plain words (what makes it work; the one thing worth learning), then call palate_direction with the job and from_research. If the user reacts to a reference (\"love R2's type\", \"not R4\"), record it: palate_research with action \"verdict\" (it shapes the ranking next time).",
|
|
959
|
+
]
|
|
960
|
+
.filter((l) => l != null)
|
|
961
|
+
.join("\n");
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// The research, abstract only: for a direction or build brief. No screenshot paths in a build brief.
|
|
965
|
+
export function researchNotes(d, { forBuild = false, only } = {}) {
|
|
966
|
+
if (!d?.kept?.length) return [];
|
|
967
|
+
const refs = only ? d.kept.filter((r) => only.includes(r.id)) : d.kept;
|
|
968
|
+
return [
|
|
969
|
+
// A builder gets no exact colours of theirs: hex values invite copying.
|
|
970
|
+
...refs.map((r) => `- ${r.id} ${r.name} (${r.role}): ${(forBuild ? r.works.map((w) => w.replace(/\s*\(#[0-9a-f]{3,8}\)/gi, "")) : r.works).slice(0, forBuild ? 3 : 4).join(" ")} Moves: ${r.moves.map((m) => m.id).join(", ") || "-"}. ${r.learn.line}`),
|
|
971
|
+
d.cliches.length ? `- What the competitors all do (avoid): ${d.cliches.map((c) => c.label).join("; ")}.` : null,
|
|
972
|
+
d.fonts?.length ? `- Their fonts (theirs: never load them): ${d.fonts.join(", ")}.` : null,
|
|
973
|
+
].filter(Boolean);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// ---- learning records (~/.palate/taste/research.jsonl)
|
|
977
|
+
export function recordVerdicts(dir, name, verdicts = []) {
|
|
978
|
+
const d = loadResearch(dir, name);
|
|
979
|
+
if (!d?.kept) throw new Error(`No research "${name ?? "latest"}" in this project. Run palate research first.`);
|
|
980
|
+
const out = [];
|
|
981
|
+
for (const v of verdicts) {
|
|
982
|
+
const r = d.kept.find((x) => x.id === v.id || x.url === v.id || x.name?.toLowerCase() === String(v.id).toLowerCase());
|
|
983
|
+
if (!r || !["like", "dislike"].includes(v.verdict)) continue;
|
|
984
|
+
const rec = { kind: "ref-verdict", at: new Date().toISOString(), project: path.basename(path.resolve(dir)), research: d.name, screen: d.understanding.screen, verdict: v.verdict, url: r.url, name: r.name, ref: r.id, traits: r.traits ?? [], labels: (r.traits ?? []).map((t) => TRAITS[t]?.label ?? t), note: String(v.note ?? "").slice(0, 400) };
|
|
985
|
+
out.push(rec);
|
|
986
|
+
}
|
|
987
|
+
if (out.length) {
|
|
988
|
+
fs.mkdirSync(TASTE_DIR, { recursive: true });
|
|
989
|
+
fs.appendFileSync(RESEARCH_LOG, out.map((x) => JSON.stringify(x)).join("\n") + "\n");
|
|
990
|
+
}
|
|
991
|
+
return out;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// A build brief was written from a research direction: which references fed it (named in the provenance line).
|
|
995
|
+
export function recordInfluence(dir, chosen, build) {
|
|
996
|
+
try {
|
|
997
|
+
const d = loadResearch(dir, chosen.research);
|
|
998
|
+
const prov = `${chosen.idea.provenance ?? ""}`;
|
|
999
|
+
const refs = (d?.kept ?? []).filter((r) => new RegExp(`\\b${r.id}\\b`).test(prov) || (r.name && prov.toLowerCase().includes(r.name.toLowerCase())));
|
|
1000
|
+
fs.mkdirSync(TASTE_DIR, { recursive: true });
|
|
1001
|
+
if (!fs.existsSync(path.join(dir, ".palate", "bench.json"))) fs.appendFileSync(RESEARCH_LOG, JSON.stringify({ kind: "influence", at: new Date().toISOString(), project: path.basename(path.resolve(dir)), research: chosen.research, screen: d?.understanding?.screen, build, route: chosen.route, direction: chosen.idea.name, signature: chosen.idea.signature ?? null, refs: refs.map((r) => ({ id: r.id, url: r.url, name: r.name, traits: r.traits })) }) + "\n");
|
|
1002
|
+
return refs;
|
|
1003
|
+
} catch {
|
|
1004
|
+
return [];
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// ---- originality against the references (novelty.mjs): every captured page this project's research looked at.
|
|
1009
|
+
export function referenceMatches(dir, rec) {
|
|
1010
|
+
const root = researchRoot(dir);
|
|
1011
|
+
if (!fs.existsSync(root) || !rec) return [];
|
|
1012
|
+
const out = [];
|
|
1013
|
+
for (const n of fs.readdirSync(root)) {
|
|
1014
|
+
const d = readJson(path.join(root, n, "research.json"), null);
|
|
1015
|
+
for (const r of [...(d?.kept ?? []), ...(d?.dropped ?? [])].filter((x) => x.layout)) out.push({ research: n, name: r.name, url: r.url, ...compare(rec, r.layout) });
|
|
1016
|
+
}
|
|
1017
|
+
return out.sort((a, b) => b.total - a.total);
|
|
1018
|
+
}
|
|
1019
|
+
export const REF_TOO_CLOSE = DUPLICATE;
|
|
1020
|
+
export const REF_CLOSE = SIMILAR;
|
|
1021
|
+
|
|
1022
|
+
// ---- starter systems (projects with no brand): AA contrast, licence-safe fonts and imagery
|
|
1023
|
+
const PAID_FONTS = /\b(söhne|sohne|gt (america|walsheim|sectra|super|alpina|flexa|maru|eesti)|graphik|circular|aeonik|neue haas|helvetica( neue)?|sf pro|san francisco|founders grotesk|tiempos|canela|suisse|abc (diatype|favorit|whyte|monument)|diatype|favorit|untitled sans|untitled serif|lyon|domaine|calibre|national 2|signifier|feature (deck|display)|ogg|editorial new|neue montreal|pp (neue|editorial|mori)|basis grotesque|apercu|brown|gilroy|proxima nova|avenir|futura pt|gotham|freight|chronicle|mercury text|druk|styrene|tobias|copernicus|tiempos)\b/i;
|
|
1024
|
+
const IMAGERY_SAFE = /\b(code|svg|css|canvas|webgl|cc0|public domain|generated|generative|pattern|procedural|own photo|photographs? (we|you) (own|take)|commissioned|none|no imagery|type only|typographic)\b/i;
|
|
1025
|
+
const hexRgb = (h) => {
|
|
1026
|
+
const m = String(h ?? "").trim().match(/^#?([0-9a-f]{3}|[0-9a-f]{6})$/i);
|
|
1027
|
+
if (!m) return null;
|
|
1028
|
+
const s = m[1].length === 3 ? m[1].split("").map((c) => c + c).join("") : m[1];
|
|
1029
|
+
return [0, 2, 4].map((i) => parseInt(s.slice(i, i + 2), 16));
|
|
1030
|
+
};
|
|
1031
|
+
const relLum = (rgb) => {
|
|
1032
|
+
const f = (v) => ((v /= 255) <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4);
|
|
1033
|
+
return 0.2126 * f(rgb[0]) + 0.7152 * f(rgb[1]) + 0.0722 * f(rgb[2]);
|
|
1034
|
+
};
|
|
1035
|
+
export function contrast(a, b) {
|
|
1036
|
+
const [x, y] = [hexRgb(a), hexRgb(b)];
|
|
1037
|
+
if (!x || !y) return null;
|
|
1038
|
+
const [l1, l2] = [relLum(x), relLum(y)].sort((p, q) => q - p);
|
|
1039
|
+
return +((l1 + 0.05) / (l2 + 0.05)).toFixed(2);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
export function validateStarter(s) {
|
|
1043
|
+
const problems = [];
|
|
1044
|
+
const warnings = [];
|
|
1045
|
+
const checks = [];
|
|
1046
|
+
if (!s || typeof s !== "object") return { ok: false, problems: ["no starter system: give colours, type, radius, spacing, motion and imagery"], warnings, checks };
|
|
1047
|
+
const c = s.colours ?? s.colors ?? {};
|
|
1048
|
+
for (const k of ["bg", "text", "muted", "accent"]) if (!hexRgb(c[k])) problems.push(`colours.${k} must be a hex colour`);
|
|
1049
|
+
const pair = (fg, bg, min, what) => {
|
|
1050
|
+
if (!c[fg] || !c[bg]) return;
|
|
1051
|
+
const r = contrast(c[fg], c[bg]);
|
|
1052
|
+
if (r == null) return;
|
|
1053
|
+
checks.push({ pair: `${fg} on ${bg}`, ratio: r, min });
|
|
1054
|
+
if (r < min) problems.push(`${what}: ${fg} ${c[fg]} on ${bg} ${c[bg]} is ${r}:1, needs ${min}:1 (AA)`);
|
|
1055
|
+
};
|
|
1056
|
+
pair("text", "bg", 4.5, "body text");
|
|
1057
|
+
pair("muted", "bg", 4.5, "muted text");
|
|
1058
|
+
pair("accent", "bg", 3, "accent (UI, large text)");
|
|
1059
|
+
if (c["on-accent"] ?? c.onAccent) {
|
|
1060
|
+
c["on-accent"] ??= c.onAccent;
|
|
1061
|
+
pair("on-accent", "accent", 4.5, "text on the accent");
|
|
1062
|
+
}
|
|
1063
|
+
if (c.surface) pair("text", "surface", 4.5, "text on surfaces");
|
|
1064
|
+
const t = s.type ?? {};
|
|
1065
|
+
if (!t.display || !t.body) problems.push("type needs display and body families");
|
|
1066
|
+
if (!/^(google-fonts|ofl|system)$/i.test(t.source ?? "")) problems.push('type.source must be "google-fonts", "ofl" or "system" (licence-safe only)');
|
|
1067
|
+
for (const f of [t.display, t.body, t.mono].filter(Boolean)) if (PAID_FONTS.test(f)) problems.push(`${f} is a paid or platform-only font: choose a Google Fonts / OFL family or system fonts`);
|
|
1068
|
+
if (!Array.isArray(t.scale) || t.scale.length < 4 || t.scale.some((x) => !Number.isFinite(Number(x)))) problems.push("type.scale needs at least 4 sizes in px");
|
|
1069
|
+
if (!s.radius || (Array.isArray(s.radius) && !s.radius.length)) problems.push("radius scale missing");
|
|
1070
|
+
if (!s.spacing) problems.push("spacing rhythm missing");
|
|
1071
|
+
if (!s.motion) problems.push("motion character missing");
|
|
1072
|
+
if (!s.imagery) problems.push("imagery approach missing");
|
|
1073
|
+
else if (!IMAGERY_SAFE.test(s.imagery)) problems.push("imagery must be licence-safe: built in code (SVG, CSS, canvas), CC0 / public-domain art, or generated patterns");
|
|
1074
|
+
if (/\b(unsplash|getty|shutterstock|stock photo|their (photos|illustrations))\b/i.test(s.imagery ?? "")) warnings.push("stock or third-party imagery named: make sure the licence allows it, or build it in code");
|
|
1075
|
+
return { ok: !problems.length, problems, warnings, checks };
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// The starter system as tokens to create first (build brief).
|
|
1079
|
+
export function starterTokens(s) {
|
|
1080
|
+
const c = s.colours ?? s.colors ?? {};
|
|
1081
|
+
const t = s.type ?? {};
|
|
1082
|
+
const fam = (f, fallback) => `"${f}", ${fallback}`;
|
|
1083
|
+
return [
|
|
1084
|
+
"```css",
|
|
1085
|
+
":root {",
|
|
1086
|
+
...Object.entries(c).map(([k, v]) => ` --color-${k}: ${v};`),
|
|
1087
|
+
` --font-display: ${fam(t.display, "ui-serif, Georgia, serif")};`,
|
|
1088
|
+
` --font-body: ${fam(t.body, "ui-sans-serif, system-ui, sans-serif")};`,
|
|
1089
|
+
t.mono ? ` --font-mono: ${fam(t.mono, "ui-monospace, monospace")};` : null,
|
|
1090
|
+
...(t.scale ?? []).map((px, i) => ` --text-${i + 1}: ${px}px;`),
|
|
1091
|
+
...(Array.isArray(s.radius) ? s.radius.map((r, i) => ` --radius-${["sm", "md", "lg", "xl"][i] ?? i}: ${String(r).replace(/^(\d+)$/, "$1px")};`) : [` /* radius: ${s.radius} */`]),
|
|
1092
|
+
typeof s.spacing === "object" ? ` --space-unit: ${s.spacing.unit ?? 8}px; /* scale: ${(s.spacing.scale ?? []).join(" ")} */` : ` /* spacing: ${s.spacing} */`,
|
|
1093
|
+
"}",
|
|
1094
|
+
"```",
|
|
1095
|
+
`Fonts: ${[t.display, t.body, t.mono].filter(Boolean).join(", ")} (${t.source}${/google/i.test(t.source ?? "") ? ": load from Google Fonts or @fontsource" : ""}). Motion: ${s.motion}. Imagery: ${s.imagery}.`,
|
|
1096
|
+
].filter((l) => l != null);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// ---- the build brief's research block (build.mjs calls this when the chosen direction came from research)
|
|
1100
|
+
// tokens: false when a blueprint exists: its tokens are the only system, so the direction's starter tokens (which
|
|
1101
|
+
// the blueprint refined, and which no longer match it) aren't printed.
|
|
1102
|
+
export function researchBuildLines(dir, chosen, { tokens = true } = {}) {
|
|
1103
|
+
const d = loadResearch(dir, chosen.research);
|
|
1104
|
+
const idea = chosen.idea;
|
|
1105
|
+
const sig = idea.signature ?? {};
|
|
1106
|
+
const scratch = chosen.brand === "scratch";
|
|
1107
|
+
return [
|
|
1108
|
+
"## The art direction (from research)",
|
|
1109
|
+
`**${idea.name}**: ${idea.metaphor}`,
|
|
1110
|
+
`- Signature moment: **${sig.move}**: ${sig.moment}`,
|
|
1111
|
+
scratch ? null : `- Brand: ${idea.brand ?? "keep the project's tokens, fonts and components"}`,
|
|
1112
|
+
idea.sections?.length ? `- Sections, in order: ${idea.sections.join(" → ")}` : null,
|
|
1113
|
+
`- Provenance: ${idea.provenance}`,
|
|
1114
|
+
"",
|
|
1115
|
+
...(scratch && idea.starter && !tokens
|
|
1116
|
+
? ["### The system", "The blueprint's tokens (below) are the only source: the direction's starter system was refined into them. Use no colour, font or size from anywhere else.", ""]
|
|
1117
|
+
: scratch && idea.starter
|
|
1118
|
+
? ["### Create these tokens first (the direction's starter system)", "This project has no brand yet: write these tokens before any component, then use only them.", ...starterTokens(idea.starter), ""]
|
|
1119
|
+
: [`### The project's brand`, `${d?.understanding?.brand?.line ?? "Keep the project's tokens."} The metaphor and the signature live inside these tokens: no new font, no new brand colour.`, ""]),
|
|
1120
|
+
"### The signature move's recipe",
|
|
1121
|
+
moveBriefSafe([sig.move]),
|
|
1122
|
+
"",
|
|
1123
|
+
...(d ? ["### What the research found (abstract notes only)", ...researchNotes(d, { forBuild: true }), ""] : []),
|
|
1124
|
+
"### Ideas, not expression",
|
|
1125
|
+
`- ${LICENCE_RULE}`,
|
|
1126
|
+
"- palate shoot compares this page with every page the research looked at; a page too close to one of them fails.",
|
|
1127
|
+
`- **Deliver the signature moment; it will be checked.** palate shoot reminds you of it, and the review asks whether it is there and works.`,
|
|
1128
|
+
"",
|
|
1129
|
+
].filter((l) => l != null);
|
|
1130
|
+
}
|