@palate/cli 0.0.0-stage → 0.0.2
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 +153 -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 +37 -4
- package/scripts/pack-assets.mjs +58 -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,1122 @@
|
|
|
1
|
+
// `palate point`: fix things by pointing at them in the running app. palate starts (or reuses) the project's
|
|
2
|
+
// dev server and puts a small proxy in front of it. The proxy injects palate's overlay into HTML pages and
|
|
3
|
+
// passes everything else through untouched, HMR websockets included, so nothing in the project changes.
|
|
4
|
+
// The user points at an element and says what should change; each request is saved with everything an
|
|
5
|
+
// agent needs (selector, DOM path, trimmed HTML, styles, component names, a crop, likely source lines) in
|
|
6
|
+
// .palate/point/<id>/request.json, and the agent takes them one by one: next → fix → done. Every `next` is
|
|
7
|
+
// also a heartbeat, so the overlay can say whether an agent is listening; `palate point --auto` (auto.mjs)
|
|
8
|
+
// is palate itself listening, running the user's coding agent headless on each pin.
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import http from "node:http";
|
|
11
|
+
import net from "node:net";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { startDevServer } from "../devserver.mjs";
|
|
15
|
+
import { HIDE_DEV, routeFile, stamp } from "../look.mjs";
|
|
16
|
+
import { TASTE_DIR, readJson } from "../loop/common.mjs";
|
|
17
|
+
import { freePort, launch, normalizeRoute } from "../shoot.mjs";
|
|
18
|
+
import { locate, phrases } from "./locate.mjs";
|
|
19
|
+
import { cleanLayer, cleanTrail, replay, trailCore, trailWords } from "./trail.mjs";
|
|
20
|
+
|
|
21
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
export const POINT_LOG = path.join(TASTE_DIR, "point.jsonl");
|
|
23
|
+
export const pointDir = (dir) => path.join(dir, ".palate", "point");
|
|
24
|
+
const serverFile = (dir) => path.join(pointDir(dir), "server.json");
|
|
25
|
+
const TAG = Buffer.from('<script src="/__palate/overlay.js" data-palate defer></script>');
|
|
26
|
+
const PUBLIC = ["id", "n", "status", "route", "selector", "label", "prompt", "chips", "box", "scroll", "viewport", "summary", "error", "created", "startedAt", "doneAt", "hasCrop", "hasAfter", "cropFound", "afterFound", "removed", "width", "alt", "other", "trail", "layer"];
|
|
27
|
+
// An agent counts as listening while it waits in `next`, while a pin is being worked on (fixing takes minutes
|
|
28
|
+
// without a call), or if it called or finished a pin within this long (a wait is 55 s).
|
|
29
|
+
export const LISTEN_MS = 120_000;
|
|
30
|
+
|
|
31
|
+
// The width a request is about, when it isn't the user's own: "a hamburger menu in mobile mode" → 390,
|
|
32
|
+
// "at 768px" → 768. Crops at the user's desktop viewport would show no change for those.
|
|
33
|
+
export function askedWidth(prompt, viewportWidth = 1440) {
|
|
34
|
+
const p = String(prompt ?? "");
|
|
35
|
+
let w = null;
|
|
36
|
+
const m = /\b(\d{3,4})\s*(?:px\b|wide\b|width\b|w\b)/i.exec(p) || /\b(?:at|below|under|width(?: of)?|on a?)\s+(\d{3,4})\b/i.exec(p);
|
|
37
|
+
if (m && +m[1] >= 280 && +m[1] <= 1280) w = +m[1];
|
|
38
|
+
if (!w && /\b(mobile|phones?|iphone|android|small[- ]screens?|narrow screens?|handheld)\b/i.test(p)) w = 390;
|
|
39
|
+
if (!w && /\b(tablets?|ipad)\b/i.test(p)) w = 768;
|
|
40
|
+
return w && w < (viewportWidth || 1440) - 40 ? w : null;
|
|
41
|
+
}
|
|
42
|
+
// Every pin is checked at two widths: the one the user pointed at and the other one, so a fix at 1440 that
|
|
43
|
+
// breaks 390 doesn't go unnoticed. The other one is the width the user's words name ("on mobile"), else a
|
|
44
|
+
// phone for a desktop pin and a desktop for a phone pin.
|
|
45
|
+
export const PHONE = 390;
|
|
46
|
+
export const DESKTOP = 1440;
|
|
47
|
+
export const widthName = (w) => (w <= 500 ? "phone" : w < 1024 ? "tablet" : "desktop");
|
|
48
|
+
export function otherWidth(viewport, prompt) {
|
|
49
|
+
const vw = viewport?.width || DESKTOP;
|
|
50
|
+
const asked = askedWidth(prompt, vw);
|
|
51
|
+
if (asked) return { width: asked, asked: true };
|
|
52
|
+
return { width: vw >= 768 ? PHONE : DESKTOP, asked: false };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// The pinned element in plain words, for notes: the button "Get invite".
|
|
56
|
+
const TAG_NAME = { a: "link", button: "button", img: "image", svg: "icon", h1: "heading", h2: "heading", h3: "heading", h4: "heading", p: "paragraph", li: "list item", ul: "list", ol: "list", nav: "navigation", header: "header", footer: "footer", section: "section", input: "field", textarea: "field", select: "menu", form: "form", table: "table", label: "label" };
|
|
57
|
+
export function elName(r) {
|
|
58
|
+
const t = r?.label?.text;
|
|
59
|
+
return `the ${TAG_NAME[r?.tag] ?? "element"}${t ? ` "${t.length > 28 ? `${t.slice(0, 27).trimEnd()}…` : t}"` : ""}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Before vs after at the other width. "broke": the page now scrolls sideways (or gets wider than the screen),
|
|
63
|
+
// the element disappeared (not asked for), or text in it is now cut off. "changed": it moved or resized beyond
|
|
64
|
+
// a few pixels, or the crop looks different (expected for shared CSS; shown, not blocked). Else "unchanged".
|
|
65
|
+
// before/after: { state: "shown"|"hidden"|"missing", box: {x,y,width,height} in page px, overflow, scrolls,
|
|
66
|
+
// clipped, cutText }; diff: { ratio, sizeChanged } from the two crops.
|
|
67
|
+
const TOL = 4;
|
|
68
|
+
export function otherVerdict(before, after, { width, asked = false, removed = false, diff = null, name = "the element" } = {}) {
|
|
69
|
+
const where = `At ${width} (${widthName(width)}, ${asked ? "the width the user asked about" : "the width you didn't point at"})`;
|
|
70
|
+
if (!before || !after || before.error || after.error)
|
|
71
|
+
return { verdict: "unknown", problems: [], changes: [], note: `${where}: not checked (${!before || before.error ? "no before shot" : "no after shot"}${(before?.error ?? after?.error) ? `: ${before?.error ?? after?.error}` : ""}).`, short: `Not checked at ${width}` };
|
|
72
|
+
// A pin inside an opened state (a modal, a menu) that palate couldn't open again: not checked, never "broke".
|
|
73
|
+
const stuck = [before, after].find((s) => s.reopen?.state === "couldn't reopen" && !(removed && s.reopen.ran));
|
|
74
|
+
if (stuck)
|
|
75
|
+
return { verdict: "unknown", problems: [], changes: [], reopen: true, note: `${where}: not checked in the opened state. palate couldn't reopen it ${stuck === before ? "for the before shot" : "after the change"}: ${stuck.reopen.why}. That isn't counted as broken.`, short: `Couldn't reopen at ${width}` };
|
|
76
|
+
const problems = [];
|
|
77
|
+
const changes = [];
|
|
78
|
+
const shown = (s) => s.state === "shown" || (s.state === undefined && s.found === true);
|
|
79
|
+
const bo = before.overflow ?? 0;
|
|
80
|
+
const ao = after.overflow ?? 0;
|
|
81
|
+
if (ao > 1 && bo <= 1) problems.push(after.scrolls === false ? `the page is now ${ao}px wider than the screen (cut off at the right)` : `the page now scrolls sideways (${ao}px wider than the screen)`);
|
|
82
|
+
else if (bo > 1 && ao > bo + 8) problems.push(`the page scrolls further sideways (${ao}px wider than the screen, was ${bo}px)`);
|
|
83
|
+
if (shown(before) && !shown(after)) {
|
|
84
|
+
if (removed) changes.push(`${name} is gone, as asked`);
|
|
85
|
+
else problems.push(`${name} ${after.state === "hidden" ? "is now hidden" : "disappeared"}`);
|
|
86
|
+
}
|
|
87
|
+
if (!shown(before) && shown(after)) changes.push(`${name} shows now (it was ${before.state === "hidden" ? "hidden" : "not on the page"})`);
|
|
88
|
+
if (shown(before) && shown(after)) {
|
|
89
|
+
if ((after.clipped ?? 0) > (before.clipped ?? 0)) problems.push(`text in ${name} is now cut off${after.cutText ? ` ("${after.cutText}")` : ""}`);
|
|
90
|
+
const a = before.box ?? {};
|
|
91
|
+
const b = after.box ?? {};
|
|
92
|
+
const dw = (b.width ?? 0) - (a.width ?? 0);
|
|
93
|
+
const dh = (b.height ?? 0) - (a.height ?? 0);
|
|
94
|
+
const dx = (b.x ?? 0) - (a.x ?? 0);
|
|
95
|
+
const dy = (b.y ?? 0) - (a.y ?? 0);
|
|
96
|
+
const size = [];
|
|
97
|
+
if (Math.abs(dw) > TOL) size.push(`${Math.abs(dw)}px ${dw < 0 ? "narrower" : "wider"}`);
|
|
98
|
+
if (Math.abs(dh) > TOL) size.push(`${Math.abs(dh)}px ${dh < 0 ? "shorter" : "taller"}`);
|
|
99
|
+
if (size.length) changes.push(`${name} is ${size.join(" and ")}`);
|
|
100
|
+
const move = [];
|
|
101
|
+
if (Math.abs(dx) > TOL) move.push(`${Math.abs(dx)}px ${dx < 0 ? "left" : "right"}`);
|
|
102
|
+
if (Math.abs(dy) > TOL) move.push(`${Math.abs(dy)}px ${dy < 0 ? "up" : "down"}`);
|
|
103
|
+
if (move.length) changes.push(`${size.length ? "it" : name} moved ${move.join(" and ")}`);
|
|
104
|
+
}
|
|
105
|
+
if (diff && !changes.length && (diff.sizeChanged || diff.ratio > 0.005)) changes.push(`${name} looks different (${Math.max(1, Math.round(diff.ratio * 100))}% of the crop changed)`);
|
|
106
|
+
const verdict = problems.length ? "broke" : changes.length ? "changed" : "unchanged";
|
|
107
|
+
const scroll = ao > 1 ? (bo > 1 && !problems.length ? "; the page already scrolled sideways before" : "") : "; no sideways scroll";
|
|
108
|
+
const cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
109
|
+
const note =
|
|
110
|
+
verdict === "broke"
|
|
111
|
+
? `${where}: broke. ${cap(problems.join("; "))}.${changes.length ? ` Also: ${changes.join("; ")}.` : ""}`
|
|
112
|
+
: verdict === "changed"
|
|
113
|
+
? `${where}: changed. ${cap(changes.join("; "))}${scroll}.`
|
|
114
|
+
: `${where}: unchanged${ao > 1 ? " (the page already scrolled sideways before)" : ""}.`;
|
|
115
|
+
const short = verdict === "broke" ? `Broke at ${width}: ${problems[0]}` : verdict === "changed" ? `Changed at ${width}: ${changes.join("; ")}` : `Unchanged at ${width}`;
|
|
116
|
+
return { verdict, problems, changes, note, short };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// palate_point_done refuses while the other width is broken, unless the agent overrides (the user accepts it).
|
|
120
|
+
export function doneBlocked(r, override = false) {
|
|
121
|
+
const o = r?.other;
|
|
122
|
+
if (override || o?.verdict !== "broke") return null;
|
|
123
|
+
return `Not done yet. ${o.note} Fix it so both widths work (palate_point_crop shows before and after at both), then call palate_point_done again. Only if the user accepts it as it is, pass override: true (CLI: --override).`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// The user asked for the element to go away.
|
|
127
|
+
const REMOVE = /\b(remove|delete|hide|get rid of|take (?:it |this |that )?out|drop|don'?t need|do not need|no need for)\b/i;
|
|
128
|
+
const words = (t) => new Set(String(t ?? "").toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? []);
|
|
129
|
+
// Is the element a shot found the one the user pointed at (same tag, most of its words), or something else
|
|
130
|
+
// that now sits at that selector?
|
|
131
|
+
function sameElement(r, sig) {
|
|
132
|
+
if (!sig) return false;
|
|
133
|
+
if (r.tag && sig.tag !== r.tag) return false;
|
|
134
|
+
const a = words(r.text);
|
|
135
|
+
if (!a.size) return true;
|
|
136
|
+
const b = words(sig.text);
|
|
137
|
+
let k = 0;
|
|
138
|
+
for (const w of a) if (b.has(w)) k++;
|
|
139
|
+
return k / a.size >= 0.5;
|
|
140
|
+
}
|
|
141
|
+
// A shot whose opened state couldn't be reopened says nothing about whether the element is gone.
|
|
142
|
+
const isRemoved = (r, shot, said) => Boolean(said) || (REMOVE.test(r.prompt ?? "") && Boolean(shot) && !(shot.reopen && !shot.reopen.ran) && (!shot.found || !sameElement(r, shot.sig)));
|
|
143
|
+
|
|
144
|
+
// Where a pin's opened state couldn't be reopened, newest first: [{ width, when, why }].
|
|
145
|
+
export function reopenTrouble(r) {
|
|
146
|
+
const o = r.other?.width ? r.other : null;
|
|
147
|
+
const out = [];
|
|
148
|
+
const add = (width, when, x) => x?.state === "couldn't reopen" && out.push({ width, when, why: x.why });
|
|
149
|
+
add(r.viewport?.width ?? null, "after", r.reopen?.after);
|
|
150
|
+
add(o?.width ?? null, "after", o?.after?.reopen);
|
|
151
|
+
add(r.viewport?.width ?? null, "before", r.reopen?.before);
|
|
152
|
+
add(o?.width ?? null, "before", o?.before?.reopen);
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// In the page, at another width: the pinned element (its selector, or the same tag with the same words when
|
|
157
|
+
// the selector finds something else), whether it shows, and what's wrong around it: sideways scroll and
|
|
158
|
+
// text cut off inside it. Runs in the browser, so it uses nothing from this file.
|
|
159
|
+
function measureInPage({ sel, tag, text, width }) {
|
|
160
|
+
const words = (s) => new Set(String(s || "").toLowerCase().match(/[\p{L}\p{N}]+/gu) || []);
|
|
161
|
+
const want = words(text);
|
|
162
|
+
const score = (el) => {
|
|
163
|
+
if (!want.size) return 1;
|
|
164
|
+
const a = words(el.textContent);
|
|
165
|
+
let k = 0;
|
|
166
|
+
for (const w of want) if (a.has(w)) k++;
|
|
167
|
+
// A parent with the same words and much more is not the same element.
|
|
168
|
+
return k / want.size - Math.abs(Math.log((a.size + 1) / (want.size + 1))) * 0.15;
|
|
169
|
+
};
|
|
170
|
+
const shows = (el) => (el.checkVisibility ? el.checkVisibility({ opacityProperty: true, visibilityProperty: true }) : el.getClientRects().length > 0) && el.getBoundingClientRect().width >= 1 && el.getBoundingClientRect().height >= 1;
|
|
171
|
+
let el = null;
|
|
172
|
+
let how = "selector";
|
|
173
|
+
try {
|
|
174
|
+
el = document.querySelector(sel);
|
|
175
|
+
} catch {}
|
|
176
|
+
if (el && ((tag && el.tagName.toLowerCase() !== tag) || score(el) < 0.5)) el = null;
|
|
177
|
+
if (!el && tag && want.size) {
|
|
178
|
+
how = "text";
|
|
179
|
+
let best = 0;
|
|
180
|
+
const all = document.getElementsByTagName(tag);
|
|
181
|
+
for (let i = 0; i < all.length && i < 4000; i++) {
|
|
182
|
+
const s = score(all[i]) + (shows(all[i]) ? 0.05 : 0);
|
|
183
|
+
if (s > best) [best, el] = [s, all[i]];
|
|
184
|
+
}
|
|
185
|
+
if (best < 0.6) el = null;
|
|
186
|
+
}
|
|
187
|
+
// Sideways scroll: try it. A phone may also zoom out to fit a too-wide page (innerWidth grows).
|
|
188
|
+
const de = document.documentElement;
|
|
189
|
+
const y0 = scrollY;
|
|
190
|
+
window.scrollTo({ left: 1e6, top: y0, behavior: "instant" });
|
|
191
|
+
const sx = scrollX;
|
|
192
|
+
window.scrollTo({ left: 0, top: y0, behavior: "instant" });
|
|
193
|
+
const scrolls = sx > 1 || innerWidth > width + 1;
|
|
194
|
+
const overflow = Math.max(0, Math.round(Math.max(sx, de.scrollWidth - de.clientWidth, innerWidth - width)));
|
|
195
|
+
const page = { overflow, scrolls, pageWidth: Math.max(de.scrollWidth, innerWidth), vw: de.clientWidth, vh: innerHeight, H: de.scrollHeight };
|
|
196
|
+
if (!el) return { state: "missing", how: null, ...page };
|
|
197
|
+
if (!shows(el)) return { state: "hidden", how, ...page, tag: el.tagName.toLowerCase() };
|
|
198
|
+
let b = el.getBoundingClientRect();
|
|
199
|
+
if ((b.bottom < 0 || b.top > innerHeight) && b.height < innerHeight - 48) {
|
|
200
|
+
el.scrollIntoView({ block: "center", inline: "nearest", behavior: "instant" });
|
|
201
|
+
b = el.getBoundingClientRect();
|
|
202
|
+
}
|
|
203
|
+
// Text cut off: the element, or something in it, hides overflow and holds more than it shows.
|
|
204
|
+
let clipped = 0;
|
|
205
|
+
let cutText = null;
|
|
206
|
+
for (const n of [el, ...el.querySelectorAll("*")].slice(0, 400)) {
|
|
207
|
+
if (!(n.textContent || "").trim()) continue;
|
|
208
|
+
const cs = getComputedStyle(n);
|
|
209
|
+
const hides = /hidden|clip/.test(cs.overflowX + cs.overflowY) || cs.textOverflow === "ellipsis" || (cs.webkitLineClamp && cs.webkitLineClamp !== "none");
|
|
210
|
+
if (hides && (n.scrollWidth > n.clientWidth + 1 || n.scrollHeight > n.clientHeight + 1)) {
|
|
211
|
+
clipped++;
|
|
212
|
+
cutText ??= n.textContent.replace(/\s+/g, " ").trim().slice(0, 40);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
state: "shown",
|
|
217
|
+
how,
|
|
218
|
+
...page,
|
|
219
|
+
box: { x: Math.round(b.left + scrollX), y: Math.round(b.top + scrollY), width: Math.round(b.width), height: Math.round(b.height) },
|
|
220
|
+
v: { x: b.left, y: b.top, w: b.width, h: b.height },
|
|
221
|
+
clipped,
|
|
222
|
+
cutText,
|
|
223
|
+
tag: el.tagName.toLowerCase(),
|
|
224
|
+
text: (el.innerText || el.textContent || "").replace(/\s+/g, " ").trim().slice(0, 600),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// How much two PNG crops differ, drawn into a canvas in the browser (scaled down together, max side 800):
|
|
229
|
+
// { ratio of changed pixels, sizeChanged }.
|
|
230
|
+
async function diffInPage([a, b]) {
|
|
231
|
+
const load = (src) =>
|
|
232
|
+
new Promise((resolve, reject) => {
|
|
233
|
+
const i = new Image();
|
|
234
|
+
i.onload = () => resolve(i);
|
|
235
|
+
i.onerror = reject;
|
|
236
|
+
i.src = src;
|
|
237
|
+
});
|
|
238
|
+
const [x, y] = await Promise.all([load(a), load(b)]);
|
|
239
|
+
const W = Math.max(x.naturalWidth, y.naturalWidth);
|
|
240
|
+
const H = Math.max(x.naturalHeight, y.naturalHeight);
|
|
241
|
+
const k = Math.min(1, 800 / Math.max(W, H));
|
|
242
|
+
const w = Math.max(1, Math.round(W * k));
|
|
243
|
+
const h = Math.max(1, Math.round(H * k));
|
|
244
|
+
const px = (img) => {
|
|
245
|
+
const c = new OffscreenCanvas(w, h);
|
|
246
|
+
const g = c.getContext("2d");
|
|
247
|
+
g.fillStyle = "#fff";
|
|
248
|
+
g.fillRect(0, 0, w, h);
|
|
249
|
+
g.drawImage(img, 0, 0, img.naturalWidth * k, img.naturalHeight * k);
|
|
250
|
+
return g.getImageData(0, 0, w, h).data;
|
|
251
|
+
};
|
|
252
|
+
const p = px(x);
|
|
253
|
+
const q = px(y);
|
|
254
|
+
let changed = 0;
|
|
255
|
+
for (let i = 0; i < p.length; i += 4) if (Math.abs(p[i] - q[i]) + Math.abs(p[i + 1] - q[i + 1]) + Math.abs(p[i + 2] - q[i + 2]) > 48) changed++;
|
|
256
|
+
return { ratio: changed / (w * h), sizeChanged: Math.abs(x.naturalWidth - y.naturalWidth) > 8 || Math.abs(x.naturalHeight - y.naturalHeight) > 8 };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// The overlay as served: trail.mjs's shared part pasted in, so the overlay and the server trim and word
|
|
260
|
+
// trails the same way.
|
|
261
|
+
const TRAIL_SRC = `(${trailCore.toString()})()`;
|
|
262
|
+
export const overlaySource = () => fs.readFileSync(path.join(here, "overlay.js"), "utf8").replace(/\/\*@trail\*\/[\s\S]*?\/\*@end\*\//, () => TRAIL_SRC);
|
|
263
|
+
|
|
264
|
+
// The trail in the taste log: what was clicked or pressed and whether it could be reopened, never typed values.
|
|
265
|
+
const trailLog = (r) =>
|
|
266
|
+
r.trail?.length
|
|
267
|
+
? { trail: r.trail.map((s) => ({ type: s.type, text: s.text ?? s.name ?? null, ...(s.key ? { key: s.key } : {}) })), layer: r.layer ?? null, reopen: reopenTrouble(r).map(({ width, when, why }) => ({ width, when, why })) }
|
|
268
|
+
: {};
|
|
269
|
+
|
|
270
|
+
const logTaste = (record) => {
|
|
271
|
+
try {
|
|
272
|
+
fs.mkdirSync(TASTE_DIR, { recursive: true });
|
|
273
|
+
fs.appendFileSync(POINT_LOG, JSON.stringify({ ...record, at: new Date().toISOString() }) + "\n");
|
|
274
|
+
} catch {}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const readBody = (req, limit = 2_000_000) =>
|
|
278
|
+
new Promise((resolve, reject) => {
|
|
279
|
+
let size = 0;
|
|
280
|
+
const chunks = [];
|
|
281
|
+
req.on("data", (c) => {
|
|
282
|
+
size += c.length;
|
|
283
|
+
if (size > limit) {
|
|
284
|
+
reject(new Error("Request too large"));
|
|
285
|
+
req.destroy();
|
|
286
|
+
} else chunks.push(c);
|
|
287
|
+
});
|
|
288
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
289
|
+
req.on("error", reject);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// A running point server for this project, if there is one (it answers its ping with the same folder).
|
|
293
|
+
export async function runningPoint(dir) {
|
|
294
|
+
const info = readJson(serverFile(dir), null);
|
|
295
|
+
if (!info?.url) return null;
|
|
296
|
+
try {
|
|
297
|
+
const r = await fetch(`${info.url}/__palate/api/ping`, { signal: AbortSignal.timeout(1500) });
|
|
298
|
+
const j = await r.json();
|
|
299
|
+
return path.resolve(j.dir) === path.resolve(dir) ? info : null;
|
|
300
|
+
} catch {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export async function startPoint(dir, { url, route = "/", port, open = true, log = (m) => console.error(m) } = {}) {
|
|
306
|
+
dir = path.resolve(dir);
|
|
307
|
+
const existing = await runningPoint(dir);
|
|
308
|
+
if (existing) {
|
|
309
|
+
// Starting point again means the user wants an agent again, even after they pressed Done.
|
|
310
|
+
await fetch(`${existing.url}/__palate/api/resume`, { method: "POST", signal: AbortSignal.timeout(1500) }).catch(() => {});
|
|
311
|
+
const link = existing.url + normalizeRoute(route);
|
|
312
|
+
if (open) (await import("../loop/pick.mjs")).openBrowser(link);
|
|
313
|
+
return { url: existing.url, link, base: existing.base, reused: true, stop: () => {}, done: new Promise(() => {}) };
|
|
314
|
+
}
|
|
315
|
+
const dev = await startDevServer(dir, { url });
|
|
316
|
+
const target = new URL(dev.base);
|
|
317
|
+
const root = pointDir(dir);
|
|
318
|
+
fs.mkdirSync(root, { recursive: true });
|
|
319
|
+
fs.writeFileSync(path.join(dir, ".palate", ".gitignore"), "*\n");
|
|
320
|
+
|
|
321
|
+
// ---------- The queue ----------
|
|
322
|
+
const requests = new Map();
|
|
323
|
+
const ready = new Map();
|
|
324
|
+
// The user's cookies (for crops of signed-in pages) stay in memory, never on disk.
|
|
325
|
+
const cookies = new Map();
|
|
326
|
+
const waiters = new Set();
|
|
327
|
+
const clients = new Set();
|
|
328
|
+
let counter = 0;
|
|
329
|
+
// Earlier requests stay: open ones are still the agent's to do, recent done ones keep their pins.
|
|
330
|
+
for (const id of fs.existsSync(root) ? fs.readdirSync(root) : []) {
|
|
331
|
+
const r = readJson(path.join(root, id, "request.json"), null);
|
|
332
|
+
if (!r?.id) continue;
|
|
333
|
+
if (r.status === "working") r.status = "queued";
|
|
334
|
+
if (r.width === undefined) r.width = askedWidth(r.prompt, r.viewport?.width);
|
|
335
|
+
// Pins from an older palate: the other width is shot before the agent takes them.
|
|
336
|
+
if (r.status === "queued" && !r.other) r.other = otherWidth(r.viewport, r.prompt);
|
|
337
|
+
const fresh = Date.now() - Date.parse(r.doneAt ?? r.created) < 12 * 3600_000;
|
|
338
|
+
if (r.status === "queued" || (["done", "canceled", "failed"].includes(r.status) && fresh)) requests.set(r.id, r);
|
|
339
|
+
counter = Math.max(counter, r.n ?? 0);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const pub = (r) => {
|
|
343
|
+
const o = Object.fromEntries(PUBLIC.map((k) => [k, r[k]]));
|
|
344
|
+
// The other width without file paths: the overlay loads its crops by name (crop-390, after-390).
|
|
345
|
+
if (r.other) {
|
|
346
|
+
const side = (s) => s && { has: Boolean(s.crop), state: s.state, found: s.found, box: s.box, overflow: s.overflow, scrolls: s.scrolls, clipped: s.clipped, removed: s.removed, shot: s.shot, error: s.error, reopen: s.reopen?.state };
|
|
347
|
+
const { width, asked, verdict, note, short, problems, changes } = r.other;
|
|
348
|
+
o.other = { width, asked, verdict, note, short, problems, changes, before: side(r.other.before), after: side(r.other.after) };
|
|
349
|
+
}
|
|
350
|
+
// Whether its opened state could be reopened for the checks (the newest trouble, if any).
|
|
351
|
+
if (r.trail?.length) {
|
|
352
|
+
const t = reopenTrouble(r);
|
|
353
|
+
o.reopen = { before: r.reopen?.before?.state ?? null, after: r.reopen?.after?.state ?? null, fail: t[0] ?? null };
|
|
354
|
+
}
|
|
355
|
+
if (r.status === "queued") o.position = [...requests.values()].filter((x) => x.status === "queued" && x.n <= r.n).length;
|
|
356
|
+
return o;
|
|
357
|
+
};
|
|
358
|
+
const save = (r) => {
|
|
359
|
+
fs.mkdirSync(path.join(root, r.id), { recursive: true });
|
|
360
|
+
fs.writeFileSync(path.join(root, r.id, "request.json"), JSON.stringify(r, null, 2));
|
|
361
|
+
};
|
|
362
|
+
const broadcast = (msg) => {
|
|
363
|
+
const line = `data: ${JSON.stringify(msg)}\n\n`;
|
|
364
|
+
for (const c of clients) c.write(line);
|
|
365
|
+
};
|
|
366
|
+
const changed = (r) => {
|
|
367
|
+
save(r);
|
|
368
|
+
broadcast({ type: "update", request: pub(r) });
|
|
369
|
+
// Queue positions move for everyone behind it.
|
|
370
|
+
for (const x of requests.values()) if (x !== r && x.status === "queued") broadcast({ type: "update", request: pub(x) });
|
|
371
|
+
for (const w of waiters) w();
|
|
372
|
+
emitAgent();
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
// ---------- Is an agent listening? ----------
|
|
376
|
+
// Every `next` is a heartbeat. The overlay shows listening, busy (a pin is being worked on) or none, and
|
|
377
|
+
// when the user sends a pin with no agent listening it says how to start one.
|
|
378
|
+
const hb = { polls: 0, seen: 0, label: null, stopAck: 0 };
|
|
379
|
+
let stopped = false;
|
|
380
|
+
const agentState = () => {
|
|
381
|
+
const working = [...requests.values()].filter((x) => x.status === "working").sort((a, b) => a.n - b.n)[0];
|
|
382
|
+
// A pin finished recently means an agent is around (unless it has since been told the user stopped).
|
|
383
|
+
const lastDone = Math.max(0, ...[...requests.values()].map((x) => Date.parse(x.doneAt ?? "") || 0));
|
|
384
|
+
const live = hb.polls > 0 || Date.now() - hb.seen < LISTEN_MS || (lastDone > hb.stopAck && Date.now() - lastDone < LISTEN_MS);
|
|
385
|
+
const queued = [...requests.values()].filter((x) => x.status === "queued").length;
|
|
386
|
+
const auto = Boolean(live && hb.label?.startsWith("auto"));
|
|
387
|
+
if (working) return { state: "busy", pin: working.n, id: working.id, label: hb.label, auto, queued };
|
|
388
|
+
if (stopped) return { state: "stopped", label: hb.label, auto, queued };
|
|
389
|
+
if (live) return { state: "listening", label: hb.label, auto, queued };
|
|
390
|
+
return { state: "none", queued };
|
|
391
|
+
};
|
|
392
|
+
let lastAgent = "";
|
|
393
|
+
function emitAgent() {
|
|
394
|
+
const a = agentState();
|
|
395
|
+
const key = JSON.stringify(a);
|
|
396
|
+
if (key === lastAgent) return;
|
|
397
|
+
lastAgent = key;
|
|
398
|
+
broadcast({ type: "agent", agent: a });
|
|
399
|
+
}
|
|
400
|
+
// A heartbeat goes stale without any request, so look every few seconds.
|
|
401
|
+
const staleTimer = setInterval(emitAgent, 5000);
|
|
402
|
+
staleTimer.unref?.();
|
|
403
|
+
|
|
404
|
+
// ---------- Crops, in a browser palate keeps warm ----------
|
|
405
|
+
let browserP = null;
|
|
406
|
+
const browser = () => (browserP ??= launch());
|
|
407
|
+
// Two shots at a time (the pinned width and the other one, side by side); more wait their turn.
|
|
408
|
+
let active = 0;
|
|
409
|
+
const waiting = [];
|
|
410
|
+
const slot = async (fn) => {
|
|
411
|
+
if (active < 2) active++;
|
|
412
|
+
else await new Promise((resolve) => waiting.push(resolve));
|
|
413
|
+
try {
|
|
414
|
+
return await fn();
|
|
415
|
+
} finally {
|
|
416
|
+
const w = waiting.shift();
|
|
417
|
+
if (w) w();
|
|
418
|
+
else active--;
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
// Same viewport and scroll as the user's tab, straight from the dev server (no overlay), with their cookies.
|
|
422
|
+
// width: shoot at that width instead (a phone when it's narrow), from the top, finding the element there.
|
|
423
|
+
// screen: at that width, always the whole screen (a request about that width: "a hamburger menu on mobile").
|
|
424
|
+
// A pin inside an opened state (r.trail: a modal, a menu, a tab): the steps are replayed with real input on
|
|
425
|
+
// the fresh page first. When a step fails, or (for a before shot) the element isn't there after them, the
|
|
426
|
+
// shot says "couldn't reopen" and falls back to the page as it loads. before: the shot taken at pin time.
|
|
427
|
+
const shootElement = (r, file, cookie, { width, screen = false, before = false } = {}) =>
|
|
428
|
+
slot(async () => {
|
|
429
|
+
const b = await browser();
|
|
430
|
+
const vp = width
|
|
431
|
+
? { width, height: width <= 500 ? 844 : width < 1000 ? 1024 : 900 }
|
|
432
|
+
: { width: Math.max(320, Math.min(r.viewport?.width ?? 1440, 2560)), height: Math.max(320, Math.min(r.viewport?.height ?? 900, 1600)) };
|
|
433
|
+
const phone = Boolean(width && width <= 500);
|
|
434
|
+
const ctx = await b.newContext({ viewport: vp, isMobile: phone, hasTouch: phone, deviceScaleFactor: vp.width < 700 || (r.box?.width ?? 0) * (r.box?.height ?? 0) < 400_000 ? 2 : 1, colorScheme: r.colorScheme === "dark" ? "dark" : "light" });
|
|
435
|
+
try {
|
|
436
|
+
if (cookie) {
|
|
437
|
+
const cookies = cookie
|
|
438
|
+
.split(/;\s*/)
|
|
439
|
+
.map((c) => c.split("="))
|
|
440
|
+
.filter(([k]) => k)
|
|
441
|
+
.map(([name, ...v]) => ({ name, value: v.join("="), url: target.origin }));
|
|
442
|
+
await ctx.addCookies(cookies).catch(() => {});
|
|
443
|
+
}
|
|
444
|
+
const page = await ctx.newPage();
|
|
445
|
+
await page.addInitScript(HIDE_DEV);
|
|
446
|
+
const load = async () => {
|
|
447
|
+
await page.goto(target.origin + r.route, { waitUntil: "load", timeout: 60_000 });
|
|
448
|
+
await page.waitForLoadState("networkidle", { timeout: 6000 }).catch(() => {});
|
|
449
|
+
await page.evaluate(({ x, y }) => window.scrollTo({ left: x, top: y, behavior: "instant" }), width ? { x: 0, y: 0 } : (r.scroll ?? { x: 0, y: 0 }));
|
|
450
|
+
await page.waitForTimeout(700);
|
|
451
|
+
};
|
|
452
|
+
await load();
|
|
453
|
+
let reopen = null;
|
|
454
|
+
if (r.trail?.length) {
|
|
455
|
+
reopen = await replay(page, r.trail, { loose: Boolean(width) }).catch((e) => ({ state: "couldn't reopen", ran: false, why: `the steps didn't run: ${String(e.message).split("\n")[0]}` }));
|
|
456
|
+
await page.waitForTimeout(200);
|
|
457
|
+
}
|
|
458
|
+
const pad = 32;
|
|
459
|
+
if (width) return await shootAtWidth(page, r, file, { width, screen, pad, reopen, before, load });
|
|
460
|
+
const findIt = () =>
|
|
461
|
+
page.evaluate((sel) => {
|
|
462
|
+
let el = null;
|
|
463
|
+
try {
|
|
464
|
+
el = document.querySelector(sel);
|
|
465
|
+
} catch {}
|
|
466
|
+
if (!el || !el.getClientRects().length) return null;
|
|
467
|
+
let b = el.getBoundingClientRect();
|
|
468
|
+
// Bring it into view when it fits, so fixed and sticky things look as they do for the user.
|
|
469
|
+
if ((b.bottom < 0 || b.top > innerHeight) && b.height < innerHeight - 48) {
|
|
470
|
+
el.scrollIntoView({ block: "center", behavior: "instant" });
|
|
471
|
+
b = el.getBoundingClientRect();
|
|
472
|
+
}
|
|
473
|
+
return { x: b.left, y: b.top, w: b.width, h: b.height, sx: scrollX, sy: scrollY, vw: document.documentElement.clientWidth, vh: innerHeight, H: document.documentElement.scrollHeight, tag: el.tagName.toLowerCase(), text: (el.innerText || el.textContent || "").replace(/\s+/g, " ").trim().slice(0, 600) };
|
|
474
|
+
}, r.selector);
|
|
475
|
+
let m = await findIt();
|
|
476
|
+
if (reopen?.state === "reopened" && !m && before) reopen = { ...reopen, state: "couldn't reopen", why: "the steps ran, but the element wasn't there after them" };
|
|
477
|
+
if (reopen?.state === "couldn't reopen") {
|
|
478
|
+
await load();
|
|
479
|
+
m = await findIt();
|
|
480
|
+
}
|
|
481
|
+
await page.waitForTimeout(150);
|
|
482
|
+
const found = Boolean(m);
|
|
483
|
+
const sig = m ? { tag: m.tag, text: m.text } : null;
|
|
484
|
+
const box = m ?? { x: r.box.x, y: r.box.y, w: r.box.width, h: r.box.height, sx: r.scroll?.x ?? 0, sy: r.scroll?.y ?? 0, vw: vp.width, vh: vp.height, H: 1e6 };
|
|
485
|
+
await cropBox(page, file, box, pad);
|
|
486
|
+
return { found, sig, width: null, box: { x: Math.round(box.x), y: Math.round(box.y), width: Math.round(box.w), height: Math.round(box.h) }, ...(reopen ? { reopen } : {}) };
|
|
487
|
+
} finally {
|
|
488
|
+
await ctx.close().catch(() => {});
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
// A crop around a box in the viewport (x, y, w, h, with the page's scroll sx/sy, viewport vw/vh and height H).
|
|
492
|
+
const cropBox = async (page, file, box, pad) => {
|
|
493
|
+
const inView = box.y >= 0 && box.y + box.h <= box.vh && box.x >= 0 && box.x + box.w <= box.vw + 1;
|
|
494
|
+
const x0 = Math.max(0, box.x - pad);
|
|
495
|
+
const w = Math.max(8, Math.min(box.vw, box.x + box.w + pad) - x0);
|
|
496
|
+
if (inView) {
|
|
497
|
+
const y0 = Math.max(0, box.y - pad);
|
|
498
|
+
await page.screenshot({ path: file, clip: { x: x0, y: y0, width: w, height: Math.max(8, Math.min(box.vh, box.y + box.h + pad) - y0) } });
|
|
499
|
+
} else {
|
|
500
|
+
const y0 = Math.max(0, box.y + box.sy - pad);
|
|
501
|
+
await page.screenshot({ path: file, fullPage: true, clip: { x: x0, y: y0, width: w, height: Math.max(8, Math.min(box.h + pad * 2, 2000, box.H - y0)) } });
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
// At another width the element may not show at all (hidden, or not on the page): then the shot is that
|
|
505
|
+
// screen around where it was on the page. When it shows, a crop of it (or the screen with it in view, for
|
|
506
|
+
// a request about that width). Measured either way, for the verdict.
|
|
507
|
+
const shootAtWidth = async (page, r, file, { width, screen, pad, reopen = null, before = false, load }) => {
|
|
508
|
+
const measure = () => page.evaluate(measureInPage, { sel: r.selector, tag: r.tag, text: r.text ?? r.label?.text ?? "", width });
|
|
509
|
+
let m = await measure();
|
|
510
|
+
if (reopen?.state === "reopened" && m.state !== "shown" && before) reopen = { ...reopen, state: "couldn't reopen", why: `the steps ran, but the element ${m.state === "hidden" ? "was hidden" : "wasn't there"} after them at ${width}` };
|
|
511
|
+
if (reopen?.state === "couldn't reopen") {
|
|
512
|
+
await load();
|
|
513
|
+
m = await measure();
|
|
514
|
+
}
|
|
515
|
+
await page.waitForTimeout(150);
|
|
516
|
+
const shown = m.state === "shown";
|
|
517
|
+
if (!shown) {
|
|
518
|
+
const y = Math.max(0, (r.box?.y ?? 0) + (r.scroll?.y ?? 0) - 120);
|
|
519
|
+
await page.evaluate((top) => window.scrollTo({ left: 0, top, behavior: "instant" }), y < (r.viewport?.height ?? 900) ? 0 : y);
|
|
520
|
+
await page.waitForTimeout(200);
|
|
521
|
+
}
|
|
522
|
+
if (screen || !shown) await page.screenshot({ path: file });
|
|
523
|
+
else await cropBox(page, file, { x: m.v.x, y: m.v.y, w: m.v.w, h: m.v.h, sx: 0, sy: m.box.y - m.v.y, vw: m.vw, vh: m.vh, H: m.H }, pad);
|
|
524
|
+
const { v, ...rest } = m;
|
|
525
|
+
return { ...rest, found: shown, sig: shown ? { tag: m.tag, text: m.text } : null, width, crop: file, shot: screen || !shown ? "screen" : "element", ...(reopen ? { reopen } : {}) };
|
|
526
|
+
};
|
|
527
|
+
// How different two crops look (null when either is missing or the browser can't say).
|
|
528
|
+
const pixelDiff = (a, b) =>
|
|
529
|
+
!a || !b || !fs.existsSync(a) || !fs.existsSync(b)
|
|
530
|
+
? Promise.resolve(null)
|
|
531
|
+
: slot(async () => {
|
|
532
|
+
const ctx = await (await browser()).newContext();
|
|
533
|
+
try {
|
|
534
|
+
const page = await ctx.newPage();
|
|
535
|
+
const url = (f) => `data:image/png;base64,${fs.readFileSync(f).toString("base64")}`;
|
|
536
|
+
return await page.evaluate(diffInPage, [url(a), url(b)]);
|
|
537
|
+
} finally {
|
|
538
|
+
await ctx.close().catch(() => {});
|
|
539
|
+
}
|
|
540
|
+
}).catch(() => null);
|
|
541
|
+
|
|
542
|
+
const sourcesFor = (body) => {
|
|
543
|
+
const f = body.framework ?? {};
|
|
544
|
+
const comps = [...(f.react?.owners ?? []).map((o) => o.name), ...(f.react?.components ?? []), f.vue?.component].filter(Boolean);
|
|
545
|
+
return locate(dir, {
|
|
546
|
+
components: [...new Set(comps)],
|
|
547
|
+
texts: phrases(body.textNodes ?? []),
|
|
548
|
+
className: body.className,
|
|
549
|
+
id: body.id,
|
|
550
|
+
files: [f.vue?.file, f.svelte?.file].filter(Boolean),
|
|
551
|
+
line: f.svelte?.line,
|
|
552
|
+
routeFile: routeFile(dir, String(body.route ?? "/").split("?")[0])?.file,
|
|
553
|
+
});
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
async function addRequest(body, cookie) {
|
|
557
|
+
// A new pin after Done means the user is pointing again.
|
|
558
|
+
stopped = false;
|
|
559
|
+
const n = ++counter;
|
|
560
|
+
const id = `${stamp()}-${n}`;
|
|
561
|
+
const r = {
|
|
562
|
+
id,
|
|
563
|
+
n,
|
|
564
|
+
status: "queued",
|
|
565
|
+
created: new Date().toISOString(),
|
|
566
|
+
project: path.basename(dir),
|
|
567
|
+
route: normalizeRoute(String(body.route ?? "/")),
|
|
568
|
+
href: body.href,
|
|
569
|
+
title: body.title,
|
|
570
|
+
prompt: String(body.prompt ?? "").slice(0, 4000),
|
|
571
|
+
chips: Array.isArray(body.chips) ? body.chips.map(String).slice(0, 10) : [],
|
|
572
|
+
selector: String(body.selector ?? ""),
|
|
573
|
+
path: body.path,
|
|
574
|
+
tag: body.tag,
|
|
575
|
+
elementId: body.id,
|
|
576
|
+
className: body.className,
|
|
577
|
+
label: body.label,
|
|
578
|
+
text: body.text,
|
|
579
|
+
outerHTML: body.outerHTML,
|
|
580
|
+
styles: body.styles,
|
|
581
|
+
framework: body.framework,
|
|
582
|
+
box: body.box,
|
|
583
|
+
viewport: body.viewport,
|
|
584
|
+
dpr: body.dpr,
|
|
585
|
+
scroll: body.scroll,
|
|
586
|
+
colorScheme: body.colorScheme,
|
|
587
|
+
// The steps that opened the state the element sits in (a modal, a menu, a tab), replayed before each crop.
|
|
588
|
+
trail: cleanTrail(body.trail),
|
|
589
|
+
layer: cleanLayer(body.layer),
|
|
590
|
+
sources: [],
|
|
591
|
+
crop: null,
|
|
592
|
+
hasCrop: false,
|
|
593
|
+
hasAfter: false,
|
|
594
|
+
};
|
|
595
|
+
if (!r.trail.length) {
|
|
596
|
+
delete r.trail;
|
|
597
|
+
delete r.layer;
|
|
598
|
+
}
|
|
599
|
+
r.width = askedWidth(r.prompt, r.viewport?.width);
|
|
600
|
+
r.other = otherWidth(r.viewport, r.prompt);
|
|
601
|
+
requests.set(id, r);
|
|
602
|
+
if (cookie) cookies.set(id, cookie);
|
|
603
|
+
save(r);
|
|
604
|
+
broadcast({ type: "update", request: pub(r) });
|
|
605
|
+
// The crops (at the pinned width and the other one, side by side) and the source search finish before
|
|
606
|
+
// the agent gets the request, in the background: the dialog has already closed.
|
|
607
|
+
const work = (async () => {
|
|
608
|
+
try {
|
|
609
|
+
r.sources = sourcesFor(body);
|
|
610
|
+
} catch (e) {
|
|
611
|
+
r.sourcesError = e.message;
|
|
612
|
+
}
|
|
613
|
+
const main = (async () => {
|
|
614
|
+
try {
|
|
615
|
+
const file = path.join(root, id, "crop.png");
|
|
616
|
+
const shot = await shootElement(r, file, cookie, { before: true });
|
|
617
|
+
r.crop = file;
|
|
618
|
+
r.hasCrop = true;
|
|
619
|
+
r.cropFound = shot.found;
|
|
620
|
+
if (shot.reopen) r.reopen = { before: shot.reopen };
|
|
621
|
+
} catch (e) {
|
|
622
|
+
r.cropError = e.message.split("\n")[0];
|
|
623
|
+
}
|
|
624
|
+
})();
|
|
625
|
+
await Promise.all([main, otherBefore(r, cookie)]);
|
|
626
|
+
save(r);
|
|
627
|
+
broadcast({ type: "update", request: pub(r) });
|
|
628
|
+
logTaste({ kind: "point-request", id, project: r.project, route: r.route, prompt: r.prompt, chips: r.chips, component: r.label?.comp ?? null, tag: r.tag, selector: r.selector, crop: r.crop, viewport: r.viewport, other: r.other ? { width: r.other.width, state: r.other.before?.state ?? null } : null, ...trailLog(r) });
|
|
629
|
+
})();
|
|
630
|
+
ready.set(id, work);
|
|
631
|
+
for (const w of waiters) w();
|
|
632
|
+
return r;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// The oldest queued pin, marked working. Pins already sent are taken even after Done; once the queue is
|
|
636
|
+
// empty a stopped session answers { stopped } (or keeps waiting, for palate's own auto runner).
|
|
637
|
+
async function next(waitMs, { gone = () => false, whenStopped = "return" } = {}) {
|
|
638
|
+
const until = Date.now() + waitMs;
|
|
639
|
+
for (;;) {
|
|
640
|
+
if (gone()) return null;
|
|
641
|
+
const r = [...requests.values()].filter((x) => x.status === "queued").sort((a, b) => a.n - b.n)[0];
|
|
642
|
+
if (r) {
|
|
643
|
+
r.status = "working";
|
|
644
|
+
r.startedAt = new Date().toISOString();
|
|
645
|
+
delete r.error;
|
|
646
|
+
changed(r);
|
|
647
|
+
await ready.get(r.id);
|
|
648
|
+
if (r.other?.width && !r.other.before) {
|
|
649
|
+
await otherBefore(r);
|
|
650
|
+
save(r);
|
|
651
|
+
}
|
|
652
|
+
return r;
|
|
653
|
+
}
|
|
654
|
+
if (stopped && whenStopped !== "wait") return { stopped: true };
|
|
655
|
+
const left = until - Date.now();
|
|
656
|
+
if (left <= 0) return null;
|
|
657
|
+
await new Promise((resolve) => {
|
|
658
|
+
const done = () => {
|
|
659
|
+
waiters.delete(done);
|
|
660
|
+
clearTimeout(t);
|
|
661
|
+
resolve();
|
|
662
|
+
};
|
|
663
|
+
const t = setTimeout(done, left);
|
|
664
|
+
waiters.add(done);
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
async function reshoot(r, file, opts) {
|
|
670
|
+
await ready.get(r.id);
|
|
671
|
+
return shootElement(r, file, cookies.get(r.id), opts);
|
|
672
|
+
}
|
|
673
|
+
// The before at the other width (390 for a desktop pin, 1440 for a phone pin, or the width the user's words
|
|
674
|
+
// name), taken before any edit. A request about that width keeps its older name too (alt).
|
|
675
|
+
async function otherBefore(r, cookie = cookies.get(r.id)) {
|
|
676
|
+
const o = r.other;
|
|
677
|
+
if (!o?.width || o.before) return;
|
|
678
|
+
const file = path.join(root, r.id, `crop-${o.width}.png`);
|
|
679
|
+
try {
|
|
680
|
+
const { sig, ...shot } = await shootElement(r, file, cookie, { width: o.width, screen: o.asked, before: true });
|
|
681
|
+
o.before = shot;
|
|
682
|
+
} catch (e) {
|
|
683
|
+
o.before = { error: e.message.split("\n")[0] };
|
|
684
|
+
}
|
|
685
|
+
if (o.asked) r.alt = o.before.error ? { width: o.width, before: false, error: o.before.error } : { width: o.width, before: true, beforeFound: o.before.found, crop: file };
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// ---------- HTTP: palate's own endpoints, then the proxy ----------
|
|
689
|
+
let proxyOrigin = "";
|
|
690
|
+
const agent = new http.Agent({ keepAlive: true, maxSockets: 64 });
|
|
691
|
+
const json = (res, code, obj) => {
|
|
692
|
+
res.writeHead(code, { "content-type": "application/json", "cache-control": "no-store" });
|
|
693
|
+
res.end(JSON.stringify(obj));
|
|
694
|
+
};
|
|
695
|
+
// Only palate's own overlay (same origin) and local tools may use the API; other sites in the user's browser may not.
|
|
696
|
+
const foreign = (req) => {
|
|
697
|
+
if (req.headers["sec-fetch-site"] === "cross-site") return true;
|
|
698
|
+
const o = req.headers.origin;
|
|
699
|
+
if (!o) return false;
|
|
700
|
+
try {
|
|
701
|
+
const u = new URL(o);
|
|
702
|
+
return !(u.port === String(proxyPort) && ["localhost", "127.0.0.1", "[::1]"].includes(u.hostname));
|
|
703
|
+
} catch {
|
|
704
|
+
return true;
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
async function palateRoute(req, res, u) {
|
|
709
|
+
if (u.pathname === "/__palate/overlay.js") {
|
|
710
|
+
res.writeHead(200, { "content-type": "text/javascript; charset=utf-8", "cache-control": "no-cache" });
|
|
711
|
+
return res.end(overlaySource());
|
|
712
|
+
}
|
|
713
|
+
if (foreign(req)) return json(res, 403, { error: "forbidden" });
|
|
714
|
+
if (u.pathname === "/__palate/events") {
|
|
715
|
+
res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-store", connection: "keep-alive" });
|
|
716
|
+
res.write(`retry: 2000\ndata: ${JSON.stringify({ type: "state", requests: [...requests.values()].map(pub), agent: agentState() })}\n\n`);
|
|
717
|
+
clients.add(res);
|
|
718
|
+
const ping = setInterval(() => res.write(": ping\n\n"), 20_000);
|
|
719
|
+
req.on("close", () => {
|
|
720
|
+
clearInterval(ping);
|
|
721
|
+
clients.delete(res);
|
|
722
|
+
});
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
const img = /^\/__palate\/img\/([\w-]+)\/((?:crop|after)(?:-\d{3,4})?)\.png$/.exec(u.pathname);
|
|
726
|
+
if (img) {
|
|
727
|
+
const r = requests.get(img[1]);
|
|
728
|
+
const file = r && path.join(root, r.id, `${img[2]}.png`);
|
|
729
|
+
if (!file || !fs.existsSync(file)) return json(res, 404, { error: "no image" });
|
|
730
|
+
res.writeHead(200, { "content-type": "image/png", "cache-control": "no-store" });
|
|
731
|
+
return fs.createReadStream(file).pipe(res);
|
|
732
|
+
}
|
|
733
|
+
const api = u.pathname.replace(/^\/__palate\/api\//, "");
|
|
734
|
+
try {
|
|
735
|
+
if (api === "ping") return json(res, 200, { palate: "point", dir, base: dev.base, url: proxyOrigin, agent: agentState() });
|
|
736
|
+
if (api === "state") return json(res, 200, { requests: [...requests.values()].map(pub), agent: agentState() });
|
|
737
|
+
if (api === "request" && req.method === "POST") {
|
|
738
|
+
if (!/application\/json/.test(req.headers["content-type"] ?? "")) return json(res, 415, { error: "json only" });
|
|
739
|
+
const r = await addRequest(JSON.parse(await readBody(req)), req.headers.cookie);
|
|
740
|
+
return json(res, 200, { ...pub(r), agent: agentState() });
|
|
741
|
+
}
|
|
742
|
+
// The overlay's Done: the agent finishes the pins already sent, then its next call says point mode stopped.
|
|
743
|
+
if ((api === "stop" || api === "resume") && req.method === "POST") {
|
|
744
|
+
stopped = api === "stop";
|
|
745
|
+
for (const w of waiters) w();
|
|
746
|
+
emitAgent();
|
|
747
|
+
if (stopped) logTaste({ kind: "point-stop", project: path.basename(dir) });
|
|
748
|
+
return json(res, 200, { ok: true, agent: agentState() });
|
|
749
|
+
}
|
|
750
|
+
if (api === "retry" && req.method === "POST") {
|
|
751
|
+
const { id } = JSON.parse(await readBody(req));
|
|
752
|
+
const r = requests.get(id);
|
|
753
|
+
if (r && r.status === "failed") {
|
|
754
|
+
r.status = "queued";
|
|
755
|
+
delete r.error;
|
|
756
|
+
stopped = false;
|
|
757
|
+
changed(r);
|
|
758
|
+
}
|
|
759
|
+
return json(res, 200, { ok: true, agent: agentState() });
|
|
760
|
+
}
|
|
761
|
+
if (api === "fail" && req.method === "POST") {
|
|
762
|
+
const { id, error } = JSON.parse(await readBody(req));
|
|
763
|
+
const r = requests.get(id);
|
|
764
|
+
if (!r) return json(res, 404, { error: `No request ${id}` });
|
|
765
|
+
if (r.status === "working") {
|
|
766
|
+
r.status = "failed";
|
|
767
|
+
r.error = String(error ?? "The agent stopped without marking it done.").slice(-2000);
|
|
768
|
+
r.failedAt = new Date().toISOString();
|
|
769
|
+
changed(r);
|
|
770
|
+
logTaste({ kind: "point-failed", id: r.id, project: r.project, route: r.route, prompt: r.prompt });
|
|
771
|
+
}
|
|
772
|
+
return json(res, 200, pub(r));
|
|
773
|
+
}
|
|
774
|
+
if (api === "cancel" && req.method === "POST") {
|
|
775
|
+
const { id } = JSON.parse(await readBody(req));
|
|
776
|
+
const r = requests.get(id);
|
|
777
|
+
if (r && (r.status === "queued" || r.status === "failed")) {
|
|
778
|
+
r.status = "canceled";
|
|
779
|
+
changed(r);
|
|
780
|
+
logTaste({ kind: "point-cancel", id: r.id, project: r.project, route: r.route });
|
|
781
|
+
}
|
|
782
|
+
return json(res, 200, { ok: true });
|
|
783
|
+
}
|
|
784
|
+
if (api === "next") {
|
|
785
|
+
const wait = Math.min(Number(u.searchParams.get("wait") ?? 55), 120) * 1000;
|
|
786
|
+
let closed = false;
|
|
787
|
+
res.on("close", () => {
|
|
788
|
+
closed = true;
|
|
789
|
+
// Wake the wait so an agent that hung up stops counting as listening.
|
|
790
|
+
for (const w of waiters) w();
|
|
791
|
+
});
|
|
792
|
+
hb.polls++;
|
|
793
|
+
hb.label = u.searchParams.get("agent") || "agent";
|
|
794
|
+
hb.seen = Date.now();
|
|
795
|
+
emitAgent();
|
|
796
|
+
let r;
|
|
797
|
+
try {
|
|
798
|
+
r = await next(wait, { gone: () => closed, whenStopped: u.searchParams.get("whenStopped") ?? "return" });
|
|
799
|
+
} finally {
|
|
800
|
+
hb.polls--;
|
|
801
|
+
hb.seen = Date.now();
|
|
802
|
+
}
|
|
803
|
+
// The agent hung up while it waited: the pin goes back to the queue instead of hanging as "working".
|
|
804
|
+
if (closed && r && !r.stopped) {
|
|
805
|
+
r.status = "queued";
|
|
806
|
+
delete r.startedAt;
|
|
807
|
+
changed(r);
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
// An agent told to stop is no longer listening.
|
|
811
|
+
if (r?.stopped) {
|
|
812
|
+
hb.seen = 0;
|
|
813
|
+
hb.stopAck = Date.now();
|
|
814
|
+
}
|
|
815
|
+
emitAgent();
|
|
816
|
+
if (r?.stopped) return json(res, 200, { stopped: true });
|
|
817
|
+
return r ? json(res, 200, r) : json(res, 200, { none: true, queued: 0 });
|
|
818
|
+
}
|
|
819
|
+
if (api === "get") {
|
|
820
|
+
const r = requests.get(u.searchParams.get("id"));
|
|
821
|
+
return r ? json(res, 200, r) : json(res, 404, { error: "no such request" });
|
|
822
|
+
}
|
|
823
|
+
if ((api === "done" || api === "crop") && req.method === "POST") {
|
|
824
|
+
const { id, summary, removed: said, override } = JSON.parse(await readBody(req));
|
|
825
|
+
const r = requests.get(id);
|
|
826
|
+
if (!r) return json(res, 404, { error: `No request ${id}` });
|
|
827
|
+
hb.seen = Date.now();
|
|
828
|
+
const stampN = Date.now();
|
|
829
|
+
const file = path.join(root, r.id, api === "done" ? "after.png" : `check-${stampN}.png`);
|
|
830
|
+
// The pinned width and the other width, shot side by side.
|
|
831
|
+
const o = r.other?.width ? r.other : null;
|
|
832
|
+
const ofile = o && path.join(root, r.id, api === "done" ? `after-${o.width}.png` : `check-${stampN}-${o.width}.png`);
|
|
833
|
+
const [shot, oshot] = await Promise.all([
|
|
834
|
+
reshoot(r, file).catch((e) => {
|
|
835
|
+
if (api === "crop") throw e;
|
|
836
|
+
return null;
|
|
837
|
+
}),
|
|
838
|
+
o ? reshoot(r, ofile, { width: o.width, screen: o.asked }).catch((e) => ({ error: e.message.split("\n")[0] })) : null,
|
|
839
|
+
]);
|
|
840
|
+
// Removed (asked for, and gone or replaced by something else): no crop of whatever sits there now.
|
|
841
|
+
if (shot?.reopen) r.reopen = { ...(r.reopen ?? {}), after: shot.reopen };
|
|
842
|
+
const gone = isRemoved(r, shot, said);
|
|
843
|
+
if (gone && api === "done") fs.rmSync(file, { force: true });
|
|
844
|
+
let alt = null;
|
|
845
|
+
if (o) {
|
|
846
|
+
const ok = oshot && !oshot.error;
|
|
847
|
+
const agone = gone || (ok && isRemoved(r, oshot, false));
|
|
848
|
+
const diff = ok ? await pixelDiff(o.before?.crop, ofile) : null;
|
|
849
|
+
const { sig, ...after } = oshot ?? { error: "not shot" };
|
|
850
|
+
o.after = { ...after, crop: ok && !(agone && api === "done") ? ofile : null, removed: agone };
|
|
851
|
+
if (!o.after.crop && ok && api === "done") fs.rmSync(ofile, { force: true });
|
|
852
|
+
Object.assign(o, otherVerdict(o.before, o.after, { width: o.width, asked: o.asked, removed: Boolean(said) || REMOVE.test(r.prompt ?? ""), diff, name: elName(r) }), { diff, checkedAt: new Date().toISOString() });
|
|
853
|
+
// A request about that width: the same shots under the older name.
|
|
854
|
+
if (o.asked) alt = { width: o.width, file: o.after.crop, found: o.after.found, removed: agone, before: o.before?.crop ?? null };
|
|
855
|
+
}
|
|
856
|
+
const other = o && { width: o.width, asked: o.asked, verdict: o.verdict, note: o.note, problems: o.problems, changes: o.changes, before: o.before?.crop ?? null, after: o.after?.crop ?? null, beforeState: o.before?.state ?? null, afterState: o.after?.state ?? null, ...(r.trail ? { beforeReopen: o.before?.reopen ?? null, afterReopen: o.after?.reopen ?? null } : {}) };
|
|
857
|
+
if (api === "crop") {
|
|
858
|
+
save(r);
|
|
859
|
+
broadcast({ type: "update", request: pub(r) });
|
|
860
|
+
return json(res, 200, { id, file, found: shot?.found, removed: gone, before: r.crop, viewport: r.viewport, alt, other, ...(r.trail ? { trail: trailWords(r.trail, r.layer), reopen: shot?.reopen ?? null } : {}) });
|
|
861
|
+
}
|
|
862
|
+
// Done waits while the other width is broken, unless the user accepts it.
|
|
863
|
+
const blocked = doneBlocked(r, override);
|
|
864
|
+
if (blocked) {
|
|
865
|
+
save(r);
|
|
866
|
+
broadcast({ type: "update", request: pub(r) });
|
|
867
|
+
return json(res, 409, { error: blocked, other });
|
|
868
|
+
}
|
|
869
|
+
r.status = "done";
|
|
870
|
+
delete r.error;
|
|
871
|
+
r.summary = String(summary ?? "").slice(0, 400);
|
|
872
|
+
r.doneAt = new Date().toISOString();
|
|
873
|
+
r.removed = gone;
|
|
874
|
+
r.after = shot && !gone ? file : null;
|
|
875
|
+
r.hasAfter = Boolean(r.after);
|
|
876
|
+
r.afterFound = shot?.found;
|
|
877
|
+
if (alt) r.alt = { ...(r.alt ?? { width: r.width }), after: Boolean(alt.file), afterFound: alt.found, removed: alt.removed, afterFile: alt.file };
|
|
878
|
+
if (o && override && o.verdict === "broke") o.overridden = true;
|
|
879
|
+
changed(r);
|
|
880
|
+
logTaste({ kind: "point-done", id: r.id, project: r.project, route: r.route, prompt: r.prompt, chips: r.chips, component: r.label?.comp ?? null, tag: r.tag, selector: r.selector, summary: r.summary, before: r.crop, after: r.after, viewport: r.viewport, other: o ? { width: o.width, asked: o.asked, verdict: o.verdict, note: o.note, overridden: Boolean(o.overridden), before: o.before?.crop ?? null, after: o.after?.crop ?? null } : null, ...trailLog(r) });
|
|
881
|
+
return json(res, 200, r);
|
|
882
|
+
}
|
|
883
|
+
return json(res, 404, { error: "unknown endpoint" });
|
|
884
|
+
} catch (e) {
|
|
885
|
+
return json(res, 500, { error: String(e?.message ?? e) });
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// Requests go to the dev server as if the browser had opened it directly (its host and origin), so
|
|
890
|
+
// dev-only checks for cross-origin requests and allowed hosts pass.
|
|
891
|
+
const forwardHeaders = (req) => {
|
|
892
|
+
const h = { ...req.headers, host: target.host };
|
|
893
|
+
if (h.origin) h.origin = target.origin;
|
|
894
|
+
if (h.referer) h.referer = h.referer.replace(/^https?:\/\/[^/]+/, target.origin);
|
|
895
|
+
return h;
|
|
896
|
+
};
|
|
897
|
+
|
|
898
|
+
const proxy = (req, res) => {
|
|
899
|
+
const headers = forwardHeaders(req);
|
|
900
|
+
const isDoc = req.method === "GET" && (req.headers["sec-fetch-dest"] === "document" || /text\/html/.test(req.headers.accept ?? ""));
|
|
901
|
+
if (isDoc) headers["accept-encoding"] = "identity";
|
|
902
|
+
const up = http.request({ hostname: target.hostname, port: target.port || 80, method: req.method, path: req.url, headers, agent }, (pres) => {
|
|
903
|
+
const h = { ...pres.headers };
|
|
904
|
+
if (h.location) h.location = h.location.replace(target.origin, proxyOrigin);
|
|
905
|
+
const html = /text\/html/.test(h["content-type"] ?? "") && !h["content-encoding"] && req.method !== "HEAD" && pres.statusCode !== 204 && pres.statusCode !== 304;
|
|
906
|
+
if (!html) {
|
|
907
|
+
res.writeHead(pres.statusCode, pres.statusMessage, h);
|
|
908
|
+
return pres.pipe(res);
|
|
909
|
+
}
|
|
910
|
+
delete h["content-length"];
|
|
911
|
+
delete h.etag;
|
|
912
|
+
res.writeHead(pres.statusCode, pres.statusMessage, h);
|
|
913
|
+
// Inject while streaming: before </head> (or <body> when there's no head), never holding the page back.
|
|
914
|
+
let carry = Buffer.alloc(0);
|
|
915
|
+
let injected = false;
|
|
916
|
+
pres.on("data", (chunk) => {
|
|
917
|
+
if (injected) return res.write(chunk);
|
|
918
|
+
const buf = Buffer.concat([carry, chunk]);
|
|
919
|
+
const s = buf.toString("latin1");
|
|
920
|
+
const m = /<\/head\s*>|<body[\s>]/i.exec(s);
|
|
921
|
+
if (m) {
|
|
922
|
+
res.write(buf.subarray(0, m.index));
|
|
923
|
+
res.write(TAG);
|
|
924
|
+
res.write(buf.subarray(m.index));
|
|
925
|
+
injected = true;
|
|
926
|
+
carry = Buffer.alloc(0);
|
|
927
|
+
} else {
|
|
928
|
+
const keep = Math.min(buf.length, 8);
|
|
929
|
+
res.write(buf.subarray(0, buf.length - keep));
|
|
930
|
+
carry = buf.subarray(buf.length - keep);
|
|
931
|
+
}
|
|
932
|
+
});
|
|
933
|
+
pres.on("end", () => {
|
|
934
|
+
if (!injected) {
|
|
935
|
+
res.write(carry);
|
|
936
|
+
res.write(TAG);
|
|
937
|
+
}
|
|
938
|
+
res.end();
|
|
939
|
+
});
|
|
940
|
+
pres.on("error", () => res.destroy());
|
|
941
|
+
});
|
|
942
|
+
up.on("error", (e) => {
|
|
943
|
+
if (res.headersSent) return res.destroy();
|
|
944
|
+
res.writeHead(502, { "content-type": "text/plain" });
|
|
945
|
+
res.end(`palate point: the dev server at ${target.origin} didn't answer (${e.code ?? e.message}).`);
|
|
946
|
+
});
|
|
947
|
+
req.pipe(up);
|
|
948
|
+
};
|
|
949
|
+
|
|
950
|
+
// The proxy rewrites Host and Origin for the dev server, which would switch off the dev server's own
|
|
951
|
+
// allowed-hosts and origin checks; so they're checked here. Only localhost names on this port (no DNS
|
|
952
|
+
// rebinding), and no other site's scripts (a link or redirect from another site still opens the page).
|
|
953
|
+
const localHost = (req) => {
|
|
954
|
+
try {
|
|
955
|
+
const h = new URL(`http://${req.headers.host}`);
|
|
956
|
+
return ["localhost", "127.0.0.1", "[::1]"].includes(h.hostname) && h.port === String(proxyPort);
|
|
957
|
+
} catch {
|
|
958
|
+
return false;
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
const server = http.createServer((req, res) => {
|
|
962
|
+
if (!localHost(req) || (req.headers["sec-fetch-site"] === "cross-site" && req.headers["sec-fetch-mode"] !== "navigate")) {
|
|
963
|
+
res.writeHead(403, { "content-type": "text/plain" });
|
|
964
|
+
return res.end("palate point: only for this machine's browser at localhost.");
|
|
965
|
+
}
|
|
966
|
+
const u = new URL(req.url, "http://x");
|
|
967
|
+
if (u.pathname.startsWith("/__palate/")) return void palateRoute(req, res, u);
|
|
968
|
+
proxy(req, res);
|
|
969
|
+
});
|
|
970
|
+
// Websockets (Next, Vite, Astro, Nuxt HMR …) are piped through byte for byte.
|
|
971
|
+
server.on("upgrade", (req, socket, head) => {
|
|
972
|
+
if (req.url.startsWith("/__palate/") || !localHost(req) || foreign(req)) return socket.destroy();
|
|
973
|
+
const up = net.connect(Number(target.port || 80), target.hostname, () => {
|
|
974
|
+
const h = forwardHeaders(req);
|
|
975
|
+
const lines = [`${req.method} ${req.url} HTTP/${req.httpVersion}`];
|
|
976
|
+
for (const [k, v] of Object.entries(h)) for (const x of Array.isArray(v) ? v : [v]) lines.push(`${k}: ${x}`);
|
|
977
|
+
up.write(lines.join("\r\n") + "\r\n\r\n");
|
|
978
|
+
if (head?.length) up.write(head);
|
|
979
|
+
up.pipe(socket);
|
|
980
|
+
socket.pipe(up);
|
|
981
|
+
});
|
|
982
|
+
up.on("error", () => socket.destroy());
|
|
983
|
+
socket.on("error", () => up.destroy());
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
const proxyPort = port ?? (await freePort());
|
|
987
|
+
await new Promise((resolve, reject) => {
|
|
988
|
+
server.once("error", reject);
|
|
989
|
+
server.listen(proxyPort, "127.0.0.1", resolve);
|
|
990
|
+
});
|
|
991
|
+
proxyOrigin = `http://localhost:${proxyPort}`;
|
|
992
|
+
fs.writeFileSync(serverFile(dir), JSON.stringify({ url: proxyOrigin, port: proxyPort, base: dev.base, pid: process.pid, dir, started: new Date().toISOString() }, null, 2));
|
|
993
|
+
const link = proxyOrigin + normalizeRoute(route);
|
|
994
|
+
log(`palate point: ${link} (in front of ${dev.name} at ${dev.base})`);
|
|
995
|
+
if (open) (await import("../loop/pick.mjs")).openBrowser(link);
|
|
996
|
+
|
|
997
|
+
let finish;
|
|
998
|
+
const done = new Promise((r) => (finish = r));
|
|
999
|
+
const stop = async () => {
|
|
1000
|
+
clearInterval(staleTimer);
|
|
1001
|
+
for (const c of clients) c.end();
|
|
1002
|
+
for (const w of waiters) w();
|
|
1003
|
+
server.close();
|
|
1004
|
+
server.closeAllConnections?.();
|
|
1005
|
+
agent.destroy();
|
|
1006
|
+
if (browserP) await (await browserP.catch(() => null))?.close().catch(() => {});
|
|
1007
|
+
dev.stop();
|
|
1008
|
+
try {
|
|
1009
|
+
if (readJson(serverFile(dir), {}).port === proxyPort) fs.rmSync(serverFile(dir), { force: true });
|
|
1010
|
+
} catch {}
|
|
1011
|
+
finish();
|
|
1012
|
+
};
|
|
1013
|
+
return { url: proxyOrigin, link, base: dev.base, reused: false, stop, done };
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// ---------- The agent's side: the same calls over HTTP, from the CLI or the MCP server ----------
|
|
1017
|
+
async function api(dir, p, { method = "GET", body, timeout = 70_000 } = {}) {
|
|
1018
|
+
const info = await runningPoint(dir);
|
|
1019
|
+
if (!info) throw new Error("palate point isn't running for this project. Start it: palate point [route] (or palate_point_start).");
|
|
1020
|
+
const r = await fetch(`${info.url}/__palate/api/${p}`, { method, headers: body ? { "content-type": "application/json" } : {}, body: body ? JSON.stringify(body) : undefined, signal: AbortSignal.timeout(timeout) });
|
|
1021
|
+
const j = await r.json();
|
|
1022
|
+
if (!r.ok) throw new Error(j.error ?? `palate point answered ${r.status}`);
|
|
1023
|
+
return j;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// The next request, null when the wait ran out, or { stopped: true } when the user pressed Done.
|
|
1027
|
+
// agent: who is listening (shown in the overlay); whenStopped "wait" keeps waiting through a Done.
|
|
1028
|
+
export const nextRequest = (dir, { wait = 55, agent = "agent", whenStopped = "return" } = {}) =>
|
|
1029
|
+
api(dir, `next?wait=${wait}&agent=${encodeURIComponent(agent)}&whenStopped=${whenStopped}`, { timeout: (wait + 20) * 1000 }).then((r) => (r.none ? null : r));
|
|
1030
|
+
// override: mark it done even though the other width broke (only when the user accepts that).
|
|
1031
|
+
export const doneRequest = (dir, id, summary, { removed = false, override = false } = {}) => api(dir, "done", { method: "POST", body: { id, summary, removed, override }, timeout: 180_000 });
|
|
1032
|
+
export const cropRequest = (dir, id) => api(dir, "crop", { method: "POST", body: { id }, timeout: 180_000 });
|
|
1033
|
+
export const getRequest = (dir, id) => api(dir, `get?id=${encodeURIComponent(id)}`);
|
|
1034
|
+
export const failRequest = (dir, id, error) => api(dir, "fail", { method: "POST", body: { id, error } });
|
|
1035
|
+
export const stopPointMode = (dir) => api(dir, "stop", { method: "POST", body: {} });
|
|
1036
|
+
export const pointState = (dir) => api(dir, "state");
|
|
1037
|
+
|
|
1038
|
+
const slash = (p) => String(p ?? "").replace(/\\/g, "/");
|
|
1039
|
+
|
|
1040
|
+
// What each quick chip asks for, so every agent reads them the same way.
|
|
1041
|
+
const CHIP_BRIEF = {
|
|
1042
|
+
"Fix spacing": "Spacing: one rhythm from the project's spacing scale. Equal gaps between siblings, padding that matches its neighbours, more space between groups than inside them.",
|
|
1043
|
+
"Fix alignment": "Alignment: shared left edges and baselines, one alignment axis per group, optically centred icons. Nothing a few pixels off.",
|
|
1044
|
+
"Type hierarchy": "Type: one obvious first read, 2–3 sizes inside this element, weight and colour before size, 45–75 characters per line, tighter tracking on large type. Use the project's type tokens.",
|
|
1045
|
+
"Make it premium": "Premium is restraint: fewer borders and boxes, precise radii and shadows from the tokens, real content, generous space, one crafted detail at most. No gradient text, glows or decoration for its own sake.",
|
|
1046
|
+
"Add motion": "Motion that explains: one purposeful transition (a state change, or an entrance that shows where something came from), 150–300 ms ease-out, transform and opacity only, prefers-reduced-motion respected, content visible without it.",
|
|
1047
|
+
"Real content & avatars": "Real content: replace placeholders with the project's real data. People get real photos or generated avatars (never initials in circles), brands their official marks. Never invent testimonials, customers or numbers.",
|
|
1048
|
+
"Variants of this": "",
|
|
1049
|
+
};
|
|
1050
|
+
|
|
1051
|
+
// The request as the agent reads it.
|
|
1052
|
+
export function formatRequest(r, dir) {
|
|
1053
|
+
const L = r.label ?? {};
|
|
1054
|
+
const f = r.framework ?? {};
|
|
1055
|
+
const comp = L.comp ?? f.react?.component ?? f.vue?.component ?? null;
|
|
1056
|
+
const owners = (f.react?.owners ?? []).filter((o, i) => i || o.name !== comp).map((o) => `${o.name}${o.server ? " (server)" : ""}`);
|
|
1057
|
+
const variants = (r.chips ?? []).includes("Variants of this");
|
|
1058
|
+
const styles = Object.entries(r.styles ?? {}).map(([k, v]) => `${k}: ${v}`).join("; ");
|
|
1059
|
+
const vp = r.viewport ?? {};
|
|
1060
|
+
const o = r.other?.width ? r.other : null;
|
|
1061
|
+
const at = (w) => `${w} (${widthName(w)})`;
|
|
1062
|
+
// Where the problem appears, first: the width the user pointed at, and the one the fix is also checked at.
|
|
1063
|
+
const where = vp.width
|
|
1064
|
+
? `**Pinned at ${vp.width}×${vp.height} (${widthName(vp.width)}).**${o?.asked ? ` **The problem is at ${at(o.width)}, from the user's words; judge the fix there.**` : ""}${o ? ` palate checks the fix at ${vp.width} and at ${at(o.width)}, before and after: palate_point_crop shows both, and palate_point_done refuses while ${o.width} is broken (the page scrolls sideways, the element disappears, or text in it is cut off).` : ""}`
|
|
1065
|
+
: "";
|
|
1066
|
+
// A pin inside an opened state: the steps that open it, in plain words.
|
|
1067
|
+
const tw = trailWords(r.trail, r.layer);
|
|
1068
|
+
const opened = tw ? `**This element is inside a state opened by: ${tw}. palate reopens it for every check:** it replays those steps on a fresh load before each crop, at both widths.` : "";
|
|
1069
|
+
const stuck = (x) => (x?.state === "couldn't reopen" ? ` palate couldn't reopen the opened state for it (${x.why}), so this is the page as it loads.` : "");
|
|
1070
|
+
const ob = o?.before;
|
|
1071
|
+
const otherLine = !o
|
|
1072
|
+
? ""
|
|
1073
|
+
: !ob
|
|
1074
|
+
? `- Before at ${at(o.width)}: none yet.`
|
|
1075
|
+
: ob.error
|
|
1076
|
+
? `- No before at ${at(o.width)}: ${ob.error}`
|
|
1077
|
+
: `- Before at ${at(o.width)}: ${slash(ob.crop)}${ob.state === "hidden" ? " (the element is hidden at this width; this is the screen around where it was)" : ob.state === "missing" ? " (the element isn't on the page at this width; this is the screen around where it was)" : o.asked ? " (that screen, with the element in view)" : ""}${ob.overflow > 1 ? `. The page already scrolls sideways there (${ob.overflow}px) before your change.` : ""}${stuck(ob.reopen)}`;
|
|
1078
|
+
const out = [
|
|
1079
|
+
`# Point request ${r.n} (id ${r.id})`,
|
|
1080
|
+
"",
|
|
1081
|
+
where,
|
|
1082
|
+
"",
|
|
1083
|
+
opened,
|
|
1084
|
+
"",
|
|
1085
|
+
`**The user asks:** ${r.prompt ? `"${r.prompt}"` : "(no text)"}${r.chips?.length ? ` · chips: ${r.chips.join(", ")}` : ""}`,
|
|
1086
|
+
"",
|
|
1087
|
+
"## The element",
|
|
1088
|
+
`- Route: ${r.route} · viewport ${r.viewport?.width}×${r.viewport?.height}, scrolled to ${r.scroll?.x ?? 0},${r.scroll?.y ?? 0}`,
|
|
1089
|
+
`- ${comp ? `<${comp}> ` : ""}${r.tag}${r.elementId ? `#${r.elementId}` : ""}${L.text ? ` "${L.text}"` : ""}, ${r.box?.width}×${r.box?.height} at (${r.box?.x}, ${r.box?.y}) in the viewport`,
|
|
1090
|
+
`- Selector: \`${r.selector}\``,
|
|
1091
|
+
`- DOM path: \`${r.path}\``,
|
|
1092
|
+
comp || owners.length ? `- Component: ${comp ?? "?"}${owners.length ? ` · rendered by: ${owners.join(" ← ")}` : ""}${f.react?.components?.length ? ` · inside: ${f.react.components.slice(0, 5).join(" ← ")}` : ""}` : "",
|
|
1093
|
+
f.vue ? `- Vue component: ${f.vue.component ?? "?"}${f.vue.file ? ` (${slash(f.vue.file)})` : ""}` : "",
|
|
1094
|
+
f.svelte ? `- Svelte source: ${slash(f.svelte.file)}:${f.svelte.line}` : "",
|
|
1095
|
+
r.crop ? `- Crop when the user pointed: ${slash(r.crop)}${stuck(r.reopen?.before) || (r.cropFound === false ? ` (the selector didn't match on a fresh load${r.trail ? " after the steps" : ""}, so this is the region where it was)` : "")}` : `- No crop${r.cropError ? `: ${r.cropError}` : ""}`,
|
|
1096
|
+
otherLine,
|
|
1097
|
+
"",
|
|
1098
|
+
"## Where it lives (best guesses; confirm by the text and classes)",
|
|
1099
|
+
...(r.sources?.length ? r.sources.map((s, i) => `${i + 1}. ${s.file}:${s.line} (${s.why})${s.snippet ? `\n \`${s.snippet.replace(/`/g, "'")}\`` : ""}`) : ["No match in the source. Search for the text or class names yourself."]),
|
|
1100
|
+
"",
|
|
1101
|
+
styles ? `## Key computed styles\n${styles}\n` : "",
|
|
1102
|
+
"## outerHTML (trimmed)",
|
|
1103
|
+
"```html",
|
|
1104
|
+
String(r.outerHTML ?? "").slice(0, 6000),
|
|
1105
|
+
"```",
|
|
1106
|
+
"",
|
|
1107
|
+
"## What to do",
|
|
1108
|
+
];
|
|
1109
|
+
if (variants)
|
|
1110
|
+
out.push(
|
|
1111
|
+
`- The user wants variants, not one edit. Run palate_vary with route "${r.route}" and section \`${r.selector}\` (CLI: palate vary ${r.route} --section "${r.selector}"), build its briefs, then palate_pick_start so the user picks. When the pick is made real, call palate_point_done with id "${r.id}" and what was chosen.`,
|
|
1112
|
+
);
|
|
1113
|
+
else out.push("- Change exactly this element (or this section, if it's a section) in its source. Keep everything around it as it is, and keep the project's tokens, components and copy rules.");
|
|
1114
|
+
for (const c of r.chips ?? []) if (CHIP_BRIEF[c]) out.push(`- ${CHIP_BRIEF[c]}`);
|
|
1115
|
+
out.push(
|
|
1116
|
+
"- If the request is unclear or would change shared components used elsewhere, say so in the summary instead of guessing.",
|
|
1117
|
+
...(r.trail ? ["- If a check says palate couldn't reopen the opened state (say a button in the steps was renamed), that width isn't checked in it; it never counts as broken and never blocks done. Look at the change in the code, and say so in the summary."] : []),
|
|
1118
|
+
`- Verify: palate_point_crop with id "${r.id}" (CLI: palate point crop ${r.id}) re-shoots this element after your edit${o ? `, at ${vp.width ?? "the pinned width"} and at ${o.width}, with a verdict for ${o.width} (unchanged, changed or broke)` : ""}; open it next to the before crop. Fix once more if it isn't better${o ? `, and fix ${o.width} too if it broke` : ""}.`,
|
|
1119
|
+
`- Then palate_point_done with id "${r.id}" and a one-line summary of what changed (CLI: palate point done ${r.id} "<summary>"). The user's pin turns done and shows before and after.${REMOVE.test(r.prompt ?? "") ? ` If you removed the element, pass removed: true (CLI: --removed) so the after view says "Removed" instead of showing what sits there now.` : ""}`,
|
|
1120
|
+
);
|
|
1121
|
+
return out.filter((l) => l !== "").join("\n").replace(/\n## /g, "\n\n## ").replace(/\n{3,}/g, "\n\n");
|
|
1122
|
+
}
|