@palate/cli 0.0.0-stage → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +151 -2
- package/bin/palate.mjs +894 -0
- package/kb/anatomy/data-table.md +24 -0
- package/kb/anatomy/empty-state.md +16 -0
- package/kb/anatomy/filter-toolbar.md +19 -0
- package/kb/anatomy/meter.md +16 -0
- package/kb/anatomy/product-window.md +18 -0
- package/kb/anatomy/settings-page.md +18 -0
- package/kb/anatomy/status-indicator.md +17 -0
- package/kb/anatomy/toast-undo.md +16 -0
- package/kb/bench/briefs.json +618 -0
- package/kb/craft/SOURCES.md +48 -0
- package/kb/craft/creativity.md +55 -0
- package/kb/craft/details.md +75 -0
- package/kb/craft/landing-demos.md +46 -0
- package/kb/craft/mistakes.md +73 -0
- package/kb/craft/motion.md +53 -0
- package/kb/craft/principles.md +68 -0
- package/kb/index/discovery.json +1 -0
- package/kb/inspiration/index.json +79 -0
- package/kb/moves/animated-comparison-table.md +58 -0
- package/kb/moves/before-after-slider.md +58 -0
- package/kb/moves/bento-live-tiles.md +57 -0
- package/kb/moves/calm-motion-system.md +63 -0
- package/kb/moves/canvas-grain.md +58 -0
- package/kb/moves/changelog-as-magazine.md +56 -0
- package/kb/moves/code-drawn-diagram.md +56 -0
- package/kb/moves/colophon.md +54 -0
- package/kb/moves/command-palette-site-nav.md +58 -0
- package/kb/moves/confetti-free-celebration.md +58 -0
- package/kb/moves/console-easter-egg.md +57 -0
- package/kb/moves/copy-confirmation.md +57 -0
- package/kb/moves/cursor-reactive-light.md +57 -0
- package/kb/moves/dark-band-contrast-section.md +55 -0
- package/kb/moves/designed-dark-mode.md +58 -0
- package/kb/moves/duotone-photography.md +55 -0
- package/kb/moves/editorial-drop-cap.md +58 -0
- package/kb/moves/editorial-grid-pull-quotes.md +55 -0
- package/kb/moves/engraving-colour-lens.md +64 -0
- package/kb/moves/explorable-control.md +57 -0
- package/kb/moves/feature-films-on-painted-stages.md +58 -0
- package/kb/moves/first-win-onboarding.md +59 -0
- package/kb/moves/footer-live-status.md +56 -0
- package/kb/moves/footer-rewards-the-end.md +58 -0
- package/kb/moves/generative-brand-pattern.md +55 -0
- package/kb/moves/gentle-error-nudge.md +59 -0
- package/kb/moves/halftone-dot-imagery.md +55 -0
- package/kb/moves/hand-drawn-annotations.md +55 -0
- package/kb/moves/hero-paints-itself-in.md +65 -0
- package/kb/moves/honest-progress-narration.md +58 -0
- package/kb/moves/honest-testimonial-wall.md +57 -0
- package/kb/moves/inline-sparklines-in-copy.md +57 -0
- package/kb/moves/inspect-our-taste.md +58 -0
- package/kb/moves/interactive-pricing-calculator.md +59 -0
- package/kb/moves/isometric-illustration-system.md +56 -0
- package/kb/moves/kinetic-variable-type.md +57 -0
- package/kb/moves/konami-secret.md +59 -0
- package/kb/moves/layered-paper-depth.md +56 -0
- package/kb/moves/lightweight-3d-object-hero.md +59 -0
- package/kb/moves/line-engraving-from-image.md +63 -0
- package/kb/moves/link-preview-peek.md +57 -0
- package/kb/moves/live-data-ticker.md +61 -0
- package/kb/moves/made-with-proof.md +51 -0
- package/kb/moves/magnetic-buttons.md +55 -0
- package/kb/moves/marquee-done-right.md +58 -0
- package/kb/moves/moves.yaml +450 -0
- package/kb/moves/museum-painting-backdrop.md +59 -0
- package/kb/moves/one-colour-brand-world.md +56 -0
- package/kb/moves/playful-404.md +58 -0
- package/kb/moves/pointer-highlight-word.md +56 -0
- package/kb/moves/product-in-context.md +56 -0
- package/kb/moves/real-world-data.md +58 -0
- package/kb/moves/referral-reward-reveal.md +59 -0
- package/kb/moves/riso-print-texture.md +57 -0
- package/kb/moves/screenshot-detail-crops.md +56 -0
- package/kb/moves/scroll-progress-chapters.md +57 -0
- package/kb/moves/scroll-scrubbed-product-film.md +62 -0
- package/kb/moves/self-playing-product-demo.md +61 -0
- package/kb/moves/shape-continuity-morph.md +58 -0
- package/kb/moves/sliding-nav-indicator.md +58 -0
- package/kb/moves/sticky-scroll-story.md +56 -0
- package/kb/moves/story-led-long-scroll.md +56 -0
- package/kb/moves/tactile-button.md +63 -0
- package/kb/moves/technical-drawing-lines.md +57 -0
- package/kb/moves/time-of-day-theming.md +56 -0
- package/kb/moves/tiny-game-easter-egg.md +60 -0
- package/kb/moves/type-decode-reveal.md +57 -0
- package/kb/moves/typographic-numeral-hero.md +58 -0
- package/kb/moves/view-transitions-between-pages.md +57 -0
- package/kb/moves/waitlist-position-live.md +60 -0
- package/kb/patterns/app-team-members.md +27 -0
- package/kb/patterns/chat-agent-approvals.md +31 -0
- package/kb/patterns/commerce-product-page.md +34 -0
- package/kb/patterns/dashboard-entity-table.md +37 -0
- package/kb/patterns/dashboard-home-today.md +24 -0
- package/kb/patterns/docs-api-reference.md +31 -0
- package/kb/patterns/marketing-product-led-landing.md +30 -0
- package/kb/patterns/mobile-daily-checklist.md +26 -0
- package/kb/references/index.json +1 -0
- package/kb/references/thumbs/004c5e9072.jpg +0 -0
- package/kb/references/thumbs/008915eb21.jpg +0 -0
- package/kb/references/thumbs/0098cf66c2.jpg +0 -0
- package/kb/references/thumbs/012e61b31f.jpg +0 -0
- package/kb/references/thumbs/014d4e46fc.jpg +0 -0
- package/kb/references/thumbs/01e54f394a.jpg +0 -0
- package/kb/references/thumbs/01e99a579c.jpg +0 -0
- package/kb/references/thumbs/01fd014575.jpg +0 -0
- package/kb/references/thumbs/022778ca72.jpg +0 -0
- package/kb/references/thumbs/02717bf1e1.jpg +0 -0
- package/kb/references/thumbs/02a5cfdbe9.jpg +0 -0
- package/kb/references/thumbs/02c9823f8d.jpg +0 -0
- package/kb/references/thumbs/02f3974613.jpg +0 -0
- package/kb/references/thumbs/032427f1ce.jpg +0 -0
- package/kb/references/thumbs/03ba677d07.jpg +0 -0
- package/kb/references/thumbs/04f2476cfe.jpg +0 -0
- package/kb/references/thumbs/0563e7c52e.jpg +0 -0
- package/kb/references/thumbs/05fa6b8eef.jpg +0 -0
- package/kb/references/thumbs/065bce1517.jpg +0 -0
- package/kb/references/thumbs/06c017f113.jpg +0 -0
- package/kb/references/thumbs/0770e2d8d3.jpg +0 -0
- package/kb/references/thumbs/078a26c4cb.jpg +0 -0
- package/kb/references/thumbs/08679b6e84.jpg +0 -0
- package/kb/references/thumbs/088c45a34c.jpg +0 -0
- package/kb/references/thumbs/089e9fd4e4.jpg +0 -0
- package/kb/references/thumbs/08f773231e.jpg +0 -0
- package/kb/references/thumbs/093ca3e2e3.jpg +0 -0
- package/kb/references/thumbs/0963320f74.jpg +0 -0
- package/kb/references/thumbs/09fc1ded85.jpg +0 -0
- package/kb/references/thumbs/0a368f3963.jpg +0 -0
- package/kb/references/thumbs/0a9e0e5201.jpg +0 -0
- package/kb/references/thumbs/0aace67ee7.jpg +0 -0
- package/kb/references/thumbs/0b61ecf793.jpg +0 -0
- package/kb/references/thumbs/0bde18ae09.jpg +0 -0
- package/kb/references/thumbs/0bf7ee8143.jpg +0 -0
- package/kb/references/thumbs/0ca20a13e0.jpg +0 -0
- package/kb/references/thumbs/0cdc0198c4.jpg +0 -0
- package/kb/references/thumbs/0cf9790b28.jpg +0 -0
- package/kb/references/thumbs/0d0202904b.jpg +0 -0
- package/kb/references/thumbs/0d053aba18.jpg +0 -0
- package/kb/references/thumbs/0d0c2c13d2.jpg +0 -0
- package/kb/references/thumbs/0d30be99b0.jpg +0 -0
- package/kb/references/thumbs/0d503da1b7.jpg +0 -0
- package/kb/references/thumbs/0ddad7b740.jpg +0 -0
- package/kb/references/thumbs/0e0a148101.jpg +0 -0
- package/kb/references/thumbs/0e401b6b75.jpg +0 -0
- package/kb/references/thumbs/0e8f32ba0f.jpg +0 -0
- package/kb/references/thumbs/0ef1966a7d.jpg +0 -0
- package/kb/references/thumbs/0efd416d11.jpg +0 -0
- package/kb/references/thumbs/0f3af263ae.jpg +0 -0
- package/kb/references/thumbs/0f9644c3a5.jpg +0 -0
- package/kb/references/thumbs/1020552737.jpg +0 -0
- package/kb/references/thumbs/1037626587.jpg +0 -0
- package/kb/references/thumbs/10512b7e94.jpg +0 -0
- package/kb/references/thumbs/106dc4a7b5.jpg +0 -0
- package/kb/references/thumbs/108943f6f7.jpg +0 -0
- package/kb/references/thumbs/10a5551d51.jpg +0 -0
- package/kb/references/thumbs/10dd01076c.jpg +0 -0
- package/kb/references/thumbs/11b06e39da.jpg +0 -0
- package/kb/references/thumbs/11baa1ec83.jpg +0 -0
- package/kb/references/thumbs/11bbe12167.jpg +0 -0
- package/kb/references/thumbs/11ca37feb8.jpg +0 -0
- package/kb/references/thumbs/11f14d3f12.jpg +0 -0
- package/kb/references/thumbs/124530ceb5.jpg +0 -0
- package/kb/references/thumbs/1276402336.jpg +0 -0
- package/kb/references/thumbs/1280e5c9d6.jpg +0 -0
- package/kb/references/thumbs/12cac0d172.jpg +0 -0
- package/kb/references/thumbs/138be6623a.jpg +0 -0
- package/kb/references/thumbs/13a0c544d1.jpg +0 -0
- package/kb/references/thumbs/13a32cb94c.jpg +0 -0
- package/kb/references/thumbs/144035d641.jpg +0 -0
- package/kb/references/thumbs/1457e41924.jpg +0 -0
- package/kb/references/thumbs/14972e3a86.jpg +0 -0
- package/kb/references/thumbs/15a47f2e4c.jpg +0 -0
- package/kb/references/thumbs/15aa15d22e.jpg +0 -0
- package/kb/references/thumbs/160dcd24b7.jpg +0 -0
- package/kb/references/thumbs/1610978529.jpg +0 -0
- package/kb/references/thumbs/16db30ba37.jpg +0 -0
- package/kb/references/thumbs/16f19ae4f3.jpg +0 -0
- package/kb/references/thumbs/16f758b03f.jpg +0 -0
- package/kb/references/thumbs/1748e5b751.jpg +0 -0
- package/kb/references/thumbs/175d0b0e69.jpg +0 -0
- package/kb/references/thumbs/178f571a36.jpg +0 -0
- package/kb/references/thumbs/17c43dc025.jpg +0 -0
- package/kb/references/thumbs/17c7f05b50.jpg +0 -0
- package/kb/references/thumbs/17f97e61dd.jpg +0 -0
- package/kb/references/thumbs/1804e17365.jpg +0 -0
- package/kb/references/thumbs/195c162ed1.jpg +0 -0
- package/kb/references/thumbs/19e3699c29.jpg +0 -0
- package/kb/references/thumbs/1a48e0a4d5.jpg +0 -0
- package/kb/references/thumbs/1a6cda265e.jpg +0 -0
- package/kb/references/thumbs/1a7e012091.jpg +0 -0
- package/kb/references/thumbs/1a9173a172.jpg +0 -0
- package/kb/references/thumbs/1b34a6b4d2.jpg +0 -0
- package/kb/references/thumbs/1b5284f3b2.jpg +0 -0
- package/kb/references/thumbs/1b93fbd6d3.jpg +0 -0
- package/kb/references/thumbs/1c6728fef6.jpg +0 -0
- package/kb/references/thumbs/1c6f101618.jpg +0 -0
- package/kb/references/thumbs/1cf7011a94.jpg +0 -0
- package/kb/references/thumbs/1d1769e539.jpg +0 -0
- package/kb/references/thumbs/1d1fd23199.jpg +0 -0
- package/kb/references/thumbs/1d7b6602ac.jpg +0 -0
- package/kb/references/thumbs/1dbbc61b9e.jpg +0 -0
- package/kb/references/thumbs/1dbdd6d21d.jpg +0 -0
- package/kb/references/thumbs/1dd873744b.jpg +0 -0
- package/kb/references/thumbs/1df4dd7cf9.jpg +0 -0
- package/kb/references/thumbs/1ece8c35b8.jpg +0 -0
- package/kb/references/thumbs/1f0e5b9aa4.jpg +0 -0
- package/kb/references/thumbs/1f1419651b.jpg +0 -0
- package/kb/references/thumbs/1f2a6da92d.jpg +0 -0
- package/kb/references/thumbs/1f651f33c0.jpg +0 -0
- package/kb/references/thumbs/1f8b697d18.jpg +0 -0
- package/kb/references/thumbs/1fee3824cd.jpg +0 -0
- package/kb/references/thumbs/1ff7757853.jpg +0 -0
- package/kb/references/thumbs/2043182f01.jpg +0 -0
- package/kb/references/thumbs/2059dabcd5.jpg +0 -0
- package/kb/references/thumbs/209d9dbb95.jpg +0 -0
- package/kb/references/thumbs/20d8923554.jpg +0 -0
- package/kb/references/thumbs/210dd3b671.jpg +0 -0
- package/kb/references/thumbs/2120719116.jpg +0 -0
- package/kb/references/thumbs/21a7384429.jpg +0 -0
- package/kb/references/thumbs/225f187391.jpg +0 -0
- package/kb/references/thumbs/230caf7dd2.jpg +0 -0
- package/kb/references/thumbs/235afad355.jpg +0 -0
- package/kb/references/thumbs/2378066687.jpg +0 -0
- package/kb/references/thumbs/241faf68a1.jpg +0 -0
- package/kb/references/thumbs/24d9f1a9b9.jpg +0 -0
- package/kb/references/thumbs/2505c8b9a0.jpg +0 -0
- package/kb/references/thumbs/25593638ca.jpg +0 -0
- package/kb/references/thumbs/25dd454f18.jpg +0 -0
- package/kb/references/thumbs/2602e123e2.jpg +0 -0
- package/kb/references/thumbs/26095c0b11.jpg +0 -0
- package/kb/references/thumbs/26260d6238.jpg +0 -0
- package/kb/references/thumbs/26bba21836.jpg +0 -0
- package/kb/references/thumbs/26bde3520b.jpg +0 -0
- package/kb/references/thumbs/272e22067c.jpg +0 -0
- package/kb/references/thumbs/27421b3226.jpg +0 -0
- package/kb/references/thumbs/2769ebca41.jpg +0 -0
- package/kb/references/thumbs/27e2f6e8b0.jpg +0 -0
- package/kb/references/thumbs/28432ba373.jpg +0 -0
- package/kb/references/thumbs/2846626f19.jpg +0 -0
- package/kb/references/thumbs/28750e821e.jpg +0 -0
- package/kb/references/thumbs/289481230f.jpg +0 -0
- package/kb/references/thumbs/2897967408.jpg +0 -0
- package/kb/references/thumbs/28c0a5fa17.jpg +0 -0
- package/kb/references/thumbs/28c181fba5.jpg +0 -0
- package/kb/references/thumbs/29806f539f.jpg +0 -0
- package/kb/references/thumbs/2997f56ee7.jpg +0 -0
- package/kb/references/thumbs/29b34a10fb.jpg +0 -0
- package/kb/references/thumbs/29bd78cdb4.jpg +0 -0
- package/kb/references/thumbs/29c8c6c22a.jpg +0 -0
- package/kb/references/thumbs/2ac23f7fae.jpg +0 -0
- package/kb/references/thumbs/2b76e13f79.jpg +0 -0
- package/kb/references/thumbs/2b84226479.jpg +0 -0
- package/kb/references/thumbs/2bf2dbf737.jpg +0 -0
- package/kb/references/thumbs/2bf89abbf6.jpg +0 -0
- package/kb/references/thumbs/2c562cd534.jpg +0 -0
- package/kb/references/thumbs/2cfe9d48c4.jpg +0 -0
- package/kb/references/thumbs/2d49f19b27.jpg +0 -0
- package/kb/references/thumbs/2dc8670da7.jpg +0 -0
- package/kb/references/thumbs/2de947c469.jpg +0 -0
- package/kb/references/thumbs/2e075ca79f.jpg +0 -0
- package/kb/references/thumbs/2eb0820e6b.jpg +0 -0
- package/kb/references/thumbs/2ec56a9dc0.jpg +0 -0
- package/kb/references/thumbs/30bb4ad607.jpg +0 -0
- package/kb/references/thumbs/30d19b8444.jpg +0 -0
- package/kb/references/thumbs/3173c71d73.jpg +0 -0
- package/kb/references/thumbs/31e59e4518.jpg +0 -0
- package/kb/references/thumbs/322b8960e8.jpg +0 -0
- package/kb/references/thumbs/325c9be6f4.jpg +0 -0
- package/kb/references/thumbs/3273338982.jpg +0 -0
- package/kb/references/thumbs/32f8387f38.jpg +0 -0
- package/kb/references/thumbs/332f6b74c3.jpg +0 -0
- package/kb/references/thumbs/334b146b6a.jpg +0 -0
- package/kb/references/thumbs/339abd60e4.jpg +0 -0
- package/kb/references/thumbs/33e88ec295.jpg +0 -0
- package/kb/references/thumbs/340931b5d4.jpg +0 -0
- package/kb/references/thumbs/34165a93bd.jpg +0 -0
- package/kb/references/thumbs/34d0575dd7.jpg +0 -0
- package/kb/references/thumbs/3549e75f0a.jpg +0 -0
- package/kb/references/thumbs/35827f1561.jpg +0 -0
- package/kb/references/thumbs/37c50280ec.jpg +0 -0
- package/kb/references/thumbs/37e66ecf11.jpg +0 -0
- package/kb/references/thumbs/3841a04fdc.jpg +0 -0
- package/kb/references/thumbs/38490c088f.jpg +0 -0
- package/kb/references/thumbs/3850319e9f.jpg +0 -0
- package/kb/references/thumbs/3856a64166.jpg +0 -0
- package/kb/references/thumbs/3881f211c2.jpg +0 -0
- package/kb/references/thumbs/38c7dde2f3.jpg +0 -0
- package/kb/references/thumbs/38ce9e486c.jpg +0 -0
- package/kb/references/thumbs/390ea8e01b.jpg +0 -0
- package/kb/references/thumbs/391b7fc27c.jpg +0 -0
- package/kb/references/thumbs/395f7214dc.jpg +0 -0
- package/kb/references/thumbs/39e81a5db2.jpg +0 -0
- package/kb/references/thumbs/3a67c8d655.jpg +0 -0
- package/kb/references/thumbs/3a89c25e3b.jpg +0 -0
- package/kb/references/thumbs/3aa0837542.jpg +0 -0
- package/kb/references/thumbs/3aae18aa8a.jpg +0 -0
- package/kb/references/thumbs/3bdbe767db.jpg +0 -0
- package/kb/references/thumbs/3c0616fc7f.jpg +0 -0
- package/kb/references/thumbs/3c18e9db1d.jpg +0 -0
- package/kb/references/thumbs/3c5a14076c.jpg +0 -0
- package/kb/references/thumbs/3ca9ec15c6.jpg +0 -0
- package/kb/references/thumbs/3d7756fd35.jpg +0 -0
- package/kb/references/thumbs/3dd45bd33d.jpg +0 -0
- package/kb/references/thumbs/3de72d5414.jpg +0 -0
- package/kb/references/thumbs/3dede597af.jpg +0 -0
- package/kb/references/thumbs/3ea948619e.jpg +0 -0
- package/kb/references/thumbs/402c12054c.jpg +0 -0
- package/kb/references/thumbs/403ab4af90.jpg +0 -0
- package/kb/references/thumbs/40773a8c63.jpg +0 -0
- package/kb/references/thumbs/407d9583db.jpg +0 -0
- package/kb/references/thumbs/415dc1d84c.jpg +0 -0
- package/kb/references/thumbs/420b676a4f.jpg +0 -0
- package/kb/references/thumbs/42a1ee85eb.jpg +0 -0
- package/kb/references/thumbs/43059221aa.jpg +0 -0
- package/kb/references/thumbs/432fa0d22c.jpg +0 -0
- package/kb/references/thumbs/43826f20e3.jpg +0 -0
- package/kb/references/thumbs/43ac55c98a.jpg +0 -0
- package/kb/references/thumbs/43d098f3c3.jpg +0 -0
- package/kb/references/thumbs/4449d80c2c.jpg +0 -0
- package/kb/references/thumbs/44625727dc.jpg +0 -0
- package/kb/references/thumbs/4482bb87a1.jpg +0 -0
- package/kb/references/thumbs/44b078acdc.jpg +0 -0
- package/kb/references/thumbs/44d0305850.jpg +0 -0
- package/kb/references/thumbs/456bb6b59e.jpg +0 -0
- package/kb/references/thumbs/45df73ce48.jpg +0 -0
- package/kb/references/thumbs/45edb45d87.jpg +0 -0
- package/kb/references/thumbs/4671d57cd5.jpg +0 -0
- package/kb/references/thumbs/46cc108518.jpg +0 -0
- package/kb/references/thumbs/470022e614.jpg +0 -0
- package/kb/references/thumbs/4719a669ce.jpg +0 -0
- package/kb/references/thumbs/47404f5ad4.jpg +0 -0
- package/kb/references/thumbs/479d3eb977.jpg +0 -0
- package/kb/references/thumbs/479ed92ceb.jpg +0 -0
- package/kb/references/thumbs/47baed50c7.jpg +0 -0
- package/kb/references/thumbs/47fdcfd7b0.jpg +0 -0
- package/kb/references/thumbs/48fd5e94c5.jpg +0 -0
- package/kb/references/thumbs/493807917b.jpg +0 -0
- package/kb/references/thumbs/49a1c67293.jpg +0 -0
- package/kb/references/thumbs/49e2d7fafe.jpg +0 -0
- package/kb/references/thumbs/49f540fcb6.jpg +0 -0
- package/kb/references/thumbs/4a67ec4028.jpg +0 -0
- package/kb/references/thumbs/4a93275b04.jpg +0 -0
- package/kb/references/thumbs/4aae6f52da.jpg +0 -0
- package/kb/references/thumbs/4b1bae66bd.jpg +0 -0
- package/kb/references/thumbs/4b75da15a9.jpg +0 -0
- package/kb/references/thumbs/4b77789b88.jpg +0 -0
- package/kb/references/thumbs/4bc2f3f3a4.jpg +0 -0
- package/kb/references/thumbs/4c48f27b88.jpg +0 -0
- package/kb/references/thumbs/4c52bd8bfb.jpg +0 -0
- package/kb/references/thumbs/4c9b4acc60.jpg +0 -0
- package/kb/references/thumbs/4cbf0752d4.jpg +0 -0
- package/kb/references/thumbs/4cd2672517.jpg +0 -0
- package/kb/references/thumbs/4dd22a8181.jpg +0 -0
- package/kb/references/thumbs/4ee3447314.jpg +0 -0
- package/kb/references/thumbs/4ef29c8144.jpg +0 -0
- package/kb/references/thumbs/4f82fadfd6.jpg +0 -0
- package/kb/references/thumbs/4fc9b643d8.jpg +0 -0
- package/kb/references/thumbs/50549524b0.jpg +0 -0
- package/kb/references/thumbs/509a615883.jpg +0 -0
- package/kb/references/thumbs/50c03ef855.jpg +0 -0
- package/kb/references/thumbs/50cf9acad6.jpg +0 -0
- package/kb/references/thumbs/51131b025c.jpg +0 -0
- package/kb/references/thumbs/51da1124f0.jpg +0 -0
- package/kb/references/thumbs/524d5e7288.jpg +0 -0
- package/kb/references/thumbs/5251247c81.jpg +0 -0
- package/kb/references/thumbs/526b91b7ec.jpg +0 -0
- package/kb/references/thumbs/52f02f6c2e.jpg +0 -0
- package/kb/references/thumbs/530ab530dc.jpg +0 -0
- package/kb/references/thumbs/5335ee4404.jpg +0 -0
- package/kb/references/thumbs/5385b90939.jpg +0 -0
- package/kb/references/thumbs/53c54654cc.jpg +0 -0
- package/kb/references/thumbs/5420794c6c.jpg +0 -0
- package/kb/references/thumbs/542a9dce8b.jpg +0 -0
- package/kb/references/thumbs/5571530a5f.jpg +0 -0
- package/kb/references/thumbs/56cc68d804.jpg +0 -0
- package/kb/references/thumbs/577b7f7c44.jpg +0 -0
- package/kb/references/thumbs/57adf8f1bb.jpg +0 -0
- package/kb/references/thumbs/57c5f5e3a6.jpg +0 -0
- package/kb/references/thumbs/57ffe4e839.jpg +0 -0
- package/kb/references/thumbs/5817dfc7d6.jpg +0 -0
- package/kb/references/thumbs/581a182a7a.jpg +0 -0
- package/kb/references/thumbs/58807679c8.jpg +0 -0
- package/kb/references/thumbs/589a1d9111.jpg +0 -0
- package/kb/references/thumbs/59167391ab.jpg +0 -0
- package/kb/references/thumbs/5a70420166.jpg +0 -0
- package/kb/references/thumbs/5b10df1b8d.jpg +0 -0
- package/kb/references/thumbs/5b7a712fdb.jpg +0 -0
- package/kb/references/thumbs/5bc46b92ad.jpg +0 -0
- package/kb/references/thumbs/5c0546a120.jpg +0 -0
- package/kb/references/thumbs/5c2b6125a8.jpg +0 -0
- package/kb/references/thumbs/5c536b9e42.jpg +0 -0
- package/kb/references/thumbs/5c8e441d30.jpg +0 -0
- package/kb/references/thumbs/5cb6f1baa5.jpg +0 -0
- package/kb/references/thumbs/5ce6c1f21e.jpg +0 -0
- package/kb/references/thumbs/5cef052c0a.jpg +0 -0
- package/kb/references/thumbs/5d08438209.jpg +0 -0
- package/kb/references/thumbs/5d0f9af9b0.jpg +0 -0
- package/kb/references/thumbs/5d300ed332.jpg +0 -0
- package/kb/references/thumbs/5d708a4794.jpg +0 -0
- package/kb/references/thumbs/5de8826a98.jpg +0 -0
- package/kb/references/thumbs/5df7bff22c.jpg +0 -0
- package/kb/references/thumbs/5e087d0f00.jpg +0 -0
- package/kb/references/thumbs/5e44e70ca0.jpg +0 -0
- package/kb/references/thumbs/5eff5d80ce.jpg +0 -0
- package/kb/references/thumbs/5f015c96b1.jpg +0 -0
- package/kb/references/thumbs/5f3289b7c4.jpg +0 -0
- package/kb/references/thumbs/5f830c40eb.jpg +0 -0
- package/kb/references/thumbs/60c4c3527b.jpg +0 -0
- package/kb/references/thumbs/60e9e594dd.jpg +0 -0
- package/kb/references/thumbs/615421d7be.jpg +0 -0
- package/kb/references/thumbs/6176e4004f.jpg +0 -0
- package/kb/references/thumbs/61f0fe5e06.jpg +0 -0
- package/kb/references/thumbs/6213785db4.jpg +0 -0
- package/kb/references/thumbs/62a670ae09.jpg +0 -0
- package/kb/references/thumbs/63cc12981b.jpg +0 -0
- package/kb/references/thumbs/641f15cc71.jpg +0 -0
- package/kb/references/thumbs/642ab2081d.jpg +0 -0
- package/kb/references/thumbs/644a59bf6e.jpg +0 -0
- package/kb/references/thumbs/644da8a8e3.jpg +0 -0
- package/kb/references/thumbs/6458986897.jpg +0 -0
- package/kb/references/thumbs/64cfbd7b93.jpg +0 -0
- package/kb/references/thumbs/661a57686a.jpg +0 -0
- package/kb/references/thumbs/66355a813a.jpg +0 -0
- package/kb/references/thumbs/664396dd44.jpg +0 -0
- package/kb/references/thumbs/66d832d65f.jpg +0 -0
- package/kb/references/thumbs/66de9e9833.jpg +0 -0
- package/kb/references/thumbs/6750964fd9.jpg +0 -0
- package/kb/references/thumbs/67c7164832.jpg +0 -0
- package/kb/references/thumbs/681600b6cd.jpg +0 -0
- package/kb/references/thumbs/682e75afe4.jpg +0 -0
- package/kb/references/thumbs/69274bc952.jpg +0 -0
- package/kb/references/thumbs/696ec08c52.jpg +0 -0
- package/kb/references/thumbs/69760111cc.jpg +0 -0
- package/kb/references/thumbs/697e32664f.jpg +0 -0
- package/kb/references/thumbs/69df92544b.jpg +0 -0
- package/kb/references/thumbs/69e48c5229.jpg +0 -0
- package/kb/references/thumbs/6a5fb75de1.jpg +0 -0
- package/kb/references/thumbs/6a7389e472.jpg +0 -0
- package/kb/references/thumbs/6a9b041e02.jpg +0 -0
- package/kb/references/thumbs/6ab2fa93ee.jpg +0 -0
- package/kb/references/thumbs/6ae3400319.jpg +0 -0
- package/kb/references/thumbs/6b21344bf3.jpg +0 -0
- package/kb/references/thumbs/6bd1b67a8a.jpg +0 -0
- package/kb/references/thumbs/6c99156c4f.jpg +0 -0
- package/kb/references/thumbs/6cc9b65384.jpg +0 -0
- package/kb/references/thumbs/6d15830a73.jpg +0 -0
- package/kb/references/thumbs/6d4c33d180.jpg +0 -0
- package/kb/references/thumbs/6e36815799.jpg +0 -0
- package/kb/references/thumbs/6eddb9cbfc.jpg +0 -0
- package/kb/references/thumbs/6f97968a7c.jpg +0 -0
- package/kb/references/thumbs/6fa0716a7d.jpg +0 -0
- package/kb/references/thumbs/6facd57184.jpg +0 -0
- package/kb/references/thumbs/6fd57b785a.jpg +0 -0
- package/kb/references/thumbs/700b3cb185.jpg +0 -0
- package/kb/references/thumbs/71197443fa.jpg +0 -0
- package/kb/references/thumbs/72082bc9ee.jpg +0 -0
- package/kb/references/thumbs/72453db479.jpg +0 -0
- package/kb/references/thumbs/72cd64d500.jpg +0 -0
- package/kb/references/thumbs/72d1e75283.jpg +0 -0
- package/kb/references/thumbs/72e13f7f5b.jpg +0 -0
- package/kb/references/thumbs/7459f8e644.jpg +0 -0
- package/kb/references/thumbs/7470d55c79.jpg +0 -0
- package/kb/references/thumbs/74c468f02a.jpg +0 -0
- package/kb/references/thumbs/74fed6eb77.jpg +0 -0
- package/kb/references/thumbs/756e05080d.jpg +0 -0
- package/kb/references/thumbs/75df7e5f49.jpg +0 -0
- package/kb/references/thumbs/76226da6ef.jpg +0 -0
- package/kb/references/thumbs/76a34aa056.jpg +0 -0
- package/kb/references/thumbs/76c7076589.jpg +0 -0
- package/kb/references/thumbs/76cccc396c.jpg +0 -0
- package/kb/references/thumbs/774654789f.jpg +0 -0
- package/kb/references/thumbs/7746cc494d.jpg +0 -0
- package/kb/references/thumbs/7766e36aa3.jpg +0 -0
- package/kb/references/thumbs/78049331ef.jpg +0 -0
- package/kb/references/thumbs/78808a0ca5.jpg +0 -0
- package/kb/references/thumbs/7890b0a310.jpg +0 -0
- package/kb/references/thumbs/78911975e6.jpg +0 -0
- package/kb/references/thumbs/78d289d5a0.jpg +0 -0
- package/kb/references/thumbs/794126267e.jpg +0 -0
- package/kb/references/thumbs/7976eb9025.jpg +0 -0
- package/kb/references/thumbs/798519ec7c.jpg +0 -0
- package/kb/references/thumbs/7a6cc5eecc.jpg +0 -0
- package/kb/references/thumbs/7ac8a7cd4a.jpg +0 -0
- package/kb/references/thumbs/7b1341d61d.jpg +0 -0
- package/kb/references/thumbs/7b9c4031e3.jpg +0 -0
- package/kb/references/thumbs/7ba5ad3779.jpg +0 -0
- package/kb/references/thumbs/7be6eb2bd0.jpg +0 -0
- package/kb/references/thumbs/7c30864f5d.jpg +0 -0
- package/kb/references/thumbs/7c379b5e17.jpg +0 -0
- package/kb/references/thumbs/7c4050eb02.jpg +0 -0
- package/kb/references/thumbs/7ca47b3221.jpg +0 -0
- package/kb/references/thumbs/7d43516bd7.jpg +0 -0
- package/kb/references/thumbs/7da9dbb98a.jpg +0 -0
- package/kb/references/thumbs/7ddda8f57c.jpg +0 -0
- package/kb/references/thumbs/7ece6d215c.jpg +0 -0
- package/kb/references/thumbs/7eebf029de.jpg +0 -0
- package/kb/references/thumbs/7f45483c15.jpg +0 -0
- package/kb/references/thumbs/7fde06eaa5.jpg +0 -0
- package/kb/references/thumbs/80407b97e9.jpg +0 -0
- package/kb/references/thumbs/80f0a6cf4b.jpg +0 -0
- package/kb/references/thumbs/8118b451b8.jpg +0 -0
- package/kb/references/thumbs/816baed4a8.jpg +0 -0
- package/kb/references/thumbs/817d2d15d9.jpg +0 -0
- package/kb/references/thumbs/8260cb430d.jpg +0 -0
- package/kb/references/thumbs/827b562899.jpg +0 -0
- package/kb/references/thumbs/8293cf1e92.jpg +0 -0
- package/kb/references/thumbs/8294653169.jpg +0 -0
- package/kb/references/thumbs/83061f871f.jpg +0 -0
- package/kb/references/thumbs/833df778ca.jpg +0 -0
- package/kb/references/thumbs/83d3acaafc.jpg +0 -0
- package/kb/references/thumbs/844c6efeff.jpg +0 -0
- package/kb/references/thumbs/84563d482a.jpg +0 -0
- package/kb/references/thumbs/848082dd94.jpg +0 -0
- package/kb/references/thumbs/8494b1da94.jpg +0 -0
- package/kb/references/thumbs/84b71014be.jpg +0 -0
- package/kb/references/thumbs/84be4a5f3c.jpg +0 -0
- package/kb/references/thumbs/84fd0fd25a.jpg +0 -0
- package/kb/references/thumbs/853e488e22.jpg +0 -0
- package/kb/references/thumbs/858c91585d.jpg +0 -0
- package/kb/references/thumbs/85903e8fe9.jpg +0 -0
- package/kb/references/thumbs/86597d43fd.jpg +0 -0
- package/kb/references/thumbs/866eb99500.jpg +0 -0
- package/kb/references/thumbs/86ac050340.jpg +0 -0
- package/kb/references/thumbs/86afec7b64.jpg +0 -0
- package/kb/references/thumbs/87238127f5.jpg +0 -0
- package/kb/references/thumbs/87321df792.jpg +0 -0
- package/kb/references/thumbs/87460f70cd.jpg +0 -0
- package/kb/references/thumbs/8770dfeaec.jpg +0 -0
- package/kb/references/thumbs/8798451a8c.jpg +0 -0
- package/kb/references/thumbs/87da1f34a3.jpg +0 -0
- package/kb/references/thumbs/882cdf8c05.jpg +0 -0
- package/kb/references/thumbs/8839f4106f.jpg +0 -0
- package/kb/references/thumbs/8844455212.jpg +0 -0
- package/kb/references/thumbs/88abba0497.jpg +0 -0
- package/kb/references/thumbs/88b372435d.jpg +0 -0
- package/kb/references/thumbs/88bd4f35fa.jpg +0 -0
- package/kb/references/thumbs/88c71a075e.jpg +0 -0
- package/kb/references/thumbs/892e5f048e.jpg +0 -0
- package/kb/references/thumbs/893cc8e920.jpg +0 -0
- package/kb/references/thumbs/896c198814.jpg +0 -0
- package/kb/references/thumbs/8a8e22f756.jpg +0 -0
- package/kb/references/thumbs/8a9d8a35cb.jpg +0 -0
- package/kb/references/thumbs/8ab1d67fec.jpg +0 -0
- package/kb/references/thumbs/8ab201fe12.jpg +0 -0
- package/kb/references/thumbs/8ac372c102.jpg +0 -0
- package/kb/references/thumbs/8b15736d62.jpg +0 -0
- package/kb/references/thumbs/8b1d8d6a05.jpg +0 -0
- package/kb/references/thumbs/8bb6050e20.jpg +0 -0
- package/kb/references/thumbs/8c6c9d1577.jpg +0 -0
- package/kb/references/thumbs/8d337851e0.jpg +0 -0
- package/kb/references/thumbs/8d44b33cc5.jpg +0 -0
- package/kb/references/thumbs/8d4536ff3b.jpg +0 -0
- package/kb/references/thumbs/8d9b99454a.jpg +0 -0
- package/kb/references/thumbs/8dda2303ff.jpg +0 -0
- package/kb/references/thumbs/8dec722d62.jpg +0 -0
- package/kb/references/thumbs/8e0ea4ca74.jpg +0 -0
- package/kb/references/thumbs/8ebba84d69.jpg +0 -0
- package/kb/references/thumbs/8ef09140c0.jpg +0 -0
- package/kb/references/thumbs/8f2928a1b0.jpg +0 -0
- package/kb/references/thumbs/8f88f9ddba.jpg +0 -0
- package/kb/references/thumbs/8fa4451009.jpg +0 -0
- package/kb/references/thumbs/90109b0e02.jpg +0 -0
- package/kb/references/thumbs/90e854e5c9.jpg +0 -0
- package/kb/references/thumbs/914e79f893.jpg +0 -0
- package/kb/references/thumbs/9161369197.jpg +0 -0
- package/kb/references/thumbs/9165c87d6f.jpg +0 -0
- package/kb/references/thumbs/9193f6558a.jpg +0 -0
- package/kb/references/thumbs/91ac7df877.jpg +0 -0
- package/kb/references/thumbs/9216bd4931.jpg +0 -0
- package/kb/references/thumbs/922ae8a26a.jpg +0 -0
- package/kb/references/thumbs/92a70e6867.jpg +0 -0
- package/kb/references/thumbs/92c77ece22.jpg +0 -0
- package/kb/references/thumbs/92e5914fbf.jpg +0 -0
- package/kb/references/thumbs/92ee36419a.jpg +0 -0
- package/kb/references/thumbs/94153a60b0.jpg +0 -0
- package/kb/references/thumbs/9449666640.jpg +0 -0
- package/kb/references/thumbs/944be6a646.jpg +0 -0
- package/kb/references/thumbs/945831476e.jpg +0 -0
- package/kb/references/thumbs/94c1af82f4.jpg +0 -0
- package/kb/references/thumbs/9599d6d589.jpg +0 -0
- package/kb/references/thumbs/960710f4cf.jpg +0 -0
- package/kb/references/thumbs/9608f12d64.jpg +0 -0
- package/kb/references/thumbs/962dabfaa0.jpg +0 -0
- package/kb/references/thumbs/965a3a95c6.jpg +0 -0
- package/kb/references/thumbs/9672ed5ae4.jpg +0 -0
- package/kb/references/thumbs/96891ae9c1.jpg +0 -0
- package/kb/references/thumbs/96e1907c99.jpg +0 -0
- package/kb/references/thumbs/96f25b6578.jpg +0 -0
- package/kb/references/thumbs/97211df9ae.jpg +0 -0
- package/kb/references/thumbs/9769d763a5.jpg +0 -0
- package/kb/references/thumbs/9792bd6d05.jpg +0 -0
- package/kb/references/thumbs/97d2837e3e.jpg +0 -0
- package/kb/references/thumbs/97d90d1d85.jpg +0 -0
- package/kb/references/thumbs/97fcd5774a.jpg +0 -0
- package/kb/references/thumbs/980f68564e.jpg +0 -0
- package/kb/references/thumbs/98511fb393.jpg +0 -0
- package/kb/references/thumbs/98f8fa694d.jpg +0 -0
- package/kb/references/thumbs/9a42c6bb97.jpg +0 -0
- package/kb/references/thumbs/9ab3600a06.jpg +0 -0
- package/kb/references/thumbs/9ab48d9130.jpg +0 -0
- package/kb/references/thumbs/9ad519197d.jpg +0 -0
- package/kb/references/thumbs/9b1382c657.jpg +0 -0
- package/kb/references/thumbs/9b6cc1e077.jpg +0 -0
- package/kb/references/thumbs/9beede8480.jpg +0 -0
- package/kb/references/thumbs/9bf8976952.jpg +0 -0
- package/kb/references/thumbs/9cace7b75a.jpg +0 -0
- package/kb/references/thumbs/9cdcc5745e.jpg +0 -0
- package/kb/references/thumbs/9d0b269074.jpg +0 -0
- package/kb/references/thumbs/9d41788b48.jpg +0 -0
- package/kb/references/thumbs/9d475bd6af.jpg +0 -0
- package/kb/references/thumbs/9e6bc543d0.jpg +0 -0
- package/kb/references/thumbs/9ed0e0a206.jpg +0 -0
- package/kb/references/thumbs/9ef538de13.jpg +0 -0
- package/kb/references/thumbs/9efc373ab4.jpg +0 -0
- package/kb/references/thumbs/9f4a939fa5.jpg +0 -0
- package/kb/references/thumbs/9f8d12d7b9.jpg +0 -0
- package/kb/references/thumbs/9fd5e3efa7.jpg +0 -0
- package/kb/references/thumbs/a0a9b9c572.jpg +0 -0
- package/kb/references/thumbs/a29ca9b185.jpg +0 -0
- package/kb/references/thumbs/a2a42884af.jpg +0 -0
- package/kb/references/thumbs/a2f5064e34.jpg +0 -0
- package/kb/references/thumbs/a3112ce672.jpg +0 -0
- package/kb/references/thumbs/a3437736d1.jpg +0 -0
- package/kb/references/thumbs/a372667e76.jpg +0 -0
- package/kb/references/thumbs/a39bae94e9.jpg +0 -0
- package/kb/references/thumbs/a3fc75d94c.jpg +0 -0
- package/kb/references/thumbs/a440487631.jpg +0 -0
- package/kb/references/thumbs/a4993d5fb5.jpg +0 -0
- package/kb/references/thumbs/a500476a86.jpg +0 -0
- package/kb/references/thumbs/a55506bf46.jpg +0 -0
- package/kb/references/thumbs/a59a5f9843.jpg +0 -0
- package/kb/references/thumbs/a5bce0eede.jpg +0 -0
- package/kb/references/thumbs/a5d921e1f0.jpg +0 -0
- package/kb/references/thumbs/a5e19d4637.jpg +0 -0
- package/kb/references/thumbs/a5f92b97d5.jpg +0 -0
- package/kb/references/thumbs/a6562c210a.jpg +0 -0
- package/kb/references/thumbs/a6f61a8d09.jpg +0 -0
- package/kb/references/thumbs/a719fdf663.jpg +0 -0
- package/kb/references/thumbs/a724e5442f.jpg +0 -0
- package/kb/references/thumbs/a852b4319f.jpg +0 -0
- package/kb/references/thumbs/a896c08448.jpg +0 -0
- package/kb/references/thumbs/a8d190a4b8.jpg +0 -0
- package/kb/references/thumbs/a8dcdd57d7.jpg +0 -0
- package/kb/references/thumbs/a951347e18.jpg +0 -0
- package/kb/references/thumbs/a9a1a0bf7d.jpg +0 -0
- package/kb/references/thumbs/a9b72986a8.jpg +0 -0
- package/kb/references/thumbs/aa11555ba8.jpg +0 -0
- package/kb/references/thumbs/aa4aab9093.jpg +0 -0
- package/kb/references/thumbs/aa89b96658.jpg +0 -0
- package/kb/references/thumbs/aaeb3739b6.jpg +0 -0
- package/kb/references/thumbs/ab25a608ec.jpg +0 -0
- package/kb/references/thumbs/ab34b62268.jpg +0 -0
- package/kb/references/thumbs/ab3b0217bb.jpg +0 -0
- package/kb/references/thumbs/ac70c04afb.jpg +0 -0
- package/kb/references/thumbs/ace0879117.jpg +0 -0
- package/kb/references/thumbs/ad0515ae55.jpg +0 -0
- package/kb/references/thumbs/ad66b25b47.jpg +0 -0
- package/kb/references/thumbs/ad68229d2f.jpg +0 -0
- package/kb/references/thumbs/add6480693.jpg +0 -0
- package/kb/references/thumbs/ae5b8c3b8c.jpg +0 -0
- package/kb/references/thumbs/ae81247b47.jpg +0 -0
- package/kb/references/thumbs/af8aa54379.jpg +0 -0
- package/kb/references/thumbs/afa80dedd4.jpg +0 -0
- package/kb/references/thumbs/afe04d730f.jpg +0 -0
- package/kb/references/thumbs/b09a7dc315.jpg +0 -0
- package/kb/references/thumbs/b0c02629f6.jpg +0 -0
- package/kb/references/thumbs/b1cb4f91de.jpg +0 -0
- package/kb/references/thumbs/b1d4c13c92.jpg +0 -0
- package/kb/references/thumbs/b20eef86db.jpg +0 -0
- package/kb/references/thumbs/b3136ccdde.jpg +0 -0
- package/kb/references/thumbs/b3f5e8d7dd.jpg +0 -0
- package/kb/references/thumbs/b44adb81c9.jpg +0 -0
- package/kb/references/thumbs/b457a5894b.jpg +0 -0
- package/kb/references/thumbs/b461281e0f.jpg +0 -0
- package/kb/references/thumbs/b4942644f0.jpg +0 -0
- package/kb/references/thumbs/b49f8eccad.jpg +0 -0
- package/kb/references/thumbs/b56c69be23.jpg +0 -0
- package/kb/references/thumbs/b59786b33c.jpg +0 -0
- package/kb/references/thumbs/b5d368ce5a.jpg +0 -0
- package/kb/references/thumbs/b5eb442406.jpg +0 -0
- package/kb/references/thumbs/b7fdabf4ab.jpg +0 -0
- package/kb/references/thumbs/b88ee8d235.jpg +0 -0
- package/kb/references/thumbs/b8a4e5be8a.jpg +0 -0
- package/kb/references/thumbs/b8c85b661c.jpg +0 -0
- package/kb/references/thumbs/ba1b8e7435.jpg +0 -0
- package/kb/references/thumbs/ba9a593eed.jpg +0 -0
- package/kb/references/thumbs/baa7afc201.jpg +0 -0
- package/kb/references/thumbs/baf74398f7.jpg +0 -0
- package/kb/references/thumbs/bb3e947c10.jpg +0 -0
- package/kb/references/thumbs/bb4c1bb7c1.jpg +0 -0
- package/kb/references/thumbs/bbaa25cc8a.jpg +0 -0
- package/kb/references/thumbs/bc7e7f88cc.jpg +0 -0
- package/kb/references/thumbs/bc9d5b58f5.jpg +0 -0
- package/kb/references/thumbs/bcc99a51f7.jpg +0 -0
- package/kb/references/thumbs/bce13dd1c1.jpg +0 -0
- package/kb/references/thumbs/bd079a6130.jpg +0 -0
- package/kb/references/thumbs/bd2d70dda7.jpg +0 -0
- package/kb/references/thumbs/bd5895c5f2.jpg +0 -0
- package/kb/references/thumbs/bde8e31d62.jpg +0 -0
- package/kb/references/thumbs/be084e78f3.jpg +0 -0
- package/kb/references/thumbs/be94f170c8.jpg +0 -0
- package/kb/references/thumbs/bed33654f7.jpg +0 -0
- package/kb/references/thumbs/beeb9a0c18.jpg +0 -0
- package/kb/references/thumbs/bef42094dc.jpg +0 -0
- package/kb/references/thumbs/bf625496fa.jpg +0 -0
- package/kb/references/thumbs/bf771f1398.jpg +0 -0
- package/kb/references/thumbs/c0ddefa3d9.jpg +0 -0
- package/kb/references/thumbs/c0f01e536e.jpg +0 -0
- package/kb/references/thumbs/c1d592b678.jpg +0 -0
- package/kb/references/thumbs/c1e83e1b3e.jpg +0 -0
- package/kb/references/thumbs/c2168f8096.jpg +0 -0
- package/kb/references/thumbs/c249cd6b17.jpg +0 -0
- package/kb/references/thumbs/c2e3d4feed.jpg +0 -0
- package/kb/references/thumbs/c3cec01df1.jpg +0 -0
- package/kb/references/thumbs/c47bedb42d.jpg +0 -0
- package/kb/references/thumbs/c4b03b4680.jpg +0 -0
- package/kb/references/thumbs/c4e6d18ec3.jpg +0 -0
- package/kb/references/thumbs/c5477e2877.jpg +0 -0
- package/kb/references/thumbs/c566794674.jpg +0 -0
- package/kb/references/thumbs/c5b945afbb.jpg +0 -0
- package/kb/references/thumbs/c5e4659497.jpg +0 -0
- package/kb/references/thumbs/c622786702.jpg +0 -0
- package/kb/references/thumbs/c6250649ca.jpg +0 -0
- package/kb/references/thumbs/c6261a9526.jpg +0 -0
- package/kb/references/thumbs/c62e609076.jpg +0 -0
- package/kb/references/thumbs/c6602d3e85.jpg +0 -0
- package/kb/references/thumbs/c6c1a16e40.jpg +0 -0
- package/kb/references/thumbs/c77f932157.jpg +0 -0
- package/kb/references/thumbs/c79788f01e.jpg +0 -0
- package/kb/references/thumbs/c7a511ad1e.jpg +0 -0
- package/kb/references/thumbs/c7acfbc3c8.jpg +0 -0
- package/kb/references/thumbs/c7bb6d1bb8.jpg +0 -0
- package/kb/references/thumbs/c829eadcb9.jpg +0 -0
- package/kb/references/thumbs/c891fd9d85.jpg +0 -0
- package/kb/references/thumbs/c89f812ee3.jpg +0 -0
- package/kb/references/thumbs/c8a85b9f61.jpg +0 -0
- package/kb/references/thumbs/c8fdf85c9a.jpg +0 -0
- package/kb/references/thumbs/c961ac1031.jpg +0 -0
- package/kb/references/thumbs/c9fd3f88d8.jpg +0 -0
- package/kb/references/thumbs/ca0d6399c3.jpg +0 -0
- package/kb/references/thumbs/ca316c83e4.jpg +0 -0
- package/kb/references/thumbs/ca3b93aa0d.jpg +0 -0
- package/kb/references/thumbs/cb0596a85b.jpg +0 -0
- package/kb/references/thumbs/cb95a5c3ef.jpg +0 -0
- package/kb/references/thumbs/cbe238b656.jpg +0 -0
- package/kb/references/thumbs/cbf530e608.jpg +0 -0
- package/kb/references/thumbs/cc173494ec.jpg +0 -0
- package/kb/references/thumbs/ccb664bddf.jpg +0 -0
- package/kb/references/thumbs/ccf69d36cd.jpg +0 -0
- package/kb/references/thumbs/ccf759db42.jpg +0 -0
- package/kb/references/thumbs/cd2a285807.jpg +0 -0
- package/kb/references/thumbs/cd4a24a8ca.jpg +0 -0
- package/kb/references/thumbs/cdb3fe8938.jpg +0 -0
- package/kb/references/thumbs/cdce1b9bc6.jpg +0 -0
- package/kb/references/thumbs/cde3aa761a.jpg +0 -0
- package/kb/references/thumbs/ce9d5cd9c2.jpg +0 -0
- package/kb/references/thumbs/ceb01f8a34.jpg +0 -0
- package/kb/references/thumbs/cee15c98d0.jpg +0 -0
- package/kb/references/thumbs/cf6944ddae.jpg +0 -0
- package/kb/references/thumbs/d15dd21cd8.jpg +0 -0
- package/kb/references/thumbs/d1633bf2ed.jpg +0 -0
- package/kb/references/thumbs/d16a838000.jpg +0 -0
- package/kb/references/thumbs/d1cf87e9f0.jpg +0 -0
- package/kb/references/thumbs/d32511d913.jpg +0 -0
- package/kb/references/thumbs/d327e9dedd.jpg +0 -0
- package/kb/references/thumbs/d35bf6c85e.jpg +0 -0
- package/kb/references/thumbs/d37afc9d61.jpg +0 -0
- package/kb/references/thumbs/d3b66655c7.jpg +0 -0
- package/kb/references/thumbs/d40b4172b6.jpg +0 -0
- package/kb/references/thumbs/d4c40d1824.jpg +0 -0
- package/kb/references/thumbs/d4cb55a4b6.jpg +0 -0
- package/kb/references/thumbs/d4f0c30f7e.jpg +0 -0
- package/kb/references/thumbs/d51cce3ae5.jpg +0 -0
- package/kb/references/thumbs/d52948f9ba.jpg +0 -0
- package/kb/references/thumbs/d62bd3cba4.jpg +0 -0
- package/kb/references/thumbs/d668a21c92.jpg +0 -0
- package/kb/references/thumbs/d685072c52.jpg +0 -0
- package/kb/references/thumbs/d6d7b96093.jpg +0 -0
- package/kb/references/thumbs/d7cd874b21.jpg +0 -0
- package/kb/references/thumbs/d7e111736a.jpg +0 -0
- package/kb/references/thumbs/d88325f832.jpg +0 -0
- package/kb/references/thumbs/d9272ded76.jpg +0 -0
- package/kb/references/thumbs/d962e7c654.jpg +0 -0
- package/kb/references/thumbs/d97f000ee2.jpg +0 -0
- package/kb/references/thumbs/d9c1ae2543.jpg +0 -0
- package/kb/references/thumbs/d9ccb3cac8.jpg +0 -0
- package/kb/references/thumbs/da99019680.jpg +0 -0
- package/kb/references/thumbs/da9b8bc615.jpg +0 -0
- package/kb/references/thumbs/daafbee2c1.jpg +0 -0
- package/kb/references/thumbs/db0f11f143.jpg +0 -0
- package/kb/references/thumbs/db5dd367f5.jpg +0 -0
- package/kb/references/thumbs/db69677866.jpg +0 -0
- package/kb/references/thumbs/db87c1956e.jpg +0 -0
- package/kb/references/thumbs/dbb86d8bda.jpg +0 -0
- package/kb/references/thumbs/dbd3a8ff2a.jpg +0 -0
- package/kb/references/thumbs/dbdf4b2794.jpg +0 -0
- package/kb/references/thumbs/dd4c19b4b1.jpg +0 -0
- package/kb/references/thumbs/dd98d9f6fa.jpg +0 -0
- package/kb/references/thumbs/ddb00e3b6a.jpg +0 -0
- package/kb/references/thumbs/dde3541a5c.jpg +0 -0
- package/kb/references/thumbs/de2a4da502.jpg +0 -0
- package/kb/references/thumbs/de4c387a66.jpg +0 -0
- package/kb/references/thumbs/de5618fa82.jpg +0 -0
- package/kb/references/thumbs/de6bec2538.jpg +0 -0
- package/kb/references/thumbs/dea13cbb1f.jpg +0 -0
- package/kb/references/thumbs/deff9202aa.jpg +0 -0
- package/kb/references/thumbs/df12835f8d.jpg +0 -0
- package/kb/references/thumbs/df57acbb3d.jpg +0 -0
- package/kb/references/thumbs/df8265009e.jpg +0 -0
- package/kb/references/thumbs/dfc99a1e8e.jpg +0 -0
- package/kb/references/thumbs/dfc9fde460.jpg +0 -0
- package/kb/references/thumbs/e038348eee.jpg +0 -0
- package/kb/references/thumbs/e04c2ae1d5.jpg +0 -0
- package/kb/references/thumbs/e070345c43.jpg +0 -0
- package/kb/references/thumbs/e0a0c84d47.jpg +0 -0
- package/kb/references/thumbs/e0af1e6b9e.jpg +0 -0
- package/kb/references/thumbs/e1025942c4.jpg +0 -0
- package/kb/references/thumbs/e11b6f5b82.jpg +0 -0
- package/kb/references/thumbs/e1553de3fc.jpg +0 -0
- package/kb/references/thumbs/e162ebe7cd.jpg +0 -0
- package/kb/references/thumbs/e1f1e531d8.jpg +0 -0
- package/kb/references/thumbs/e20842e5c9.jpg +0 -0
- package/kb/references/thumbs/e24e7d949b.jpg +0 -0
- package/kb/references/thumbs/e2a3e5d10a.jpg +0 -0
- package/kb/references/thumbs/e2de02615a.jpg +0 -0
- package/kb/references/thumbs/e305dad7dd.jpg +0 -0
- package/kb/references/thumbs/e52e3ef97f.jpg +0 -0
- package/kb/references/thumbs/e570e92d06.jpg +0 -0
- package/kb/references/thumbs/e583932fb9.jpg +0 -0
- package/kb/references/thumbs/e5da3c038f.jpg +0 -0
- package/kb/references/thumbs/e657634af3.jpg +0 -0
- package/kb/references/thumbs/e67946e3e1.jpg +0 -0
- package/kb/references/thumbs/e69e0f3639.jpg +0 -0
- package/kb/references/thumbs/e6ebe66031.jpg +0 -0
- package/kb/references/thumbs/e7db9563c9.jpg +0 -0
- package/kb/references/thumbs/e84a4c3b7b.jpg +0 -0
- package/kb/references/thumbs/e84cf52c27.jpg +0 -0
- package/kb/references/thumbs/e8707cf6e8.jpg +0 -0
- package/kb/references/thumbs/e888890b40.jpg +0 -0
- package/kb/references/thumbs/e8a502b418.jpg +0 -0
- package/kb/references/thumbs/e8ba68371d.jpg +0 -0
- package/kb/references/thumbs/e9098f54cc.jpg +0 -0
- package/kb/references/thumbs/e9144deae9.jpg +0 -0
- package/kb/references/thumbs/e93e98d757.jpg +0 -0
- package/kb/references/thumbs/e9762dd435.jpg +0 -0
- package/kb/references/thumbs/e9db4544c8.jpg +0 -0
- package/kb/references/thumbs/e9ddffeacd.jpg +0 -0
- package/kb/references/thumbs/e9e2cf1d7e.jpg +0 -0
- package/kb/references/thumbs/ea1e23e623.jpg +0 -0
- package/kb/references/thumbs/ea41200f5e.jpg +0 -0
- package/kb/references/thumbs/eaf4916e1a.jpg +0 -0
- package/kb/references/thumbs/eb2c505fc8.jpg +0 -0
- package/kb/references/thumbs/eb47943693.jpg +0 -0
- package/kb/references/thumbs/eb4ffd6625.jpg +0 -0
- package/kb/references/thumbs/eb82ac0a4d.jpg +0 -0
- package/kb/references/thumbs/ebeb345ade.jpg +0 -0
- package/kb/references/thumbs/ec4e2aba0a.jpg +0 -0
- package/kb/references/thumbs/ed683bc9f7.jpg +0 -0
- package/kb/references/thumbs/ed9e236a84.jpg +0 -0
- package/kb/references/thumbs/ee45e1caea.jpg +0 -0
- package/kb/references/thumbs/ee50bbf2e7.jpg +0 -0
- package/kb/references/thumbs/eed6ad3b7b.jpg +0 -0
- package/kb/references/thumbs/eee247fd70.jpg +0 -0
- package/kb/references/thumbs/ef87b6cb99.jpg +0 -0
- package/kb/references/thumbs/ef87dccf04.jpg +0 -0
- package/kb/references/thumbs/ef941bba1e.jpg +0 -0
- package/kb/references/thumbs/effc14fb2c.jpg +0 -0
- package/kb/references/thumbs/f04aa5a191.jpg +0 -0
- package/kb/references/thumbs/f13f12485f.jpg +0 -0
- package/kb/references/thumbs/f16a5c0fd4.jpg +0 -0
- package/kb/references/thumbs/f1ba7a7a8f.jpg +0 -0
- package/kb/references/thumbs/f1bc4c9c19.jpg +0 -0
- package/kb/references/thumbs/f1ee827878.jpg +0 -0
- package/kb/references/thumbs/f21736aeff.jpg +0 -0
- package/kb/references/thumbs/f22bc41213.jpg +0 -0
- package/kb/references/thumbs/f266aaffcc.jpg +0 -0
- package/kb/references/thumbs/f2c0ed0d65.jpg +0 -0
- package/kb/references/thumbs/f32ac710fd.jpg +0 -0
- package/kb/references/thumbs/f32b697ff6.jpg +0 -0
- package/kb/references/thumbs/f39c883904.jpg +0 -0
- package/kb/references/thumbs/f3b9d55e6e.jpg +0 -0
- package/kb/references/thumbs/f3c1763e14.jpg +0 -0
- package/kb/references/thumbs/f40853fd43.jpg +0 -0
- package/kb/references/thumbs/f4fd07dc4d.jpg +0 -0
- package/kb/references/thumbs/f50b756ee5.jpg +0 -0
- package/kb/references/thumbs/f51272e9b3.jpg +0 -0
- package/kb/references/thumbs/f538895eee.jpg +0 -0
- package/kb/references/thumbs/f5d28081b5.jpg +0 -0
- package/kb/references/thumbs/f6046c9501.jpg +0 -0
- package/kb/references/thumbs/f62e4af316.jpg +0 -0
- package/kb/references/thumbs/f6f6bffeb8.jpg +0 -0
- package/kb/references/thumbs/f736e00af6.jpg +0 -0
- package/kb/references/thumbs/f7932da47d.jpg +0 -0
- package/kb/references/thumbs/f879e118ca.jpg +0 -0
- package/kb/references/thumbs/f88257215d.jpg +0 -0
- package/kb/references/thumbs/f88c45a6d7.jpg +0 -0
- package/kb/references/thumbs/f89a7b646e.jpg +0 -0
- package/kb/references/thumbs/f8a1e7a274.jpg +0 -0
- package/kb/references/thumbs/f9183cf2b6.jpg +0 -0
- package/kb/references/thumbs/f995de5ce9.jpg +0 -0
- package/kb/references/thumbs/f9ada2c3e1.jpg +0 -0
- package/kb/references/thumbs/f9d4616f89.jpg +0 -0
- package/kb/references/thumbs/f9f4307bb0.jpg +0 -0
- package/kb/references/thumbs/fa070122c7.jpg +0 -0
- package/kb/references/thumbs/fa29cc1369.jpg +0 -0
- package/kb/references/thumbs/fa791a4dab.jpg +0 -0
- package/kb/references/thumbs/fa8060509e.jpg +0 -0
- package/kb/references/thumbs/fafeed4003.jpg +0 -0
- package/kb/references/thumbs/fb1c024f38.jpg +0 -0
- package/kb/references/thumbs/fb77aa28b3.jpg +0 -0
- package/kb/references/thumbs/fb9f7f1966.jpg +0 -0
- package/kb/references/thumbs/fba8a8c2da.jpg +0 -0
- package/kb/references/thumbs/fce2754c36.jpg +0 -0
- package/kb/references/thumbs/fce2fb83df.jpg +0 -0
- package/kb/references/thumbs/fd4c51bb47.jpg +0 -0
- package/kb/references/thumbs/fd8c269817.jpg +0 -0
- package/kb/references/thumbs/fdb7b250ca.jpg +0 -0
- package/kb/references/thumbs/fe18088de8.jpg +0 -0
- package/kb/references/thumbs/fe4f742ba7.jpg +0 -0
- package/kb/references/thumbs/fe64742ce7.jpg +0 -0
- package/kb/references/thumbs/feb5d62161.jpg +0 -0
- package/kb/references/thumbs/fed21b3e98.jpg +0 -0
- package/kb/references/thumbs/fed2205951.jpg +0 -0
- package/kb/references/thumbs/ffdacf1715.jpg +0 -0
- package/kb/sections/_buttons.md +93 -0
- package/kb/sections/_finish.md +325 -0
- package/kb/sections/about-founder-note.md +85 -0
- package/kb/sections/about-team-roster.md +86 -0
- package/kb/sections/alternating-rows-live-tiles.md +110 -0
- package/kb/sections/annotated-product-shot.md +105 -0
- package/kb/sections/asymmetric-bento-dominant.md +114 -0
- package/kb/sections/before-after-features.md +104 -0
- package/kb/sections/before-after-repair.md +107 -0
- package/kb/sections/bento-lead-tile.md +112 -0
- package/kb/sections/book-a-slot-honest.md +116 -0
- package/kb/sections/capability-prompt-ledger.md +68 -0
- package/kb/sections/case-study-band.md +113 -0
- package/kb/sections/centred-stack-hero.md +107 -0
- package/kb/sections/closing-demo-final-state.md +81 -0
- package/kb/sections/closing-full-bleed-line-and-form.md +86 -0
- package/kb/sections/closing-result-previewed.md +88 -0
- package/kb/sections/closing-three-real-steps.md +87 -0
- package/kb/sections/command-reference-table.md +70 -0
- package/kb/sections/comparison-old-way-bars.md +88 -0
- package/kb/sections/comparison-same-task-two-ways.md +88 -0
- package/kb/sections/day-of-the-place.md +102 -0
- package/kb/sections/docs-reading-rail.md +88 -0
- package/kb/sections/editorial-columns-hero.md +107 -0
- package/kb/sections/editorial-essay-pull-quote.md +112 -0
- package/kb/sections/events-calendar-list.md +107 -0
- package/kb/sections/explorable-control-demo.md +102 -0
- package/kb/sections/faq-open-answers-editorial.md +85 -0
- package/kb/sections/faq-ruled-accordion-aside.md +87 -0
- package/kb/sections/footer-closing-scene.md +84 -0
- package/kb/sections/footer-editorial-sitemap.md +85 -0
- package/kb/sections/footer-minimal-credits.md +80 -0
- package/kb/sections/full-bleed-image-band.md +103 -0
- package/kb/sections/full-bleed-image-hero.md +105 -0
- package/kb/sections/gallery-first-hero.md +109 -0
- package/kb/sections/horizontal-scroll-strip.md +101 -0
- package/kb/sections/how-i-work-own-voice.md +111 -0
- package/kb/sections/how-it-works-in-place.md +109 -0
- package/kb/sections/impact-numbers-sourced.md +109 -0
- package/kb/sections/install-steps-per-agent.md +77 -0
- package/kb/sections/masonry-real-work.md +109 -0
- package/kb/sections/numbered-editorial-columns.md +105 -0
- package/kb/sections/object-first-hero.md +106 -0
- package/kb/sections/object-timeline.md +105 -0
- package/kb/sections/one-huge-number.md +100 -0
- package/kb/sections/one-person-story.md +103 -0
- package/kb/sections/order-ahead-ticket.md +119 -0
- package/kb/sections/press-strip-quoted.md +106 -0
- package/kb/sections/pricing-comparison-table-sticky.md +94 -0
- package/kb/sections/pricing-free-pro-upgrade-moment.md +85 -0
- package/kb/sections/pricing-plan-beside-product.md +88 -0
- package/kb/sections/pricing-plans-ledger.md +94 -0
- package/kb/sections/pricing-usage-calculator.md +93 -0
- package/kb/sections/printed-menu-card.md +110 -0
- package/kb/sections/process-happening.md +102 -0
- package/kb/sections/project-index-ruled.md +106 -0
- package/kb/sections/proof-logo-cloud-with-reasons.md +82 -0
- package/kb/sections/proof-measured-numbers.md +88 -0
- package/kb/sections/proof-output-wall.md +84 -0
- package/kb/sections/proof-single-honest-quote.md +81 -0
- package/kb/sections/resources-searchable-list.md +110 -0
- package/kb/sections/rooms-and-rates.md +107 -0
- package/kb/sections/ruled-facts-table.md +108 -0
- package/kb/sections/ruled-list-pinned-visual.md +107 -0
- package/kb/sections/scroll-scrubbed-product-demo.md +103 -0
- package/kb/sections/self-playing-run-demo.md +104 -0
- package/kb/sections/services-price-ledger.md +106 -0
- package/kb/sections/signup-form-with-next-steps.md +86 -0
- package/kb/sections/signup-waitlist-place-in-line.md +90 -0
- package/kb/sections/spec-sheet-features.md +108 -0
- package/kb/sections/split-hero-live-object.md +108 -0
- package/kb/sections/sticky-feature-narrative.md +104 -0
- package/kb/sections/todays-board-hero.md +112 -0
- package/kb/sections/touchable-tool-hero.md +111 -0
- package/kb/sections/typographic-statement-hero.md +99 -0
- package/kb/sections/visit-hours-map.md +110 -0
- package/kb/sections/ways-to-help-honest.md +115 -0
- package/kb/sources/design-reference.md +991 -0
- package/kb/sources/product-and-docs.md +37 -0
- package/kb/systems/1win/DESIGN.md +463 -0
- package/kb/systems/LICENSE-awesome-design-md +21 -0
- package/kb/systems/README.md +7 -0
- package/kb/systems/airbnb/DESIGN.md +545 -0
- package/kb/systems/airtable/DESIGN.md +554 -0
- package/kb/systems/aljazeera/DESIGN.md +594 -0
- package/kb/systems/apple/DESIGN.md +562 -0
- package/kb/systems/binance/DESIGN.md +634 -0
- package/kb/systems/bmw/DESIGN.md +544 -0
- package/kb/systems/bmw-m/DESIGN.md +503 -0
- package/kb/systems/bugatti/DESIGN.md +454 -0
- package/kb/systems/cal/DESIGN.md +542 -0
- package/kb/systems/categories.json +87 -0
- package/kb/systems/claude/DESIGN.md +589 -0
- package/kb/systems/clay/DESIGN.md +541 -0
- package/kb/systems/clickhouse/DESIGN.md +544 -0
- package/kb/systems/cohere/DESIGN.md +451 -0
- package/kb/systems/coinbase/DESIGN.md +570 -0
- package/kb/systems/composio/DESIGN.md +506 -0
- package/kb/systems/cursor/DESIGN.md +537 -0
- package/kb/systems/dell-1996/DESIGN.md +632 -0
- package/kb/systems/dji/DESIGN.md +882 -0
- package/kb/systems/elevenlabs/DESIGN.md +504 -0
- package/kb/systems/expo/DESIGN.md +526 -0
- package/kb/systems/ferrari/DESIGN.md +531 -0
- package/kb/systems/figma/DESIGN.md +578 -0
- package/kb/systems/framer/DESIGN.md +544 -0
- package/kb/systems/hashicorp/DESIGN.md +575 -0
- package/kb/systems/hp/DESIGN.md +670 -0
- package/kb/systems/ibm/DESIGN.md +550 -0
- package/kb/systems/intercom/DESIGN.md +546 -0
- package/kb/systems/jobright/DESIGN.md +525 -0
- package/kb/systems/kraken/DESIGN.md +125 -0
- package/kb/systems/lamborghini/DESIGN.md +288 -0
- package/kb/systems/linear.app/DESIGN.md +548 -0
- package/kb/systems/lovable/DESIGN.md +298 -0
- package/kb/systems/mastercard/DESIGN.md +365 -0
- package/kb/systems/mercury/DESIGN.md +568 -0
- package/kb/systems/meta/DESIGN.md +683 -0
- package/kb/systems/minimax/DESIGN.md +746 -0
- package/kb/systems/mintlify/DESIGN.md +852 -0
- package/kb/systems/miro/DESIGN.md +825 -0
- package/kb/systems/mistral.ai/DESIGN.md +773 -0
- package/kb/systems/mongodb/DESIGN.md +767 -0
- package/kb/systems/mydealz/DESIGN.md +551 -0
- package/kb/systems/nerdwallet/DESIGN.md +722 -0
- package/kb/systems/nike/DESIGN.md +575 -0
- package/kb/systems/nintendo-2001/DESIGN.md +649 -0
- package/kb/systems/notion/DESIGN.md +821 -0
- package/kb/systems/nvidia/DESIGN.md +640 -0
- package/kb/systems/ollama/DESIGN.md +539 -0
- package/kb/systems/opencode.ai/DESIGN.md +521 -0
- package/kb/systems/oracle/DESIGN.md +778 -0
- package/kb/systems/oura/DESIGN.md +616 -0
- package/kb/systems/pinterest/DESIGN.md +597 -0
- package/kb/systems/playstation/DESIGN.md +661 -0
- package/kb/systems/posthog/DESIGN.md +690 -0
- package/kb/systems/raycast/DESIGN.md +669 -0
- package/kb/systems/renault/DESIGN.md +589 -0
- package/kb/systems/replicate/DESIGN.md +616 -0
- package/kb/systems/resend/DESIGN.md +585 -0
- package/kb/systems/revolut/DESIGN.md +636 -0
- package/kb/systems/runwayml/DESIGN.md +244 -0
- package/kb/systems/sanity/DESIGN.md +357 -0
- package/kb/systems/sap/DESIGN.md +639 -0
- package/kb/systems/sentry/DESIGN.md +551 -0
- package/kb/systems/shopify/DESIGN.md +516 -0
- package/kb/systems/slack/DESIGN.md +482 -0
- package/kb/systems/spacex/DESIGN.md +363 -0
- package/kb/systems/spotify/DESIGN.md +246 -0
- package/kb/systems/starbucks/DESIGN.md +580 -0
- package/kb/systems/stripe/DESIGN.md +487 -0
- package/kb/systems/supabase/DESIGN.md +462 -0
- package/kb/systems/superhuman/DESIGN.md +448 -0
- package/kb/systems/tesla/DESIGN.md +286 -0
- package/kb/systems/theverge/DESIGN.md +339 -0
- package/kb/systems/together.ai/DESIGN.md +633 -0
- package/kb/systems/uber/DESIGN.md +636 -0
- package/kb/systems/vercel/DESIGN.md +736 -0
- package/kb/systems/vodafone/DESIGN.md +538 -0
- package/kb/systems/voltagent/DESIGN.md +521 -0
- package/kb/systems/warp/DESIGN.md +526 -0
- package/kb/systems/webflow/DESIGN.md +588 -0
- package/kb/systems/wired/DESIGN.md +497 -0
- package/kb/systems/wise/DESIGN.md +544 -0
- package/kb/systems/x.ai/DESIGN.md +465 -0
- package/kb/systems/zapier/DESIGN.md +537 -0
- package/kb/taste/garvit-by-job.md +46 -0
- package/kb/taste/garvit-portfolio.md +94 -0
- package/kb/universal/color-roles.md +26 -0
- package/kb/universal/copy.md +16 -0
- package/kb/universal/details.md +31 -0
- package/kb/universal/first-screen.md +30 -0
- package/kb/universal/fit-the-codebase.md +29 -0
- package/kb/universal/layout-width.md +28 -0
- package/kb/universal/motion-feedback.md +23 -0
- package/kb/universal/signature.md +29 -0
- package/kb/universal/states.md +17 -0
- package/kb/universal/type-app.md +33 -0
- package/kb/universal/type-marketing.md +28 -0
- package/package.json +36 -4
- package/scripts/pack-assets.mjs +48 -0
- package/skill/palate/SKILL.md +261 -0
- package/src/auth-recheck.mjs +4 -0
- package/src/auth.mjs +416 -0
- package/src/capture.mjs +182 -0
- package/src/catalog/classify.mjs +96 -0
- package/src/catalog/curated.mjs +128 -0
- package/src/catalog/index-build.mjs +67 -0
- package/src/catalog/literal.mjs +146 -0
- package/src/catalog/store.mjs +40 -0
- package/src/catalog/vibrant.mjs +126 -0
- package/src/check.mjs +93 -0
- package/src/concept.mjs +157 -0
- package/src/context.mjs +226 -0
- package/src/copy.mjs +188 -0
- package/src/devserver.mjs +135 -0
- package/src/fontgenre.mjs +44 -0
- package/src/guard-agents.mjs +213 -0
- package/src/guard.mjs +377 -0
- package/src/kb.mjs +91 -0
- package/src/look.mjs +312 -0
- package/src/loop/ambition.mjs +620 -0
- package/src/loop/animate.mjs +70 -0
- package/src/loop/artdirection.mjs +264 -0
- package/src/loop/bench.mjs +184 -0
- package/src/loop/benchpick.mjs +155 -0
- package/src/loop/blueprint.mjs +620 -0
- package/src/loop/board.html +180 -0
- package/src/loop/build.mjs +393 -0
- package/src/loop/buttons.mjs +413 -0
- package/src/loop/common.mjs +206 -0
- package/src/loop/cost.mjs +63 -0
- package/src/loop/craft.mjs +152 -0
- package/src/loop/craftchecks.mjs +148 -0
- package/src/loop/critique.mjs +393 -0
- package/src/loop/details.mjs +225 -0
- package/src/loop/direction.html +168 -0
- package/src/loop/direction.mjs +256 -0
- package/src/loop/directions.json +8 -0
- package/src/loop/explore.mjs +219 -0
- package/src/loop/fail.mjs +110 -0
- package/src/loop/fix.mjs +194 -0
- package/src/loop/floor.mjs +677 -0
- package/src/loop/forget.mjs +52 -0
- package/src/loop/gate.mjs +1053 -0
- package/src/loop/grid.html +123 -0
- package/src/loop/growth.mjs +118 -0
- package/src/loop/icons.mjs +109 -0
- package/src/loop/imagery.mjs +1280 -0
- package/src/loop/judge-api.mjs +245 -0
- package/src/loop/judge.mjs +167 -0
- package/src/loop/layouts.mjs +492 -0
- package/src/loop/library.mjs +82 -0
- package/src/loop/lookplan.mjs +182 -0
- package/src/loop/lookvec.mjs +645 -0
- package/src/loop/match.mjs +923 -0
- package/src/loop/matched.mjs +26 -0
- package/src/loop/myrefs.mjs +151 -0
- package/src/loop/pick.mjs +213 -0
- package/src/loop/picker.html +118 -0
- package/src/loop/plan.mjs +289 -0
- package/src/loop/polish.mjs +39 -0
- package/src/loop/research-learn.mjs +41 -0
- package/src/loop/research.mjs +1130 -0
- package/src/loop/rulereview.mjs +146 -0
- package/src/loop/rules-board.html +199 -0
- package/src/loop/rules.mjs +939 -0
- package/src/loop/similar.mjs +276 -0
- package/src/loop/states-source.mjs +258 -0
- package/src/loop/states.mjs +1547 -0
- package/src/loop/stats.mjs +78 -0
- package/src/loop/taste.mjs +81 -0
- package/src/loop/timeline.mjs +149 -0
- package/src/loop/variants.mjs +228 -0
- package/src/loop/vary.mjs +249 -0
- package/src/mcp/server.mjs +1246 -0
- package/src/motion.mjs +207 -0
- package/src/moves.mjs +242 -0
- package/src/novelty.mjs +116 -0
- package/src/page-ambition.js +181 -0
- package/src/page-craft.js +724 -0
- package/src/page-details.js +415 -0
- package/src/page-devices.js +365 -0
- package/src/page-fingerprint.js +281 -0
- package/src/page-gate.js +1307 -0
- package/src/page-look.js +698 -0
- package/src/page-metrics.js +78 -0
- package/src/page-principles.js +1167 -0
- package/src/page-rules.js +308 -0
- package/src/page-sections.js +211 -0
- package/src/page-states.js +703 -0
- package/src/paths.mjs +13 -0
- package/src/png.mjs +107 -0
- package/src/point/auto.mjs +224 -0
- package/src/point/locate.mjs +114 -0
- package/src/point/overlay.js +1516 -0
- package/src/point/server.mjs +1122 -0
- package/src/point/trail.mjs +219 -0
- package/src/pool.mjs +80 -0
- package/src/principles.mjs +279 -0
- package/src/refs.mjs +299 -0
- package/src/robots.mjs +145 -0
- package/src/sections.mjs +210 -0
- package/src/shoot.mjs +235 -0
- package/src/systems/apply.mjs +355 -0
- package/src/systems/load.mjs +251 -0
- package/src/systems/pick.mjs +213 -0
- package/templates/vite-react-tw/index.html +12 -0
- package/templates/vite-react-tw/package.json +25 -0
- package/templates/vite-react-tw/src/App.tsx +3 -0
- package/templates/vite-react-tw/src/index.css +1 -0
- package/templates/vite-react-tw/src/main.tsx +10 -0
- package/templates/vite-react-tw/tsconfig.app.json +26 -0
- package/templates/vite-react-tw/tsconfig.json +7 -0
- package/templates/vite-react-tw/tsconfig.node.json +23 -0
- package/templates/vite-react-tw/vite.config.ts +8 -0
|
@@ -0,0 +1,939 @@
|
|
|
1
|
+
// The user's rules: what palate has learned this user won't accept, written as short, testable rules in
|
|
2
|
+
// ~/.palate/taste/rules.json. The taste profile says what the user likes; rules say what must hold. Every brief
|
|
3
|
+
// opens with the rules for its kind of screen ("Your rules (must pass)") and the failures for that kind, every
|
|
4
|
+
// `palate shoot` checks the measurable ones, the judge penalises variants that fail one, and the rules that
|
|
5
|
+
// can't be measured become questions in the judge's rubric.
|
|
6
|
+
// rule: { id, rule, kinds: ["landing"] | ["all"], where?: { route?, component?, project? }, status, source, check, added,
|
|
7
|
+
// also?: [more sources], evidence?: [what it was learned from] }
|
|
8
|
+
// status: "active" (enforced; a rule without one is active) or "proposed" (waiting for the user's OK, never enforced)
|
|
9
|
+
// check: { type: "metric", metric, op: "<=" | ">=" | "=", value } a fact measured in the page (page-rules.js)
|
|
10
|
+
// { type: "dom", selector, min?, max?, visible?, before?: "demo" } a count of matching elements
|
|
11
|
+
// { type: "judge", question } asked of the judge, per variant
|
|
12
|
+
// The user's own words are rules at once (`palate rule add`, notes on their references). What palate infers
|
|
13
|
+
// (agreed critique findings, failures, repeated point requests: `palate taste learn`, `palate fail`) is only
|
|
14
|
+
// proposed, with the scopes its evidence supports; it applies nowhere until the user keeps it (`palate rule
|
|
15
|
+
// review`, `ok`, `no`). A discarded proposal is remembered (`rejected` in the file) and not proposed again from
|
|
16
|
+
// the same evidence.
|
|
17
|
+
import fs from "node:fs";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
import { PALATE_HOME } from "../catalog/store.mjs";
|
|
21
|
+
import { loopOfRoute } from "./timeline.mjs";
|
|
22
|
+
import { growthProposals } from "./growth.mjs";
|
|
23
|
+
import { researchProposals } from "./research-learn.mjs";
|
|
24
|
+
|
|
25
|
+
const TASTE = path.join(PALATE_HOME, "taste");
|
|
26
|
+
export const RULES_FILE = path.join(TASTE, "rules.json");
|
|
27
|
+
export const FAIL_DIR = path.join(PALATE_HOME, "failures");
|
|
28
|
+
export const FAILS_LOG = path.join(FAIL_DIR, "failures.jsonl");
|
|
29
|
+
const STATE = path.join(TASTE, "state.json");
|
|
30
|
+
|
|
31
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
const script = (f) => fs.readFileSync(path.join(here, "..", f), "utf8").trim().replace(/;\s*$/, "");
|
|
33
|
+
const slash = (p) => String(p).replace(/\\/g, "/");
|
|
34
|
+
const readJson = (f, fb) => {
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(fs.readFileSync(f, "utf8"));
|
|
37
|
+
} catch {
|
|
38
|
+
return fb;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
export const readLines = (f) => (fs.existsSync(f) ? fs.readFileSync(f, "utf8").split("\n").filter(Boolean).map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean) : []);
|
|
42
|
+
|
|
43
|
+
// ---- the facts a metric check can read (page-rules.js), with how to say them
|
|
44
|
+
export const METRICS = {
|
|
45
|
+
framedScreenshots: "framed static screenshots",
|
|
46
|
+
staticScreenshots: "static screenshots",
|
|
47
|
+
kickerSections: "sections with a kicker label",
|
|
48
|
+
repeatedDevices: "devices repeated in 3+ sections",
|
|
49
|
+
minLogoMarkPx: "smallest logo mark (px)",
|
|
50
|
+
monochromeLogoRows: "logo rows in one grey",
|
|
51
|
+
stepBarsBeforeDemo: "step bars before the demo",
|
|
52
|
+
stepBars: "step bars",
|
|
53
|
+
frameworkLogosBeforeDemo: "framework logos before the demo",
|
|
54
|
+
blackPrimaryButtons: "black primary buttons",
|
|
55
|
+
distinctRadii: "distinct corner radii",
|
|
56
|
+
movingElements: "things that move on load or scroll",
|
|
57
|
+
blackAccents: "black chips, buttons and labels",
|
|
58
|
+
buttonInsideInputBar: "submit buttons inside the input's bordered bar",
|
|
59
|
+
dotGrids: "dotted grid backdrops",
|
|
60
|
+
demoMissingControls: "demo controls missing (Skip, Replay)",
|
|
61
|
+
gradientButtons: "buttons with a gradient fill",
|
|
62
|
+
// Measured by `palate states` (the latest run for the route), not by the page as it loads.
|
|
63
|
+
listsWithoutEmptyState: "lists with no empty state",
|
|
64
|
+
missingLoadingStates: "async parts with no loading state",
|
|
65
|
+
errorsSwallowed: "failures nothing on screen reports",
|
|
66
|
+
doubleSubmits: "actions sent twice by a double press",
|
|
67
|
+
brokenImages: "parts showing a broken-image icon",
|
|
68
|
+
textOverflows: "places long text or 320px breaks",
|
|
69
|
+
};
|
|
70
|
+
export const STATE_METRICS = new Set(["listsWithoutEmptyState", "missingLoadingStates", "errorsSwallowed", "doubleSubmits", "brokenImages", "textOverflows"]);
|
|
71
|
+
|
|
72
|
+
// The state facts from the latest `palate states` run for a route (.palate/states/<slug>/<stamp>/findings.json).
|
|
73
|
+
export function statesFactsFor(dir, route) {
|
|
74
|
+
const base = path.join(dir, ".palate", "states", (route ?? "/").replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/g, "") || "home");
|
|
75
|
+
if (!fs.existsSync(base)) return null;
|
|
76
|
+
const last = fs.readdirSync(base).filter((s) => fs.existsSync(path.join(base, s, "findings.json"))).sort().at(-1);
|
|
77
|
+
const f = last ? readJson(path.join(base, last, "findings.json"), null) : null;
|
|
78
|
+
return f?.facts ? { ...f.facts, statesRun: last } : null;
|
|
79
|
+
}
|
|
80
|
+
// Facts that only mean something with motion on (the judge renders with reduced motion).
|
|
81
|
+
const NEEDS_MOTION = new Set(["movingElements"]);
|
|
82
|
+
|
|
83
|
+
// ---- kinds of screen. A rule applies when one of its kinds is in the screen's tags (or it says "all").
|
|
84
|
+
export const KINDS = ["all", "landing", "marketing", "hero", "demo", "logo cloud", "features", "pricing", "cta", "app", "table", "dashboard", "team", "settings", "onboarding", "auth", "docs"];
|
|
85
|
+
const KIND_RE = [
|
|
86
|
+
["landing", /\b(landing|home|homepage|waitlist|marketing page)\b/],
|
|
87
|
+
["hero", /\bhero\b/],
|
|
88
|
+
["demo", /\b(demo|showcase|mockup|live product)\b/],
|
|
89
|
+
["logo cloud", /\blogo[s ]*(cloud|row|wall|strip)|works with|any stack|integrations?\b/],
|
|
90
|
+
["features", /\bfeatures?\b|bento/],
|
|
91
|
+
["pricing", /\b(pricing|plans?)\b/],
|
|
92
|
+
["cta", /\b(cta|signup|sign up|newsletter|call to action)\b/],
|
|
93
|
+
["table", /\b(table|list|records|pipeline|companies)\b/],
|
|
94
|
+
["dashboard", /\b(dashboard|overview|analytics)\b/],
|
|
95
|
+
["team", /\b(team|members?)\b/],
|
|
96
|
+
["settings", /\bsettings?\b/],
|
|
97
|
+
["onboarding", /\b(onboarding|setup|welcome)\b/],
|
|
98
|
+
["auth", /\b(login|sign in)\b/],
|
|
99
|
+
["docs", /\b(docs|documentation|blog|article)\b/],
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
// The tags for a screen, from what palate knows about it: its loop's screen and kind, its route, a section's
|
|
103
|
+
// kind, any words. "/" is a site's landing page; landing, hero, demo, features, pricing and logo clouds are marketing.
|
|
104
|
+
export function kindTags({ screen, kind, route, section, words } = {}) {
|
|
105
|
+
const tags = new Set();
|
|
106
|
+
if (screen && screen !== "other") tags.add(screen);
|
|
107
|
+
if (kind) tags.add(kind);
|
|
108
|
+
if (route === "/") tags.add("landing");
|
|
109
|
+
const t = `${section ?? ""} ${words ?? ""} ${route && route !== "/" ? route.replace(/[^a-z]+/gi, " ") : ""}`.toLowerCase();
|
|
110
|
+
for (const [k, re] of KIND_RE) if (re.test(t)) tags.add(k);
|
|
111
|
+
if (["landing", "hero", "demo", "logo cloud", "features", "pricing", "cta"].some((k) => tags.has(k))) tags.add("marketing");
|
|
112
|
+
if (["table", "dashboard", "team", "settings", "onboarding"].some((k) => tags.has(k))) tags.add("app");
|
|
113
|
+
return [...tags];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// The tags for a route: its loop's screen and kind (a variant), the page a vary loop copied, or the route.
|
|
117
|
+
export function routeTags(dir, route, { title = "", sections = [] } = {}) {
|
|
118
|
+
const loop = loopOfRoute(dir, route);
|
|
119
|
+
const meta = loop ? readJson(path.join(dir, ".palate", "variants", loop, "loop.json"), {}) : {};
|
|
120
|
+
if (meta.vary) return kindTags({ route: meta.vary.route, kind: "marketing", section: meta.kind, words: meta.vary.section });
|
|
121
|
+
if (loop) return kindTags({ screen: meta.screen, kind: meta.kind, words: `${loop} ${meta.job ?? ""}`.slice(0, 400) });
|
|
122
|
+
return kindTags({ route, words: `${title} ${route === "/" ? "" : sections.slice(0, 3).map((s) => s.name).join(" ")}` });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---- the rules file
|
|
126
|
+
const readFile = () => readJson(RULES_FILE, { rules: [] });
|
|
127
|
+
export function loadRules() {
|
|
128
|
+
return readFile().rules ?? [];
|
|
129
|
+
}
|
|
130
|
+
// Proposals the user discarded: { id, rule, tpl?, check?, keys: [evidence], at, since?: [newer evidence] }.
|
|
131
|
+
export const rejectedRules = () => readFile().rejected ?? [];
|
|
132
|
+
function saveRules(rules, rejected = rejectedRules()) {
|
|
133
|
+
fs.mkdirSync(TASTE, { recursive: true });
|
|
134
|
+
fs.writeFileSync(RULES_FILE, JSON.stringify({ note: "Your rules: palate puts the active ones at the top of every brief and checks them on every shoot. Proposed rules wait for your OK (palate rule review). Edit with palate rule add | ok | no | edit | rm.", rules, ...(rejected.length ? { rejected } : {}) }, null, 1));
|
|
135
|
+
}
|
|
136
|
+
export const isActive = (r) => (r.status ?? "active") === "active";
|
|
137
|
+
export const isProposed = (r) => r.status === "proposed";
|
|
138
|
+
export const proposedRules = () => loadRules().filter(isProposed);
|
|
139
|
+
|
|
140
|
+
// A rule for one component can't be measured (a check reads the whole page), so the judge asks it about that
|
|
141
|
+
// component; the measured check is kept and comes back if the rule is widened.
|
|
142
|
+
function enforced(r) {
|
|
143
|
+
const c = r.where?.component;
|
|
144
|
+
if (!c) return r;
|
|
145
|
+
const q = r.check?.type === "judge" && r.check.question ? r.check.question : `Does it follow this rule: "${r.rule}"?`;
|
|
146
|
+
return { ...r, check: { type: "judge", question: `Only for <${c}>: ${q}` }, measured: r.check };
|
|
147
|
+
}
|
|
148
|
+
// What palate enforces: the rules the user wrote or kept, never proposals. Every brief, shoot and judge reads these.
|
|
149
|
+
export const activeRules = () => loadRules().filter(isActive).map(enforced);
|
|
150
|
+
|
|
151
|
+
const normRoute = (r) => (r ? `/${String(r).trim().replace(/^\/+|\/+$/g, "")}` : null);
|
|
152
|
+
// A rule for /team holds on /team and below it (/team/42).
|
|
153
|
+
export const onRoute = (scope, route) => {
|
|
154
|
+
const [a, b] = [normRoute(scope), normRoute(route)];
|
|
155
|
+
return Boolean(a && b) && (a === b || (a !== "/" && b.startsWith(`${a}/`)));
|
|
156
|
+
};
|
|
157
|
+
// A rule applies when one of its kinds is in the screen's tags (or it says "all"), and, if it is for one route,
|
|
158
|
+
// only on that route (route unknown: not applied), and, if it is for one project, only in that project (project
|
|
159
|
+
// unknown: not applied, so a new project never inherits another one's look).
|
|
160
|
+
export const projectOf = (dir) => path.basename(path.resolve(dir));
|
|
161
|
+
const sameProject = (a, b) => Boolean(a && b) && String(a).toLowerCase() === String(b).toLowerCase();
|
|
162
|
+
export const appliesTo = (r, tags, { route, project } = {}) =>
|
|
163
|
+
(r.kinds.includes("all") || r.kinds.some((k) => tags.includes(k))) && (!r.where?.route || onRoute(r.where.route, route)) && (!r.where?.project || sameProject(r.where.project, project));
|
|
164
|
+
export const rulesFor = (tags, { route, project } = {}) => activeRules().filter((r) => appliesTo(r, tags, { route, project }));
|
|
165
|
+
|
|
166
|
+
// ---- scope: where a rule applies, in words
|
|
167
|
+
const KIND_NAMES = { landing: "landing pages", marketing: "marketing pages", hero: "heroes", demo: "demos", "logo cloud": "logo clouds", features: "feature sections", pricing: "pricing pages", cta: "calls to action", app: "app screens", table: "tables and lists", dashboard: "dashboards", team: "team pages", settings: "settings pages", onboarding: "onboarding screens", auth: "sign-in screens", docs: "docs pages" };
|
|
168
|
+
const UMBRELLA = new Set(["all", "app", "marketing"]);
|
|
169
|
+
const cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
170
|
+
// "Team and settings pages", "Landing pages, heroes and demos"; with a project: "Only in palate-site",
|
|
171
|
+
// "Landing pages, only in palate-site".
|
|
172
|
+
export function kindsLabel(kinds, { project } = {}) {
|
|
173
|
+
if (project) return !kinds?.length || kinds.includes("all") ? `Only in ${project}` : `${kindsLabel(kinds)}, only in ${project}`;
|
|
174
|
+
if (!kinds?.length || kinds.includes("all")) return "Everywhere";
|
|
175
|
+
const names = kinds.map((k) => KIND_NAMES[k] ?? `${k} screens`);
|
|
176
|
+
const pages = names.every((n) => n.endsWith(" pages"));
|
|
177
|
+
const parts = pages ? names.map((n) => n.replace(/ pages$/, "")) : names;
|
|
178
|
+
const list = parts.length > 1 ? `${parts.slice(0, -1).join(", ")} and ${parts.at(-1)}` : parts[0];
|
|
179
|
+
return cap(`${list}${pages ? " pages" : ""}`);
|
|
180
|
+
}
|
|
181
|
+
const inProject = (p) => (p ? ` in ${p}` : "");
|
|
182
|
+
export const scopeLabel = (r) => (r.where?.component ? `Only <${r.where.component}>${inProject(r.where.project)}` : r.where?.route ? `Only ${r.where.route}${inProject(r.where.project)}` : kindsLabel(r.kinds, { project: r.where?.project }));
|
|
183
|
+
|
|
184
|
+
// ---- whose taste a rule is. palate decides, not the user: a rule tied to one product's identity (a colour, a font,
|
|
185
|
+
// the brand's name, "our", what the page says, its agents and demos) stays in that project; a judgement about
|
|
186
|
+
// design (clarity, hierarchy, density, motion, states, licensing) is general taste. Simple signals, so the reason
|
|
187
|
+
// can be said in one line. Evidence from two or more projects is always general.
|
|
188
|
+
const COLOURS = /\b(cream|beige|stone|sand|ivory|navy|blue|teal|cyan|green|emerald|mint|lime|olive|yellow|amber|gold|orange|coral|red|crimson|pink|rose|magenta|purple|violet|lilac|lavender|brown|pastels?|neon)\b/i;
|
|
189
|
+
const FONTS = /\b(inter|geist|fraunces|helvetica|arial|roboto|s[öo]hne|satoshi|manrope|garamond|georgia|playfair|ibm plex|jetbrains mono|space grotesk|dm sans|poppins|montserrat|lato|figtree)\b/i;
|
|
190
|
+
const FAMILY = /\b[A-Z][a-z]+ (Sans|Serif|Mono|Grotesk)\b/;
|
|
191
|
+
const NEGATED = /\b(no|never|not|avoid|without|don'?t|nor)\b/i;
|
|
192
|
+
const OURS = /\b(our|ours|we|palate)\b/i;
|
|
193
|
+
const SAYS = /\b(says?|tells?|mentions?) (that|it|the)\b/i;
|
|
194
|
+
const PRODUCT = /\b(agents?|subagents?|logos?|demos?)\b/gi;
|
|
195
|
+
const JUDGEMENT = /\b(clear|clarity|hierarch\w*|restrain\w*|calm|dens\w*|spacing|align\w*|sizes?|small|large|tiny|legible|readable|read|contrast|motion|animat\w*|states?|empty|loading|errors?|licen[cs]\w*|public domain|cc0|credited|focal|consistent|scale)\b|px\b/gi;
|
|
196
|
+
const GENERIC = new Set(["site", "app", "web", "www", "demo", "landing", "home", "page", "pages", "frontend", "client", "ui", "my", "the", "new", "test", "main", "project"]);
|
|
197
|
+
// The brand's own words: the project folder's name and its package name, without words like "site" or "app".
|
|
198
|
+
function brandWords(project, dir) {
|
|
199
|
+
let pkg = "";
|
|
200
|
+
try {
|
|
201
|
+
if (dir) pkg = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8")).name ?? "";
|
|
202
|
+
} catch {}
|
|
203
|
+
return [...new Set(`${project ?? ""} ${pkg}`.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length >= 3 && !GENERIC.has(w)))];
|
|
204
|
+
}
|
|
205
|
+
// { project: the project it's tied to, or null for general taste; signals; why: the reason in a few words }.
|
|
206
|
+
export function classifyScope(text, { project, evidence = [], dir } = {}) {
|
|
207
|
+
const t = String(text ?? "");
|
|
208
|
+
const spread = [...new Set(evidence.map((e) => e.project).filter(Boolean))];
|
|
209
|
+
const judged = [...new Set((t.match(JUDGEMENT) ?? []).map((w) => w.toLowerCase()))];
|
|
210
|
+
const general = (why) => ({ project: null, signals: [], why });
|
|
211
|
+
if (spread.length > 1) return general(`seen in ${spread.length} projects`);
|
|
212
|
+
project ??= spread[0] ?? null;
|
|
213
|
+
const signals = [];
|
|
214
|
+
const hex = t.match(/#[0-9a-f]{3,8}\b/i);
|
|
215
|
+
const colour = t.split(/[.;,:()—]/).find((c) => COLOURS.test(c) && !NEGATED.test(c));
|
|
216
|
+
if (hex || colour) signals.push(`colour (${hex?.[0] ?? colour.match(COLOURS)[0].toLowerCase()})`);
|
|
217
|
+
const font = t.match(FONTS) ?? t.match(FAMILY);
|
|
218
|
+
if (font) signals.push(`font (${font[0]})`);
|
|
219
|
+
const brand = brandWords(project, dir).find((w) => new RegExp(`\\b${w}\\b`, "i").test(t)) ?? t.match(OURS)?.[0].toLowerCase();
|
|
220
|
+
if (brand) signals.push(`brand (${brand})`);
|
|
221
|
+
if (SAYS.test(t)) signals.push("what the page says");
|
|
222
|
+
// Agents, logos and demos only count when the rule is about them (twice or more) more than about a judgement.
|
|
223
|
+
const product = (t.match(PRODUCT) ?? []).map((w) => w.toLowerCase());
|
|
224
|
+
if (!signals.length && product.length >= 2 && product.length > judged.length) signals.push(`product content (${[...new Set(product)].join(", ")})`);
|
|
225
|
+
if (!signals.length) return general(judged.length ? `a judgement about design (${judged.slice(0, 3).join(", ")})` : "nothing ties it to one product");
|
|
226
|
+
return { project, signals, why: signals.join(" and ") };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// The scopes a proposal's evidence supports, narrowest first, and the one to suggest: the narrowest that holds
|
|
230
|
+
// across all of it (one component → that component; tied to one product's identity → that project; one route →
|
|
231
|
+
// that route; routes of one kind → that kind; evidence across kinds → everywhere), with why, in one line.
|
|
232
|
+
export function scopeOptions(r) {
|
|
233
|
+
const ev = r.evidence ?? [];
|
|
234
|
+
const options = [];
|
|
235
|
+
const add = (o) => options.some((x) => x.id === o.id) || options.push(o);
|
|
236
|
+
const comps = [...new Set(ev.map((e) => e.component).filter(Boolean))];
|
|
237
|
+
const routes = [...new Set(ev.map((e) => normRoute(e.route)).filter(Boolean))];
|
|
238
|
+
for (const c of comps.slice(0, 2)) add({ id: `component:${c}`, label: `Only <${c}>`, kinds: ["all"], where: { component: c } });
|
|
239
|
+
for (const rt of routes.slice(0, 2)) add({ id: `route:${rt}`, label: `Only ${rt}`, kinds: ["all"], where: { route: rt } });
|
|
240
|
+
// Evidence from one project can stay in it. Not when it all names one component: that already lives in one codebase.
|
|
241
|
+
const projects = [...new Set(ev.map((e) => e.project ?? null))];
|
|
242
|
+
const oneComponent = comps.length === 1 && ev.every((e) => e.component === comps[0]);
|
|
243
|
+
if (projects.length === 1 && projects[0] && !oneComponent) add({ id: `project:${projects[0]}`, label: `Only in ${projects[0]}`, kinds: ["all"], where: { project: projects[0] } });
|
|
244
|
+
const tagsOf = ev.map((e) => (e.kinds?.length ? e.kinds : e.route ? kindTags({ route: e.route }) : [])).filter((t) => t.length);
|
|
245
|
+
const shared = (pick) => (tagsOf.length ? tagsOf.map((t) => t.filter(pick)).reduce((a, b) => a.filter((k) => b.includes(k))) : []);
|
|
246
|
+
const union = (pick) => [...new Set(tagsOf.flatMap((t) => t.filter(pick)))];
|
|
247
|
+
const specific = (k) => !UMBRELLA.has(k);
|
|
248
|
+
const umbrella = (k) => k !== "all" && UMBRELLA.has(k);
|
|
249
|
+
const kindsOpt = (ks) => ks.length && add({ id: `kinds:${ks.join(",")}`, label: kindsLabel(ks), kinds: ks });
|
|
250
|
+
const [sSpec, sUmb] = [shared(specific), shared(umbrella)];
|
|
251
|
+
kindsOpt(sSpec.length ? sSpec : union(specific));
|
|
252
|
+
kindsOpt(sUmb.length ? sUmb : union(umbrella));
|
|
253
|
+
if (!ev.length && !r.kinds.includes("all")) kindsOpt(r.kinds);
|
|
254
|
+
add({ id: "all", label: "Everywhere", kinds: ["all"] });
|
|
255
|
+
const taste = classifyScope(r.rule, { evidence: ev });
|
|
256
|
+
let suggest = "all";
|
|
257
|
+
if (oneComponent) suggest = `component:${comps[0]}`;
|
|
258
|
+
else if (taste.project && options.some((o) => o.id === `project:${taste.project}`)) suggest = `project:${taste.project}`;
|
|
259
|
+
else if (routes.length === 1 && ev.every((e) => normRoute(e.route) === routes[0])) suggest = `route:${routes[0]}`;
|
|
260
|
+
else if (sSpec.length) suggest = `kinds:${sSpec.join(",")}`;
|
|
261
|
+
else if (sUmb.length) suggest = `kinds:${sUmb.join(",")}`;
|
|
262
|
+
else if (!ev.length && !r.kinds.includes("all")) suggest = `kinds:${r.kinds.join(",")}`;
|
|
263
|
+
const label = options.find((o) => o.id === suggest)?.label ?? "Everywhere";
|
|
264
|
+
// The reason matches the suggestion: the narrowest scope the evidence supports, said plainly.
|
|
265
|
+
const why = suggest.startsWith("component:") ? `all of it on ${label.replace(/^Only /, "")}: ${label.toLowerCase()}` : suggest.startsWith("project:") ? `${taste.why}: ${label.toLowerCase()}` : suggest.startsWith("route:") ? `all the evidence is from ${suggest.slice(6)}, so ${label.toLowerCase()} for now; it reads as general taste, so keep it wider if it holds elsewhere` : `general taste, ${taste.why}: ${label.toLowerCase()}`;
|
|
266
|
+
return { options, suggest, why };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// A scope from what the user chose: an option id, its number (1…), its label, "suggested", "everywhere",
|
|
270
|
+
// or { component } / { route } / { kinds } / { project } (a project narrows any of the others: { project, kinds }).
|
|
271
|
+
export function resolveScope(r, choice) {
|
|
272
|
+
const { options, suggest } = scopeOptions(r);
|
|
273
|
+
const byId = (id) => options.find((o) => o.id === id);
|
|
274
|
+
if (choice == null || choice === "" || choice === "suggested") return byId(suggest);
|
|
275
|
+
if (typeof choice === "object" && choice.project) {
|
|
276
|
+
const p = String(choice.project).trim();
|
|
277
|
+
const { project, scope, ...rest } = choice;
|
|
278
|
+
const base = scope != null ? resolveScope(r, scope) : Object.values(rest).some((v) => v != null && v !== "" && !(Array.isArray(v) && !v.length)) ? resolveScope(r, rest) : byId("all");
|
|
279
|
+
const s = { ...base, where: { ...(base.where ?? {}), project: p } };
|
|
280
|
+
return { ...s, id: `${base.id === "all" ? "" : `${base.id}@`}project:${p}`, label: scopeLabel(s) };
|
|
281
|
+
}
|
|
282
|
+
if (typeof choice === "object") {
|
|
283
|
+
if (choice.component) {
|
|
284
|
+
const c = String(choice.component).trim().replace(/^<|\/?>$/g, "");
|
|
285
|
+
return { id: `component:${c}`, label: `Only <${c}>`, kinds: ["all"], where: { component: c } };
|
|
286
|
+
}
|
|
287
|
+
if (choice.route) return { id: `route:${normRoute(choice.route)}`, label: `Only ${normRoute(choice.route)}`, kinds: ["all"], where: { route: normRoute(choice.route) } };
|
|
288
|
+
if (choice.kinds?.length) {
|
|
289
|
+
const ks = [...new Set(choice.kinds.map((k) => String(k).trim().toLowerCase()).filter(Boolean))];
|
|
290
|
+
return ks.includes("all") ? byId("all") : { id: `kinds:${ks.join(",")}`, label: kindsLabel(ks), kinds: ks };
|
|
291
|
+
}
|
|
292
|
+
return byId(suggest);
|
|
293
|
+
}
|
|
294
|
+
const c = String(choice).trim();
|
|
295
|
+
if (/^(all|everywhere|global)$/i.test(c)) return byId("all");
|
|
296
|
+
if (/^\d+$/.test(c) && options[Number(c) - 1]) return options[Number(c) - 1];
|
|
297
|
+
const hit = byId(c) ?? options.find((o) => o.label.toLowerCase() === c.toLowerCase());
|
|
298
|
+
if (hit) return hit;
|
|
299
|
+
if (/^component:/.test(c)) return resolveScope(r, { component: c.slice(10) });
|
|
300
|
+
if (/^route:/.test(c)) return resolveScope(r, { route: c.slice(6) });
|
|
301
|
+
if (/^kinds?:/.test(c)) return resolveScope(r, { kinds: c.replace(/^kinds?:/, "").split(",") });
|
|
302
|
+
if (/^project:/.test(c)) return resolveScope(r, { project: c.slice(8) });
|
|
303
|
+
if (c.startsWith("/")) return resolveScope(r, { route: c });
|
|
304
|
+
throw new Error(`No scope "${c}". Choose one of: ${options.map((o, i) => `${i + 1} ${o.id} (${o.label})`).join(", ")}; or --only <Component>, --route /x, --kind team, --project <name>.`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const WORDS = (s) => new Set((s.toLowerCase().match(/[a-z]{3,}/g) ?? []).filter((w) => !["the", "and", "not", "never", "with", "only", "one", "its", "for", "every", "page", "section"].includes(w)));
|
|
308
|
+
function similar(a, b) {
|
|
309
|
+
const [x, y] = [WORDS(a), WORDS(b)];
|
|
310
|
+
const both = [...x].filter((w) => y.has(w)).length;
|
|
311
|
+
return both / Math.max(1, Math.min(x.size, y.size));
|
|
312
|
+
}
|
|
313
|
+
const sameCheck = (a, b) => a && b && a.type !== "judge" && a.type === b.type && (a.metric ?? a.selector) === (b.metric ?? b.selector);
|
|
314
|
+
const alike = (r, { rule, check, tpl }) => (tpl && r.tpl === tpl) || sameCheck(r.check, check) || similar(r.rule, rule) >= 0.7;
|
|
315
|
+
// Does an active rule's scope already hold where this evidence came from? (Then the evidence only strengthens it.)
|
|
316
|
+
const covers = (r, evidence = []) =>
|
|
317
|
+
evidence.every((e) => {
|
|
318
|
+
if (r.where?.project && !sameProject(r.where.project, e.project)) return false;
|
|
319
|
+
if (r.where?.component) return e.component === r.where.component;
|
|
320
|
+
if (r.where?.route) return onRoute(r.where.route, e.route);
|
|
321
|
+
const ks = e.kinds?.length ? e.kinds : e.route ? kindTags({ route: e.route }) : [];
|
|
322
|
+
return r.kinds.includes("all") || !ks.length || r.kinds.some((k) => ks.includes(k));
|
|
323
|
+
});
|
|
324
|
+
const mergeEvidence = (a = [], b = []) => [...new Map([...a, ...b].map((e) => [e.key, e])).values()];
|
|
325
|
+
const nextId = (rules, rejected) => `R${[...rules, ...rejected].reduce((m, r) => Math.max(m, Number(String(r.id).slice(1)) || 0), 0) + 1}`;
|
|
326
|
+
const gain = (r, source) => {
|
|
327
|
+
if (source && r.source !== source && !(r.also ?? []).includes(source)) r.also = [...(r.also ?? []), source];
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
// Adds a rule unless one like it exists (the same template, the same measured check, or nearly the same
|
|
331
|
+
// words); then the existing rule gains the new source instead. Returns { rule, added, proposed?, discarded? }.
|
|
332
|
+
// status "active": the user's own words, enforced at once (a proposal like it becomes theirs).
|
|
333
|
+
// status "proposed": inferred by palate, with evidence. An active rule whose scope holds the evidence is only
|
|
334
|
+
// strengthened; a proposal like it gains the evidence; one the user discarded stays discarded until clearly new
|
|
335
|
+
// evidence arrives (more new pieces than the rejection was based on).
|
|
336
|
+
// where: the user's own narrower scope ({ component } or { route }, and/or { project }), for an active rule.
|
|
337
|
+
// A project alone keeps the kinds (landing pages in that project); a component or route holds on any kind.
|
|
338
|
+
export function addRule({ rule, kinds = ["all"], source, check, tpl, status = "active", evidence = [], where } = {}) {
|
|
339
|
+
if (!rule?.trim()) throw new Error('Usage: palate rule add "<the rule, in the user\'s words>" [--kind landing] [--only <Component> | --route /x]');
|
|
340
|
+
const rules = loadRules();
|
|
341
|
+
const rejected = rejectedRules();
|
|
342
|
+
kinds = [...new Set(kinds.map((k) => k.trim().toLowerCase()).filter(Boolean))];
|
|
343
|
+
if (!kinds.length || where?.component || where?.route) kinds = ["all"];
|
|
344
|
+
const likes = rules.filter((r) => alike(r, { rule, check, tpl }));
|
|
345
|
+
let id;
|
|
346
|
+
const sameWhere = (r) => (r.where?.component ?? null) === (where?.component ?? null) && normRoute(r.where?.route) === normRoute(where?.route) && (r.where?.project ?? null) === (where?.project ?? null);
|
|
347
|
+
if (status === "active") {
|
|
348
|
+
// A rule that already holds everywhere covers a narrower one; two different narrow scopes are two rules.
|
|
349
|
+
const twin = where ? likes.find((r) => isProposed(r) || !r.where || sameWhere(r)) : likes.find(isActive) ?? likes[0];
|
|
350
|
+
if (twin) {
|
|
351
|
+
gain(twin, source);
|
|
352
|
+
// The user said it themselves: a proposal like it is now theirs, with their scope.
|
|
353
|
+
const promoted = isProposed(twin);
|
|
354
|
+
if (promoted || (twin.where && !sameWhere(twin))) {
|
|
355
|
+
twin.status = "active";
|
|
356
|
+
twin.kinds = kinds;
|
|
357
|
+
if (where) twin.where = where;
|
|
358
|
+
else delete twin.where;
|
|
359
|
+
if (promoted) twin.approved = new Date().toISOString();
|
|
360
|
+
} else if (!where) for (const k of kinds) if (!twin.kinds.includes(k) && !twin.kinds.includes("all")) twin.kinds.push(k);
|
|
361
|
+
saveRules(rules, rejected);
|
|
362
|
+
return { rule: twin, added: promoted };
|
|
363
|
+
}
|
|
364
|
+
} else {
|
|
365
|
+
const strong = likes.find((r) => isActive(r) && covers(r, evidence));
|
|
366
|
+
if (strong) {
|
|
367
|
+
gain(strong, source);
|
|
368
|
+
saveRules(rules, rejected);
|
|
369
|
+
return { rule: strong, added: false };
|
|
370
|
+
}
|
|
371
|
+
const twin = likes.find(isProposed);
|
|
372
|
+
if (twin) {
|
|
373
|
+
gain(twin, source);
|
|
374
|
+
twin.evidence = mergeEvidence(twin.evidence, evidence);
|
|
375
|
+
for (const k of kinds) if (!twin.kinds.includes(k) && !twin.kinds.includes("all")) twin.kinds.push(k);
|
|
376
|
+
saveRules(rules, rejected);
|
|
377
|
+
return { rule: twin, added: false, proposed: true };
|
|
378
|
+
}
|
|
379
|
+
const no = rejected.find((x) => alike(x, { rule, check, tpl }));
|
|
380
|
+
if (no) {
|
|
381
|
+
// Only evidence the rejection didn't see counts, and it adds up across learns.
|
|
382
|
+
const fresh = mergeEvidence((no.since ?? []).map((key) => ({ key })), evidence.filter((e) => !no.keys.includes(e.key)));
|
|
383
|
+
if (fresh.length <= no.keys.length) {
|
|
384
|
+
no.since = fresh.map((e) => e.key);
|
|
385
|
+
saveRules(rules, rejected);
|
|
386
|
+
return { rule: null, added: false, discarded: no, needs: no.keys.length + 1 - fresh.length };
|
|
387
|
+
}
|
|
388
|
+
// Proposed again, under a new id (the discarded one stays retired).
|
|
389
|
+
id = nextId(rules, rejected);
|
|
390
|
+
rejected.splice(rejected.indexOf(no), 1);
|
|
391
|
+
evidence = evidence.map((e) => ({ ...e, afterNo: no.at }));
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
const r = { id: id ?? nextId(rules, rejected), rule: rule.trim(), kinds, status, source: source ?? "user", check: check ?? { type: "judge", question: `Does it follow this rule: "${rule.trim()}"?` }, added: new Date().toISOString(), ...(where && status === "active" ? { where } : {}), ...(tpl ? { tpl } : {}), ...(status === "proposed" ? { evidence } : {}) };
|
|
395
|
+
rules.push(r);
|
|
396
|
+
saveRules(rules, rejected);
|
|
397
|
+
return { rule: r, added: true, proposed: status === "proposed" };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const findRule = (rules, id) => rules.find((x) => x.id.toLowerCase() === String(id).trim().toLowerCase().replace(/^(\d)/, "r$1"));
|
|
401
|
+
const setText = (r, text) => {
|
|
402
|
+
r.rule = text.trim();
|
|
403
|
+
if (r.check?.type === "judge") r.check.question = `Does it follow this rule: "${r.rule}"?`;
|
|
404
|
+
delete r.inTheirWords;
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
// The user keeps a proposal: it becomes active with the scope they chose (default: the suggested one), in
|
|
408
|
+
// their words if they gave new ones. When an active rule like it had a narrower scope, that rule takes the
|
|
409
|
+
// wider scope instead (no twin rules); otherwise the kept rule stands on its own.
|
|
410
|
+
export function approveRule(id, { scope, text } = {}) {
|
|
411
|
+
const rules = loadRules();
|
|
412
|
+
const r = findRule(rules, id);
|
|
413
|
+
if (!r) throw new Error(`No rule ${id}. palate rule list shows them.`);
|
|
414
|
+
if (!isProposed(r)) throw new Error(`${r.id} is already one of your rules (palate rule edit ${r.id} changes it).`);
|
|
415
|
+
const s = resolveScope(r, scope);
|
|
416
|
+
if (text?.trim() && text.trim() !== r.rule) setText(r, text);
|
|
417
|
+
r.kinds = s.kinds;
|
|
418
|
+
if (s.where) r.where = s.where;
|
|
419
|
+
else delete r.where;
|
|
420
|
+
r.status = "active";
|
|
421
|
+
r.approved = new Date().toISOString();
|
|
422
|
+
const twin = rules.find((x) => x !== r && isActive(x) && alike(x, r));
|
|
423
|
+
if (twin && !s.where && (twin.where || !twin.kinds.includes("all"))) {
|
|
424
|
+
if (twin.where) {
|
|
425
|
+
delete twin.where;
|
|
426
|
+
twin.kinds = s.kinds;
|
|
427
|
+
} else twin.kinds = s.kinds.includes("all") ? ["all"] : [...new Set([...twin.kinds, ...s.kinds])];
|
|
428
|
+
for (const src of [r.source, ...(r.also ?? [])]) gain(twin, src);
|
|
429
|
+
if (text?.trim()) setText(twin, r.rule);
|
|
430
|
+
rules.splice(rules.indexOf(r), 1);
|
|
431
|
+
saveRules(rules);
|
|
432
|
+
return { rule: twin, scope: { ...s, label: scopeLabel(twin) }, mergedFrom: r.id };
|
|
433
|
+
}
|
|
434
|
+
saveRules(rules);
|
|
435
|
+
return { rule: r, scope: s };
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// The user discards a proposal: it goes, and is remembered with its evidence, so the same evidence never
|
|
439
|
+
// proposes it again.
|
|
440
|
+
export function rejectRule(id) {
|
|
441
|
+
const rules = loadRules();
|
|
442
|
+
const r = findRule(rules, id);
|
|
443
|
+
if (!r) throw new Error(`No rule ${id}. palate rule list shows them.`);
|
|
444
|
+
if (!isProposed(r)) throw new Error(`${r.id} is one of your rules, not a proposal. To remove it: palate rule rm ${r.id}`);
|
|
445
|
+
rules.splice(rules.indexOf(r), 1);
|
|
446
|
+
const rejected = rejectedRules();
|
|
447
|
+
rejected.push({ id: r.id, rule: r.rule, ...(r.tpl ? { tpl: r.tpl } : {}), ...(r.check?.type !== "judge" ? { check: r.check } : {}), keys: (r.evidence ?? []).map((e) => e.key), at: new Date().toISOString() });
|
|
448
|
+
saveRules(rules, rejected.slice(-300));
|
|
449
|
+
return r;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// project: keep the rule to one project ("" or "all": every project again). New kinds drop a route or component
|
|
453
|
+
// scope but keep the project.
|
|
454
|
+
export function editRule(id, { rule, kinds, project } = {}) {
|
|
455
|
+
const rules = loadRules();
|
|
456
|
+
const r = findRule(rules, id);
|
|
457
|
+
if (!r) throw new Error(`No rule ${id}. palate rule list shows them.`);
|
|
458
|
+
if (rule) setText(r, rule);
|
|
459
|
+
if (kinds?.length) {
|
|
460
|
+
r.kinds = kinds;
|
|
461
|
+
r.where = r.where?.project ? { project: r.where.project } : undefined;
|
|
462
|
+
}
|
|
463
|
+
if (project != null) {
|
|
464
|
+
const p = String(project).trim();
|
|
465
|
+
r.where = { ...(r.where ?? {}), project: /^(|all|everywhere|any)$/i.test(p) ? undefined : p };
|
|
466
|
+
}
|
|
467
|
+
if (r.where) for (const k of Object.keys(r.where)) if (r.where[k] == null) delete r.where[k];
|
|
468
|
+
if (!r.where || !Object.keys(r.where).length) delete r.where;
|
|
469
|
+
saveRules(rules);
|
|
470
|
+
return r;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export function removeRule(id) {
|
|
474
|
+
const rules = loadRules();
|
|
475
|
+
const r = findRule(rules, id);
|
|
476
|
+
if (!r) throw new Error(`No rule ${id}.`);
|
|
477
|
+
rules.splice(rules.indexOf(r), 1);
|
|
478
|
+
saveRules(rules);
|
|
479
|
+
return r;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// ---- evidence: what a proposal was learned from, in plain words
|
|
483
|
+
const pinNo = (id) => String(id ?? "").match(/-(\d+)$/)?.[1] ?? "";
|
|
484
|
+
export const pinEvidence = (p, said) => ({ key: `pin:${p.id}`, type: "pin", pin: pinNo(p.id), said: said ?? p.prompt ?? (p.chips ?? []).join(", "), typed: Boolean(p.prompt), project: p.project, route: p.route, component: p.component ?? null, kinds: kindTags({ route: p.route }), crop: p.crop ?? p.before ?? null, at: p.at });
|
|
485
|
+
const placeOf = (e) => (e.component ? `<${e.component}>${e.route ? ` in ${e.route}` : ""}` : e.route ?? "");
|
|
486
|
+
const one = (e) =>
|
|
487
|
+
e.type === "pin"
|
|
488
|
+
? `pin${e.pin ? ` ${e.pin}` : ""} “${short(e.said ?? "", 60)}” on ${placeOf(e)}`
|
|
489
|
+
: e.type === "failure"
|
|
490
|
+
? `you called ${e.route ?? "a screen"} bad: “${short(e.said ?? "", 80)}”`
|
|
491
|
+
: e.type === "critique"
|
|
492
|
+
? `a critique finding you agreed with on ${e.route ?? "a page"}: “${short(e.said ?? "", 80)}”`
|
|
493
|
+
: `“${short(e.said ?? "", 80)}”`;
|
|
494
|
+
// "from pin 3 “make this calmer” on <RoleBadge> in /team, and 2 more asks like it": the newest evidence in the
|
|
495
|
+
// user's own words first, then a count of the rest.
|
|
496
|
+
export function evidenceText(r) {
|
|
497
|
+
const ev = [...(r.evidence ?? [])].sort((a, b) => Number(Boolean(b.typed)) - Number(Boolean(a.typed)) || String(b.at ?? "").localeCompare(String(a.at ?? "")));
|
|
498
|
+
if (!ev.length) return `from ${[r.source, ...(r.also ?? [])].join(", ")}`;
|
|
499
|
+
const rest = ev.slice(1);
|
|
500
|
+
const pins = rest.every((e) => e.type === "pin");
|
|
501
|
+
const more = rest.length ? `, and ${rest.length} more ${pins ? (rest.length === 1 ? "ask" : "asks") : rest.length === 1 ? "one" : "ones"} like it` : "";
|
|
502
|
+
const no = ev.find((e) => e.afterNo)?.afterNo;
|
|
503
|
+
return `from ${one(ev[0])}${more}${no ? ` (you discarded it on ${no.slice(0, 10)}; it has come up again since)` : ""}`;
|
|
504
|
+
}
|
|
505
|
+
// "asked 3 times", "2 failures", "1 agreed critique finding"
|
|
506
|
+
export function evidenceCount(r) {
|
|
507
|
+
const ev = r.evidence ?? [];
|
|
508
|
+
const n = (t) => ev.filter((e) => e.type === t).length;
|
|
509
|
+
return [n("pin") && `asked ${n("pin")} time${n("pin") > 1 ? "s" : ""}`, n("failure") && `${n("failure")} failure${n("failure") > 1 ? "s" : ""}`, n("critique") && `${n("critique")} agreed critique finding${n("critique") > 1 ? "s" : ""}`].filter(Boolean).join(", ") || "1 source";
|
|
510
|
+
}
|
|
511
|
+
// How a rule is checked, in words (a rule for one component is asked of the judge).
|
|
512
|
+
export const checkWords = (r) => {
|
|
513
|
+
const c = enforced(r).check;
|
|
514
|
+
return c?.type === "metric" ? `measured on every shoot: ${METRICS[c.metric] ?? c.metric} ${c.op} ${c.value}` : c?.type === "dom" ? `measured on every shoot: ${c.selector}` : "the judge asks";
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
// ---- templates: the general form of what the user keeps rejecting, with a check. Evidence in any words
|
|
518
|
+
// ("logos too small", a critique finding, a failure's why) is matched to one, so it becomes the same rule
|
|
519
|
+
// with the same check instead of a new note. Order matters: specific before general.
|
|
520
|
+
const metric = (m, op, value) => ({ type: "metric", metric: m, op, value });
|
|
521
|
+
// The user asking for a demo without controls ("no skip button", "loop on its own", "automatic, without a button").
|
|
522
|
+
const NO_CONTROLS = /\b(no|without|never|instead of)\b[^.]{0,40}\b(skip|replay|button)\b|loops? on its own|automatic(ally)?\b[^.]{0,30}\bloop|\bloop[^.]{0,30}\bautomatic/i;
|
|
523
|
+
|
|
524
|
+
export const TEMPLATES = [
|
|
525
|
+
// States, measured by `palate states` (every screen; n/a where a state can't happen, e.g. static data).
|
|
526
|
+
{ tpl: "states-empty", re: /empty states?|no (items|results|records|members) (message|state)|blank list|nothing to show/i, kinds: ["all"], rule: "Every list, table and grid has an empty state: what is empty, and the next action as a button.", check: metric("listsWithoutEmptyState", "<=", 0) },
|
|
527
|
+
{ tpl: "states-loading", re: /loading (state|indicator|spinner)|skeletons?\b|blank while (it )?loads|while (it'?s )?loading/i, kinds: ["all"], rule: "Everything that loads shows it is loading (a skeleton shaped like the content), never a blank or a false empty state.", check: metric("missingLoadingStates", "<=", 0) },
|
|
528
|
+
{ tpl: "states-error", re: /error (states?|messages?)|swallow|fails? silently|errors? (are |is )?(hidden|ignored)|nothing happens when it fails/i, kinds: ["all"], rule: "Every failure is shown: what happened and what to do, with Try again; never a silent fail or a fall-through to the empty state.", check: metric("errorsSwallowed", "<=", 0) },
|
|
529
|
+
{ tpl: "states-double-submit", re: /double.?(submit|click|press)|(sent|submitted|pressed|clicked) twice/i, kinds: ["all"], rule: "A second press while an action runs does nothing: the button shows it is pending and the request goes once.", check: metric("doubleSubmits", "<=", 0) },
|
|
530
|
+
{ tpl: "states-images", re: /broken (images?|avatars?)|image fallback|avatar fallback|missing (images?|avatars?)/i, kinds: ["all"], rule: "Images and avatars fall back gracefully when they fail; never the browser's broken-image icon.", check: metric("brokenImages", "<=", 0) },
|
|
531
|
+
{ tpl: "states-long-text", re: /long (names?|text|emails?|words?|titles?)|text overflows?|overflowing text|clipped text/i, kinds: ["all"], rule: "Long names, emails and titles wrap or truncate with a way to read them; nothing spills, clips or scrolls the page sideways at 320px.", check: metric("textOverflows", "<=", 0) },
|
|
532
|
+
{ tpl: "signup-button-outside", re: /button.{0,60}(outside|inside|beside).{0,40}(input|field|bar)|inside one bordered bar/i, kinds: ["landing", "cta"], rule: "The primary signup button sits outside the input bar, as its own button beside the field group, not inside one bordered bar.", check: metric("buttonInsideInputBar", "<=", 0) },
|
|
533
|
+
{ tpl: "no-black-accents", re: /pure.black|no black|black (ui )?(chips?|accents?|labels?)/i, kinds: ["landing", "marketing"], rule: "No pure-black UI accents (chips, buttons, labels): use the brand colour or refined neutrals.", check: metric("blackAccents", "<=", 0) },
|
|
534
|
+
{ tpl: "cards-autoplay", re: /animate by themselves|auto.?play|play on their own|static demo card/i, kinds: ["landing", "marketing", "features"], rule: "Feature cards animate by themselves and show the feature working (auto-play when in view); a static demo card loses.", check: { type: "judge", question: "Does every feature card play by itself when in view, showing the feature working (no static demo cards)?" } },
|
|
535
|
+
{ tpl: "pastel-stages", re: /pastel|dotted (grey |gray )?grids?|dots? (background|grid)|instead of dots/i, kinds: ["landing", "marketing", "features"], rule: "Card stages use soft pastel backgrounds, never dotted grey grids.", check: metric("dotGrids", "<=", 0) },
|
|
536
|
+
{ tpl: "no-gradient-buttons", re: /gradient (buttons?|ctas?|fills?)|gradients? on (the )?(buttons?|ctas?)|buttons? with (a )?gradients?/i, kinds: ["all"], rule: "No gradient fills on buttons: a solid brand colour or a refined neutral.", check: metric("gradientButtons", "<=", 0) },
|
|
537
|
+
{ tpl: "real-agent-ui", re: /agent (ui|interface)|prompt box|tool.call/i, kinds: ["landing", "demo"], rule: "An agent UI in a demo looks like a real best-in-class agent interface: a real prompt box, tool-call and step components.", check: { type: "judge", question: "Does the agent UI in the demo look like a real best-in-class agent interface (a real prompt box, tool calls, steps)?" } },
|
|
538
|
+
{ tpl: "one-company-agents", re: /rival|one company'?s agents|mixed in one build|subagents/i, kinds: ["landing", "demo"], rule: "Demos stay realistic: one company's agents build together (e.g. Claude Code subagents), never rival companies' agents mixed in one build.", check: { type: "judge", question: "Is the demo realistic: one company's agents working together, no rival agents mixed in one build?" } },
|
|
539
|
+
// Said with a "no"/"without", the controls flip meaning: the user wants the demo to loop on its own.
|
|
540
|
+
{ tpl: "demo-loops", re: NO_CONTROLS, kinds: ["landing", "demo"], rule: "A demo on a landing page loops on its own, with no Skip or Replay button: it finishes its story in about 15 seconds, holds the result, then plays again, pausing while the visitor hovers or focuses inside it.", check: { type: "judge", question: "Does the demo loop on its own (no Skip or Replay button), finish its story in about 15 seconds, and pause while the visitor hovers or focuses inside it?" } },
|
|
541
|
+
{ tpl: "demo-15s", re: /\b15 ?s\b|skip and replay|finishes its story/i, unless: NO_CONTROLS, kinds: ["landing", "demo"], rule: "A demo on a landing page finishes its story in about 15 seconds, with Skip and Replay.", check: metric("demoMissingControls", "<=", 0) },
|
|
542
|
+
{ tpl: "framework-logos-placement", re: /framework logos?|frameworks?.{0,50}(own|logo.cloud|section|further down)|only (the )?(coding[- ])?agent(s'?)? logos|logos in the wrong place/i, kinds: ["landing"], rule: "Before the product demo, only the logos of the agents it runs in. Framework and other integration logos go in their own logo-cloud section further down.", check: metric("frameworkLogosBeforeDemo", "<=", 0) },
|
|
543
|
+
{ tpl: "no-step-bars", re: /step ?bars?|progress (bar|line)|✓ ?plan|decorative (step|progress)|bar (with|that has) no purpose/i, kinds: ["landing", "marketing"], rule: "No decorative step or progress bars (\"✓ Plan — Build — Choose…\") that do nothing; let the product show its steps.", check: metric("stepBarsBeforeDemo", "<=", 0) },
|
|
544
|
+
{ tpl: "demo-premium", re: /demo.{0,80}(premium|dashboard of panels|at its best|isn'?t the product)|dashboard of panels/i, kinds: ["landing", "demo"], rule: "The product demo is the most premium thing on the page: the product visibly working, in detail; never a small dashboard of panels.", check: { type: "judge", question: "Is the product demo the most premium thing on the page, the product visibly working in detail (not a small dashboard of panels)?" } },
|
|
545
|
+
{ tpl: "no-framed-screenshots", re: /screenshots?|static (pictures?|images?)|only (ever )?(shown )?as pictures|framed|instead of the product working/i, kinds: ["landing"], rule: "Never show the product only as framed screenshots on a landing page: show it working, built in code (one plain still at most).", check: metric("framedScreenshots", "<=", 0) },
|
|
546
|
+
{ tpl: "kicker-once", re: /kicker|eyebrow|section numbers?|same label|label on every|mono "?0\d/i, kinds: ["landing", "marketing"], rule: "A kicker label or section number on one section at most; let sections open differently.", check: metric("kickerSections", "<=", 1) },
|
|
547
|
+
{ tpl: "no-repeated-device", re: /same (opening )?device|device (on|in) every|every section (the same|opens)/i, kinds: ["landing", "marketing"], rule: "No device on every section (frames, rules, two-column heads, backdrops, dark bands): a device used everywhere means nothing.", check: metric("repeatedDevices", "<=", 0) },
|
|
548
|
+
{ tpl: "logo-size", re: /logos?.{0,50}(too )?(small|tiny|faint|unreadable|can'?t see|16px)|marks?.{0,30}(small|tiny|faint)|small logos?/i, kinds: ["landing", "marketing", "logo cloud"], rule: "Logo marks at least 24px tall, at a size you can read; never logos as faint texture.", check: metric("minLogoMarkPx", ">=", 24) },
|
|
549
|
+
{ tpl: "logo-colour", re: /logos?.{0,50}(grey|gray|one colou?r|monochrome|brand colou?rs?)|official (brand )?colou?rs/i, kinds: ["landing", "marketing", "logo cloud"], rule: "Brand logos in their official colours, not painted one grey.", check: metric("monochromeLogoRows", "<=", 0) },
|
|
550
|
+
{ tpl: "logo-cloud-presence", re: /logo.?cloud/i, kinds: ["logo cloud"], rule: "A logo cloud has presence: brand-colour marks at 32–40px with their names, one deliberate rhythm, under a real heading (never a 16px label alone), and a reason to be on the page.", check: { type: "judge", question: "Does the logo cloud have presence: brand-colour marks at 32–40px with names, one rhythm, a real heading, a reason to exist?" } },
|
|
551
|
+
{ tpl: "motion", re: /nothing moves|no motion|feels dead|static apart|doesn'?t move|purposeful (motion|animation)/i, kinds: ["landing", "marketing"], rule: "The page moves with purpose: a short entrance, the product playing, state changes animated; reduced motion respected and nothing hidden without JavaScript.", check: metric("movingElements", ">=", 1) },
|
|
552
|
+
{ tpl: "radius-scale", re: /radi(us|i)|corner/i, kinds: ["all"], rule: "One radius scale (three or four values, nested concentrically) and one black.", check: metric("distinctRadii", "<=", 4) },
|
|
553
|
+
{ tpl: "social-meta", re: /og:image|social (image|card)|twitter:card|apple-touch/i, kinds: ["landing"], rule: "A page people will share has an og:image, a twitter card and an apple-touch-icon.", check: { type: "dom", selector: 'meta[property="og:image"], meta[name="og:image"]', min: 1 } },
|
|
554
|
+
{ tpl: "brand-primary", re: /not black|black (button|primary)|brand colou?r.{0,30}primary|primary.{0,30}brand colou?r/i, kinds: ["landing"], rule: "Primary actions use the brand colour, not black.", check: metric("blackPrimaryButtons", "<=", 0) },
|
|
555
|
+
{ tpl: "documentation-row", re: /documentation|how it works|seven-column|process row|step list|explains? the loop/i, kinds: ["landing", "marketing"], rule: "No row of process steps that explains how it works like documentation; show the process happening.", check: { type: "judge", question: "Is there a row or list of process steps that explains the product like documentation, instead of showing it happen? (yes fails)" } },
|
|
556
|
+
{ tpl: "readable-thumbnails", re: /tiny (screenshots|thumbnails)|wall of|unreadable/i, kinds: ["all"], rule: "Every preview or thumbnail is large enough to read; no wall of tiny screenshots.", check: { type: "judge", question: "Is every preview or thumbnail large enough to read at 100% (no wall of tiny screenshots)?" } },
|
|
557
|
+
{ tpl: "one-focal-number", re: /compete|one (big|lead) number|several (numbers|figures)|at the heading'?s size/i, kinds: ["all"], rule: "One focal point per band: one lead number or line; other figures a step smaller.", check: { type: "judge", question: "Does each band have one focal point (one lead number or line, the rest a step smaller)?" } },
|
|
558
|
+
{ tpl: "closing-cta", re: /closing (signup|cta|call)|repeats the hero/i, kinds: ["landing"], rule: "The closing call to action gives something new (the result, a reward), not a repeat of the hero form.", check: { type: "judge", question: "Does the closing call to action add something new instead of repeating the hero form?" } },
|
|
559
|
+
{ tpl: "show-not-tell", re: /tells you|never shows|without showing|toggle .{0,30}(screenshot|too small)|change is too small to see/i, kinds: ["landing", "marketing"], rule: "Show every claim happening (learning, polish, the pick) in live UI; don't just state it or flip a toggle over a picture.", check: { type: "judge", question: "Is every claim the page makes shown happening in live UI, rather than stated or shown as a toggle over a picture?" } },
|
|
560
|
+
{ tpl: "premium", re: /premium|apple.level|best.designed|merely correct|still not there|won'?t believe an ai/i, kinds: ["landing", "marketing", "hero"], rule: "Premium, not merely correct: the finish of the best product pages. Would a designer believe an AI built this?", check: { type: "judge", question: "Is it premium, not merely correct: would a designer believe an AI built this? (correct and clean but forgettable fails)" } },
|
|
561
|
+
{ tpl: "clarity-over-spectacle", re: /spectacle|theatre|theater|clever.concept/i, kinds: ["all"], rule: "Clarity before spectacle: a new idea must be at least as clear as what it replaces.", check: { type: "judge", question: "Is it at least as clear as the current version, with any spectacle serving that clarity?" } },
|
|
562
|
+
{ tpl: "all-at-once", re: /one at a time|behind (tabs?|interaction|steps)|hidden behind|configure before/i, kinds: ["all"], rule: "Show everything the job needs at once; never hide content behind tabs, steps or one card at a time.", check: { type: "judge", question: "Is all the content this job needs visible at once (not behind tabs, steps or one card at a time)?" } },
|
|
563
|
+
{ tpl: "no-props", re: /\bprops\b|stamps?|tickets?|tear-off|clutter/i, kinds: ["all"], rule: "Density of content, never of chrome: one crafted touch at most, and no props (stamps, tickets, slips, folders).", check: { type: "judge", question: "Is the density content rather than chrome, with one crafted touch at most and no props?" } },
|
|
564
|
+
];
|
|
565
|
+
export const matchTemplate = (text) => TEMPLATES.find((t) => t.re.test(text)) ?? null;
|
|
566
|
+
export const matchTemplates = (text) => TEMPLATES.filter((t) => t.re.test(text) && !t.unless?.test(text));
|
|
567
|
+
const kindsWith = (t, kinds) => (t.kinds.includes("all") || !kinds?.length ? t.kinds : [...new Set([...t.kinds, ...kinds])]);
|
|
568
|
+
const fromTemplate = (t, { kinds, source, status, evidence, where } = {}) => addRule({ rule: t.rule, kinds: kindsWith(t, kinds), source, check: t.check, tpl: t.tpl, status, evidence, where });
|
|
569
|
+
|
|
570
|
+
// Point chips the user can tap, as the rule a repeated tap means.
|
|
571
|
+
const CHIP_RULES = {
|
|
572
|
+
"Make it premium": "premium",
|
|
573
|
+
"Add motion": "motion",
|
|
574
|
+
"Fix spacing": { rule: "Spacing on one scale, the same between like parts.", kinds: ["all"] },
|
|
575
|
+
"Fix alignment": { rule: "Shared edges: every block aligns to the layout's grid.", kinds: ["all"] },
|
|
576
|
+
"Type hierarchy": { rule: "One clear type hierarchy: one focal size per screen and few sizes in all.", kinds: ["all"] },
|
|
577
|
+
"Real content & avatars": { rule: "Real content and real faces (or generated avatars); never placeholders or initials.", kinds: ["all"] },
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
// Evidence in any words → rules: the general form of every template it matches, else the words themselves.
|
|
581
|
+
// status "proposed" (with evidence) when palate inferred it; the user's own words are active.
|
|
582
|
+
export function ruleFrom(text, { kinds, source, status, evidence, where } = {}) {
|
|
583
|
+
const ts = matchTemplates(text);
|
|
584
|
+
if (ts.length) return ts.map((t) => fromTemplate(t, { kinds, source, status, evidence, where }));
|
|
585
|
+
return [addRule({ rule: text, kinds, source, status, evidence, where })];
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// A note on one of the user's references (`palate refs add <src> --like "…" --dislike "…"`) → rules tied to it
|
|
589
|
+
// (source "ref:<id>"). A like is a judge rule naming that reference; a dislike takes the general form of every
|
|
590
|
+
// template it matches (measured when palate can measure it), else becomes a judge rule naming the reference.
|
|
591
|
+
export function ruleFromRefNote(ref, kind, text, { kinds = ["all"] } = {}) {
|
|
592
|
+
const note = String(text ?? "").trim().replace(/[.\s]+$/, "");
|
|
593
|
+
if (!note) return [];
|
|
594
|
+
const where = ref.kind === "url" ? ref.source.replace(/^https?:\/\/(www\.)?/, "").replace(/\/$/, "") : path.basename(ref.source);
|
|
595
|
+
const source = `ref:${ref.id}`;
|
|
596
|
+
if (kind === "like") return [addRule({ rule: `Like your reference ${where}: ${note}.`, kinds, source, check: { type: "judge", question: `Does it have what the user likes about their reference ${where}: "${note}"?` }, tpl: `ref-like:${ref.id}:${note.toLowerCase()}` })];
|
|
597
|
+
const ts = matchTemplates(note);
|
|
598
|
+
if (ts.length) return ts.map((t) => fromTemplate(t, { kinds: kinds.includes("all") ? undefined : kinds, source }));
|
|
599
|
+
return [addRule({ rule: `Avoid ${note} (you disliked it in ${where}).`, kinds, source, check: { type: "judge", question: `Is it free of what the user dislikes in their reference ${where}: "${note}"? (present fails)` }, tpl: `ref-dislike:${ref.id}:${note.toLowerCase()}` })];
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Forgets a source (a removed reference): rules that came only from it go; rules it only strengthened lose it.
|
|
603
|
+
export function dropSource(source) {
|
|
604
|
+
const rules = loadRules();
|
|
605
|
+
const kept = [];
|
|
606
|
+
const removed = [];
|
|
607
|
+
for (const r of rules) {
|
|
608
|
+
if (r.source === source) {
|
|
609
|
+
if (!r.also?.length) {
|
|
610
|
+
removed.push(r);
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
r.source = r.also.shift();
|
|
614
|
+
if (!r.also.length) delete r.also;
|
|
615
|
+
} else if (r.also?.includes(source)) {
|
|
616
|
+
r.also = r.also.filter((s) => s !== source);
|
|
617
|
+
if (!r.also.length) delete r.also;
|
|
618
|
+
}
|
|
619
|
+
kept.push(r);
|
|
620
|
+
}
|
|
621
|
+
saveRules(kept);
|
|
622
|
+
return removed;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// ---- checking
|
|
626
|
+
const OPS = { "<=": (a, b) => a <= b, ">=": (a, b) => a >= b, "=": (a, b) => a === b, "<": (a, b) => a < b, ">": (a, b) => a > b };
|
|
627
|
+
const domChecks = (rules) => rules.filter((r) => r.check?.type === "dom").map((r) => ({ id: r.id, selector: r.check.selector, visible: Boolean(r.check.visible), before: r.check.before }));
|
|
628
|
+
|
|
629
|
+
// Measures the facts the rules read, in an open page (scrolled through first, so lazy parts are there).
|
|
630
|
+
export async function measureRules(page, rules = activeRules()) {
|
|
631
|
+
const sections = await page.evaluate(`(${script("page-sections.js")})()`);
|
|
632
|
+
const devices = await page.evaluate(`(${script("page-devices.js")})(${JSON.stringify(sections)})`);
|
|
633
|
+
const facts = await page.evaluate(`(${script("page-rules.js")})(${JSON.stringify(sections)}, ${JSON.stringify(devices)}, ${JSON.stringify(domChecks(rules))})`);
|
|
634
|
+
return { facts, sections };
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Each rule against the facts: pass, fail, n/a (nothing to measure, e.g. no logos) or judge (not measurable).
|
|
638
|
+
export function checkRules(rules, facts, { motion = true } = {}) {
|
|
639
|
+
return rules.map((r) => {
|
|
640
|
+
const c = r.check ?? { type: "judge" };
|
|
641
|
+
if (c.type === "judge") return { id: r.id, rule: r.rule, status: "judge", question: c.question };
|
|
642
|
+
if (!facts) return { id: r.id, rule: r.rule, status: "na", why: "not measured" };
|
|
643
|
+
if (c.type === "dom") {
|
|
644
|
+
const n = facts.dom?.[r.id];
|
|
645
|
+
if (n == null) return { id: r.id, rule: r.rule, status: "na", why: "not measured in this render" };
|
|
646
|
+
const ok = (c.min == null || n >= c.min) && (c.max == null || n <= c.max);
|
|
647
|
+
return { id: r.id, rule: r.rule, status: ok ? "pass" : "fail", value: n, want: [c.min != null && `≥ ${c.min}`, c.max != null && `≤ ${c.max}`].filter(Boolean).join(" and "), label: `elements matching ${c.selector}` };
|
|
648
|
+
}
|
|
649
|
+
if (NEEDS_MOTION.has(c.metric) && !motion) return { id: r.id, rule: r.rule, status: "na", why: "rendered with reduced motion" };
|
|
650
|
+
const v = facts[c.metric];
|
|
651
|
+
if (v == null && STATE_METRICS.has(c.metric)) return { id: r.id, rule: r.rule, status: "na", why: facts.statesRun ? `can't happen on this screen (palate states ${facts.statesRun})` : "measured by palate states; run it on this route" };
|
|
652
|
+
if (v == null) return { id: r.id, rule: r.rule, status: "na", why: `no ${METRICS[c.metric] ?? c.metric} on the page` };
|
|
653
|
+
const ok = (OPS[c.op] ?? OPS["<="])(v, c.value);
|
|
654
|
+
return { id: r.id, rule: r.rule, status: ok ? "pass" : "fail", value: v, want: `${c.op} ${c.value}`, label: METRICS[c.metric] ?? c.metric, detail: STATE_METRICS.has(c.metric) ? `palate states ${facts.statesRun}` : detailOf(c.metric, facts) };
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
const detailOf = (m, f) =>
|
|
658
|
+
({
|
|
659
|
+
repeatedDevices: f.repeatedList?.join("; "),
|
|
660
|
+
blackAccents: f.blackAccentTexts?.map((t) => `"${t}"`).join(", "),
|
|
661
|
+
demoMissingControls: f.demoMissingList?.length ? `missing: ${f.demoMissingList.join(", ")}` : null,
|
|
662
|
+
minLogoMarkPx: f.logoRowList?.join("; "),
|
|
663
|
+
monochromeLogoRows: f.logoRowList?.filter((l) => /one colour/.test(l)).join("; "),
|
|
664
|
+
stepBarsBeforeDemo: f.stepBarTexts?.map((t) => `"${t}"`).join(", "),
|
|
665
|
+
distinctRadii: f.radiiList?.length ? `${f.radiiList.join(", ")}px` : null,
|
|
666
|
+
frameworkLogosBeforeDemo: `${(f.frameworksBeforeDemo ?? []).join(", ")}${f.demo ? ` before the demo (section ${f.demo.section}, "${f.demo.name}")` : " on the first screen (no demo found)"}`,
|
|
667
|
+
})[m] ?? null;
|
|
668
|
+
|
|
669
|
+
const short = (s, n = 90) => (s.length > n ? `${s.slice(0, n - 1).replace(/\s+\S*$/, "")}…` : s);
|
|
670
|
+
const measuredAs = (r) => (r.status === "fail" || r.status === "pass" ? `${r.label}: ${r.value}, must be ${r.want}${r.status === "fail" && r.detail ? `; ${r.detail}` : ""}` : "");
|
|
671
|
+
|
|
672
|
+
// "Rules: 7 pass, 1 fail: R3 …" and one line per measured rule.
|
|
673
|
+
export function rulesSummary(results) {
|
|
674
|
+
const pass = results.filter((r) => r.status === "pass");
|
|
675
|
+
const fail = results.filter((r) => r.status === "fail");
|
|
676
|
+
const judged = results.filter((r) => r.status === "judge").length;
|
|
677
|
+
const head = `Rules: ${pass.length} pass, ${fail.length} fail${fail.length ? `: ${fail.map((r) => `${r.id} (${short(r.rule, 60)})`).join("; ")}` : ""}${judged ? ` · ${judged} more for the judge` : ""}`;
|
|
678
|
+
return { head, pass, fail, judged };
|
|
679
|
+
}
|
|
680
|
+
export function rulesReport(results) {
|
|
681
|
+
if (!results.length) return [];
|
|
682
|
+
const { head } = rulesSummary(results);
|
|
683
|
+
return [
|
|
684
|
+
`## Your rules`,
|
|
685
|
+
`- ${head}`,
|
|
686
|
+
...results.filter((r) => r.status === "fail").map((r) => `- ⚠ Rule ${r.id} fails: ${r.rule} (${measuredAs(r)})`),
|
|
687
|
+
...results.filter((r) => r.status === "pass").map((r) => `- ✓ ${r.id} ${short(r.rule, 70)} (${measuredAs(r)})`),
|
|
688
|
+
...results.filter((r) => r.status === "na").map((r) => `- · ${r.id} ${short(r.rule, 70)} (n/a: ${r.why})`),
|
|
689
|
+
...(results.some((r) => r.status === "judge") ? [`- Not measurable, judge them yourself on the screenshot: ${results.filter((r) => r.status === "judge").map((r) => (/^Only for </.test(r.question ?? "") ? `${r.id} (${r.question.match(/^Only for (<[^>]+>)/)[1]} only)` : r.id)).join(", ")}`] : []),
|
|
690
|
+
];
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// ---- failures: what the user called bad, with a screenshot, in ~/.palate/failures
|
|
694
|
+
export function failuresFor(tags, n = 4) {
|
|
695
|
+
return readLines(FAILS_LOG).filter((f) => !f.kinds?.length || f.kinds.includes("all") || f.kinds.some((k) => tags.includes(k))).slice(-n).reverse();
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
export function recordFailure({ project, route, why, kinds, shots = [], at } = {}) {
|
|
699
|
+
fs.mkdirSync(FAIL_DIR, { recursive: true });
|
|
700
|
+
const id = `${(at ?? new Date().toISOString()).slice(0, 10)}-${project}-${(route ?? "/").replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/g, "") || "home"}-${Math.random().toString(36).slice(2, 6)}`;
|
|
701
|
+
const kept = shots.filter((f) => f && fs.existsSync(f)).map((f, i) => {
|
|
702
|
+
const dest = path.join(FAIL_DIR, `${id}${shots.length > 1 ? `-${i + 1}` : ""}${path.extname(f) || ".png"}`);
|
|
703
|
+
fs.copyFileSync(f, dest);
|
|
704
|
+
return slash(dest);
|
|
705
|
+
});
|
|
706
|
+
const rec = { id, at: at ?? new Date().toISOString(), project, route, why, kinds, shots: kept };
|
|
707
|
+
fs.appendFileSync(FAILS_LOG, JSON.stringify(rec) + "\n");
|
|
708
|
+
return { failure: rec, learned: dedupe(learnFromFailure(rec)) };
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Each sentence of the why that matches a template proposes (or strengthens) a rule; else the whole why does.
|
|
712
|
+
// Proposed, not active: the user said this screen is bad, not where else it holds; they choose the scope.
|
|
713
|
+
export const failEvidence = (f, said) => ({ key: `failure:${f.id}`, type: "failure", said: said ?? f.why, project: f.project, route: f.route, component: null, kinds: f.kinds ?? [], crop: f.shots?.[0] ?? null, at: f.at });
|
|
714
|
+
function learnFromFailure(f, { onlyTemplates = false } = {}) {
|
|
715
|
+
const learned = [];
|
|
716
|
+
const opts = (said) => ({ kinds: f.kinds, source: `failure:${f.id}`, status: "proposed", evidence: [failEvidence(f, said)] });
|
|
717
|
+
const parts = f.why.split(/(?<=[.;!?])\s+|\n+/).map((s) => s.trim()).filter((s) => s.length > 6);
|
|
718
|
+
for (const p of parts) if (matchTemplate(p)) learned.push(...ruleFrom(p, opts(p)));
|
|
719
|
+
if (!learned.length && !onlyTemplates) learned.push(...ruleFrom(f.why, opts()));
|
|
720
|
+
return learned;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// One entry per rule; "added" if any of its matches added it. A discarded proposal that came up again is kept once.
|
|
724
|
+
export function dedupe(list) {
|
|
725
|
+
const by = new Map();
|
|
726
|
+
for (const l of list) {
|
|
727
|
+
if (!l.rule) by.set(`no:${l.discarded?.id}`, l);
|
|
728
|
+
else by.set(l.rule.id, { ...l, added: Boolean(by.get(l.rule.id)?.added || l.added) });
|
|
729
|
+
}
|
|
730
|
+
return [...by.values()];
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// ---- the brief block
|
|
734
|
+
// Picks as clicks: one grid click writes a record per loser, all within the same second.
|
|
735
|
+
export function pickCount() {
|
|
736
|
+
return new Set(readLines(path.join(TASTE, "picks.jsonl")).map((p) => `${p.project}/${p.loop}/${String(p.at).slice(0, 19)}`)).size;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// The line that opens every brief, and the "Your rules (must pass)" block for this kind of screen with the
|
|
740
|
+
// failures for it ("don't do this"). results: this page's current rule results, to mark what fails now.
|
|
741
|
+
// route: the route the brief is for (rules kept for one route apply only there); project: the project folder's
|
|
742
|
+
// name (rules kept for one project apply only there; unknown: not applied). Proposals never appear here.
|
|
743
|
+
export function rulesBlock(tags, { results, failures = true, route, project } = {}) {
|
|
744
|
+
const rules = rulesFor(tags, { route, project });
|
|
745
|
+
const picks = pickCount();
|
|
746
|
+
// Which rules hold as general taste, which only here, and which another project kept for itself (skipped).
|
|
747
|
+
const general = rules.filter((r) => !r.where?.project).length;
|
|
748
|
+
const own = rules.length - general;
|
|
749
|
+
const skipped = {};
|
|
750
|
+
for (const r of activeRules()) if (r.where?.project && !appliesTo(r, tags, { route, project }) && appliesTo({ ...r, where: { ...r.where, project: undefined } }, tags, { route })) skipped[r.where.project] = (skipped[r.where.project] ?? 0) + 1;
|
|
751
|
+
const others = Object.entries(skipped).map(([p, n]) => `${n} only for ${p} skipped`);
|
|
752
|
+
const split = own || others.length ? `${general} general rule${general === 1 ? "" : "s"}${own ? `, ${own} only for ${project}` : ""}${others.length ? `; ${others.join("; ")}` : ""}` : "";
|
|
753
|
+
const line = `Built from ${rules.length} of your rules and ${picks} past picks${split ? ` (${split})` : ""}.`;
|
|
754
|
+
if (!rules.length) return { line, lines: [], rules, split };
|
|
755
|
+
const now = Object.fromEntries((results ?? []).map((r) => [r.id, r]));
|
|
756
|
+
const how = (r) => (r.check?.type === "metric" ? `measured: ${METRICS[r.check.metric] ?? r.check.metric} ${r.check.op} ${r.check.value}` : r.check?.type === "dom" ? `measured: ${r.check.selector}${r.check.min != null ? ` ≥ ${r.check.min}` : ""}${r.check.max != null ? ` ≤ ${r.check.max}` : ""}` : "the judge asks");
|
|
757
|
+
const where = (r) => (r.where?.component ? `Only for <${r.where.component}>: ` : r.where?.route ? `Only on ${r.where.route}: ` : "") + (r.where?.project ? `Only in ${r.where.project}: ` : "");
|
|
758
|
+
const fails = failures ? failuresFor(tags) : [];
|
|
759
|
+
const lines = [
|
|
760
|
+
"## Your rules (must pass)",
|
|
761
|
+
`What this user has said no to, as rules for ${tags.filter((t) => t !== "all").join(", ") || "every screen"}. They beat references, directions and your own ideas. palate shoot measures the [measured] ones on every shot ("Rules: n pass, n fail"), and the judge ranks down any variant that fails one.`,
|
|
762
|
+
...(split ? [`Here: ${split}. General rules are this user's taste; rules another project kept for its own look don't apply.`] : []),
|
|
763
|
+
...rules.map((r) => `- ${now[r.id]?.status === "fail" ? "✗ FAILS NOW: " : ""}**${r.id}** ${where(r)}${r.rule} [${how(r)}]`),
|
|
764
|
+
"",
|
|
765
|
+
...(fails.length
|
|
766
|
+
? [
|
|
767
|
+
"### Don't do this (what this user called bad before, for this kind of screen)",
|
|
768
|
+
...fails.map((f) => `- ${f.at.slice(0, 10)} ${f.project} ${f.route}: ${short(f.why.replace(/\s+/g, " "), 480)}${f.shots?.length ? ` Screenshot${f.shots.length > 1 ? "s" : ""}: ${f.shots.join(", ")}` : ""}`),
|
|
769
|
+
"",
|
|
770
|
+
]
|
|
771
|
+
: []),
|
|
772
|
+
];
|
|
773
|
+
return { line, lines, rules, split };
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// ---- learning: new evidence → proposed rules, de-duplicated. One pin is a one-off fix; a pattern (the same ask
|
|
777
|
+
// twice, an agreed critique finding, a failure) becomes a proposal with the scopes its evidence supports, and
|
|
778
|
+
// applies nowhere until the user keeps it.
|
|
779
|
+
export function learnRules({ all = false } = {}) {
|
|
780
|
+
const state = readJson(STATE, {});
|
|
781
|
+
const since = all ? "" : state.rulesLearnedAt ?? "";
|
|
782
|
+
const out = [];
|
|
783
|
+
const take = (res, evidence) => out.push({ ...res, evidence });
|
|
784
|
+
const proposed = (evidence) => ({ status: "proposed", evidence });
|
|
785
|
+
// Critique findings the user agreed with, in general form.
|
|
786
|
+
for (const c of readLines(path.join(TASTE, "critiques.jsonl")).filter((c) => c.at > since && c.verdict === "agree")) {
|
|
787
|
+
// The finding's own words first (its fix can name other things); the fix only when they match nothing.
|
|
788
|
+
const text = `${c.what} ${c.note ?? ""}`;
|
|
789
|
+
const ts = matchTemplates(text).length ? matchTemplates(text) : matchTemplates(c.fix ?? "").slice(0, 1);
|
|
790
|
+
const kinds = kindTags({ route: c.route, words: c.kind });
|
|
791
|
+
const source = `critique:${c.project}${c.route}#${c.id}`;
|
|
792
|
+
const ev = [{ key: `critique:${c.project}${c.route}#${c.id}@${c.at}`, type: "critique", said: c.note || c.what, project: c.project, route: c.route, component: null, kinds, crop: null, at: c.at }];
|
|
793
|
+
if (ts.length) for (const t of ts) take(fromTemplate(t, { source, ...proposed(ev) }), `agreed critique ${c.id}: ${short(c.what, 80)}`);
|
|
794
|
+
else if (c.note) take(addRule({ rule: c.note, kinds, source, ...proposed(ev) }), `agreed critique ${c.id}, their note`);
|
|
795
|
+
else if (c.severity !== "low") {
|
|
796
|
+
const res = addRule({ rule: `Avoid: ${short(c.what, 160)}`, kinds, source, ...proposed(ev) });
|
|
797
|
+
if (res.added && res.rule) {
|
|
798
|
+
editFlag(res.rule.id);
|
|
799
|
+
res.rule.inTheirWords = true;
|
|
800
|
+
}
|
|
801
|
+
take(res, `agreed critique ${c.id} (in the critique's words: say it generally when you keep it)`);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
// Failures recorded since (palate fail already proposed from them; this keeps them in sync if added by hand).
|
|
805
|
+
for (const f of readLines(FAILS_LOG).filter((f) => f.at > since)) for (const res of learnFromFailure(f, { onlyTemplates: true })) take(res, `failure ${f.id}`);
|
|
806
|
+
// Point requests: the same chip or the same kind of complaint twice is a proposal; once is a one-off fix.
|
|
807
|
+
const points = [...new Map(readLines(path.join(TASTE, "point.jsonl")).filter((x) => x.at > since && x.kind !== "point-cancel").map((x) => [x.id, x])).values()];
|
|
808
|
+
const chips = {};
|
|
809
|
+
for (const p of points) for (const c of p.chips ?? []) (chips[c] ??= []).push(p);
|
|
810
|
+
for (const [c, list] of Object.entries(chips)) {
|
|
811
|
+
const m = CHIP_RULES[c];
|
|
812
|
+
if (list.length < 2 || !m) continue;
|
|
813
|
+
const t = typeof m === "string" ? TEMPLATES.find((x) => x.tpl === m) : null;
|
|
814
|
+
const ev = proposed(list.map((p) => pinEvidence(p, p.prompt || c)));
|
|
815
|
+
take(t ? addRule({ rule: t.rule, kinds: t.kinds, source: `point:${c} ×${list.length}`, check: t.check, tpl: t.tpl, ...ev }) : addRule({ ...m, source: `point:${c} ×${list.length}`, ...ev }), `"${c}" asked ${list.length} times`);
|
|
816
|
+
}
|
|
817
|
+
const byTpl = {};
|
|
818
|
+
for (const p of points.filter((p) => p.prompt)) {
|
|
819
|
+
const t = matchTemplate(p.prompt);
|
|
820
|
+
if (t) (byTpl[t.tpl] ??= []).push(p);
|
|
821
|
+
}
|
|
822
|
+
for (const [tpl, list] of Object.entries(byTpl)) {
|
|
823
|
+
if (list.length < 2) continue;
|
|
824
|
+
const t = TEMPLATES.find((x) => x.tpl === tpl);
|
|
825
|
+
take(addRule({ rule: t.rule, kinds: t.kinds, source: `point:"${short(list[0].prompt, 40)}" ×${list.length}`, check: t.check, tpl, ...proposed(list.map((p) => pinEvidence(p))) }), `asked ${list.length} times in their words`);
|
|
826
|
+
}
|
|
827
|
+
// palate ambition and the similarity gate: an upgrade move the user kept or undid twice, and another
|
|
828
|
+
// project's look the gate keeps catching. Proposals only, like everything above.
|
|
829
|
+
for (const g of growthProposals(since)) {
|
|
830
|
+
// "feature grids section" → features (marketing only when nothing narrower fits).
|
|
831
|
+
const tags = g.section ? kindTags({ section: g.section }) : ["all"];
|
|
832
|
+
const kinds = tags.length > 1 ? tags.filter((k) => k !== "marketing") : tags;
|
|
833
|
+
take(addRule({ rule: g.rule, kinds: kinds.length ? kinds : ["all"], source: g.source, ...proposed(g.evidence) }), g.evidence.length > 1 ? `${g.evidence.length} times: ${g.evidence[0].said}` : g.evidence[0]?.said ?? g.source);
|
|
834
|
+
}
|
|
835
|
+
// palate research: the same trait liked or disliked in two research references (research-learn.mjs).
|
|
836
|
+
for (const p of researchProposals(since)) take(addRule({ rule: p.rule, kinds: p.kinds, source: p.source, check: p.check, tpl: p.tpl, ...proposed(p.evidence) }), `${p.evidence.length} research references: ${p.evidence.map((e) => e.said).join("; ")}`);
|
|
837
|
+
fs.mkdirSync(TASTE, { recursive: true });
|
|
838
|
+
if (!all) fs.writeFileSync(STATE, JSON.stringify({ ...readJson(STATE, {}), rulesLearnedAt: new Date().toISOString() }));
|
|
839
|
+
return out;
|
|
840
|
+
}
|
|
841
|
+
function editFlag(id) {
|
|
842
|
+
const rules = loadRules();
|
|
843
|
+
const r = rules.find((x) => x.id === id);
|
|
844
|
+
if (r) r.inTheirWords = true;
|
|
845
|
+
saveRules(rules);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// How to answer proposals, for the agent and the CLI.
|
|
849
|
+
export const reviewHint = (ids = []) =>
|
|
850
|
+
`Nothing carries into later builds until the user keeps it. Show them each rule with its scope: palate rule review opens a board in their browser (MCP: palate_rule with action "review_start"). Or in chat: palate rule ok ${ids[0] ?? "<id>"} [--only <Component> | --route /x | --kind team | --scope all] [--text "…"], or palate rule no ${ids[0] ?? "<id>"}.`;
|
|
851
|
+
|
|
852
|
+
// A proposal in a few lines: the rule, its suggested scope, how it's checked, and what it came from.
|
|
853
|
+
export function proposalLine(r) {
|
|
854
|
+
const { options, suggest, why } = scopeOptions(r);
|
|
855
|
+
return `- ${r.id} ${r.rule}\n suggested scope: ${options.find((o) => o.id === suggest)?.label ?? "Everywhere"}, because ${why} (or: ${options.filter((o) => o.id !== suggest).map((o) => o.label).join(", ")}) · ${checkWords(r)}\n ${cap(evidenceText(r))} (${evidenceCount(r)})`;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
export function learnedLines(list) {
|
|
859
|
+
const first = new Map();
|
|
860
|
+
for (const l of list.filter((l) => l.rule)) if (!first.has(l.rule.id) || (l.added && !first.get(l.rule.id).added)) first.set(l.rule.id, l);
|
|
861
|
+
// The rule as it is now (a later match may have added evidence to it).
|
|
862
|
+
const now = new Map(loadRules().map((r) => [r.id, r]));
|
|
863
|
+
const entries = [...first.values()].map((l) => ({ ...l, rule: now.get(l.rule.id) ?? l.rule }));
|
|
864
|
+
const proposals = entries.filter((l) => isProposed(l.rule));
|
|
865
|
+
const known = entries.filter((l) => !isProposed(l.rule) && !l.added);
|
|
866
|
+
const mine = entries.filter((l) => !isProposed(l.rule) && l.added);
|
|
867
|
+
const still = [...new Map(list.filter((l) => !l.rule && l.discarded).map((l) => [l.discarded.id, l])).values()];
|
|
868
|
+
if (!entries.length && !still.length) return ["No new rules: one pin is a one-off fix. A rule needs a pattern: the same ask twice, a critique finding the user agreed with, or a failure."];
|
|
869
|
+
return [
|
|
870
|
+
proposals.length ? `Proposed rules, waiting for your OK (${proposals.length}):` : "No new proposals.",
|
|
871
|
+
...proposals.map((l) => `${proposalLine(l.rule)}${l.rule.inTheirWords ? "\n (in the critique's words: say it generally when you keep it)" : ""}`),
|
|
872
|
+
...(mine.length ? [`New rules in your words: ${mine.map((l) => `${l.rule.id} ${l.rule.rule}`).join("; ")}`] : []),
|
|
873
|
+
...(known.length ? [`Strengthened (already one of your rules): ${[...new Set(known.map((l) => l.rule.id))].join(", ")}`] : []),
|
|
874
|
+
...(still.length ? [`Still discarded (you said no before): ${still.map((l) => `"${short(l.discarded.rule, 60)}" (back only after ${l.needs} more new ask${l.needs > 1 ? "s" : ""})`).join(", ")}`] : []),
|
|
875
|
+
...(proposals.length ? [reviewHint(proposals.map((l) => l.rule.id))] : []),
|
|
876
|
+
];
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// ---- how palate is doing: rules, picks, and how often its top-ranked variant was the user's pick
|
|
880
|
+
export function learnedSummary(dir) {
|
|
881
|
+
const rules = loadRules().filter(isActive);
|
|
882
|
+
const byKind = {};
|
|
883
|
+
for (const r of rules) for (const k of r.kinds) byKind[k] = (byKind[k] ?? 0) + 1;
|
|
884
|
+
const measured = rules.filter((r) => r.check && r.check.type !== "judge").length;
|
|
885
|
+
const picks = readLines(path.join(TASTE, "picks.jsonl"));
|
|
886
|
+
const loops = [...new Map(picks.map((p) => [`${p.project}/${p.loop}`, p])).values()];
|
|
887
|
+
// For each loop, find its session: the record's dir, this project, or a sibling folder named like the project.
|
|
888
|
+
const sessionOf = (p) => {
|
|
889
|
+
const cands = [p.dir, dir && path.basename(path.resolve(dir)) === p.project ? dir : null, dir ? path.join(path.dirname(path.resolve(dir)), p.project) : null].filter(Boolean);
|
|
890
|
+
for (const d of cands) {
|
|
891
|
+
const s = path.join(d, ".palate", "variants", p.loop);
|
|
892
|
+
if (fs.existsSync(path.join(s, "result.json"))) return s;
|
|
893
|
+
}
|
|
894
|
+
return null;
|
|
895
|
+
};
|
|
896
|
+
let judged = 0;
|
|
897
|
+
let hits = 0;
|
|
898
|
+
let led = 0;
|
|
899
|
+
let ledHits = 0;
|
|
900
|
+
const misses = [];
|
|
901
|
+
for (const p of loops) {
|
|
902
|
+
const s = sessionOf(p);
|
|
903
|
+
if (!s) continue;
|
|
904
|
+
const result = readJson(path.join(s, "result.json"), null);
|
|
905
|
+
const winner = result?.ranking?.[0]?.wins ? result.ranking[0].route : null;
|
|
906
|
+
if (!winner) continue;
|
|
907
|
+
const judge = readJson(path.join(s, "judge.json"), null);
|
|
908
|
+
if (judge?.keep?.length) {
|
|
909
|
+
judged++;
|
|
910
|
+
if (judge.keep[0] === winner) hits++;
|
|
911
|
+
else misses.push(`${p.project}/${p.loop}: judge ranked ${judge.keep[0]} first, the user picked ${winner}`);
|
|
912
|
+
}
|
|
913
|
+
// Direction a is the one palate leads with (what has won for this user, or their reference).
|
|
914
|
+
const meta = readJson(path.join(s, "loop.json"), {});
|
|
915
|
+
if (!meta.vary && meta.directions?.length > 1) {
|
|
916
|
+
led++;
|
|
917
|
+
if (/-a\d+$/.test(winner)) ledHits++;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
return { rules, proposed: proposedRules(), byKind, measured, picks: pickCount(), loops: loops.length, judged, hits, misses, led, ledHits, failures: readLines(FAILS_LOG).length };
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
export function learnedText(dir) {
|
|
924
|
+
const s = learnedSummary(dir);
|
|
925
|
+
const pct = (a, b) => (b ? `${Math.round((a / b) * 100)}%` : "n/a");
|
|
926
|
+
const newest = [...s.rules].sort((a, b) => (a.added < b.added ? 1 : -1)).slice(0, 5);
|
|
927
|
+
return [
|
|
928
|
+
"# What palate has learned about you",
|
|
929
|
+
"",
|
|
930
|
+
`- ${s.rules.length} rules (${s.measured} measured on every shoot, ${s.rules.length - s.measured} asked by the judge), from ${s.picks} picks in ${s.loops} loops and ${s.failures} failures. File: ${slash(RULES_FILE)}`,
|
|
931
|
+
`- Rules by kind of screen: ${Object.entries(s.byKind).sort((a, b) => b[1] - a[1]).map(([k, n]) => `${k} ${n}`).join(" · ") || "none yet"}`,
|
|
932
|
+
`- First-choice hit rate: palate's top-ranked variant was your pick in ${s.hits} of ${s.judged} judged loops (${pct(s.hits, s.judged)})${s.led ? `; its lead direction (a) won ${s.ledHits} of ${s.led} loops (${pct(s.ledHits, s.led)})` : ""}.`,
|
|
933
|
+
...s.misses.slice(-3).map((m) => ` - missed: ${m}`),
|
|
934
|
+
newest.length ? "- Newest rules:" : "- No rules yet. Yours come from `palate rule add \"…\"` and notes on your references; palate proposes more from critique findings you agree with, `palate fail` and repeated asks, for your OK.",
|
|
935
|
+
...newest.map((r) => ` - ${r.id} [${scopeLabel(r)}] ${r.rule}`),
|
|
936
|
+
...(s.proposed.length ? [`- ${s.proposed.length} proposed rule${s.proposed.length > 1 ? "s" : ""} waiting for your OK (not applied yet): ${s.proposed.map((r) => r.id).join(", ")}. palate rule review`] : []),
|
|
937
|
+
"",
|
|
938
|
+
].join("\n");
|
|
939
|
+
}
|