From 120006efcf24e20ad5747ce4776417347b683db5 Mon Sep 17 00:00:00 2001 From: Kailas Mahavarkar <66670953+KailasMahavarkar@users.noreply.github.com> Date: Wed, 8 Apr 2026 20:19:48 +0530 Subject: [PATCH 01/65] feat: add 7 new plugins with snippet-based architecture (#1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add 7 new plugins with snippet-based architecture Adds lenis, react, echo, golang, rust, design-tokens, and ui-ux plugins to unified-mcp. All code examples are stored as .md files in snippets/ and loaded at runtime via createSnippetLoader โ€” no inline template literals in any data.ts file. - 370 snippet files across 7 new plugins - Each plugin follows the established reactflow/motion pattern - golang merges best-practices + design-patterns from two skill sources - design-tokens covers full Tailwind v4 OKLCH token system with procedures - ui-ux covers typography, color, spacing, elevation, motion, a11y principles - All plugins registered in src/index.ts Co-Authored-By: Claude Sonnet 4.6 * docs: update README with all 9 plugins and snippet architecture Adds all 7 new plugins (lenis, react, echo, golang, rust, design-tokens, ui-ux) to the plugins table, tools section, and architecture diagram. Each plugin section is collapsible. Architecture section now documents the snippet-based .md file pattern. Co-Authored-By: Claude Sonnet 4.6 * docs: improve README badges - split into rows, proper logos per library Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Claude Sonnet 4.6 --- README.md | 262 ++++++-- .../design-tokens/categories/border-radius.md | 26 + snippets/design-tokens/categories/colors.md | 51 ++ .../categories/component-sizing.md | 76 +++ snippets/design-tokens/categories/density.md | 38 ++ snippets/design-tokens/categories/motion.md | 46 ++ snippets/design-tokens/categories/opacity.md | 29 + .../categories/shadows-elevation.md | 45 ++ snippets/design-tokens/categories/spacing.md | 24 + .../design-tokens/categories/typography.md | 80 +++ snippets/design-tokens/categories/z-index.md | 24 + .../design-tokens/procedures/step-1-colors.md | 30 + .../procedures/step-2-spacing.md | 52 ++ .../procedures/step-3-typography.md | 26 + .../procedures/step-4-component-sizing.md | 21 + .../procedures/step-5-remaining.md | 22 + .../procedures/step-6-accessibility.md | 23 + .../procedures/step-7-validation.md | 23 + .../procedures/step-8-deliverables.md | 15 + .../design-tokens/references/color-roles.md | 58 ++ .../references/token-checklist.md | 58 ++ .../templates/colors-tailwind-v4.md | 86 +++ snippets/design-tokens/templates/motion.md | 45 ++ snippets/design-tokens/templates/spacing.md | 34 + .../design-tokens/templates/typography.md | 80 +++ snippets/echo/cheatsheet.md | 130 ++++ snippets/echo/middleware/basic-auth.md | 8 + snippets/echo/middleware/body-limit.md | 1 + snippets/echo/middleware/cors.md | 4 + snippets/echo/middleware/csrf.md | 3 + snippets/echo/middleware/gzip.md | 3 + snippets/echo/middleware/jwt.md | 5 + snippets/echo/middleware/key-auth.md | 6 + snippets/echo/middleware/logger.md | 3 + snippets/echo/middleware/rate-limiter.md | 22 + snippets/echo/middleware/recover.md | 1 + snippets/echo/middleware/request-id.md | 3 + snippets/echo/middleware/secure.md | 6 + snippets/echo/middleware/timeout.md | 3 + snippets/echo/recipes/auto-tls.md | 37 ++ snippets/echo/recipes/cors.md | 32 + snippets/echo/recipes/crud-api.md | 93 +++ snippets/echo/recipes/embed-resources.md | 34 + snippets/echo/recipes/file-download.md | 49 ++ snippets/echo/recipes/file-upload.md | 66 ++ snippets/echo/recipes/graceful-shutdown.md | 44 ++ snippets/echo/recipes/hello-world.md | 17 + snippets/echo/recipes/http2.md | 39 ++ snippets/echo/recipes/jsonp.md | 32 + snippets/echo/recipes/jwt-auth.md | 77 +++ snippets/echo/recipes/middleware-chain.md | 60 ++ snippets/echo/recipes/reverse-proxy.md | 41 ++ snippets/echo/recipes/route-groups.md | 48 ++ snippets/echo/recipes/sse.md | 43 ++ snippets/echo/recipes/streaming-response.md | 35 ++ snippets/echo/recipes/subdomain-routing.md | 39 ++ snippets/echo/recipes/timeout.md | 34 + snippets/echo/recipes/websocket.md | 70 +++ snippets/golang/cheatsheet.md | 120 ++++ snippets/golang/patterns/adapter.md | 14 + snippets/golang/patterns/command.md | 42 ++ .../patterns/consumer-side-interface-anti.md | 4 + .../patterns/consumer-side-interface.md | 15 + snippets/golang/patterns/fan-out-fan-in.md | 42 ++ .../golang/patterns/functional-options.md | 17 + .../golang/patterns/middleware-decorator.md | 20 + snippets/golang/patterns/observer.md | 45 ++ snippets/golang/patterns/pipeline.md | 20 + snippets/golang/patterns/strategy.md | 13 + snippets/golang/patterns/worker-pool.md | 14 + .../golang/practices/config-env-vars-bad.md | 10 + .../golang/practices/config-env-vars-good.md | 27 + .../practices/constructor-pattern-good.md | 5 + .../practices/context-first-param-bad.md | 1 + .../practices/context-first-param-good.md | 1 + snippets/golang/practices/crypto-rand-bad.md | 2 + snippets/golang/practices/crypto-rand-good.md | 3 + .../practices/database-repository-bad.md | 11 + .../practices/database-repository-good.md | 42 ++ snippets/golang/practices/errgroup-bad.md | 5 + snippets/golang/practices/errgroup-good.md | 4 + .../golang/practices/error-wrapping-bad.md | 3 + .../golang/practices/error-wrapping-good.md | 3 + snippets/golang/practices/errors-is-as-bad.md | 2 + .../golang/practices/errors-is-as-good.md | 3 + .../golang/practices/golangci-lint-good.md | 27 + .../practices/goroutine-lifecycle-bad.md | 3 + .../practices/goroutine-lifecycle-good.md | 9 + .../practices/graceful-shutdown-good.md | 8 + snippets/golang/practices/handle-once-bad.md | 4 + snippets/golang/practices/handle-once-good.md | 3 + .../practices/naming-conventions-bad.md | 2 + .../practices/naming-conventions-good.md | 2 + .../practices/parameterized-queries-bad.md | 1 + .../practices/parameterized-queries-good.md | 1 + .../golang/practices/small-interfaces-bad.md | 5 + .../golang/practices/small-interfaces-good.md | 3 + .../practices/structured-logging-bad.md | 10 + .../practices/structured-logging-good.md | 26 + .../practices/table-driven-tests-good.md | 18 + .../golang/practices/thin-handlers-good.md | 7 + snippets/lenis/cheatsheet.md | 67 ++ snippets/lenis/css/prevent-scroll.md | 16 + snippets/lenis/css/required.md | 22 + .../lenis/examples/lenis-ref-imperative.md | 25 + .../lenis/examples/react-lenis-container.md | 12 + snippets/lenis/examples/react-lenis-ref.md | 12 + snippets/lenis/examples/react-lenis-root.md | 14 + snippets/lenis/examples/use-lenis-modal.md | 12 + snippets/lenis/examples/use-lenis-parallax.md | 18 + snippets/lenis/examples/use-lenis-progress.md | 17 + .../lenis/examples/use-lenis-scroll-to.md | 12 + snippets/lenis/options/horizontal.md | 8 + snippets/lenis/options/tuned-marketing.md | 10 + snippets/lenis/patterns/accessibility.md | 23 + snippets/lenis/patterns/custom-container.md | 26 + .../patterns/framer-motion-integration.md | 35 ++ snippets/lenis/patterns/full-page.md | 17 + snippets/lenis/patterns/gsap-integration.md | 43 ++ snippets/lenis/patterns/next-js.md | 28 + snippets/lenis/patterns/scroll-to-nav.md | 32 + snippets/lenis/recipes/back-to-top.md | 14 + snippets/lenis/recipes/direction-indicator.md | 21 + snippets/lenis/recipes/gsap-complete.md | 53 ++ .../recipes/horizontal-scroll-section.md | 29 + snippets/lenis/recipes/parallax-layer.md | 21 + snippets/lenis/recipes/scroll-locked-modal.md | 25 + snippets/lenis/recipes/scroll-progress-bar.md | 24 + snippets/lenis/usage/lenis-options.md | 13 + snippets/lenis/usage/lenis-ref.md | 16 + snippets/lenis/usage/react-lenis.md | 15 + snippets/lenis/usage/use-lenis.md | 21 + snippets/react/cheatsheet.md | 88 +++ snippets/react/patterns/component-template.md | 17 + snippets/react/patterns/composition-anti.md | 5 + .../react/patterns/composition-pattern.md | 15 + snippets/react/patterns/data-fetching-anti.md | 6 + snippets/react/patterns/data-fetching-rsc.md | 15 + snippets/react/patterns/nextjs-metadata.md | 23 + snippets/react/patterns/rsc-anti.md | 5 + snippets/react/patterns/rsc-default.md | 13 + .../react/patterns/state-hierarchy-anti.md | 2 + snippets/react/patterns/state-hierarchy.md | 15 + snippets/react/patterns/suspense-boundary.md | 12 + snippets/react/patterns/zustand-store.md | 24 + snippets/rust/cheatsheet.md | 71 +++ .../practices/avoid-clone-in-loops-bad.md | 1 + .../practices/avoid-clone-in-loops-good.md | 2 + .../rust/practices/benchmark-release-bad.md | 1 + .../rust/practices/benchmark-release-good.md | 2 + .../rust/practices/borrow-over-clone-bad.md | 1 + .../rust/practices/borrow-over-clone-good.md | 1 + snippets/rust/practices/copy-by-value-good.md | 2 + .../practices/cow-ambiguous-ownership-good.md | 8 + .../practices/descriptive-test-names-bad.md | 5 + .../practices/descriptive-test-names-good.md | 5 + snippets/rust/practices/doc-tests-good.md | 7 + .../rust/practices/expect-over-allow-bad.md | 2 + .../rust/practices/expect-over-allow-good.md | 2 + .../rust/practices/no-unwrap-in-prod-bad.md | 1 + .../rust/practices/no-unwrap-in-prod-good.md | 1 + .../practices/one-assertion-per-test-good.md | 2 + .../rust/practices/prefer-iterators-bad.md | 2 + .../rust/practices/prefer-iterators-good.md | 5 + .../rust/practices/result-not-panic-bad.md | 4 + .../rust/practices/result-not-panic-good.md | 4 + snippets/rust/practices/send-sync-bad.md | 3 + snippets/rust/practices/send-sync-good.md | 4 + .../static-over-dynamic-dispatch-good.md | 5 + .../rust/practices/str-over-string-bad.md | 2 + .../rust/practices/str-over-string-good.md | 2 + .../practices/thiserror-vs-anyhow-good.md | 12 + .../rust/practices/type-state-pattern-good.md | 17 + snippets/ui-ux/cheatsheet.md | 51 ++ snippets/ui-ux/components/badge.md | 12 + snippets/ui-ux/components/button.md | 8 + snippets/ui-ux/components/card.md | 19 + snippets/ui-ux/components/form-input.md | 31 + snippets/ui-ux/principles/4px-grid.md | 7 + .../ui-ux/principles/5-elevation-levels.md | 7 + snippets/ui-ux/principles/dark-mode.md | 6 + snippets/ui-ux/principles/easing-rules.md | 3 + snippets/ui-ux/principles/focus-management.md | 4 + snippets/ui-ux/principles/mobile-first.md | 3 + snippets/ui-ux/principles/oklch-color.md | 4 + snippets/ui-ux/principles/prose-width.md | 1 + snippets/ui-ux/principles/reduced-motion.md | 8 + .../principles/semantic-status-colors.md | 4 + snippets/ui-ux/principles/touch-targets.md | 6 + snippets/ui-ux/principles/type-scale.md | 4 + snippets/ui-ux/principles/warm-shadows.md | 5 + snippets/ui-ux/principles/warm-vs-cool.md | 10 + snippets/ui-ux/principles/wcag-contrast.md | 8 + snippets/ui-ux/references/elevation-table.md | 18 + snippets/ui-ux/references/motion-table.md | 22 + snippets/ui-ux/references/spacing-table.md | 26 + snippets/ui-ux/references/type-scale-table.md | 25 + snippets/ui-ux/references/wcag-table.md | 25 + src/index.ts | 19 +- src/plugins/design-tokens/data.ts | 587 ++++++++++++++++++ src/plugins/design-tokens/index.ts | 24 + src/plugins/design-tokens/loader.ts | 2 + src/plugins/design-tokens/tools/generate.ts | 54 ++ .../design-tokens/tools/get-category.ts | 43 ++ .../design-tokens/tools/get-color-ramp.ts | 41 ++ .../design-tokens/tools/get-gotchas.ts | 29 + .../design-tokens/tools/get-procedure.ts | 40 ++ .../design-tokens/tools/list-categories.ts | 42 ++ src/plugins/design-tokens/tools/search.ts | 37 ++ src/plugins/echo/data.ts | 501 +++++++++++++++ src/plugins/echo/index.ts | 19 + src/plugins/echo/loader.ts | 3 + src/plugins/echo/tools/decision-matrix.ts | 54 ++ src/plugins/echo/tools/get-middleware.ts | 30 + src/plugins/echo/tools/get-recipe.ts | 30 + src/plugins/echo/tools/list-middleware.ts | 35 ++ src/plugins/echo/tools/list-recipes.ts | 34 + src/plugins/echo/tools/search-docs.ts | 55 ++ src/plugins/golang/data.ts | 326 ++++++++++ src/plugins/golang/index.ts | 22 + src/plugins/golang/loader.ts | 3 + src/plugins/golang/tools/get-antipatterns.ts | 17 + src/plugins/golang/tools/get-pattern.ts | 30 + src/plugins/golang/tools/get-practice.ts | 29 + src/plugins/golang/tools/list-patterns.ts | 36 ++ src/plugins/golang/tools/list-practices.ts | 33 + src/plugins/golang/tools/search-docs.ts | 31 + src/plugins/lenis/data.ts | 463 ++++++++++++++ src/plugins/lenis/index.ts | 22 + src/plugins/lenis/loader.ts | 3 + src/plugins/lenis/tools/cheatsheet.ts | 117 ++++ src/plugins/lenis/tools/generate-setup.ts | 281 +++++++++ src/plugins/lenis/tools/get-api.ts | 29 + src/plugins/lenis/tools/get-pattern.ts | 46 ++ src/plugins/lenis/tools/list-apis.ts | 34 + src/plugins/lenis/tools/search-docs.ts | 61 ++ src/plugins/react/data.ts | 186 ++++++ src/plugins/react/index.ts | 18 + src/plugins/react/loader.ts | 3 + src/plugins/react/tools/get-constraints.ts | 22 + src/plugins/react/tools/get-pattern.ts | 39 ++ src/plugins/react/tools/list-patterns.ts | 35 ++ src/plugins/react/tools/search-docs.ts | 45 ++ src/plugins/rust/data.ts | 205 ++++++ src/plugins/rust/index.ts | 18 + src/plugins/rust/loader.ts | 2 + src/plugins/rust/tools/cheatsheet.ts | 15 + src/plugins/rust/tools/get-practice.ts | 33 + src/plugins/rust/tools/list-practices.ts | 33 + src/plugins/rust/tools/search-docs.ts | 24 + src/plugins/ui-ux/data.ts | 385 ++++++++++++ src/plugins/ui-ux/index.ts | 22 + src/plugins/ui-ux/loader.ts | 2 + src/plugins/ui-ux/tools/get-checklist.ts | 43 ++ .../ui-ux/tools/get-component-pattern.ts | 39 ++ src/plugins/ui-ux/tools/get-gotchas.ts | 36 ++ src/plugins/ui-ux/tools/get-principle.ts | 47 ++ src/plugins/ui-ux/tools/list-principles.ts | 35 ++ src/plugins/ui-ux/tools/search.ts | 47 ++ src/registry.ts | 0 src/shared/loader-factory.ts | 0 261 files changed, 8960 insertions(+), 47 deletions(-) mode change 100644 => 100755 README.md create mode 100644 snippets/design-tokens/categories/border-radius.md create mode 100644 snippets/design-tokens/categories/colors.md create mode 100644 snippets/design-tokens/categories/component-sizing.md create mode 100644 snippets/design-tokens/categories/density.md create mode 100644 snippets/design-tokens/categories/motion.md create mode 100644 snippets/design-tokens/categories/opacity.md create mode 100644 snippets/design-tokens/categories/shadows-elevation.md create mode 100644 snippets/design-tokens/categories/spacing.md create mode 100644 snippets/design-tokens/categories/typography.md create mode 100644 snippets/design-tokens/categories/z-index.md create mode 100644 snippets/design-tokens/procedures/step-1-colors.md create mode 100644 snippets/design-tokens/procedures/step-2-spacing.md create mode 100644 snippets/design-tokens/procedures/step-3-typography.md create mode 100644 snippets/design-tokens/procedures/step-4-component-sizing.md create mode 100644 snippets/design-tokens/procedures/step-5-remaining.md create mode 100644 snippets/design-tokens/procedures/step-6-accessibility.md create mode 100644 snippets/design-tokens/procedures/step-7-validation.md create mode 100644 snippets/design-tokens/procedures/step-8-deliverables.md create mode 100644 snippets/design-tokens/references/color-roles.md create mode 100644 snippets/design-tokens/references/token-checklist.md create mode 100644 snippets/design-tokens/templates/colors-tailwind-v4.md create mode 100644 snippets/design-tokens/templates/motion.md create mode 100644 snippets/design-tokens/templates/spacing.md create mode 100644 snippets/design-tokens/templates/typography.md create mode 100644 snippets/echo/cheatsheet.md create mode 100644 snippets/echo/middleware/basic-auth.md create mode 100644 snippets/echo/middleware/body-limit.md create mode 100644 snippets/echo/middleware/cors.md create mode 100644 snippets/echo/middleware/csrf.md create mode 100644 snippets/echo/middleware/gzip.md create mode 100644 snippets/echo/middleware/jwt.md create mode 100644 snippets/echo/middleware/key-auth.md create mode 100644 snippets/echo/middleware/logger.md create mode 100644 snippets/echo/middleware/rate-limiter.md create mode 100644 snippets/echo/middleware/recover.md create mode 100644 snippets/echo/middleware/request-id.md create mode 100644 snippets/echo/middleware/secure.md create mode 100644 snippets/echo/middleware/timeout.md create mode 100644 snippets/echo/recipes/auto-tls.md create mode 100644 snippets/echo/recipes/cors.md create mode 100644 snippets/echo/recipes/crud-api.md create mode 100644 snippets/echo/recipes/embed-resources.md create mode 100644 snippets/echo/recipes/file-download.md create mode 100644 snippets/echo/recipes/file-upload.md create mode 100644 snippets/echo/recipes/graceful-shutdown.md create mode 100644 snippets/echo/recipes/hello-world.md create mode 100644 snippets/echo/recipes/http2.md create mode 100644 snippets/echo/recipes/jsonp.md create mode 100644 snippets/echo/recipes/jwt-auth.md create mode 100644 snippets/echo/recipes/middleware-chain.md create mode 100644 snippets/echo/recipes/reverse-proxy.md create mode 100644 snippets/echo/recipes/route-groups.md create mode 100644 snippets/echo/recipes/sse.md create mode 100644 snippets/echo/recipes/streaming-response.md create mode 100644 snippets/echo/recipes/subdomain-routing.md create mode 100644 snippets/echo/recipes/timeout.md create mode 100644 snippets/echo/recipes/websocket.md create mode 100644 snippets/golang/cheatsheet.md create mode 100644 snippets/golang/patterns/adapter.md create mode 100644 snippets/golang/patterns/command.md create mode 100644 snippets/golang/patterns/consumer-side-interface-anti.md create mode 100644 snippets/golang/patterns/consumer-side-interface.md create mode 100644 snippets/golang/patterns/fan-out-fan-in.md create mode 100644 snippets/golang/patterns/functional-options.md create mode 100644 snippets/golang/patterns/middleware-decorator.md create mode 100644 snippets/golang/patterns/observer.md create mode 100644 snippets/golang/patterns/pipeline.md create mode 100644 snippets/golang/patterns/strategy.md create mode 100644 snippets/golang/patterns/worker-pool.md create mode 100644 snippets/golang/practices/config-env-vars-bad.md create mode 100644 snippets/golang/practices/config-env-vars-good.md create mode 100644 snippets/golang/practices/constructor-pattern-good.md create mode 100644 snippets/golang/practices/context-first-param-bad.md create mode 100644 snippets/golang/practices/context-first-param-good.md create mode 100644 snippets/golang/practices/crypto-rand-bad.md create mode 100644 snippets/golang/practices/crypto-rand-good.md create mode 100644 snippets/golang/practices/database-repository-bad.md create mode 100644 snippets/golang/practices/database-repository-good.md create mode 100644 snippets/golang/practices/errgroup-bad.md create mode 100644 snippets/golang/practices/errgroup-good.md create mode 100644 snippets/golang/practices/error-wrapping-bad.md create mode 100644 snippets/golang/practices/error-wrapping-good.md create mode 100644 snippets/golang/practices/errors-is-as-bad.md create mode 100644 snippets/golang/practices/errors-is-as-good.md create mode 100644 snippets/golang/practices/golangci-lint-good.md create mode 100644 snippets/golang/practices/goroutine-lifecycle-bad.md create mode 100644 snippets/golang/practices/goroutine-lifecycle-good.md create mode 100644 snippets/golang/practices/graceful-shutdown-good.md create mode 100644 snippets/golang/practices/handle-once-bad.md create mode 100644 snippets/golang/practices/handle-once-good.md create mode 100644 snippets/golang/practices/naming-conventions-bad.md create mode 100644 snippets/golang/practices/naming-conventions-good.md create mode 100644 snippets/golang/practices/parameterized-queries-bad.md create mode 100644 snippets/golang/practices/parameterized-queries-good.md create mode 100644 snippets/golang/practices/small-interfaces-bad.md create mode 100644 snippets/golang/practices/small-interfaces-good.md create mode 100644 snippets/golang/practices/structured-logging-bad.md create mode 100644 snippets/golang/practices/structured-logging-good.md create mode 100644 snippets/golang/practices/table-driven-tests-good.md create mode 100644 snippets/golang/practices/thin-handlers-good.md create mode 100644 snippets/lenis/cheatsheet.md create mode 100644 snippets/lenis/css/prevent-scroll.md create mode 100644 snippets/lenis/css/required.md create mode 100644 snippets/lenis/examples/lenis-ref-imperative.md create mode 100644 snippets/lenis/examples/react-lenis-container.md create mode 100644 snippets/lenis/examples/react-lenis-ref.md create mode 100644 snippets/lenis/examples/react-lenis-root.md create mode 100644 snippets/lenis/examples/use-lenis-modal.md create mode 100644 snippets/lenis/examples/use-lenis-parallax.md create mode 100644 snippets/lenis/examples/use-lenis-progress.md create mode 100644 snippets/lenis/examples/use-lenis-scroll-to.md create mode 100644 snippets/lenis/options/horizontal.md create mode 100644 snippets/lenis/options/tuned-marketing.md create mode 100644 snippets/lenis/patterns/accessibility.md create mode 100644 snippets/lenis/patterns/custom-container.md create mode 100644 snippets/lenis/patterns/framer-motion-integration.md create mode 100644 snippets/lenis/patterns/full-page.md create mode 100644 snippets/lenis/patterns/gsap-integration.md create mode 100644 snippets/lenis/patterns/next-js.md create mode 100644 snippets/lenis/patterns/scroll-to-nav.md create mode 100644 snippets/lenis/recipes/back-to-top.md create mode 100644 snippets/lenis/recipes/direction-indicator.md create mode 100644 snippets/lenis/recipes/gsap-complete.md create mode 100644 snippets/lenis/recipes/horizontal-scroll-section.md create mode 100644 snippets/lenis/recipes/parallax-layer.md create mode 100644 snippets/lenis/recipes/scroll-locked-modal.md create mode 100644 snippets/lenis/recipes/scroll-progress-bar.md create mode 100644 snippets/lenis/usage/lenis-options.md create mode 100644 snippets/lenis/usage/lenis-ref.md create mode 100644 snippets/lenis/usage/react-lenis.md create mode 100644 snippets/lenis/usage/use-lenis.md create mode 100644 snippets/react/cheatsheet.md create mode 100644 snippets/react/patterns/component-template.md create mode 100644 snippets/react/patterns/composition-anti.md create mode 100644 snippets/react/patterns/composition-pattern.md create mode 100644 snippets/react/patterns/data-fetching-anti.md create mode 100644 snippets/react/patterns/data-fetching-rsc.md create mode 100644 snippets/react/patterns/nextjs-metadata.md create mode 100644 snippets/react/patterns/rsc-anti.md create mode 100644 snippets/react/patterns/rsc-default.md create mode 100644 snippets/react/patterns/state-hierarchy-anti.md create mode 100644 snippets/react/patterns/state-hierarchy.md create mode 100644 snippets/react/patterns/suspense-boundary.md create mode 100644 snippets/react/patterns/zustand-store.md create mode 100644 snippets/rust/cheatsheet.md create mode 100644 snippets/rust/practices/avoid-clone-in-loops-bad.md create mode 100644 snippets/rust/practices/avoid-clone-in-loops-good.md create mode 100644 snippets/rust/practices/benchmark-release-bad.md create mode 100644 snippets/rust/practices/benchmark-release-good.md create mode 100644 snippets/rust/practices/borrow-over-clone-bad.md create mode 100644 snippets/rust/practices/borrow-over-clone-good.md create mode 100644 snippets/rust/practices/copy-by-value-good.md create mode 100644 snippets/rust/practices/cow-ambiguous-ownership-good.md create mode 100644 snippets/rust/practices/descriptive-test-names-bad.md create mode 100644 snippets/rust/practices/descriptive-test-names-good.md create mode 100644 snippets/rust/practices/doc-tests-good.md create mode 100644 snippets/rust/practices/expect-over-allow-bad.md create mode 100644 snippets/rust/practices/expect-over-allow-good.md create mode 100644 snippets/rust/practices/no-unwrap-in-prod-bad.md create mode 100644 snippets/rust/practices/no-unwrap-in-prod-good.md create mode 100644 snippets/rust/practices/one-assertion-per-test-good.md create mode 100644 snippets/rust/practices/prefer-iterators-bad.md create mode 100644 snippets/rust/practices/prefer-iterators-good.md create mode 100644 snippets/rust/practices/result-not-panic-bad.md create mode 100644 snippets/rust/practices/result-not-panic-good.md create mode 100644 snippets/rust/practices/send-sync-bad.md create mode 100644 snippets/rust/practices/send-sync-good.md create mode 100644 snippets/rust/practices/static-over-dynamic-dispatch-good.md create mode 100644 snippets/rust/practices/str-over-string-bad.md create mode 100644 snippets/rust/practices/str-over-string-good.md create mode 100644 snippets/rust/practices/thiserror-vs-anyhow-good.md create mode 100644 snippets/rust/practices/type-state-pattern-good.md create mode 100644 snippets/ui-ux/cheatsheet.md create mode 100644 snippets/ui-ux/components/badge.md create mode 100644 snippets/ui-ux/components/button.md create mode 100644 snippets/ui-ux/components/card.md create mode 100644 snippets/ui-ux/components/form-input.md create mode 100644 snippets/ui-ux/principles/4px-grid.md create mode 100644 snippets/ui-ux/principles/5-elevation-levels.md create mode 100644 snippets/ui-ux/principles/dark-mode.md create mode 100644 snippets/ui-ux/principles/easing-rules.md create mode 100644 snippets/ui-ux/principles/focus-management.md create mode 100644 snippets/ui-ux/principles/mobile-first.md create mode 100644 snippets/ui-ux/principles/oklch-color.md create mode 100644 snippets/ui-ux/principles/prose-width.md create mode 100644 snippets/ui-ux/principles/reduced-motion.md create mode 100644 snippets/ui-ux/principles/semantic-status-colors.md create mode 100644 snippets/ui-ux/principles/touch-targets.md create mode 100644 snippets/ui-ux/principles/type-scale.md create mode 100644 snippets/ui-ux/principles/warm-shadows.md create mode 100644 snippets/ui-ux/principles/warm-vs-cool.md create mode 100644 snippets/ui-ux/principles/wcag-contrast.md create mode 100644 snippets/ui-ux/references/elevation-table.md create mode 100644 snippets/ui-ux/references/motion-table.md create mode 100644 snippets/ui-ux/references/spacing-table.md create mode 100644 snippets/ui-ux/references/type-scale-table.md create mode 100644 snippets/ui-ux/references/wcag-table.md mode change 100644 => 100755 src/index.ts create mode 100644 src/plugins/design-tokens/data.ts create mode 100644 src/plugins/design-tokens/index.ts create mode 100644 src/plugins/design-tokens/loader.ts create mode 100644 src/plugins/design-tokens/tools/generate.ts create mode 100644 src/plugins/design-tokens/tools/get-category.ts create mode 100644 src/plugins/design-tokens/tools/get-color-ramp.ts create mode 100644 src/plugins/design-tokens/tools/get-gotchas.ts create mode 100644 src/plugins/design-tokens/tools/get-procedure.ts create mode 100644 src/plugins/design-tokens/tools/list-categories.ts create mode 100644 src/plugins/design-tokens/tools/search.ts create mode 100644 src/plugins/echo/data.ts create mode 100644 src/plugins/echo/index.ts create mode 100644 src/plugins/echo/loader.ts create mode 100644 src/plugins/echo/tools/decision-matrix.ts create mode 100644 src/plugins/echo/tools/get-middleware.ts create mode 100644 src/plugins/echo/tools/get-recipe.ts create mode 100644 src/plugins/echo/tools/list-middleware.ts create mode 100644 src/plugins/echo/tools/list-recipes.ts create mode 100644 src/plugins/echo/tools/search-docs.ts create mode 100644 src/plugins/golang/data.ts create mode 100644 src/plugins/golang/index.ts create mode 100644 src/plugins/golang/loader.ts create mode 100644 src/plugins/golang/tools/get-antipatterns.ts create mode 100644 src/plugins/golang/tools/get-pattern.ts create mode 100644 src/plugins/golang/tools/get-practice.ts create mode 100644 src/plugins/golang/tools/list-patterns.ts create mode 100644 src/plugins/golang/tools/list-practices.ts create mode 100644 src/plugins/golang/tools/search-docs.ts create mode 100644 src/plugins/lenis/data.ts create mode 100644 src/plugins/lenis/index.ts create mode 100644 src/plugins/lenis/loader.ts create mode 100644 src/plugins/lenis/tools/cheatsheet.ts create mode 100644 src/plugins/lenis/tools/generate-setup.ts create mode 100644 src/plugins/lenis/tools/get-api.ts create mode 100644 src/plugins/lenis/tools/get-pattern.ts create mode 100644 src/plugins/lenis/tools/list-apis.ts create mode 100644 src/plugins/lenis/tools/search-docs.ts create mode 100644 src/plugins/react/data.ts create mode 100644 src/plugins/react/index.ts create mode 100644 src/plugins/react/loader.ts create mode 100644 src/plugins/react/tools/get-constraints.ts create mode 100644 src/plugins/react/tools/get-pattern.ts create mode 100644 src/plugins/react/tools/list-patterns.ts create mode 100644 src/plugins/react/tools/search-docs.ts create mode 100644 src/plugins/rust/data.ts create mode 100644 src/plugins/rust/index.ts create mode 100644 src/plugins/rust/loader.ts create mode 100644 src/plugins/rust/tools/cheatsheet.ts create mode 100644 src/plugins/rust/tools/get-practice.ts create mode 100644 src/plugins/rust/tools/list-practices.ts create mode 100644 src/plugins/rust/tools/search-docs.ts create mode 100644 src/plugins/ui-ux/data.ts create mode 100644 src/plugins/ui-ux/index.ts create mode 100644 src/plugins/ui-ux/loader.ts create mode 100644 src/plugins/ui-ux/tools/get-checklist.ts create mode 100644 src/plugins/ui-ux/tools/get-component-pattern.ts create mode 100644 src/plugins/ui-ux/tools/get-gotchas.ts create mode 100644 src/plugins/ui-ux/tools/get-principle.ts create mode 100644 src/plugins/ui-ux/tools/list-principles.ts create mode 100644 src/plugins/ui-ux/tools/search.ts mode change 100644 => 100755 src/registry.ts mode change 100644 => 100755 src/shared/loader-factory.ts diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 587d32a..16aa234 --- a/README.md +++ b/README.md @@ -2,21 +2,36 @@ # unified-mcp -**One MCP server. All frontend libraries. No conflicts.** +**One MCP server. Every library your AI needs. Zero conflicts.**

+ CI - License - MCP - React Flow v12 - Motion v12 - Stars + License + Stars + TypeScript + MCP compatible +

+

+ + React Flow v12 + Motion v12 + Lenis + React 19 + Tailwind v4 +

+

+ + Echo Go + Go + Rust + UI/UX


-> Plugin-based MCP server that gives your AI assistant deep knowledge of frontend libraries - -> API refs, patterns, code generation - all through a single process with namespaced tools. +> Plugin-based MCP server that gives your AI assistant deep knowledge of frontend and backend libraries - +> API refs, patterns, code generation, design systems - all through a single process with namespaced tools. @@ -24,7 +39,7 @@ ## ๐Ÿค Companion -This MCP server pairs with **[unified-skill](https://github.com/orkait/unified-skill)** - a Claude Code skill that teaches your AI assistant when and how to use these tools. The skill handles judgment and gotchas; this server handles the data. +This server pairs with **[unified-skill](https://github.com/orkait/unified-skill)** - a Claude Code skill that teaches your AI assistant *when and how* to use these tools. The skill handles judgment and gotchas; this server handles the data. Install both to get the full experience. @@ -32,16 +47,24 @@ Install both to get the full experience. ## ๐Ÿงฉ Plugins -| Plugin | Library | Tools | What's included | -|--------|---------|:-----:|-----------------| -| **reactflow** | [@xyflow/react](https://reactflow.dev) v12 | 8 | 56 APIs ยท 17 patterns ยท 3 templates ยท migration guide | -| **motion** | [Motion for React](https://motion.dev) v12 | 6 | 33 APIs ยท 14 example categories ยท transition reference | +| Plugin | Library / Domain | Tools | What's included | +|--------|-----------------|:-----:|-----------------| +| **reactflow** | [@xyflow/react](https://reactflow.dev) v12 | 8 | 56 APIs, 17 patterns, 3 templates, migration guide | +| **motion** | [Motion for React](https://motion.dev) v12 | 6 | 33 APIs, 14 example categories, transition reference | +| **lenis** | [Lenis](https://lenis.darkroom.engineering) smooth scroll | 6 | API reference, 7 patterns, 7 recipes, CSS rules, GSAP integration | +| **react** | React 19 + Next.js App Router | 4 | RSC patterns, state hierarchy, data fetching, Zustand, composition | +| **echo** | [Echo](https://echo.labstack.com) Go web framework | 6 | 19 recipes, 13 middleware, decision matrix, cheatsheet | +| **golang** | Go best practices + design patterns | 6 | 18 best practices, 10 design patterns, anti-patterns, cheatsheet | +| **rust** | Rust best practices | 4 | 18 practices (good/bad pairs), ownership guide, cheatsheet | +| **design-tokens** | Tailwind v4 + OKLCH token system | 7 | 10 token categories, 8 build procedures, color ramp templates | +| **ui-ux** | UI/UX design principles | 6 | Typography, color, spacing, elevation, motion, a11y, component patterns | --- ## ๐Ÿ› ๏ธ Tools -### โš›๏ธ React Flow - `reactflow_*` +
+โš›๏ธ React Flow - reactflow_* | Tool | What it does | |------|-------------| @@ -59,11 +82,11 @@ Install both to get the full experience. `zustand-store` ยท `undo-redo` ยท `drag-and-drop` ยท `auto-layout-dagre` ยท `auto-layout-elk` ยท `context-menu` ยท `copy-paste` ยท `save-restore` ยท `prevent-cycles` ยท `keyboard-shortcuts` ยท `performance` ยท `dark-mode` ยท `ssr` ยท `subflows` ยท `edge-reconnection` ยท `custom-connection-line` ยท `auto-layout-on-mount` +
---- - -### ๐ŸŽฌ Motion for React - `motion_*` +
+๐ŸŽฌ Motion for React - motion_* | Tool | What it does | |------|-------------| @@ -79,11 +102,143 @@ Install both to get the full experience. `animation` ยท `gestures` ยท `scroll` ยท `layout` ยท `exit` ยท `drag` ยท `hover` ยท `svg` ยท `transitions` ยท `variants` ยท `keyframes` ยท `spring` ยท `reorder` ยท `performance` +
+ + +
+๐ŸŒŠ Lenis - lenis_* + +| Tool | What it does | +|------|-------------| +| `lenis_list_apis` | Browse all Lenis APIs - options, methods, events | +| `lenis_get_api` | Full reference for any API with usage snippet | +| `lenis_get_pattern` | Integration patterns: Next.js, GSAP, Framer Motion, custom container | +| `lenis_generate_setup` | Generate a complete Lenis setup from a description | +| `lenis_cheatsheet` | Required CSS, `data-lenis-prevent` usage, pitfalls table | +| `lenis_search_docs` | Full-text search across all Lenis docs | + +
+7 patterns and 7 recipes + +**Patterns:** `full-page` ยท `next-js` ยท `gsap-integration` ยท `framer-motion-integration` ยท `custom-container` ยท `accessibility` ยท `scroll-to-nav` + +**Recipes:** `scroll-progress-bar` ยท `back-to-top` ยท `horizontal-scroll-section` ยท `scroll-locked-modal` ยท `parallax-layer` ยท `direction-indicator` ยท `gsap-complete` + +
+
+ +
+โš›๏ธ React + Next.js - react_* + +| Tool | What it does | +|------|-------------| +| `react_list_patterns` | List all React/Next.js patterns with categories | +| `react_get_pattern` | Full pattern: code, anti-pattern, tips | +| `react_get_constraints` | Hard rules and banned patterns (no `useEffect` for fetching, no Redux, etc.) | +| `react_search_docs` | Search across patterns and rules | + +
+ +
+๐Ÿน Echo (Go) - echo_* + +| Tool | What it does | +|------|-------------| +| `echo_list_recipes` | Browse all 19 recipes by category | +| `echo_get_recipe` | Full recipe with complete runnable code | +| `echo_list_middleware` | Browse all 13 middleware with purpose and order guidance | +| `echo_get_middleware` | Full middleware reference with usage and gotchas | +| `echo_decision_matrix` | When to use what - Echo vs stdlib vs alternatives | +| `echo_search_docs` | Full-text search across all recipes and middleware | + +
+19 recipes + +`hello-world` ยท `crud-api` ยท `jwt-auth` ยท `websocket` ยท `sse` ยท `file-upload` ยท `file-download` ยท `graceful-shutdown` ยท `middleware-chain` ยท `cors` ยท `route-groups` ยท `http2` ยท `auto-tls` ยท `reverse-proxy` ยท `streaming-response` ยท `embed-resources` ยท `timeout` ยท `subdomain-routing` ยท `jsonp` + +
+
+ +
+๐Ÿน Golang - golang_* + +| Tool | What it does | +|------|-------------| +| `golang_list_practices` | Browse all 18 best practices by topic | +| `golang_get_practice` | Full practice: rule, reason, good/bad code examples | +| `golang_list_patterns` | Browse all 10 design patterns by category | +| `golang_get_pattern` | Full pattern with Go-idiomatic implementation | +| `golang_get_antipatterns` | Common Go mistakes and their fixes | +| `golang_search_docs` | Search across practices and patterns | + +
+Topics and patterns + +**Practice topics:** `fundamentals` ยท `error-handling` ยท `concurrency` ยท `api-server` ยท `database` ยท `config` ยท `logging` ยท `security` ยท `testing` + +**Pattern categories:** `creational` (functional-options) ยท `structural` (adapter, middleware-decorator, consumer-side-interface) ยท `behavioral` (strategy, observer, command) ยท `concurrency` (worker-pool, pipeline, fan-out-fan-in) + +
+
+ +
+๐Ÿฆ€ Rust - rust_* + +| Tool | What it does | +|------|-------------| +| `rust_list_practices` | Browse all 18 best practices by topic | +| `rust_get_practice` | Full practice: rule, reason, good/bad examples | +| `rust_search_docs` | Search across all practices | +| `rust_cheatsheet` | Ownership rules, pointer type table, performance tips | + +
+ +
+๐ŸŽจ Design Tokens - design_tokens_* + +| Tool | What it does | +|------|-------------| +| `design_tokens_list_categories` | Browse all 10 token categories with descriptions | +| `design_tokens_get_category` | Full CSS + rules + gotchas for a token category | +| `design_tokens_get_color_ramp` | Color ramp reference: stops, oklch values, semantic roles | +| `design_tokens_get_procedure` | Step-by-step token build procedures (8 steps) | +| `design_tokens_get_gotchas` | All gotchas across every category and procedure | +| `design_tokens_generate` | Generate a complete Tailwind v4 token file from a palette | +| `design_tokens_search` | Search across all categories, ramps, and procedures | + +
+10 token categories + +`colors` ยท `spacing` ยท `typography` ยท `component-sizing` ยท `border-radius` ยท `shadows-elevation` ยท `motion` ยท `z-index` ยท `opacity` ยท `grid-layout` + +
+
+ +
+๐Ÿ’… UI/UX Principles - ui_ux_* + +| Tool | What it does | +|------|-------------| +| `ui_ux_list_principles` | Browse all principles by domain | +| `ui_ux_get_principle` | Full principle: rule, detail, CSS example, anti-patterns | +| `ui_ux_get_component_pattern` | Component spec: variants, states, sizing rules, CSS | +| `ui_ux_get_checklist` | Pre-ship checklist per domain (typography, color, a11y, motion) | +| `ui_ux_get_gotchas` | All common UI mistakes and their fixes | +| `ui_ux_search` | Search across principles, patterns, and gotchas | + +
+Domains and components + +**Domains:** `typography` ยท `color` ยท `spacing` ยท `elevation` ยท `motion` ยท `accessibility` ยท `responsive` ยท `components` + +**Component patterns:** `button` ยท `card` ยท `badge` ยท `form-input` + +
--- -### ๐Ÿ“„ Resources +## ๐Ÿ“„ Resources | Resource | URI | Description | |----------|-----|-------------| @@ -162,8 +317,7 @@ npm install && npm run build ## ๐Ÿ’ก Why unified? -Running separate MCP servers per library means one Docker container per server at startup. -Two libraries = two containers. Ten libraries = ten containers - every session. +Running a separate MCP server per library means one Docker container per server at startup. Two libraries = two containers. Ten libraries = ten containers - every session. `unified-mcp` runs everything in **one process**. All plugins share the same server, same connection, same container. @@ -174,14 +328,15 @@ Tool names are namespaced per plugin (`reactflow_list_apis` vs `motion_list_apis ## ๐Ÿ”Œ Adding a Plugin 1. Create `src/plugins//` with: - - `data.ts` or `data/` - your library's reference data (keep code examples in `snippets//`) + - `data.ts` - reference data (keep all code examples in `snippets//` as `.md` files) + - `loader.ts` - `export const snippet = createSnippetLoader("")` - `tools/.ts` - one file per tool, each exporting `register(server)`; prefix all tool names with `_` - `index.ts` - export `const Plugin: Plugin = { name: "", register }` 2. Register in `src/index.ts`: ```typescript import { shadcnPlugin } from "./plugins/shadcn/index.js"; - loadPlugins(server, [reactflowPlugin, motionPlugin, shadcnPlugin]); + loadPlugins(server, [...existingPlugins, shadcnPlugin]); ``` 3. Rebuild and redeploy: @@ -199,30 +354,34 @@ No changes to your MCP config required. ``` src/ -โ”œโ”€โ”€ index.ts # Entry - creates McpServer, loads plugins, starts StdioTransport -โ”œโ”€โ”€ registry.ts # Plugin interface + loadPlugins() +โ”œโ”€โ”€ index.ts # Entry - creates McpServer, loads all plugins +โ”œโ”€โ”€ registry.ts # Plugin interface + loadPlugins() +โ”œโ”€โ”€ shared/ +โ”‚ โ””โ”€โ”€ loader-factory.ts # createSnippetLoader() - reads .md files at runtime โ””โ”€โ”€ plugins/ - โ”œโ”€โ”€ reactflow/ - โ”‚ โ”œโ”€โ”€ index.ts # Exports reactflowPlugin - โ”‚ โ”œโ”€โ”€ tools/ # One file per tool, all prefixed reactflow_* - โ”‚ โ””โ”€โ”€ data/ # React Flow v12 API reference data - โ””โ”€โ”€ motion/ - โ”œโ”€โ”€ index.ts # Exports motionPlugin - โ”œโ”€โ”€ tools/ # One file per tool, all prefixed motion_* - โ””โ”€โ”€ data.ts # Motion for React v12 API reference data + โ”œโ”€โ”€ reactflow/ # @xyflow/react v12 + โ”œโ”€โ”€ motion/ # motion/react v12 + โ”œโ”€โ”€ lenis/ # Lenis smooth scroll + โ”œโ”€โ”€ react/ # React 19 + Next.js App Router + โ”œโ”€โ”€ echo/ # Echo Go framework + โ”œโ”€โ”€ golang/ # Go best practices + design patterns + โ”œโ”€โ”€ rust/ # Rust best practices + โ”œโ”€โ”€ design-tokens/ # Tailwind v4 OKLCH token system + โ””โ”€โ”€ ui-ux/ # UI/UX design principles snippets/ -โ”œโ”€โ”€ reactflow/ # .md files loaded at runtime via snippet() -โ”‚ โ”œโ”€โ”€ examples/ # Per-API example code -โ”‚ โ”œโ”€โ”€ patterns/ # Enterprise pattern implementations -โ”‚ โ”œโ”€โ”€ templates/ # Production-ready starter templates -โ”‚ โ””โ”€โ”€ usage/ # Per-API usage snippets -โ””โ”€โ”€ motion/ - โ”œโ”€โ”€ examples/ # Per-API animation examples - โ””โ”€โ”€ usage/ # Per-API usage snippets +โ”œโ”€โ”€ reactflow/ # 94 .md files +โ”œโ”€โ”€ motion/ # 79 .md files +โ”œโ”€โ”€ lenis/ # 31 .md files +โ”œโ”€โ”€ react/ # 13 .md files +โ”œโ”€โ”€ echo/ # 33 .md files +โ”œโ”€โ”€ golang/ # 43 .md files +โ”œโ”€โ”€ rust/ # 28 .md files +โ”œโ”€โ”€ design-tokens/ # 24 .md files +โ””โ”€โ”€ ui-ux/ # 25 .md files scripts/ -โ””โ”€โ”€ start-mcp.sh # Single-container Docker wrapper +โ””โ”€โ”€ start-mcp.sh # Single-container Docker wrapper ``` **Plugin interface:** @@ -233,27 +392,38 @@ export interface Plugin { } ``` +Every plugin stores all code examples as `.md` files loaded at runtime: +```typescript +// loader.ts +export const snippet = createSnippetLoader("golang"); + +// data.ts +good: snippet("practices/error-wrapping-good.md"), +bad: snippet("practices/error-wrapping-bad.md"), +``` + --- ## ๐Ÿ›  Development ```bash npm install -npm run build # compile TypeScript โ†’ dist/ +npm run build # compile TypeScript to dist/ npm run dev # watch mode npm start # run server directly ``` ```bash -# Verify all tools are registered with correct prefixes +# Verify all plugins load and tools are registered correctly node --input-type=module <<'EOF' import { reactflowPlugin } from './dist/plugins/reactflow/index.js'; import { motionPlugin } from './dist/plugins/motion/index.js'; +import { lenisPlugin } from './dist/plugins/lenis/index.js'; +import { golangPlugin } from './dist/plugins/golang/index.js'; const tools = []; const fake = { tool: (n) => tools.push(n), resource: () => {} }; -reactflowPlugin.register(fake); -motionPlugin.register(fake); -console.log('Tools:', tools); +[reactflowPlugin, motionPlugin, lenisPlugin, golangPlugin].forEach(p => p.register(fake)); +console.log('Tools registered:', tools.length); EOF ``` diff --git a/snippets/design-tokens/categories/border-radius.md b/snippets/design-tokens/categories/border-radius.md new file mode 100644 index 0000000..ceab94a --- /dev/null +++ b/snippets/design-tokens/categories/border-radius.md @@ -0,0 +1,26 @@ +/* Border radius scale */ +@theme { + --radius-none: 0; + --radius-xs: 0.125rem; /* 2px โ€” sharp, subtle rounding */ + --radius-sm: 0.25rem; /* 4px โ€” inputs, tags */ + --radius-md: 0.375rem; /* 6px โ€” buttons, cards */ + --radius-lg: 0.5rem; /* 8px โ€” modals, large cards */ + --radius-xl: 0.75rem; /* 12px โ€” feature cards */ + --radius-2xl: 1rem; /* 16px โ€” large containers */ + --radius-3xl: 1.5rem; /* 24px โ€” hero sections */ + --radius-full: 9999px; /* pill */ +} + +/* Semantic radius tokens */ +:root { + --radius-btn: var(--radius-md); /* 6px */ + --radius-input: var(--radius-sm); /* 4px */ + --radius-card: var(--radius-lg); /* 8px */ + --radius-card-feature: var(--radius-xl); /* 12px */ + --radius-modal: var(--radius-xl); /* 12px */ + --radius-badge: var(--radius-full); /* pill */ + --radius-tag: var(--radius-sm); /* 4px */ + --radius-tooltip: var(--radius-sm); /* 4px */ + --radius-popover: var(--radius-lg); /* 8px */ + --radius-avatar: var(--radius-full); /* circular */ +} \ No newline at end of file diff --git a/snippets/design-tokens/categories/colors.md b/snippets/design-tokens/categories/colors.md new file mode 100644 index 0000000..ad327a9 --- /dev/null +++ b/snippets/design-tokens/categories/colors.md @@ -0,0 +1,51 @@ +/* Layer 1 โ€“ Primitives (@theme) */ +@theme { + --color-brand-50: oklch(0.97 0.02 291); + --color-brand-100: oklch(0.94 0.04 291); + --color-brand-200: oklch(0.88 0.06 291); + --color-brand-300: oklch(0.78 0.09 291); + --color-brand-400: oklch(0.70 0.12 291); + --color-brand-500: oklch(0.62 0.14 291); + --color-brand-600: oklch(0.54 0.14 291); + --color-brand-700: oklch(0.46 0.13 291); + --color-brand-800: oklch(0.38 0.11 291); + --color-brand-900: oklch(0.28 0.08 291); + --color-brand-950: oklch(0.18 0.05 291); +} + +/* Layer 2 โ€“ Semantics (:root / .dark) */ +:root { + --color-bg: var(--color-neutral-50); + --color-bg-subtle: var(--color-neutral-100); + --color-surface: #ffffff; + --color-border: var(--color-neutral-200); + --color-border-strong: var(--color-neutral-300); + --color-text: var(--color-neutral-900); + --color-text-muted: var(--color-neutral-500); + --color-primary: var(--color-brand-500); + --color-primary-hover: var(--color-brand-600); + --color-primary-fg: #ffffff; +} + +.dark { + --color-bg: oklch(0.18 0.008 265); + --color-bg-subtle: oklch(0.21 0.008 265); + --color-surface: oklch(0.24 0.008 265); + --color-border: oklch(0.30 0.008 265); + --color-border-strong: oklch(0.36 0.008 265); + --color-text: oklch(0.94 0.008 265); + --color-text-muted: oklch(0.60 0.008 265); + --color-primary: var(--color-brand-400); + --color-primary-hover: var(--color-brand-300); + --color-primary-fg: oklch(0.18 0.005 291); +} + +/* Layer 3 โ€“ @theme inline (bridges CSS vars to Tailwind utilities) */ +@theme inline { + --color-background: var(--color-bg); + --color-foreground: var(--color-text); + --color-primary: var(--color-primary); + --color-primary-foreground: var(--color-primary-fg); + --color-border: var(--color-border); + --color-muted: var(--color-text-muted); +} \ No newline at end of file diff --git a/snippets/design-tokens/categories/component-sizing.md b/snippets/design-tokens/categories/component-sizing.md new file mode 100644 index 0000000..adfba92 --- /dev/null +++ b/snippets/design-tokens/categories/component-sizing.md @@ -0,0 +1,76 @@ +/* Component sizing tokens */ +:root { + /* Touch targets (WCAG 2.5.5) */ + --size-touch-min: 44px; /* WCAG minimum */ + --size-touch-comfortable: 48px; /* recommended */ + + /* Button sizes */ + --btn-h-xs: 1.75rem; /* 28px */ + --btn-h-sm: 2rem; /* 32px */ + --btn-h-md: 2.5rem; /* 40px */ + --btn-h-lg: 2.75rem; /* 44px โ€” matches touch-min */ + --btn-h-xl: 3.25rem; /* 52px */ + + --btn-px-xs: 0.625rem; /* 10px */ + --btn-px-sm: 0.875rem; /* 14px */ + --btn-px-md: 1rem; /* 16px */ + --btn-px-lg: 1.25rem; /* 20px */ + --btn-px-xl: 1.75rem; /* 28px */ + + --btn-text-xs: 0.6875rem; /* 11px */ + --btn-text-sm: 0.8125rem; /* 13px */ + --btn-text-md: 0.9375rem; /* 15px */ + --btn-text-lg: 1rem; /* 16px */ + --btn-text-xl: 1.0625rem; /* 17px */ + + /* Input sizes */ + --input-h-sm: 2rem; /* 32px */ + --input-h-md: 2.5rem; /* 40px */ + --input-h-lg: 2.75rem; /* 44px */ + --input-px-md: 0.875rem; /* 14px */ + --input-px-lg: 1rem; /* 16px */ + + /* Icon sizes */ + --icon-xs: 0.75rem; /* 12px */ + --icon-sm: 1rem; /* 16px */ + --icon-md: 1.25rem; /* 20px */ + --icon-lg: 1.5rem; /* 24px */ + --icon-xl: 2rem; /* 32px */ + --icon-2xl: 3rem; /* 48px */ + + /* Avatar sizes */ + --avatar-xs: 1.5rem; /* 24px */ + --avatar-sm: 2rem; /* 32px */ + --avatar-md: 2.5rem; /* 40px */ + --avatar-lg: 3rem; /* 48px */ + --avatar-xl: 4rem; /* 64px */ + --avatar-2xl: 5rem; /* 80px */ +} + +/* Component class example */ +@layer components { + .btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--spacing-inline); + height: var(--btn-h-md); + padding-inline: var(--btn-px-md); + font-size: var(--btn-text-md); + font-weight: var(--font-weight-strong); + border-radius: var(--radius-md); + transition: background-color 150ms ease-out, opacity 150ms ease-out; + cursor: pointer; + user-select: none; + } + .btn-sm { + height: var(--btn-h-sm); + padding-inline: var(--btn-px-sm); + font-size: var(--btn-text-sm); + } + .btn-lg { + height: var(--btn-h-lg); + padding-inline: var(--btn-px-lg); + font-size: var(--btn-text-lg); + } +} \ No newline at end of file diff --git a/snippets/design-tokens/categories/density.md b/snippets/design-tokens/categories/density.md new file mode 100644 index 0000000..0406ecd --- /dev/null +++ b/snippets/design-tokens/categories/density.md @@ -0,0 +1,38 @@ +/* Density mode system */ +:root { + --density-scale: 1; /* default */ + --density-text-adjust: 0; /* rem offset for font sizes */ +} + +.density-compact { + --density-scale: 0.75; + --density-text-adjust: -0.0625rem; /* -1px */ + + /* Override spacing tokens */ + --spacing-card: 1.25rem; /* vs 1.75rem default */ + --spacing-card-sm: 0.875rem; /* vs 1.25rem default */ + --spacing-section-y: 4rem; /* vs 6rem default */ + --spacing-grid-cards: 1rem; /* vs 1.5rem default */ + + /* Override sizing tokens */ + --btn-h-md: 2rem; /* vs 2.5rem default */ + --btn-h-lg: 2.25rem; /* vs 2.75rem default */ + --input-h-md: 2rem; /* vs 2.5rem default */ + --input-h-lg: 2.25rem; /* vs 2.75rem default */ +} + +.density-comfortable { + --density-scale: 1.125; + --density-text-adjust: 0.0625rem; /* +1px */ + + --spacing-card: 2.25rem; /* vs 1.75rem default */ + --spacing-card-sm: 1.625rem; /* vs 1.25rem default */ + --spacing-section-y: 8rem; /* vs 6rem default */ + --spacing-grid-cards: 2rem; /* vs 1.5rem default */ + + --btn-h-md: 2.75rem; /* vs 2.5rem default */ + --btn-h-lg: 3rem; /* vs 2.75rem default */ + --input-h-md: 2.75rem; /* vs 2.5rem default */ +} + +/* Usage: apply class to or top-level wrapper */ \ No newline at end of file diff --git a/snippets/design-tokens/categories/motion.md b/snippets/design-tokens/categories/motion.md new file mode 100644 index 0000000..6d945a1 --- /dev/null +++ b/snippets/design-tokens/categories/motion.md @@ -0,0 +1,46 @@ +/* Motion tokens */ +@theme { + /* Durations */ + --duration-instant: 50ms; + --duration-fast: 100ms; + --duration-normal: 200ms; + --duration-slow: 300ms; + --duration-slower: 400ms; + --duration-slowest: 600ms; + + /* Easing curves */ + --ease-default: cubic-bezier(0.4, 0, 0.2, 1); /* standard โ€” enter & reposition */ + --ease-in: cubic-bezier(0.4, 0, 1, 1); /* accelerate โ€” exiting */ + --ease-out: cubic-bezier(0, 0, 0.2, 1); /* decelerate โ€” entering */ + --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); /* symmetric */ + --ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1); /* slight overshoot */ + --ease-spring: cubic-bezier(0.175, 0.885, 0.32, 1.275); /* spring */ + --ease-snappy: cubic-bezier(0.2, 0, 0, 1); /* fast start, gentle end */ + --ease-linear: linear; /* only for progress/loaders */ +} + +/* Semantic motion tokens */ +:root { + --transition-hover: background-color var(--duration-fast) var(--ease-out), + color var(--duration-fast) var(--ease-out), + border-color var(--duration-fast) var(--ease-out), + opacity var(--duration-fast) var(--ease-out); + + --transition-transform: transform var(--duration-normal) var(--ease-out); + --transition-panel: transform var(--duration-slow) var(--ease-out), + opacity var(--duration-slow) var(--ease-out); + --transition-fade: opacity var(--duration-normal) var(--ease-default); + --transition-layout: all var(--duration-slow) var(--ease-default); +} + +/* Reduced motion โ€” ALWAYS include */ +@layer base { + @media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + } +} \ No newline at end of file diff --git a/snippets/design-tokens/categories/opacity.md b/snippets/design-tokens/categories/opacity.md new file mode 100644 index 0000000..dd88df6 --- /dev/null +++ b/snippets/design-tokens/categories/opacity.md @@ -0,0 +1,29 @@ +/* Opacity scale */ +@theme { + --opacity-disabled: 0.5; /* exact WCAG-aligned disabled state */ + --opacity-muted: 0.65; /* secondary text, subtle elements */ + --opacity-ghost: 0.4; /* very subtle ghost elements */ + --opacity-overlay: 0.5; /* modal backdrop overlay */ + --opacity-overlay-heavy: 0.75; /* strong modal overlay */ + --opacity-glass: 0.8; /* frosted glass backgrounds */ + --opacity-glass-light: 0.6; /* lighter glass variant */ + --opacity-skeleton: 0.08; /* skeleton loading shimmer base */ + --opacity-hover: 0.9; /* subtle hover brightness */ +} + +/* Semantic usage in components */ +@layer components { + [disabled], [aria-disabled="true"] { + opacity: var(--opacity-disabled); + cursor: not-allowed; + pointer-events: none; + } + + .text-muted { opacity: var(--opacity-muted); } + + .glass { + background: oklch(from var(--color-surface) l c h / var(--opacity-glass)); + backdrop-filter: blur(12px) saturate(150%); + -webkit-backdrop-filter: blur(12px) saturate(150%); + } +} \ No newline at end of file diff --git a/snippets/design-tokens/categories/shadows-elevation.md b/snippets/design-tokens/categories/shadows-elevation.md new file mode 100644 index 0000000..dc832fc --- /dev/null +++ b/snippets/design-tokens/categories/shadows-elevation.md @@ -0,0 +1,45 @@ +/* Shadow / Elevation scale */ +:root { + /* Warm-tinted shadows โ€” oklch, NOT rgba(0,0,0) */ + --shadow-xs: 0 1px 2px 0 oklch(0.30 0.02 60 / 0.08); + --shadow-sm: + 0 1px 3px 0 oklch(0.30 0.02 60 / 0.10), + 0 1px 2px -1px oklch(0.30 0.02 60 / 0.06); + --shadow-md: + 0 4px 6px -1px oklch(0.30 0.02 60 / 0.10), + 0 2px 4px -2px oklch(0.30 0.02 60 / 0.06); + --shadow-lg: + 0 10px 15px -3px oklch(0.30 0.02 60 / 0.10), + 0 4px 6px -4px oklch(0.30 0.02 60 / 0.05); + --shadow-xl: + 0 20px 25px -5px oklch(0.30 0.02 60 / 0.10), + 0 8px 10px -6px oklch(0.30 0.02 60 / 0.04); + --shadow-2xl: + 0 25px 50px -12px oklch(0.30 0.02 60 / 0.25); + + /* Focus ring */ + --shadow-focus: 0 0 0 2px var(--color-bg), 0 0 0 4px var(--color-primary); + + /* Semantic elevation tokens */ + --shadow-btn: var(--shadow-xs); + --shadow-card: var(--shadow-sm); + --shadow-modal: var(--shadow-xl); + --shadow-dropdown: var(--shadow-lg); + --shadow-tooltip: var(--shadow-md); +} + +/* Dark mode โ€” use bg-color elevation, skip shadows */ +.dark { + --shadow-xs: none; + --shadow-sm: none; + --shadow-md: none; + --shadow-lg: none; + --shadow-xl: none; + --shadow-2xl: none; + + /* Dark elevation via background lightness steps */ + --color-surface-1: oklch(0.22 0.008 265); /* slightly elevated */ + --color-surface-2: oklch(0.26 0.008 265); + --color-surface-3: oklch(0.30 0.008 265); + --color-surface-4: oklch(0.35 0.008 265); /* modal/floating level */ +} \ No newline at end of file diff --git a/snippets/design-tokens/categories/spacing.md b/snippets/design-tokens/categories/spacing.md new file mode 100644 index 0000000..4037805 --- /dev/null +++ b/snippets/design-tokens/categories/spacing.md @@ -0,0 +1,24 @@ +/* Tailwind v4 base multiplier โ€” DO NOT override individual steps here */ +@theme { + --spacing: 0.25rem; /* 4px base โ€” generates p-1=4px, p-2=8px, p-4=16px... */ +} + +/* Named semantic spacing tokens */ +:root { + --spacing-section-y: 6rem; /* 96px โ€” major vertical section padding */ + --spacing-section-x: 1.5rem; /* 24px โ€” horizontal section padding (mobile) */ + --spacing-card: 1.75rem; /* 28px โ€” card internal padding */ + --spacing-card-sm: 1.25rem; /* 20px โ€” small card padding */ + --spacing-grid-cards: 1.5rem; /* 24px โ€” gap between grid cards */ + --spacing-grid-cards-lg: 2rem; /* 32px โ€” larger gap variant */ + --spacing-inline: 0.5rem; /* 8px โ€” inline element gaps (icon + label) */ + --spacing-form-gap: 1.25rem; /* 20px โ€” gap between form fields */ + --spacing-nav-x: 1.5rem; /* 24px โ€” horizontal nav padding */ + --spacing-nav-y: 1rem; /* 16px โ€” vertical nav padding */ + --spacing-container-max: 80rem; /* 1280px โ€” max content width */ + --spacing-container-md: 65rem; /* 1040px โ€” medium content width */ + --spacing-container-narrow: 42rem; /* 672px โ€” prose/narrow content */ +} + +/* Named tokens generate Tailwind utilities automatically in v4 */ +/* Usage: p-card, py-section-y, gap-grid-cards, max-w-container-max */ \ No newline at end of file diff --git a/snippets/design-tokens/categories/typography.md b/snippets/design-tokens/categories/typography.md new file mode 100644 index 0000000..a4997d2 --- /dev/null +++ b/snippets/design-tokens/categories/typography.md @@ -0,0 +1,80 @@ +/* Typography scale tokens */ +:root { + /* Font families */ + --font-sans: "Inter Variable", ui-sans-serif, system-ui, sans-serif; + --font-mono: "JetBrains Mono Variable", ui-monospace, monospace; + --font-display: "Cal Sans", var(--font-sans); /* optional display font */ + + /* Type scale โ€” fluid for headings, fixed for body */ + --text-display: clamp(3rem, 5vw + 1rem, 4.5rem); /* 48โ€“72px */ + --text-h1: clamp(2.25rem, 3.5vw + 0.5rem, 3rem); /* 36โ€“48px */ + --text-h2: clamp(1.75rem, 2.5vw + 0.25rem, 2.25rem); /* 28โ€“36px */ + --text-h3: clamp(1.375rem, 1.5vw + 0.25rem, 1.5rem); /* 22โ€“24px */ + --text-subtitle: 1.125rem; /* 18px โ€” fixed */ + --text-body: 1rem; /* 16px โ€” fixed, never fluid */ + --text-body-sm: 0.875rem; /* 14px */ + --text-caption: 0.75rem; /* 12px */ + --text-overline: 0.6875rem; /* 11px โ€” all-caps label */ + + /* Line heights โ€” tight for large, generous for small */ + --leading-display: 1.05; + --leading-heading: 1.2; + --leading-subtitle: 1.35; + --leading-body: 1.7; + --leading-body-sm: 1.6; + --leading-caption: 1.5; + + /* Letter spacing */ + --tracking-tight: -0.03em; /* display headings */ + --tracking-heading: -0.02em; /* h1โ€“h2 */ + --tracking-snug: -0.01em; /* h3 */ + --tracking-normal: 0em; /* body */ + --tracking-wide: 0.06em; /* overlines */ + --tracking-wider: 0.10em; /* strong overlines */ + + /* Font weights */ + --font-weight-display: 800; + --font-weight-heading: 700; + --font-weight-subheading: 600; + --font-weight-body: 400; + --font-weight-strong: 500; + --font-weight-mono: 450; + + /* Prose max-width */ + --prose-width: 65ch; +} + +/* Semantic typography classes */ +@layer components { + .text-display { + font-size: var(--text-display); + line-height: var(--leading-display); + letter-spacing: var(--tracking-tight); + font-weight: var(--font-weight-display); + } + .text-h1 { + font-size: var(--text-h1); + line-height: var(--leading-heading); + letter-spacing: var(--tracking-heading); + font-weight: var(--font-weight-heading); + } + .text-h2 { + font-size: var(--text-h2); + line-height: var(--leading-heading); + letter-spacing: var(--tracking-heading); + font-weight: var(--font-weight-heading); + } + .text-h3 { + font-size: var(--text-h3); + line-height: var(--leading-subtitle); + letter-spacing: var(--tracking-snug); + font-weight: var(--font-weight-subheading); + } + .text-overline { + font-size: var(--text-overline); + line-height: var(--leading-caption); + letter-spacing: var(--tracking-wider); + font-weight: var(--font-weight-subheading); + text-transform: uppercase; + } +} \ No newline at end of file diff --git a/snippets/design-tokens/categories/z-index.md b/snippets/design-tokens/categories/z-index.md new file mode 100644 index 0000000..7ef7321 --- /dev/null +++ b/snippets/design-tokens/categories/z-index.md @@ -0,0 +1,24 @@ +/* Z-index scale */ +@theme { + --z-base: 0; + --z-raised: 1; + --z-dropdown: 1000; + --z-sticky: 1010; + --z-fixed: 1020; + --z-overlay: 1030; /* modal backdrop */ + --z-modal: 1050; /* modal content */ + --z-popover: 1060; /* popovers on top of modal */ + --z-tooltip: 1070; /* tooltips highest */ + --z-toast: 1080; /* toast notifications */ + --z-max: 9999; /* nuclear option โ€” use sparingly */ +} + +/* Usage in components */ +@layer components { + .dropdown-menu { z-index: var(--z-dropdown); } + .sticky-header { z-index: var(--z-sticky); } + .modal-overlay { z-index: var(--z-overlay); } + .modal-content { z-index: var(--z-modal); } + .tooltip { z-index: var(--z-tooltip); } + .toast { z-index: var(--z-toast); } +} \ No newline at end of file diff --git a/snippets/design-tokens/procedures/step-1-colors.md b/snippets/design-tokens/procedures/step-1-colors.md new file mode 100644 index 0000000..5577617 --- /dev/null +++ b/snippets/design-tokens/procedures/step-1-colors.md @@ -0,0 +1,30 @@ +/* tailwind.css or global.css */ +@import "tailwindcss"; + +@theme { + /* Brand ramp โ€” H=291 (violet-purple) */ + --color-brand-50: oklch(0.97 0.02 291); + --color-brand-100: oklch(0.94 0.04 291); + --color-brand-200: oklch(0.88 0.06 291); + --color-brand-300: oklch(0.78 0.09 291); + --color-brand-400: oklch(0.70 0.12 291); + --color-brand-500: oklch(0.62 0.14 291); + --color-brand-600: oklch(0.54 0.14 291); + --color-brand-700: oklch(0.46 0.13 291); + --color-brand-800: oklch(0.38 0.11 291); + --color-brand-900: oklch(0.28 0.08 291); + --color-brand-950: oklch(0.18 0.05 291); + + /* Neutral ramp โ€” warm (H=60) */ + --color-neutral-50: oklch(0.98 0.008 60); + --color-neutral-100: oklch(0.96 0.008 60); + --color-neutral-200: oklch(0.92 0.008 60); + --color-neutral-300: oklch(0.84 0.008 60); + --color-neutral-400: oklch(0.72 0.008 60); + --color-neutral-500: oklch(0.58 0.010 60); + --color-neutral-600: oklch(0.46 0.010 60); + --color-neutral-700: oklch(0.35 0.010 60); + --color-neutral-800: oklch(0.26 0.010 60); + --color-neutral-900: oklch(0.18 0.008 60); + --color-neutral-950: oklch(0.12 0.005 60); +} \ No newline at end of file diff --git a/snippets/design-tokens/procedures/step-2-spacing.md b/snippets/design-tokens/procedures/step-2-spacing.md new file mode 100644 index 0000000..78ad593 --- /dev/null +++ b/snippets/design-tokens/procedures/step-2-spacing.md @@ -0,0 +1,52 @@ +:root { + /* Backgrounds */ + --color-bg: var(--color-neutral-50); + --color-bg-subtle: var(--color-neutral-100); + --color-surface: #ffffff; + --color-surface-raised: var(--color-neutral-50); + + /* Borders */ + --color-border: var(--color-neutral-200); + --color-border-strong: var(--color-neutral-300); + --color-border-focus: var(--color-brand-500); + + /* Text */ + --color-text: var(--color-neutral-900); + --color-text-muted: var(--color-neutral-500); + --color-text-subtle: var(--color-neutral-400); + --color-text-inverted: #ffffff; + + /* Interactive */ + --color-primary: var(--color-brand-500); + --color-primary-hover: var(--color-brand-600); + --color-primary-fg: #ffffff; + --color-secondary: var(--color-neutral-200); + --color-secondary-hover: var(--color-neutral-300); + --color-secondary-fg: var(--color-neutral-900); + + /* Status */ + --color-success: oklch(0.55 0.14 145); + --color-success-bg: oklch(0.96 0.04 145); + --color-success-fg: #ffffff; + --color-error: oklch(0.55 0.20 25); + --color-error-bg: oklch(0.97 0.04 25); + --color-error-fg: #ffffff; + --color-warning: oklch(0.65 0.18 75); + --color-warning-bg: oklch(0.97 0.04 75); + --color-warning-fg: oklch(0.20 0.05 75); +} + +.dark { + --color-bg: oklch(0.15 0.008 265); + --color-bg-subtle: oklch(0.18 0.008 265); + --color-surface: oklch(0.21 0.008 265); + --color-surface-raised: oklch(0.24 0.008 265); + --color-border: oklch(0.28 0.008 265); + --color-border-strong: oklch(0.35 0.008 265); + --color-text: oklch(0.95 0.008 265); + --color-text-muted: oklch(0.65 0.008 265); + --color-text-subtle: oklch(0.50 0.008 265); + --color-primary: var(--color-brand-400); + --color-primary-hover: var(--color-brand-300); + --color-primary-fg: oklch(0.15 0.005 291); +} \ No newline at end of file diff --git a/snippets/design-tokens/procedures/step-3-typography.md b/snippets/design-tokens/procedures/step-3-typography.md new file mode 100644 index 0000000..c891820 --- /dev/null +++ b/snippets/design-tokens/procedures/step-3-typography.md @@ -0,0 +1,26 @@ +/* @theme inline READS from CSS vars at runtime */ +/* This means dark mode changes propagate to Tailwind utilities automatically */ +@theme inline { + /* Map semantic tokens to Tailwind utility names */ + --color-background: var(--color-bg); + --color-foreground: var(--color-text); + --color-muted: var(--color-text-muted); + --color-border: var(--color-border); + --color-surface: var(--color-surface); + + --color-primary: var(--color-primary); + --color-primary-foreground: var(--color-primary-fg); + --color-secondary: var(--color-secondary); + --color-secondary-foreground: var(--color-secondary-fg); + + --color-success: var(--color-success); + --color-success-foreground: var(--color-success-fg); + --color-error: var(--color-error); + --color-error-foreground: var(--color-error-fg); + --color-warning: var(--color-warning); + --color-warning-foreground: var(--color-warning-fg); +} + +/* Now these work as Tailwind utilities: */ +/* bg-background, text-foreground, bg-primary, text-primary-foreground */ +/* border-border, text-muted, bg-surface, bg-success, text-error... */ \ No newline at end of file diff --git a/snippets/design-tokens/procedures/step-4-component-sizing.md b/snippets/design-tokens/procedures/step-4-component-sizing.md new file mode 100644 index 0000000..6197a58 --- /dev/null +++ b/snippets/design-tokens/procedures/step-4-component-sizing.md @@ -0,0 +1,21 @@ +@theme { + /* 4px base โ€” sets the multiplier for p-1, p-2, p-4, etc. */ + --spacing: 0.25rem; +} + +/* Named semantic tokens in :root */ +:root { + --spacing-section-y: 6rem; /* 96px */ + --spacing-section-x: 1.5rem; /* 24px mobile, override at md */ + --spacing-card: 1.75rem; /* 28px */ + --spacing-card-sm: 1.25rem; /* 20px */ + --spacing-grid-cards: 1.5rem; /* 24px */ + --spacing-inline: 0.5rem; /* 8px */ + --spacing-form-gap: 1.25rem; /* 20px */ +} + +@media (min-width: 768px) { + :root { + --spacing-section-x: 3rem; /* 48px desktop */ + } +} \ No newline at end of file diff --git a/snippets/design-tokens/procedures/step-5-remaining.md b/snippets/design-tokens/procedures/step-5-remaining.md new file mode 100644 index 0000000..0a11b00 --- /dev/null +++ b/snippets/design-tokens/procedures/step-5-remaining.md @@ -0,0 +1,22 @@ +:root { + --font-sans: "Inter Variable", ui-sans-serif, system-ui, sans-serif; + --font-mono: "JetBrains Mono Variable", ui-monospace, monospace; + + --text-display: clamp(3rem, 5vw + 1rem, 4.5rem); + --text-h1: clamp(2.25rem, 3.5vw + 0.5rem, 3rem); + --text-h2: clamp(1.75rem, 2.5vw + 0.25rem, 2.25rem); + --text-h3: clamp(1.375rem, 1.5vw + 0.25rem, 1.5rem); + --text-body: 1rem; + --text-body-sm: 0.875rem; + --text-caption: 0.75rem; + --text-overline: 0.6875rem; + + --leading-display: 1.05; + --leading-heading: 1.2; + --leading-body: 1.7; + + --tracking-tight: -0.03em; + --tracking-heading: -0.02em; + --tracking-normal: 0em; + --tracking-wide: 0.10em; +} \ No newline at end of file diff --git a/snippets/design-tokens/procedures/step-6-accessibility.md b/snippets/design-tokens/procedures/step-6-accessibility.md new file mode 100644 index 0000000..9d1c6b6 --- /dev/null +++ b/snippets/design-tokens/procedures/step-6-accessibility.md @@ -0,0 +1,23 @@ +:root { + --shadow-xs: 0 1px 2px 0 oklch(0.30 0.02 60 / 0.08); + --shadow-sm: + 0 1px 3px 0 oklch(0.30 0.02 60 / 0.10), + 0 1px 2px -1px oklch(0.30 0.02 60 / 0.06); + --shadow-md: + 0 4px 6px -1px oklch(0.30 0.02 60 / 0.10), + 0 2px 4px -2px oklch(0.30 0.02 60 / 0.06); + --shadow-lg: + 0 10px 15px -3px oklch(0.30 0.02 60 / 0.10), + 0 4px 6px -4px oklch(0.30 0.02 60 / 0.05); + --shadow-xl: + 0 20px 25px -5px oklch(0.30 0.02 60 / 0.10), + 0 8px 10px -6px oklch(0.30 0.02 60 / 0.04); +} + +.dark { + --shadow-xs: none; --shadow-sm: none; + --shadow-md: none; --shadow-lg: none; --shadow-xl: none; + --color-surface-1: oklch(0.22 0.008 265); + --color-surface-2: oklch(0.26 0.008 265); + --color-surface-3: oklch(0.30 0.008 265); +} \ No newline at end of file diff --git a/snippets/design-tokens/procedures/step-7-validation.md b/snippets/design-tokens/procedures/step-7-validation.md new file mode 100644 index 0000000..2783a26 --- /dev/null +++ b/snippets/design-tokens/procedures/step-7-validation.md @@ -0,0 +1,23 @@ +@theme { + --duration-fast: 100ms; + --duration-normal: 200ms; + --duration-slow: 300ms; + --ease-out: cubic-bezier(0, 0, 0.2, 1); + --ease-in: cubic-bezier(0.4, 0, 1, 1); + --ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1); + + --z-dropdown: 1000; + --z-sticky: 1010; + --z-modal: 1050; + --z-tooltip: 1070; + --z-toast: 1080; +} + +@layer base { + @media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } + } +} \ No newline at end of file diff --git a/snippets/design-tokens/procedures/step-8-deliverables.md b/snippets/design-tokens/procedures/step-8-deliverables.md new file mode 100644 index 0000000..fb03a18 --- /dev/null +++ b/snippets/design-tokens/procedures/step-8-deliverables.md @@ -0,0 +1,15 @@ +/* Run contrast checks on these pairs: */ +/* --color-text on --color-bg โ†’ target 7:1 (AAA) */ +/* --color-text-muted on --color-bg โ†’ target 4.5:1 (AA) */ +/* --color-primary-fg on --color-primary โ†’ target 4.5:1 (AA) */ +/* --color-success-fg on --color-success โ†’ target 4.5:1 (AA) */ +/* --color-error-fg on --color-error โ†’ target 4.5:1 (AA) */ +/* --color-border on --color-bg โ†’ target 1.1:1+ (perceptible) */ + +/* Tools: */ +/* - https://oklch.com โ€” build and preview oklch colors */ +/* - https://www.myndex.com/APCA/ โ€” APCA contrast checker */ +/* - Storybook a11y addon โ€” automated component-level checks */ + +/* Check for stale hue references: */ +/* grep -r "violet-\|purple-\|blue-" src/ (old hardcoded Tailwind hue names) */ \ No newline at end of file diff --git a/snippets/design-tokens/references/color-roles.md b/snippets/design-tokens/references/color-roles.md new file mode 100644 index 0000000..d2cb504 --- /dev/null +++ b/snippets/design-tokens/references/color-roles.md @@ -0,0 +1,58 @@ +# Color Ramp Role Map + +Per-stop semantic roles for all three ramps. + +## Brand Ramp (--color-brand-*) + +| Stop | Light mode role | Dark mode role | +|------|----------------|---------------| +| 50 | Section bg tint, hover states | โ€” | +| 100 | Badge/tag fills, subtle backgrounds | Text on dark fills | +| 200 | Selected row bg, active tab bg, UI element fill | Subtitle text on dark fills | +| 300 | Hovered UI element, input focus border | โ€” | +| 400 | Active nav pill, toggle-on state | Primary text on dark surfaces | +| 500 | Icon color, brand fill, solid background | Brighter solid for dark mode | +| 600 | Button background, link text, primary action | โ€” | +| 700 | Dark CTA background, headings on color | โ€” | +| 800 | Title text on 50/100 backgrounds | โ€” | +| 900 | Footer, deep accents, dark surfaces | Base-level dark card | +| 950 | Darkest โ€” dark mode page background | โ€” | + +## Pop/Accent Ramp (--color-pop-*) + +| Stop | Role | +|------|------| +| 50 | Soft tint (eyebrow badge bg, swap state) | +| 100 | Badge fill, notification dot bg | +| 200 | Light decorative fill | +| 300 | Hover state for pop elements | +| 400 | Border accent (badge border, active accent) | +| 500 | Secondary button fill, callout accent | +| 600 | Hover/active for pop buttons | +| 700 | Label text on pop-50/100 fills (AA contrast) | +| 800โ€“950 | Dark mode reversed (800=text, 950=bg) | + +## Neutral Ramp (--color-warm-* / --color-neutral-*) + +| Stop | Semantic mapping | +|------|-----------------| +| 50 | --background (light) โ€” page bg | +| 100 | --muted โ€” alternate section bg, inset panels | +| 200 | --border โ€” dividers, input outlines | +| 300 | Stronger border โ€” hover, active outlines | +| 400 | Placeholder text | +| 500 | --muted-foreground โ€” secondary text | +| 600 | Tertiary text, footer in light mode | +| 700 | --muted (dark) โ€” dark mode muted surfaces | +| 800 | --card (dark) โ€” dark mode card background | +| 900 | --background (dark) โ€” dark mode page bg | +| 950 | Deepest โ€” dark mode overlays | + +## Ramp Construction Rules + +1. All 11 stops (50โ€“950) required โ€” no gaps +2. Lightness (L) monotonically decreasing 50โ†’950 +3. Chroma (C) peaks at 400โ€“600, tapers at extremes +4. Hue (H) stays within ยฑ5ยฐ across the ramp +5. Neutrals: chroma 0.003โ€“0.015 (lower reads as cold gray) +6. Text stops (700โ€“900) must achieve 4.5:1 on fill stops (50โ€“200) diff --git a/snippets/design-tokens/references/token-checklist.md b/snippets/design-tokens/references/token-checklist.md new file mode 100644 index 0000000..a0ea181 --- /dev/null +++ b/snippets/design-tokens/references/token-checklist.md @@ -0,0 +1,58 @@ +# Complete Token Categories โ€” Production Checklist + +Every production design system needs all 10 categories. Missing any = inconsistency. + +## 1. Colors +- [ ] 3 ramps ร— 11 stops = 33 primitive tokens (@theme) +- [ ] 19+ semantic roles in :root/.dark (background, foreground, card, primary, muted, border, ring...) +- [ ] Status: success, warning, error, info (+ foreground for each) +- [ ] Chart: chart-1 through chart-8 +- [ ] Sidebar tokens: sidebar, sidebar-foreground, sidebar-primary... +- [ ] All :root tokens have .dark overrides + +## 2. Spacing +- [ ] --spacing: 0.25rem (base multiplier) +- [ ] Named semantic tokens: section-y, section-x, card, grid-cards, stack, inline +- [ ] Responsive variants: -tablet, -mobile + +## 3. Typography +- [ ] Font families: --font-sans, --font-mono +- [ ] Type scale: display through overline (10 stops) +- [ ] Line heights: per role +- [ ] Font weights: 400/500/600/700/800 +- [ ] Tracking scale: tighter through widest (7 stops) + +## 4. Component Sizing +- [ ] Buttons: sm/md/lg/xl +- [ ] Inputs: sm/md/lg +- [ ] Icons: xs/sm/md/lg/xl +- [ ] Avatars: xs/sm/md/lg/xl +- [ ] Touch targets: --size-touch-min (44px), --size-touch-comfortable (48px) +- [ ] Nav height, page-max, content-max, prose-max + +## 5. Border Radius +- [ ] Scale: none/sm/md/DEFAULT/lg/xl/2xl/3xl/pill +- [ ] Rule: buttons=pill, cards=lg, inputs=DEFAULT + +## 6. Shadows/Elevation +- [ ] Scale: xs/sm/md/lg/xl/2xl/inner/none +- [ ] Component shadows: card, card-hover, button, nav +- [ ] Surface levels: 0-4 (bg+shadow combined) +- [ ] Dark mode: bg-color elevation (not shadow) + +## 7. Motion +- [ ] Duration: instant/fast/normal/slow/slower/slowest +- [ ] Easing: default/in/out/bounce/spring/snappy +- [ ] prefers-reduced-motion in @layer base with !important + +## 8. Z-Index +- [ ] Scale: base/dropdown(1000)/sticky(1020)/fixed(1030)/modal-backdrop(1040)/modal(1050)/popover(1060)/tooltip(1070)/toast(1080)/max(9999) + +## 9. Opacity +- [ ] disabled(0.5)/muted(0.6)/overlay(0.45)/glass(0.80)/ghost(0.06) + +## 10. Grid/Layout +- [ ] --grid-columns: 12 +- [ ] --grid-gutter, --grid-gutter-sm +- [ ] --grid-margin, --grid-margin-md, --grid-margin-sm +- [ ] Density modes: compact(0.75ร—), comfortable(1.125ร—) diff --git a/snippets/design-tokens/templates/colors-tailwind-v4.md b/snippets/design-tokens/templates/colors-tailwind-v4.md new file mode 100644 index 0000000..a136506 --- /dev/null +++ b/snippets/design-tokens/templates/colors-tailwind-v4.md @@ -0,0 +1,86 @@ +/* ============================================================ + DESIGN TOKEN SYSTEM โ€” Colors (Tailwind v4 + shadcn/ui) + Architecture: @theme primitives โ†’ :root/:dark semantics โ†’ @theme inline utilities + ============================================================ */ + +@import "tailwindcss"; + +/* โ”€โ”€ Layer 1: Primitive ramps โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +@theme { + /* Brand ramp โ€” change hue angle here to swap palette globally */ + --color-brand-50: oklch(0.97 0.02 291); + --color-brand-100: oklch(0.94 0.04 291); + --color-brand-200: oklch(0.88 0.06 291); + --color-brand-300: oklch(0.78 0.09 291); + --color-brand-400: oklch(0.70 0.12 291); + --color-brand-500: oklch(0.62 0.14 291); /* default primary */ + --color-brand-600: oklch(0.54 0.14 291); /* hover */ + --color-brand-700: oklch(0.46 0.13 291); + --color-brand-800: oklch(0.38 0.11 291); + --color-brand-900: oklch(0.28 0.08 291); + --color-brand-950: oklch(0.18 0.05 291); + + /* Warm neutral ramp */ + --color-neutral-50: oklch(0.98 0.008 60); + --color-neutral-100: oklch(0.96 0.008 60); + --color-neutral-200: oklch(0.92 0.008 60); + --color-neutral-300: oklch(0.84 0.008 60); + --color-neutral-400: oklch(0.72 0.008 60); + --color-neutral-500: oklch(0.58 0.010 60); + --color-neutral-600: oklch(0.46 0.010 60); + --color-neutral-700: oklch(0.35 0.010 60); + --color-neutral-800: oklch(0.26 0.010 60); + --color-neutral-900: oklch(0.18 0.008 60); + --color-neutral-950: oklch(0.12 0.005 60); +} + +/* โ”€โ”€ Layer 2: Semantic tokens โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +:root { + --color-bg: var(--color-neutral-50); + --color-bg-subtle: var(--color-neutral-100); + --color-surface: #ffffff; + --color-border: var(--color-neutral-200); + --color-border-strong: var(--color-neutral-300); + --color-text: var(--color-neutral-900); + --color-text-muted: var(--color-neutral-500); + --color-primary: var(--color-brand-500); + --color-primary-hover: var(--color-brand-600); + --color-primary-fg: #ffffff; + --color-success: oklch(0.55 0.14 145); + --color-success-bg: oklch(0.96 0.04 145); + --color-error: oklch(0.55 0.20 25); + --color-error-bg: oklch(0.97 0.04 25); + --color-warning: oklch(0.65 0.18 75); + --color-warning-bg: oklch(0.97 0.04 75); + --color-warning-fg: oklch(0.20 0.05 75); +} + +.dark { + --color-bg: oklch(0.15 0.008 265); + --color-bg-subtle: oklch(0.18 0.008 265); + --color-surface: oklch(0.21 0.008 265); + --color-border: oklch(0.28 0.008 265); + --color-border-strong: oklch(0.35 0.008 265); + --color-text: oklch(0.95 0.008 265); + --color-text-muted: oklch(0.65 0.008 265); + --color-primary: var(--color-brand-400); + --color-primary-hover: var(--color-brand-300); + --color-primary-fg: oklch(0.15 0.005 291); +} + +/* โ”€โ”€ Layer 3: Tailwind utility bridge โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ +@theme inline { + --color-background: var(--color-bg); + --color-foreground: var(--color-text); + --color-muted: var(--color-text-muted); + --color-border: var(--color-border); + --color-surface: var(--color-surface); + --color-primary: var(--color-primary); + --color-primary-foreground: var(--color-primary-fg); + --color-success: var(--color-success); + --color-error: var(--color-error); + --color-warning: var(--color-warning); +} + +/* Dark mode variant */ +@custom-variant dark (&:is(.dark *)); \ No newline at end of file diff --git a/snippets/design-tokens/templates/motion.md b/snippets/design-tokens/templates/motion.md new file mode 100644 index 0000000..567cbd4 --- /dev/null +++ b/snippets/design-tokens/templates/motion.md @@ -0,0 +1,45 @@ +/* ============================================================ + DESIGN TOKEN SYSTEM โ€” Motion + ============================================================ */ + +@theme { + /* Durations */ + --duration-instant: 50ms; + --duration-fast: 100ms; + --duration-normal: 200ms; + --duration-slow: 300ms; + --duration-slower: 400ms; + --duration-slowest: 600ms; + + /* Easing */ + --ease-out: cubic-bezier(0, 0, 0.2, 1); /* entering */ + --ease-in: cubic-bezier(0.4, 0, 1, 1); /* exiting */ + --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); /* repositioning */ + --ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1); /* playful */ + --ease-spring: cubic-bezier(0.175, 0.885, 0.32, 1.275); + --ease-snappy: cubic-bezier(0.2, 0, 0, 1); + --ease-linear: linear; /* progress bars only */ +} + +:root { + --transition-hover: background-color var(--duration-fast) var(--ease-out), + color var(--duration-fast) var(--ease-out), + border-color var(--duration-fast) var(--ease-out), + opacity var(--duration-fast) var(--ease-out); + --transition-transform: transform var(--duration-normal) var(--ease-out); + --transition-panel: transform var(--duration-slow) var(--ease-out), + opacity var(--duration-slow) var(--ease-out); + --transition-fade: opacity var(--duration-normal) var(--ease-in-out); +} + +/* REQUIRED โ€” prefers-reduced-motion */ +@layer base { + @media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + } +} \ No newline at end of file diff --git a/snippets/design-tokens/templates/spacing.md b/snippets/design-tokens/templates/spacing.md new file mode 100644 index 0000000..07b1517 --- /dev/null +++ b/snippets/design-tokens/templates/spacing.md @@ -0,0 +1,34 @@ +/* ============================================================ + DESIGN TOKEN SYSTEM โ€” Spacing (4px grid + named semantics) + ============================================================ */ + +@theme { + --spacing: 0.25rem; /* 4px base multiplier */ +} + +:root { + /* Section-level spacing */ + --spacing-section-y: 6rem; /* 96px */ + --spacing-section-x: 1.5rem; /* 24px mobile */ + + /* Component spacing */ + --spacing-card: 1.75rem; /* 28px */ + --spacing-card-sm: 1.25rem; /* 20px */ + --spacing-grid-cards: 1.5rem; /* 24px */ + --spacing-grid-cards-lg: 2rem; /* 32px */ + --spacing-inline: 0.5rem; /* 8px โ€” icon + label */ + --spacing-form-gap: 1.25rem; /* 20px */ + --spacing-nav-x: 1.5rem; /* 24px */ + --spacing-nav-y: 1rem; /* 16px */ + + /* Container widths */ + --spacing-container-max: 80rem; /* 1280px */ + --spacing-container-md: 65rem; /* 1040px */ + --spacing-container-narrow: 42rem; /* 672px */ +} + +@media (min-width: 768px) { + :root { + --spacing-section-x: 3rem; /* 48px desktop */ + } +} \ No newline at end of file diff --git a/snippets/design-tokens/templates/typography.md b/snippets/design-tokens/templates/typography.md new file mode 100644 index 0000000..813aac2 --- /dev/null +++ b/snippets/design-tokens/templates/typography.md @@ -0,0 +1,80 @@ +/* ============================================================ + DESIGN TOKEN SYSTEM โ€” Typography + ============================================================ */ + +:root { + /* Font stacks */ + --font-sans: "Inter Variable", ui-sans-serif, system-ui, sans-serif; + --font-mono: "JetBrains Mono Variable", ui-monospace, monospace; + + /* Fluid headings via clamp() โ€” body stays fixed */ + --text-display: clamp(3rem, 5vw + 1rem, 4.5rem); /* 48โ€“72px */ + --text-h1: clamp(2.25rem, 3.5vw + 0.5rem, 3rem); /* 36โ€“48px */ + --text-h2: clamp(1.75rem, 2.5vw + 0.25rem, 2.25rem); /* 28โ€“36px */ + --text-h3: clamp(1.375rem, 1.5vw + 0.25rem, 1.5rem); /* 22โ€“24px */ + --text-subtitle: 1.125rem; /* 18px โ€” fixed */ + --text-body: 1rem; /* 16px โ€” NEVER fluid */ + --text-body-sm: 0.875rem; /* 14px */ + --text-caption: 0.75rem; /* 12px */ + --text-overline: 0.6875rem;/* 11px */ + + /* Line heights */ + --leading-display: 1.05; + --leading-heading: 1.2; + --leading-subtitle: 1.35; + --leading-body: 1.7; + --leading-body-sm: 1.6; + --leading-caption: 1.5; + + /* Letter spacing */ + --tracking-tight: -0.03em; + --tracking-heading: -0.02em; + --tracking-snug: -0.01em; + --tracking-normal: 0em; + --tracking-wide: 0.06em; + --tracking-wider: 0.10em; + + /* Weights */ + --font-weight-display: 800; + --font-weight-heading: 700; + --font-weight-subheading: 600; + --font-weight-body: 400; + --font-weight-strong: 500; + + /* Prose */ + --prose-width: 65ch; +} + +@layer components { + .text-display { + font-size: var(--text-display); + line-height: var(--leading-display); + letter-spacing: var(--tracking-tight); + font-weight: var(--font-weight-display); + } + .text-h1 { + font-size: var(--text-h1); + line-height: var(--leading-heading); + letter-spacing: var(--tracking-heading); + font-weight: var(--font-weight-heading); + } + .text-h2 { + font-size: var(--text-h2); + line-height: var(--leading-heading); + letter-spacing: var(--tracking-heading); + font-weight: var(--font-weight-heading); + } + .text-h3 { + font-size: var(--text-h3); + line-height: var(--leading-subtitle); + letter-spacing: var(--tracking-snug); + font-weight: var(--font-weight-subheading); + } + .text-overline { + font-size: var(--text-overline); + line-height: var(--leading-caption); + letter-spacing: var(--tracking-wider); + font-weight: var(--font-weight-subheading); + text-transform: uppercase; + } +} \ No newline at end of file diff --git a/snippets/echo/cheatsheet.md b/snippets/echo/cheatsheet.md new file mode 100644 index 0000000..884b86a --- /dev/null +++ b/snippets/echo/cheatsheet.md @@ -0,0 +1,130 @@ +# Echo Framework Quick Reference + +## Setup + +```go +e := echo.New() +e.Use(middleware.Logger()) +e.Use(middleware.Recover()) +e.Logger.Fatal(e.Start(":8080")) +``` + +## Routing + +```go +e.GET("/users", handler) +e.POST("/users", handler) +e.PUT("/users/:id", handler) +e.PATCH("/users/:id", handler) +e.DELETE("/users/:id", handler) +e.Any("/path", handler) // all methods + +id := c.Param("id") // path param +q := c.QueryParam("search") // query string +``` + +## Groups & Middleware + +```go +v1 := e.Group("/v1") +v1.Use(myMiddleware) +v1.GET("/users", handler) + +// Pre-router middleware (before routing) +e.Pre(middleware.HTTPSRedirect()) +``` + +## Request Binding + +```go +type CreateReq struct { Name string `json:"name"` } +var req CreateReq +if err := c.Bind(&req); err != nil { return err } + +// Path param binder (typed) +id := 0 +echo.PathParamsBinder(c).Int("id", &id).BindError() +``` + +## Responses + +```go +c.JSON(200, data) +c.String(200, "hello") +c.NoContent(204) +c.Redirect(301, "/new-path") +c.File("public/index.html") +c.Attachment("path/to/file", "filename.pdf") // download +c.Inline("path/to/file", "filename.pdf") // browser display +c.JSONP(200, "callback", data) +``` + +## Error Handling + +```go +return echo.NewHTTPError(400, "bad request") +return echo.ErrUnauthorized +return echo.ErrNotFound + +// Custom error handler +e.HTTPErrorHandler = func(err error, c echo.Context) { + code := http.StatusInternalServerError + var he *echo.HTTPError + if errors.As(err, &he) { code = he.Code } + c.JSON(code, map[string]any{"error": err.Error()}) +} +``` + +## Middleware Order (Production) + +``` +Logger โ†’ Recover โ†’ CORS โ†’ RateLimiter โ†’ RequestID โ†’ Auth โ†’ Custom +``` + +## Graceful Shutdown + +```go +go func() { + if err := e.Start(":8080"); err != nil && err != http.ErrServerClosed { + e.Logger.Fatal(err) + } +}() +quit := make(chan os.Signal, 1) +signal.Notify(quit, os.Interrupt, syscall.SIGTERM) +<-quit +ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) +defer cancel() +e.Shutdown(ctx) +``` + +## TLS + +```go +// Auto TLS (Let's Encrypt) +e.AutoTLSManager.HostPolicy = autocert.HostWhitelist("example.com") +e.AutoTLSManager.Cache = autocert.DirCache("/var/www/.cache") +e.Logger.Fatal(e.StartAutoTLS(":443")) + +// Manual TLS +e.Logger.Fatal(e.StartTLS(":443", "cert.pem", "key.pem")) +``` + +## SSE Headers (Required) + +```go +c.Response().Header().Set("Content-Type", "text/event-stream") +c.Response().Header().Set("Cache-Control", "no-cache") +c.Response().Header().Set("Connection", "keep-alive") +// After each write: +fmt.Fprintf(c.Response(), "data: %s\n\n", payload) +c.Response().Flush() +``` + +## Key Gotchas + +- `c.Bind()` reads body **once** โ€” never call twice +- `Logger` MUST come before `Recover` +- `Flush()` is REQUIRED for SSE and streaming responses +- Disable Gzip on SSE/streaming routes +- `echo.Context` is NOT goroutine-safe โ€” copy values before goroutines +- `AllowCredentials: true` + wildcard origins = browser rejection \ No newline at end of file diff --git a/snippets/echo/middleware/basic-auth.md b/snippets/echo/middleware/basic-auth.md new file mode 100644 index 0000000..3228cbc --- /dev/null +++ b/snippets/echo/middleware/basic-auth.md @@ -0,0 +1,8 @@ +group.Use(middleware.BasicAuth(func(username, password string, c echo.Context) (bool, error) { + // Use constant-time comparison to prevent timing attacks + if subtle.ConstantTimeCompare([]byte(username), []byte("admin")) == 1 && + subtle.ConstantTimeCompare([]byte(password), []byte("secret")) == 1 { + return true, nil + } + return false, nil +})) diff --git a/snippets/echo/middleware/body-limit.md b/snippets/echo/middleware/body-limit.md new file mode 100644 index 0000000..a3a975f --- /dev/null +++ b/snippets/echo/middleware/body-limit.md @@ -0,0 +1 @@ +e.Use(middleware.BodyLimit("2M")) // supports B, K, M, G suffixes diff --git a/snippets/echo/middleware/cors.md b/snippets/echo/middleware/cors.md new file mode 100644 index 0000000..cebac71 --- /dev/null +++ b/snippets/echo/middleware/cors.md @@ -0,0 +1,4 @@ +e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ + AllowOrigins: []string{"https://example.com"}, + AllowMethods: []string{echo.GET, echo.POST, echo.PUT, echo.DELETE}, +})) \ No newline at end of file diff --git a/snippets/echo/middleware/csrf.md b/snippets/echo/middleware/csrf.md new file mode 100644 index 0000000..6da5dfd --- /dev/null +++ b/snippets/echo/middleware/csrf.md @@ -0,0 +1,3 @@ +e.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{ + TokenLookup: "header:X-CSRF-Token", +})) \ No newline at end of file diff --git a/snippets/echo/middleware/gzip.md b/snippets/echo/middleware/gzip.md new file mode 100644 index 0000000..207aadf --- /dev/null +++ b/snippets/echo/middleware/gzip.md @@ -0,0 +1,3 @@ +e.Use(middleware.GzipWithConfig(middleware.GzipConfig{ + Level: 5, // 1=fast, 9=best compression +})) \ No newline at end of file diff --git a/snippets/echo/middleware/jwt.md b/snippets/echo/middleware/jwt.md new file mode 100644 index 0000000..ebb562f --- /dev/null +++ b/snippets/echo/middleware/jwt.md @@ -0,0 +1,5 @@ +import echojwt "github.com/labstack/echo-jwt/v4" + +group.Use(echojwt.WithConfig(echojwt.Config{ + SigningKey: []byte("secret"), +})) \ No newline at end of file diff --git a/snippets/echo/middleware/key-auth.md b/snippets/echo/middleware/key-auth.md new file mode 100644 index 0000000..e5eea2e --- /dev/null +++ b/snippets/echo/middleware/key-auth.md @@ -0,0 +1,6 @@ +e.Use(middleware.KeyAuthWithConfig(middleware.KeyAuthConfig{ + KeyLookup: "header:X-API-Key", + Validator: func(key string, c echo.Context) (bool, error) { + return key == os.Getenv("API_KEY"), nil + }, +})) diff --git a/snippets/echo/middleware/logger.md b/snippets/echo/middleware/logger.md new file mode 100644 index 0000000..20b4440 --- /dev/null +++ b/snippets/echo/middleware/logger.md @@ -0,0 +1,3 @@ +e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ + Format: "${method} ${uri} ${status} ${latency_human}\n", +})) \ No newline at end of file diff --git a/snippets/echo/middleware/rate-limiter.md b/snippets/echo/middleware/rate-limiter.md new file mode 100644 index 0000000..db533fd --- /dev/null +++ b/snippets/echo/middleware/rate-limiter.md @@ -0,0 +1,22 @@ +import "golang.org/x/time/rate" + +e.Use(middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{ + Skipper: middleware.DefaultSkipper, + Store: middleware.NewRateLimiterMemoryStoreWithConfig( + middleware.RateLimiterMemoryStoreConfig{ + Rate: rate.Limit(10), // 10 req/s + Burst: 30, + ExpiresIn: 3 * time.Minute, + }, + ), + IdentifierExtractor: func(ctx echo.Context) (string, error) { + id := ctx.RealIP() + return id, nil + }, + ErrorHandler: func(context echo.Context, err error) error { + return context.JSON(http.StatusTooManyRequests, map[string]string{"error": "rate limit exceeded"}) + }, + DenyHandler: func(context echo.Context, identifier string, err error) error { + return context.JSON(http.StatusTooManyRequests, map[string]string{"error": "rate limit exceeded"}) + }, +})) \ No newline at end of file diff --git a/snippets/echo/middleware/recover.md b/snippets/echo/middleware/recover.md new file mode 100644 index 0000000..4301ee7 --- /dev/null +++ b/snippets/echo/middleware/recover.md @@ -0,0 +1 @@ +e.Use(middleware.Recover()) \ No newline at end of file diff --git a/snippets/echo/middleware/request-id.md b/snippets/echo/middleware/request-id.md new file mode 100644 index 0000000..6f134da --- /dev/null +++ b/snippets/echo/middleware/request-id.md @@ -0,0 +1,3 @@ +e.Use(middleware.RequestID()) +// Access in handler: +// reqID := c.Response().Header().Get(echo.HeaderXRequestID) \ No newline at end of file diff --git a/snippets/echo/middleware/secure.md b/snippets/echo/middleware/secure.md new file mode 100644 index 0000000..a46ac4d --- /dev/null +++ b/snippets/echo/middleware/secure.md @@ -0,0 +1,6 @@ +e.Use(middleware.SecureWithConfig(middleware.SecureConfig{ + XSSProtection: "1; mode=block", + ContentTypeNosniff: "nosniff", + XFrameOptions: "SAMEORIGIN", + HSTSMaxAge: 31536000, +})) \ No newline at end of file diff --git a/snippets/echo/middleware/timeout.md b/snippets/echo/middleware/timeout.md new file mode 100644 index 0000000..72fe612 --- /dev/null +++ b/snippets/echo/middleware/timeout.md @@ -0,0 +1,3 @@ +e.Use(middleware.TimeoutWithConfig(middleware.TimeoutConfig{ + Timeout: 30 * time.Second, +})) \ No newline at end of file diff --git a/snippets/echo/recipes/auto-tls.md b/snippets/echo/recipes/auto-tls.md new file mode 100644 index 0000000..4e9675d --- /dev/null +++ b/snippets/echo/recipes/auto-tls.md @@ -0,0 +1,37 @@ +package main + +import ( + "net/http" + + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "golang.org/x/crypto/acme/autocert" +) + +func main() { + e := echo.New() + e.AutoTLSManager.Prompt = autocert.AcceptTOS + e.AutoTLSManager.HostPolicy = autocert.HostWhitelist("example.com", "www.example.com") + // Certificates are cached to disk + e.AutoTLSManager.Cache = autocert.DirCache("/var/www/.cache") + + e.Use(middleware.Logger()) + e.Use(middleware.Recover()) + + // Redirect HTTP โ†’ HTTPS + e.Pre(middleware.HTTPSRedirect()) + + e.GET("/", func(c echo.Context) error { + return c.String(http.StatusOK, "Hello, TLS!") + }) + + // Start HTTP server for ACME challenge + redirect + go func() { + if err := e.Start(":80"); err != http.ErrServerClosed { + e.Logger.Fatal(err) + } + }() + + // Start HTTPS server + e.Logger.Fatal(e.StartAutoTLS(":443")) +} \ No newline at end of file diff --git a/snippets/echo/recipes/cors.md b/snippets/echo/recipes/cors.md new file mode 100644 index 0000000..5fa648f --- /dev/null +++ b/snippets/echo/recipes/cors.md @@ -0,0 +1,32 @@ +package main + +import ( + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" +) + +func main() { + e := echo.New() + + e.Use(middleware.Logger()) + e.Use(middleware.Recover()) + + // Permissive CORS for development + e.Use(middleware.CORS()) + + // OR: Production CORS with explicit config + e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ + AllowOrigins: []string{"https://app.example.com", "https://admin.example.com"}, + AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}, + AllowHeaders: []string{"Authorization", "Content-Type", "X-Request-ID"}, + ExposeHeaders: []string{"X-Request-ID"}, + AllowCredentials: true, + MaxAge: 86400, // 24 hours preflight cache + })) + + e.GET("/api/data", func(c echo.Context) error { + return c.JSON(200, map[string]string{"status": "ok"}) + }) + + e.Logger.Fatal(e.Start(":8080")) +} \ No newline at end of file diff --git a/snippets/echo/recipes/crud-api.md b/snippets/echo/recipes/crud-api.md new file mode 100644 index 0000000..67d7b5b --- /dev/null +++ b/snippets/echo/recipes/crud-api.md @@ -0,0 +1,93 @@ +package main + +import ( + "net/http" + + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" +) + +type User struct { + ID int `json:"id"` + Name string `json:"name"` + Email string `json:"email"` +} + +var users = map[int]User{ + 1: {ID: 1, Name: "Alice", Email: "alice@example.com"}, +} +var nextID = 2 + +func getUsers(c echo.Context) error { + list := make([]User, 0, len(users)) + for _, u := range users { + list = append(list, u) + } + return c.JSON(http.StatusOK, list) +} + +func getUser(c echo.Context) error { + id := 0 + if err := echo.PathParamsBinder(c).Int("id", &id).BindError(); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "invalid id") + } + u, ok := users[id] + if !ok { + return echo.NewHTTPError(http.StatusNotFound, "user not found") + } + return c.JSON(http.StatusOK, u) +} + +func createUser(c echo.Context) error { + u := new(User) + if err := c.Bind(u); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + if err := c.Validate(u); err != nil { + return err + } + u.ID = nextID + nextID++ + users[u.ID] = *u + return c.JSON(http.StatusCreated, u) +} + +func updateUser(c echo.Context) error { + id := 0 + if err := echo.PathParamsBinder(c).Int("id", &id).BindError(); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "invalid id") + } + if _, ok := users[id]; !ok { + return echo.NewHTTPError(http.StatusNotFound, "user not found") + } + u := new(User) + if err := c.Bind(u); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + u.ID = id + users[id] = *u + return c.JSON(http.StatusOK, u) +} + +func deleteUser(c echo.Context) error { + id := 0 + if err := echo.PathParamsBinder(c).Int("id", &id).BindError(); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "invalid id") + } + delete(users, id) + return c.NoContent(http.StatusNoContent) +} + +func main() { + e := echo.New() + e.Use(middleware.Logger()) + e.Use(middleware.Recover()) + + e.GET("/users", getUsers) + e.POST("/users", createUser) + e.GET("/users/:id", getUser) + e.PUT("/users/:id", updateUser) + e.DELETE("/users/:id", deleteUser) + + e.Logger.Fatal(e.Start(":8080")) +} \ No newline at end of file diff --git a/snippets/echo/recipes/embed-resources.md b/snippets/echo/recipes/embed-resources.md new file mode 100644 index 0000000..1b63e95 --- /dev/null +++ b/snippets/echo/recipes/embed-resources.md @@ -0,0 +1,34 @@ +package main + +import ( + "embed" + "io/fs" + "net/http" + + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" +) + +//go:embed public +var publicFS embed.FS + +func main() { + e := echo.New() + e.Use(middleware.Logger()) + e.Use(middleware.Recover()) + + // Serve embedded static files โ€” no external filesystem needed at runtime + subFS, err := fs.Sub(publicFS, "public") + if err != nil { + e.Logger.Fatal(err) + } + + e.GET("/", func(c echo.Context) error { + return c.File("public/index.html") + }) + + // Serve entire embedded directory under /static + e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", http.FileServer(http.FS(subFS))))) + + e.Logger.Fatal(e.Start(":8080")) +} \ No newline at end of file diff --git a/snippets/echo/recipes/file-download.md b/snippets/echo/recipes/file-download.md new file mode 100644 index 0000000..b332139 --- /dev/null +++ b/snippets/echo/recipes/file-download.md @@ -0,0 +1,49 @@ +package main + +import ( + "net/http" + + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" +) + +func downloadFile(c echo.Context) error { + filename := c.Param("filename") + + // IMPORTANT: Validate/sanitize filename to prevent path traversal + // In production, look up the file from a database by ID, not by user-supplied name + filePath := "uploads/" + filename + + // c.Attachment() sets Content-Disposition: attachment โ€” triggers browser download dialog + return c.Attachment(filePath, filename) +} + +func viewFile(c echo.Context) error { + filename := c.Param("filename") + filePath := "uploads/" + filename + + // c.Inline() sets Content-Disposition: inline โ€” browser renders if it can + return c.Inline(filePath, filename) +} + +func serveFile(c echo.Context) error { + // For general static file serving with cache headers + return c.File("public/index.html") +} + +func main() { + e := echo.New() + e.Use(middleware.Logger()) + e.Use(middleware.Recover()) + + e.GET("/download/:filename", downloadFile) + e.GET("/view/:filename", viewFile) + e.GET("/", serveFile) + + // Serve entire directory + e.Static("/static", "assets") + + if err := e.Start(":8080"); err != http.ErrServerClosed { + e.Logger.Fatal(err) + } +} \ No newline at end of file diff --git a/snippets/echo/recipes/file-upload.md b/snippets/echo/recipes/file-upload.md new file mode 100644 index 0000000..08e287d --- /dev/null +++ b/snippets/echo/recipes/file-upload.md @@ -0,0 +1,66 @@ +package main + +import ( + "fmt" + "io" + "net/http" + "os" + "path/filepath" + + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" +) + +func uploadFile(c echo.Context) error { + // Retrieve the file from form data + file, err := c.FormFile("file") + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "file is required") + } + + // Validate file size (10 MB limit) + if file.Size > 10<<20 { + return echo.NewHTTPError(http.StatusRequestEntityTooLarge, "file too large") + } + + // Open the uploaded file + src, err := file.Open() + if err != nil { + return err + } + defer src.Close() + + // Create destination file โ€” sanitize the filename + safeFilename := filepath.Base(file.Filename) + dst, err := os.Create(fmt.Sprintf("uploads/%s", safeFilename)) + if err != nil { + return err + } + defer dst.Close() + + // Copy content + if _, err = io.Copy(dst, src); err != nil { + return err + } + + return c.JSON(http.StatusOK, map[string]string{ + "filename": safeFilename, + "size": fmt.Sprintf("%d bytes", file.Size), + }) +} + +func main() { + e := echo.New() + e.Use(middleware.Logger()) + e.Use(middleware.Recover()) + + // Limit request body size at middleware level + e.Use(middleware.BodyLimit("10M")) + + e.POST("/upload", uploadFile) + + // Ensure upload directory exists + os.MkdirAll("uploads", 0755) + + e.Logger.Fatal(e.Start(":8080")) +} \ No newline at end of file diff --git a/snippets/echo/recipes/graceful-shutdown.md b/snippets/echo/recipes/graceful-shutdown.md new file mode 100644 index 0000000..bd5d531 --- /dev/null +++ b/snippets/echo/recipes/graceful-shutdown.md @@ -0,0 +1,44 @@ +package main + +import ( + "context" + "net/http" + "os" + "os/signal" + "time" + + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" +) + +func main() { + e := echo.New() + e.Use(middleware.Logger()) + e.Use(middleware.Recover()) + + e.GET("/", func(c echo.Context) error { + // Simulate a slow handler + time.Sleep(2 * time.Second) + return c.String(http.StatusOK, "ok") + }) + + // Start server in a goroutine + go func() { + if err := e.Start(":8080"); err != nil && err != http.ErrServerClosed { + e.Logger.Fatal("shutting down the server:", err) + } + }() + + // Wait for interrupt signal (Ctrl+C or SIGTERM from container orchestrator) + quit := make(chan os.Signal, 1) + signal.Notify(quit, os.Interrupt) + <-quit + + // Graceful shutdown with 10-second timeout + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + if err := e.Shutdown(ctx); err != nil { + e.Logger.Fatal(err) + } +} \ No newline at end of file diff --git a/snippets/echo/recipes/hello-world.md b/snippets/echo/recipes/hello-world.md new file mode 100644 index 0000000..f302d7b --- /dev/null +++ b/snippets/echo/recipes/hello-world.md @@ -0,0 +1,17 @@ +package main + +import ( + "net/http" + + "github.com/labstack/echo/v4" +) + +func main() { + e := echo.New() + + e.GET("/", func(c echo.Context) error { + return c.String(http.StatusOK, "Hello, World!") + }) + + e.Logger.Fatal(e.Start(":8080")) +} \ No newline at end of file diff --git a/snippets/echo/recipes/http2.md b/snippets/echo/recipes/http2.md new file mode 100644 index 0000000..f4cd229 --- /dev/null +++ b/snippets/echo/recipes/http2.md @@ -0,0 +1,39 @@ +package main + +import ( + "net/http" + + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "golang.org/x/net/http2" +) + +func main() { + e := echo.New() + e.Use(middleware.Logger()) + e.Use(middleware.Recover()) + + e.GET("/", func(c echo.Context) error { + // HTTP/2 Server Push + pusher, ok := c.Response().Writer.(http.Pusher) + if ok { + if err := pusher.Push("/static/style.css", nil); err != nil { + e.Logger.Warn("Failed to push:", err) + } + } + return c.File("public/index.html") + }) + + e.Static("/static", "assets") + + // Start with TLS (HTTP/2 requires TLS in browsers) + s := e.TLSServer + s.Addr = ":443" + + // Enable HTTP/2 explicitly + if err := http2.ConfigureServer(s, &http2.Server{}); err != nil { + e.Logger.Fatal(err) + } + + e.Logger.Fatal(e.StartTLS(":443", "cert.pem", "key.pem")) +} \ No newline at end of file diff --git a/snippets/echo/recipes/jsonp.md b/snippets/echo/recipes/jsonp.md new file mode 100644 index 0000000..46027aa --- /dev/null +++ b/snippets/echo/recipes/jsonp.md @@ -0,0 +1,32 @@ +package main + +import ( + "net/http" + + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" +) + +func dataHandler(c echo.Context) error { + data := map[string]string{"message": "hello", "status": "ok"} + + // c.JSONP() wraps JSON in a callback function for cross-domain requests + // The callback name is read from the query param (default: "callback") + // e.g. GET /data?callback=myFunc โ†’ myFunc({"message":"hello",...}) + callback := c.QueryParam("callback") + if callback == "" { + // Fallback to regular JSON if no callback specified + return c.JSON(http.StatusOK, data) + } + return c.JSONP(http.StatusOK, callback, data) +} + +func main() { + e := echo.New() + e.Use(middleware.Logger()) + e.Use(middleware.Recover()) + + e.GET("/data", dataHandler) + + e.Logger.Fatal(e.Start(":8080")) +} \ No newline at end of file diff --git a/snippets/echo/recipes/jwt-auth.md b/snippets/echo/recipes/jwt-auth.md new file mode 100644 index 0000000..637efd0 --- /dev/null +++ b/snippets/echo/recipes/jwt-auth.md @@ -0,0 +1,77 @@ +package main + +import ( + "net/http" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + echojwt "github.com/labstack/echo-jwt/v4" +) + +type JWTClaims struct { + UserID int `json:"user_id"` + Username string `json:"username"` + jwt.RegisteredClaims +} + +var jwtSecret = []byte("your-secret-key-change-in-production") + +func login(c echo.Context) error { + username := c.FormValue("username") + password := c.FormValue("password") + + // Validate credentials (use bcrypt comparison in production) + if username != "alice" || password != "secret" { + return echo.ErrUnauthorized + } + + claims := &JWTClaims{ + UserID: 1, + Username: username, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), + IssuedAt: jwt.NewNumericDate(time.Now()), + Issuer: "myapp", + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + signed, err := token.SignedString(jwtSecret) + if err != nil { + return err + } + return c.JSON(http.StatusOK, map[string]string{"token": signed}) +} + +func profile(c echo.Context) error { + // Claims are set by the JWT middleware + token := c.Get("user").(*jwt.Token) + claims := token.Claims.(*JWTClaims) + return c.JSON(http.StatusOK, map[string]any{ + "user_id": claims.UserID, + "username": claims.Username, + }) +} + +func main() { + e := echo.New() + e.Use(middleware.Logger()) + e.Use(middleware.Recover()) + + // Public routes + e.POST("/login", login) + + // Protected routes + protected := e.Group("/api") + protected.Use(echojwt.WithConfig(echojwt.Config{ + SigningKey: jwtSecret, + NewClaimsFunc: func(c echo.Context) jwt.Claims { + return new(JWTClaims) + }, + })) + protected.GET("/profile", profile) + + e.Logger.Fatal(e.Start(":8080")) +} \ No newline at end of file diff --git a/snippets/echo/recipes/middleware-chain.md b/snippets/echo/recipes/middleware-chain.md new file mode 100644 index 0000000..85c3735 --- /dev/null +++ b/snippets/echo/recipes/middleware-chain.md @@ -0,0 +1,60 @@ +package main + +import ( + "net/http" + "time" + + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" +) + +// Custom middleware: request timing +func timingMiddleware(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + start := time.Now() + err := next(c) + c.Logger().Infof("Request took %s", time.Since(start)) + return err + } +} + +// Custom middleware: require API key header +func apiKeyMiddleware(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + key := c.Request().Header.Get("X-API-Key") + if key != "valid-key" { + return echo.NewHTTPError(http.StatusUnauthorized, "invalid api key") + } + return next(c) + } +} + +func main() { + e := echo.New() + + // ORDER MATTERS: + // 1. Logger FIRST โ€” logs ALL requests including those that panic (Recover catches the panic, Logger records it) + // 2. Recover SECOND โ€” catches panics from all subsequent middleware and handlers + // 3. CORS (if applicable) โ€” must be before auth so OPTIONS preflight passes + // 4. Auth middleware โ€” reject unauthorized before expensive processing + // 5. Custom business middleware last + + e.Use(middleware.Logger()) // 1: log every request + e.Use(middleware.Recover()) // 2: recover from panics + e.Use(middleware.RequestID()) // 3: attach X-Request-ID + e.Use(timingMiddleware) // 4: time the full request + + // Route-level middleware (more specific) + api := e.Group("/api") + api.Use(apiKeyMiddleware) // only on /api routes + + e.GET("/health", func(c echo.Context) error { + return c.JSON(http.StatusOK, map[string]string{"status": "ok"}) + }) + + api.GET("/data", func(c echo.Context) error { + return c.JSON(http.StatusOK, map[string]string{"data": "secret"}) + }) + + e.Logger.Fatal(e.Start(":8080")) +} \ No newline at end of file diff --git a/snippets/echo/recipes/reverse-proxy.md b/snippets/echo/recipes/reverse-proxy.md new file mode 100644 index 0000000..42e609a --- /dev/null +++ b/snippets/echo/recipes/reverse-proxy.md @@ -0,0 +1,41 @@ +package main + +import ( + "net/http/httputil" + "net/url" + + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" +) + +func main() { + e := echo.New() + e.Use(middleware.Logger()) + e.Use(middleware.Recover()) + + // Single backend target + target, err := url.Parse("http://localhost:9090") + if err != nil { + e.Logger.Fatal(err) + } + + // Using Echo's built-in proxy middleware with multiple targets (load balancing) + targets := []*middleware.ProxyTarget{ + {URL: target}, + // Add more targets for load balancing + // {URL: mustParse("http://localhost:9091")}, + } + + e.Use(middleware.ProxyWithConfig(middleware.ProxyConfig{ + Balancer: middleware.NewRoundRobinBalancer(targets), + })) + + // OR: Manual proxy for specific routes only + proxy := httputil.NewSingleHostReverseProxy(target) + e.Any("/api/*", func(c echo.Context) error { + proxy.ServeHTTP(c.Response(), c.Request()) + return nil + }) + + e.Logger.Fatal(e.Start(":8080")) +} \ No newline at end of file diff --git a/snippets/echo/recipes/route-groups.md b/snippets/echo/recipes/route-groups.md new file mode 100644 index 0000000..cce90c6 --- /dev/null +++ b/snippets/echo/recipes/route-groups.md @@ -0,0 +1,48 @@ +package main + +import ( + "net/http" + + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" +) + +func main() { + e := echo.New() + e.Use(middleware.Logger()) + e.Use(middleware.Recover()) + + // Health endpoint โ€” no auth + e.GET("/health", func(c echo.Context) error { + return c.JSON(http.StatusOK, map[string]string{"status": "ok"}) + }) + + // API v1 group + v1 := e.Group("/v1") + v1.GET("/status", func(c echo.Context) error { + return c.JSON(http.StatusOK, map[string]string{"version": "1"}) + }) + + // Admin group with its own middleware + admin := e.Group("/admin") + admin.Use(middleware.BasicAuth(func(username, password string, c echo.Context) (bool, error) { + return username == "admin" && password == "secret", nil + })) + admin.GET("/dashboard", func(c echo.Context) error { + return c.String(http.StatusOK, "admin dashboard") + }) + + // Nested groups for sub-resources + users := v1.Group("/users") + users.GET("", func(c echo.Context) error { + return c.JSON(http.StatusOK, []string{"alice", "bob"}) + }) + users.GET("/:id", func(c echo.Context) error { + return c.JSON(http.StatusOK, map[string]string{"id": c.Param("id")}) + }) + users.GET("/:id/posts", func(c echo.Context) error { + return c.JSON(http.StatusOK, map[string]string{"user": c.Param("id"), "posts": "..."}) + }) + + e.Logger.Fatal(e.Start(":8080")) +} \ No newline at end of file diff --git a/snippets/echo/recipes/sse.md b/snippets/echo/recipes/sse.md new file mode 100644 index 0000000..eec68d9 --- /dev/null +++ b/snippets/echo/recipes/sse.md @@ -0,0 +1,43 @@ +package main + +import ( + "fmt" + "net/http" + "time" + + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" +) + +func sseHandler(c echo.Context) error { + c.Response().Header().Set("Content-Type", "text/event-stream") + c.Response().Header().Set("Cache-Control", "no-cache") + c.Response().Header().Set("Connection", "keep-alive") + c.Response().WriteHeader(http.StatusOK) + + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + + for { + select { + case <-c.Request().Context().Done(): + // Client disconnected + return nil + case t := <-ticker.C: + // Write SSE event + fmt.Fprintf(c.Response(), "data: %s\n\n", t.Format(time.RFC3339)) + // Flush is REQUIRED โ€” without it the client never receives data + c.Response().Flush() + } + } +} + +func main() { + e := echo.New() + e.Use(middleware.Logger()) + e.Use(middleware.Recover()) + + e.GET("/events", sseHandler) + + e.Logger.Fatal(e.Start(":8080")) +} \ No newline at end of file diff --git a/snippets/echo/recipes/streaming-response.md b/snippets/echo/recipes/streaming-response.md new file mode 100644 index 0000000..8b859c6 --- /dev/null +++ b/snippets/echo/recipes/streaming-response.md @@ -0,0 +1,35 @@ +package main + +import ( + "fmt" + "net/http" + "time" + + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" +) + +func streamHandler(c echo.Context) error { + c.Response().Header().Set(echo.HeaderContentType, echo.MIMETextPlainCharsetUTF8) + c.Response().WriteHeader(http.StatusOK) + + for i := 1; i <= 10; i++ { + // Write chunk + fmt.Fprintf(c.Response(), "chunk %d of 10\n", i) + // Flush sends the chunk immediately to the client + c.Response().Flush() + // Simulate work + time.Sleep(500 * time.Millisecond) + } + return nil +} + +func main() { + e := echo.New() + e.Use(middleware.Logger()) + e.Use(middleware.Recover()) + + e.GET("/stream", streamHandler) + + e.Logger.Fatal(e.Start(":8080")) +} \ No newline at end of file diff --git a/snippets/echo/recipes/subdomain-routing.md b/snippets/echo/recipes/subdomain-routing.md new file mode 100644 index 0000000..ebffd7a --- /dev/null +++ b/snippets/echo/recipes/subdomain-routing.md @@ -0,0 +1,39 @@ +package main + +import ( + "net/http" + + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" +) + +func main() { + e := echo.New() + e.Use(middleware.Logger()) + e.Use(middleware.Recover()) + + // Subdomain routing via Host header matching + // api.example.com routes + api := e.Host("api.example.com") + api.GET("/users", func(c echo.Context) error { + return c.JSON(http.StatusOK, map[string]string{"host": "api"}) + }) + + // admin.example.com routes + admin := e.Host("admin.example.com") + admin.Use(middleware.BasicAuth(func(u, p string, c echo.Context) (bool, error) { + return u == "admin" && p == "secret", nil + })) + admin.GET("/", func(c echo.Context) error { + return c.String(http.StatusOK, "admin panel") + }) + + // Wildcard subdomain โ€” :subdomain captures the dynamic part + wildcard := e.Host(":subdomain.example.com") + wildcard.GET("/", func(c echo.Context) error { + sub := c.Param("subdomain") + return c.JSON(http.StatusOK, map[string]string{"subdomain": sub}) + }) + + e.Logger.Fatal(e.Start(":8080")) +} \ No newline at end of file diff --git a/snippets/echo/recipes/timeout.md b/snippets/echo/recipes/timeout.md new file mode 100644 index 0000000..0eb2a1a --- /dev/null +++ b/snippets/echo/recipes/timeout.md @@ -0,0 +1,34 @@ +package main + +import ( + "net/http" + "time" + + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" +) + +func slowHandler(c echo.Context) error { + // Simulate slow work โ€” this will be cancelled if it exceeds the timeout + select { + case <-c.Request().Context().Done(): + return c.Request().Context().Err() + case <-time.After(5 * time.Second): + return c.String(http.StatusOK, "done") + } +} + +func main() { + e := echo.New() + e.Use(middleware.Logger()) + e.Use(middleware.Recover()) + + // Global timeout โ€” cancels any request exceeding 30s + e.Use(middleware.TimeoutWithConfig(middleware.TimeoutConfig{ + Timeout: 30 * time.Second, + })) + + e.GET("/slow", slowHandler) + + e.Logger.Fatal(e.Start(":8080")) +} \ No newline at end of file diff --git a/snippets/echo/recipes/websocket.md b/snippets/echo/recipes/websocket.md new file mode 100644 index 0000000..1d8be7d --- /dev/null +++ b/snippets/echo/recipes/websocket.md @@ -0,0 +1,70 @@ +package main + +import ( + "fmt" + "net/http" + + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "golang.org/x/net/websocket" +) + +func wsHandler(c echo.Context) error { + websocket.Handler(func(ws *websocket.Conn) { + defer ws.Close() + + for { + // Read message + msg := "" + if err := websocket.Message.Receive(ws, &msg); err != nil { + c.Logger().Error("ws receive:", err) + break + } + + // Echo the message back + reply := fmt.Sprintf("echo: %s", msg) + if err := websocket.Message.Send(ws, reply); err != nil { + c.Logger().Error("ws send:", err) + break + } + } + }).ServeHTTP(c.Response(), c.Request()) + return nil +} + +// Using gorilla/websocket (more common in production): +// import "github.com/gorilla/websocket" +// +// var upgrader = websocket.Upgrader{ +// CheckOrigin: func(r *http.Request) bool { return true }, +// } +// +// func wsHandler(c echo.Context) error { +// ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil) +// if err != nil { +// return err +// } +// defer ws.Close() +// +// for { +// mt, msg, err := ws.ReadMessage() +// if err != nil { +// break +// } +// if err := ws.WriteMessage(mt, msg); err != nil { +// break +// } +// } +// return nil +// } + +func main() { + e := echo.New() + e.Use(middleware.Logger()) + e.Use(middleware.Recover()) + + e.GET("/ws", wsHandler) + e.Static("/", "public") + + e.Logger.Fatal(e.Start(":8080")) +} \ No newline at end of file diff --git a/snippets/golang/cheatsheet.md b/snippets/golang/cheatsheet.md new file mode 100644 index 0000000..d6b20ae --- /dev/null +++ b/snippets/golang/cheatsheet.md @@ -0,0 +1,120 @@ +# Go Engineering Quick Reference + +## Error Handling + +```go +// Wrap with context +return fmt.Errorf("userRepo.FindByID %s: %w", id, err) + +// Check sentinel errors (works through wrapping) +if errors.Is(err, ErrNotFound) { ... } + +// Extract typed error +var dbErr *DBError +if errors.As(err, &dbErr) { ... } + +// Handle once: return OR log, never both +``` + +## Interfaces + +```go +// Small (1-2 methods), defined at consumer, not producer +type Storer interface { Store(ctx context.Context, item Item) error } + +// Accept interfaces, return concrete types +func NewService(store Storer) *Service { return &Service{store: store} } +``` + +## Concurrency + +```go +// Always pass context +func FetchUser(ctx context.Context, id string) (*User, error) { ... } + +// errgroup over WaitGroup when errors matter +g, ctx := errgroup.WithContext(ctx) +g.Go(func() error { return fetchUsers(ctx) }) +if err := g.Wait(); err != nil { return err } + +// Goroutines need a clear exit +go func() { + for { select { case <-ctx.Done(): return; case job := <-jobCh: process(job) } } +}() +``` + +## Security + +```go +// crypto/rand โ€” NEVER math/rand for security +token := make([]byte, 32) +crypto_rand.Read(token) + +// Parameterized queries โ€” NEVER string concat +db.QueryRowContext(ctx, "SELECT * FROM users WHERE id = $1", id) + +// Bcrypt for passwords +hash, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) +bcrypt.CompareHashAndPassword(hash, []byte(input)) +``` + +## Constructor Pattern + +```go +func NewServer(addr string, opts ...Option) (*Server, error) { + if addr == "" { return nil, errors.New("addr required") } + s := &Server{addr: addr, timeout: 30 * time.Second} + for _, opt := range opts { opt(s) } + return s, nil +} +``` + +## Functional Options + +```go +type Option func(*Server) +func WithTimeout(d time.Duration) Option { return func(s *Server) { s.timeout = d } } +srv := NewServer(":8080", WithTimeout(60*time.Second)) +``` + +## Graceful Shutdown + +```go +go func() { e.Start(":8080") }() +quit := make(chan os.Signal, 1) +signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) +<-quit +ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) +defer cancel() +server.Shutdown(ctx) +``` + +## Patterns Quick Reference + +| Pattern | When | +|---------|------| +| Functional Options | 5+ optional constructor params | +| Adapter | Wrap external/legacy type | +| Middleware/Decorator | Cross-cutting behavior | +| Worker Pool | Bounded parallelism | +| Pipeline | Stage-by-stage stream processing | +| Fan-Out/Fan-In | Parallel work + merge results | +| Consumer-Side Interface | Always โ€” decouple without import cycles | +| Strategy | Swappable algorithms at runtime | +| Observer | Event bus with channels | +| Command | Job queues, closures as tasks | + +## Anti-Patterns to Avoid + +| Anti-Pattern | Fix | +|---|---| +| Global mutable state | Dependency injection | +| Ignoring errors (`_ =`) | Always handle: return, wrap, or log | +| Business logic in handlers | Thin handlers, service layer | +| SQL string concatenation | Parameterized queries | +| `math/rand` for security | `crypto/rand` | +| Goroutine leaks | Always pass ctx, select on ctx.Done() | +| Missing context | First param is always `context.Context` | +| Not closing rows | `defer rows.Close()` immediately after query | +| Log AND return error | Choose one | +| `time.Sleep` in tests | Use channels/sync primitives | \ No newline at end of file diff --git a/snippets/golang/patterns/adapter.md b/snippets/golang/patterns/adapter.md new file mode 100644 index 0000000..103b569 --- /dev/null +++ b/snippets/golang/patterns/adapter.md @@ -0,0 +1,14 @@ +// External type you don't control +type ThirdPartyLogger struct { ... } +func (l *ThirdPartyLogger) LogMessage(msg string) { ... } + +// Your interface +type Logger interface { Log(ctx context.Context, msg string) } + +// Adapter wraps it +type loggerAdapter struct { inner *ThirdPartyLogger } +func (a *loggerAdapter) Log(_ context.Context, msg string) { a.inner.LogMessage(msg) } + +func NewLogger(inner *ThirdPartyLogger) Logger { + return &loggerAdapter{inner: inner} +} \ No newline at end of file diff --git a/snippets/golang/patterns/command.md b/snippets/golang/patterns/command.md new file mode 100644 index 0000000..e79c17f --- /dev/null +++ b/snippets/golang/patterns/command.md @@ -0,0 +1,42 @@ +// Command pattern using function closures โ€” idiomatic Go +type Job func(context.Context) error + +type Worker struct { + jobs chan Job + results chan error +} + +func NewWorker(bufSize int) *Worker { + return &Worker{ + jobs: make(chan Job, bufSize), + results: make(chan error, bufSize), + } +} + +func (w *Worker) Start(ctx context.Context) { + go func() { + for { + select { + case <-ctx.Done(): + return + case job, ok := <-w.jobs: + if !ok { return } + w.results <- job(ctx) + } + } + }() +} + +func (w *Worker) Submit(job Job) { w.jobs <- job } + +// Usage โ€” commands are just closures +worker := NewWorker(100) +worker.Start(ctx) + +worker.Submit(func(ctx context.Context) error { + return sendEmail(ctx, "alice@example.com", "Hello") +}) + +worker.Submit(func(ctx context.Context) error { + return processPayment(ctx, orderID) +}) \ No newline at end of file diff --git a/snippets/golang/patterns/consumer-side-interface-anti.md b/snippets/golang/patterns/consumer-side-interface-anti.md new file mode 100644 index 0000000..286918b --- /dev/null +++ b/snippets/golang/patterns/consumer-side-interface-anti.md @@ -0,0 +1,4 @@ +// โŒ producer defines interface โ€” creates import cycle risk +package postgres +type UserStorer interface { FindByID(ctx, id) (*User, error) } +type UserRepo struct{} // implements its own interface \ No newline at end of file diff --git a/snippets/golang/patterns/consumer-side-interface.md b/snippets/golang/patterns/consumer-side-interface.md new file mode 100644 index 0000000..5c8d0e0 --- /dev/null +++ b/snippets/golang/patterns/consumer-side-interface.md @@ -0,0 +1,15 @@ +// โœ… consumer package defines what it needs +package service + +type UserStore interface { + FindByID(ctx context.Context, id string) (*User, error) +} + +type UserService struct { store UserStore } + +// โœ… implementing package has no knowledge of the interface +package postgres + +type UserRepo struct { db *pgx.Conn } +func (r *UserRepo) FindByID(ctx context.Context, id string) (*User, error) { ... } +// UserRepo satisfies service.UserStore implicitly โ€” no import of service needed \ No newline at end of file diff --git a/snippets/golang/patterns/fan-out-fan-in.md b/snippets/golang/patterns/fan-out-fan-in.md new file mode 100644 index 0000000..0e8de53 --- /dev/null +++ b/snippets/golang/patterns/fan-out-fan-in.md @@ -0,0 +1,42 @@ +// Fan-out: distribute work across multiple goroutines +func fanOut(in <-chan int, workers int) []<-chan int { + channels := make([]<-chan int, workers) + for i := 0; i < workers; i++ { + channels[i] = worker(in) + } + return channels +} + +func worker(in <-chan int) <-chan int { + out := make(chan int) + go func() { + defer close(out) + for n := range in { + out <- n * n // do work + } + }() + return out +} + +// Fan-in: merge multiple channels into one +func fanIn(channels ...<-chan int) <-chan int { + out := make(chan int) + var wg sync.WaitGroup + + for _, ch := range channels { + wg.Add(1) + go func(c <-chan int) { + defer wg.Done() + for n := range c { out <- n } + }(ch) + } + + go func() { wg.Wait(); close(out) }() + return out +} + +// Usage +source := generate(1, 2, 3, 4, 5, 6, 7, 8) +workers := fanOut(source, 4) +results := fanIn(workers...) +for r := range results { fmt.Println(r) } \ No newline at end of file diff --git a/snippets/golang/patterns/functional-options.md b/snippets/golang/patterns/functional-options.md new file mode 100644 index 0000000..8f16efb --- /dev/null +++ b/snippets/golang/patterns/functional-options.md @@ -0,0 +1,17 @@ +type Option func(*Server) + +func WithTimeout(d time.Duration) Option { + return func(s *Server) { s.timeout = d } +} +func WithMaxConns(n int) Option { + return func(s *Server) { s.maxConns = n } +} + +func NewServer(addr string, opts ...Option) *Server { + s := &Server{addr: addr, timeout: 30 * time.Second, maxConns: 100} + for _, opt := range opts { opt(s) } + return s +} + +// Usage +srv := NewServer(":8080", WithTimeout(60*time.Second), WithMaxConns(200)) \ No newline at end of file diff --git a/snippets/golang/patterns/middleware-decorator.md b/snippets/golang/patterns/middleware-decorator.md new file mode 100644 index 0000000..d29bf26 --- /dev/null +++ b/snippets/golang/patterns/middleware-decorator.md @@ -0,0 +1,20 @@ +type Handler func(ctx context.Context, req Request) (Response, error) + +func WithLogging(next Handler) Handler { + return func(ctx context.Context, req Request) (Response, error) { + start := time.Now() + resp, err := next(ctx, req) + log.Printf("req=%v duration=%v err=%v", req, time.Since(start), err) + return resp, err + } +} + +func WithAuth(next Handler) Handler { + return func(ctx context.Context, req Request) (Response, error) { + if !isAuthorized(ctx) { return Response{}, ErrUnauthorized } + return next(ctx, req) + } +} + +// Stack middleware +handler := WithLogging(WithAuth(actualHandler)) \ No newline at end of file diff --git a/snippets/golang/patterns/observer.md b/snippets/golang/patterns/observer.md new file mode 100644 index 0000000..8b053bf --- /dev/null +++ b/snippets/golang/patterns/observer.md @@ -0,0 +1,45 @@ +// Observer using channels โ€” idiomatic Go over callbacks +type Event struct { + Type string + Payload any +} + +type EventBus struct { + mu sync.RWMutex + subscribers []chan Event +} + +func (b *EventBus) Subscribe() <-chan Event { + ch := make(chan Event, 10) // buffered to avoid blocking publisher + b.mu.Lock() + b.subscribers = append(b.subscribers, ch) + b.mu.Unlock() + return ch +} + +func (b *EventBus) Publish(e Event) { + b.mu.RLock() + defer b.mu.RUnlock() + for _, ch := range b.subscribers { + select { + case ch <- e: + default: // drop if subscriber is full โ€” or use blocking if required + } + } +} + +func (b *EventBus) Close() { + b.mu.Lock() + defer b.mu.Unlock() + for _, ch := range b.subscribers { close(ch) } +} + +// Usage +bus := &EventBus{} +events := bus.Subscribe() +go func() { + for e := range events { + fmt.Printf("received: %s\n", e.Type) + } +}() +bus.Publish(Event{Type: "user.created", Payload: userID}) \ No newline at end of file diff --git a/snippets/golang/patterns/pipeline.md b/snippets/golang/patterns/pipeline.md new file mode 100644 index 0000000..c63a018 --- /dev/null +++ b/snippets/golang/patterns/pipeline.md @@ -0,0 +1,20 @@ +func generate(nums ...int) <-chan int { + out := make(chan int) + go func() { + defer close(out) + for _, n := range nums { out <- n } + }() + return out +} + +func square(in <-chan int) <-chan int { + out := make(chan int) + go func() { + defer close(out) + for n := range in { out <- n * n } + }() + return out +} + +// Usage: pipeline +for n := range square(generate(2, 3, 4)) { fmt.Println(n) } \ No newline at end of file diff --git a/snippets/golang/patterns/strategy.md b/snippets/golang/patterns/strategy.md new file mode 100644 index 0000000..91a5c52 --- /dev/null +++ b/snippets/golang/patterns/strategy.md @@ -0,0 +1,13 @@ +type Sorter interface { Sort([]Item) []Item } + +type PriceSort struct{} +func (p PriceSort) Sort(items []Item) []Item { /* sort by price */ return items } + +type DateSort struct{} +func (d DateSort) Sort(items []Item) []Item { /* sort by date */ return items } + +type Catalog struct { sorter Sorter } +func (c *Catalog) List() []Item { return c.sorter.Sort(c.items) } + +// Swap at runtime +catalog := &Catalog{sorter: PriceSort{}} \ No newline at end of file diff --git a/snippets/golang/patterns/worker-pool.md b/snippets/golang/patterns/worker-pool.md new file mode 100644 index 0000000..0db861d --- /dev/null +++ b/snippets/golang/patterns/worker-pool.md @@ -0,0 +1,14 @@ +func WorkerPool(ctx context.Context, jobs <-chan Job, concurrency int) error { + g, ctx := errgroup.WithContext(ctx) + sem := make(chan struct{}, concurrency) + + for job := range jobs { + job := job // capture + sem <- struct{}{} + g.Go(func() error { + defer func() { <-sem }() + return process(ctx, job) + }) + } + return g.Wait() +} \ No newline at end of file diff --git a/snippets/golang/practices/config-env-vars-bad.md b/snippets/golang/practices/config-env-vars-bad.md new file mode 100644 index 0000000..b6c8a5b --- /dev/null +++ b/snippets/golang/practices/config-env-vars-bad.md @@ -0,0 +1,10 @@ +// โŒ Scattered os.Getenv calls throughout the codebase +func ConnectDB() *sql.DB { + dsn := os.Getenv("DATABASE_URL") // no validation โ€” empty string silently fails + db, _ := sql.Open("postgres", dsn) + return db +} + +func GetJWTSecret() string { + return os.Getenv("JWT_SECRET") // could return empty string +} \ No newline at end of file diff --git a/snippets/golang/practices/config-env-vars-good.md b/snippets/golang/practices/config-env-vars-good.md new file mode 100644 index 0000000..0d380c3 --- /dev/null +++ b/snippets/golang/practices/config-env-vars-good.md @@ -0,0 +1,27 @@ +// Load typed config from environment variables at startup +type Config struct { + Port string `env:"PORT,required"` + DatabaseURL string `env:"DATABASE_URL,required"` + JWTSecret string `env:"JWT_SECRET,required"` + LogLevel string `env:"LOG_LEVEL" envDefault:"info"` + Timeout time.Duration `env:"REQUEST_TIMEOUT" envDefault:"30s"` +} + +func LoadConfig() (*Config, error) { + var cfg Config + if err := env.Parse(&cfg); err != nil { + return nil, fmt.Errorf("config: %w", err) + } + return &cfg, nil +} + +func main() { + cfg, err := LoadConfig() + if err != nil { + log.Fatalf("failed to load config: %v", err) + } + // Pass cfg to constructors โ€” never use os.Getenv() deep in the code + db := NewDB(cfg.DatabaseURL) + srv := NewServer(cfg.Port, db) + srv.Run() +} \ No newline at end of file diff --git a/snippets/golang/practices/constructor-pattern-good.md b/snippets/golang/practices/constructor-pattern-good.md new file mode 100644 index 0000000..f8efc19 --- /dev/null +++ b/snippets/golang/practices/constructor-pattern-good.md @@ -0,0 +1,5 @@ +func NewServer(addr string, timeout time.Duration) (*Server, error) { + if addr == "" { return nil, errors.New("addr required") } + if timeout <= 0 { timeout = 30 * time.Second } + return &Server{addr: addr, timeout: timeout}, nil +} \ No newline at end of file diff --git a/snippets/golang/practices/context-first-param-bad.md b/snippets/golang/practices/context-first-param-bad.md new file mode 100644 index 0000000..5517d30 --- /dev/null +++ b/snippets/golang/practices/context-first-param-bad.md @@ -0,0 +1 @@ +func (s *Service) FetchUser(id string) (*User, error) { ... } // no cancellation possible \ No newline at end of file diff --git a/snippets/golang/practices/context-first-param-good.md b/snippets/golang/practices/context-first-param-good.md new file mode 100644 index 0000000..18d9371 --- /dev/null +++ b/snippets/golang/practices/context-first-param-good.md @@ -0,0 +1 @@ +func (s *Service) FetchUser(ctx context.Context, id string) (*User, error) { ... } \ No newline at end of file diff --git a/snippets/golang/practices/crypto-rand-bad.md b/snippets/golang/practices/crypto-rand-bad.md new file mode 100644 index 0000000..bb3b324 --- /dev/null +++ b/snippets/golang/practices/crypto-rand-bad.md @@ -0,0 +1,2 @@ +import "math/rand" +token := rand.Int63() // predictable, guessable \ No newline at end of file diff --git a/snippets/golang/practices/crypto-rand-good.md b/snippets/golang/practices/crypto-rand-good.md new file mode 100644 index 0000000..c34746f --- /dev/null +++ b/snippets/golang/practices/crypto-rand-good.md @@ -0,0 +1,3 @@ +import "crypto/rand" +token := make([]byte, 32) +rand.Read(token) \ No newline at end of file diff --git a/snippets/golang/practices/database-repository-bad.md b/snippets/golang/practices/database-repository-bad.md new file mode 100644 index 0000000..0f14700 --- /dev/null +++ b/snippets/golang/practices/database-repository-bad.md @@ -0,0 +1,11 @@ +// โŒ No interface โ€” untestable, tightly coupled to postgres +type UserService struct { db *sql.DB } + +func (s *UserService) FindUser(id string) *User { + // โŒ No context โ€” can't cancel or set deadlines + // โŒ String concatenation โ€” SQL injection risk + row := s.db.QueryRow("SELECT * FROM users WHERE id = " + id) + var u User + row.Scan(&u.ID, &u.Name) // โŒ error ignored + return &u +} \ No newline at end of file diff --git a/snippets/golang/practices/database-repository-good.md b/snippets/golang/practices/database-repository-good.md new file mode 100644 index 0000000..0e561aa --- /dev/null +++ b/snippets/golang/practices/database-repository-good.md @@ -0,0 +1,42 @@ +// Define interface at consumer side +type UserRepository interface { + FindByID(ctx context.Context, id string) (*User, error) + Save(ctx context.Context, user *User) error + Delete(ctx context.Context, id string) error +} + +// Postgres implementation +type postgresUserRepo struct { db *pgxpool.Pool } + +func (r *postgresUserRepo) FindByID(ctx context.Context, id string) (*User, error) { + var u User + err := r.db.QueryRow(ctx, + "SELECT id, name, email FROM users WHERE id = $1", id, + ).Scan(&u.ID, &u.Name, &u.Email) + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrNotFound + } + if err != nil { + return nil, fmt.Errorf("userRepo.FindByID: %w", err) + } + return &u, nil +} + +// Always defer rows.Close() +func (r *postgresUserRepo) List(ctx context.Context) ([]*User, error) { + rows, err := r.db.Query(ctx, "SELECT id, name, email FROM users") + if err != nil { + return nil, fmt.Errorf("userRepo.List: %w", err) + } + defer rows.Close() + + var users []*User + for rows.Next() { + var u User + if err := rows.Scan(&u.ID, &u.Name, &u.Email); err != nil { + return nil, err + } + users = append(users, &u) + } + return users, rows.Err() +} \ No newline at end of file diff --git a/snippets/golang/practices/errgroup-bad.md b/snippets/golang/practices/errgroup-bad.md new file mode 100644 index 0000000..552b503 --- /dev/null +++ b/snippets/golang/practices/errgroup-bad.md @@ -0,0 +1,5 @@ +var wg sync.WaitGroup +wg.Add(2) +go func() { defer wg.Done(); fetchUsers(ctx) }() // error silently swallowed +go func() { defer wg.Done(); fetchOrders(ctx) }() +wg.Wait() \ No newline at end of file diff --git a/snippets/golang/practices/errgroup-good.md b/snippets/golang/practices/errgroup-good.md new file mode 100644 index 0000000..5abd30a --- /dev/null +++ b/snippets/golang/practices/errgroup-good.md @@ -0,0 +1,4 @@ +g, ctx := errgroup.WithContext(ctx) +g.Go(func() error { return fetchUsers(ctx) }) +g.Go(func() error { return fetchOrders(ctx) }) +if err := g.Wait(); err != nil { return err } \ No newline at end of file diff --git a/snippets/golang/practices/error-wrapping-bad.md b/snippets/golang/practices/error-wrapping-bad.md new file mode 100644 index 0000000..eea3135 --- /dev/null +++ b/snippets/golang/practices/error-wrapping-bad.md @@ -0,0 +1,3 @@ +if err := db.Query(ctx, q); err != nil { + return err // context lost โ€” which query failed? +} \ No newline at end of file diff --git a/snippets/golang/practices/error-wrapping-good.md b/snippets/golang/practices/error-wrapping-good.md new file mode 100644 index 0000000..6aa9638 --- /dev/null +++ b/snippets/golang/practices/error-wrapping-good.md @@ -0,0 +1,3 @@ +if err := db.Query(ctx, q); err != nil { + return fmt.Errorf("userRepo.FindByID %s: %w", id, err) +} \ No newline at end of file diff --git a/snippets/golang/practices/errors-is-as-bad.md b/snippets/golang/practices/errors-is-as-bad.md new file mode 100644 index 0000000..a41f1d9 --- /dev/null +++ b/snippets/golang/practices/errors-is-as-bad.md @@ -0,0 +1,2 @@ +if err == ErrNotFound { ... } // breaks with wrapped errors +if _, ok := err.(*DBError); ok { ... } // type assertion breaks wrapping \ No newline at end of file diff --git a/snippets/golang/practices/errors-is-as-good.md b/snippets/golang/practices/errors-is-as-good.md new file mode 100644 index 0000000..f4894b2 --- /dev/null +++ b/snippets/golang/practices/errors-is-as-good.md @@ -0,0 +1,3 @@ +if errors.Is(err, ErrNotFound) { ... } +var dbErr *DBError +if errors.As(err, &dbErr) { ... } \ No newline at end of file diff --git a/snippets/golang/practices/golangci-lint-good.md b/snippets/golang/practices/golangci-lint-good.md new file mode 100644 index 0000000..b8cd016 --- /dev/null +++ b/snippets/golang/practices/golangci-lint-good.md @@ -0,0 +1,27 @@ +# .golangci.yml โ€” recommended production configuration +linters: + enable: + - errcheck # ensure errors are handled + - gosimple # simplification suggestions + - govet # suspicious constructs + - ineffassign # unused assignments + - staticcheck # advanced static analysis + - unused # unused code + - gofmt # formatting + - goimports # import ordering + - gosec # security issues + - revive # opinionated linter + - bodyclose # HTTP response body not closed + - noctx # HTTP requests without context + - exhaustive # exhaustive enum switches + +linters-settings: + errcheck: + check-type-assertions: true + gosec: + excludes: + - G104 # only if intentional error suppression + +run: + timeout: 5m + go: "1.21" \ No newline at end of file diff --git a/snippets/golang/practices/goroutine-lifecycle-bad.md b/snippets/golang/practices/goroutine-lifecycle-bad.md new file mode 100644 index 0000000..6148d52 --- /dev/null +++ b/snippets/golang/practices/goroutine-lifecycle-bad.md @@ -0,0 +1,3 @@ +go func() { + for job := range jobCh { process(job) } // what if jobCh never closes? +}() \ No newline at end of file diff --git a/snippets/golang/practices/goroutine-lifecycle-good.md b/snippets/golang/practices/goroutine-lifecycle-good.md new file mode 100644 index 0000000..b1ad794 --- /dev/null +++ b/snippets/golang/practices/goroutine-lifecycle-good.md @@ -0,0 +1,9 @@ +go func() { + defer wg.Done() + for { + select { + case <-ctx.Done(): return // clean exit + case job := <-jobCh: process(job) + } + } +}() \ No newline at end of file diff --git a/snippets/golang/practices/graceful-shutdown-good.md b/snippets/golang/practices/graceful-shutdown-good.md new file mode 100644 index 0000000..1c89c97 --- /dev/null +++ b/snippets/golang/practices/graceful-shutdown-good.md @@ -0,0 +1,8 @@ +quit := make(chan os.Signal, 1) +signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) +<-quit +ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) +defer cancel() +if err := server.Shutdown(ctx); err != nil { + log.Fatal("Server forced shutdown:", err) +} \ No newline at end of file diff --git a/snippets/golang/practices/handle-once-bad.md b/snippets/golang/practices/handle-once-bad.md new file mode 100644 index 0000000..429a82a --- /dev/null +++ b/snippets/golang/practices/handle-once-bad.md @@ -0,0 +1,4 @@ +if err != nil { + log.Printf("error: %v", err) + return err // logged AND returned โ€” duplicate log entry upstream +} \ No newline at end of file diff --git a/snippets/golang/practices/handle-once-good.md b/snippets/golang/practices/handle-once-good.md new file mode 100644 index 0000000..0f3dc31 --- /dev/null +++ b/snippets/golang/practices/handle-once-good.md @@ -0,0 +1,3 @@ +if err != nil { + return fmt.Errorf("service.Process: %w", err) // return only โ€” log at the top level +} \ No newline at end of file diff --git a/snippets/golang/practices/naming-conventions-bad.md b/snippets/golang/practices/naming-conventions-bad.md new file mode 100644 index 0000000..b34c73f --- /dev/null +++ b/snippets/golang/practices/naming-conventions-bad.md @@ -0,0 +1,2 @@ +func new_user_service() *userService { ... } +var UserCount int // unexported concept, wrong casing \ No newline at end of file diff --git a/snippets/golang/practices/naming-conventions-good.md b/snippets/golang/practices/naming-conventions-good.md new file mode 100644 index 0000000..66feef6 --- /dev/null +++ b/snippets/golang/practices/naming-conventions-good.md @@ -0,0 +1,2 @@ +func NewUserService() *UserService { ... } +var userCount int \ No newline at end of file diff --git a/snippets/golang/practices/parameterized-queries-bad.md b/snippets/golang/practices/parameterized-queries-bad.md new file mode 100644 index 0000000..6c3ea0a --- /dev/null +++ b/snippets/golang/practices/parameterized-queries-bad.md @@ -0,0 +1 @@ +row := db.QueryRowContext(ctx, "SELECT * FROM users WHERE id = " + id) // SQL injection \ No newline at end of file diff --git a/snippets/golang/practices/parameterized-queries-good.md b/snippets/golang/practices/parameterized-queries-good.md new file mode 100644 index 0000000..d125fb0 --- /dev/null +++ b/snippets/golang/practices/parameterized-queries-good.md @@ -0,0 +1 @@ +row := db.QueryRowContext(ctx, "SELECT * FROM users WHERE id = $1", id) \ No newline at end of file diff --git a/snippets/golang/practices/small-interfaces-bad.md b/snippets/golang/practices/small-interfaces-bad.md new file mode 100644 index 0000000..dd314c5 --- /dev/null +++ b/snippets/golang/practices/small-interfaces-bad.md @@ -0,0 +1,5 @@ +// Giant interface โ€” forces implementation of everything +type Repository interface { + Find() []Item; Save(Item) error; Delete(id string) error + Update(Item) error; Count() int; Exists(id string) bool +} \ No newline at end of file diff --git a/snippets/golang/practices/small-interfaces-good.md b/snippets/golang/practices/small-interfaces-good.md new file mode 100644 index 0000000..8d89b41 --- /dev/null +++ b/snippets/golang/practices/small-interfaces-good.md @@ -0,0 +1,3 @@ +// In the consumer package +type Writer interface { Write([]byte) (int, error) } +type Storer interface { Store(ctx context.Context, item Item) error } \ No newline at end of file diff --git a/snippets/golang/practices/structured-logging-bad.md b/snippets/golang/practices/structured-logging-bad.md new file mode 100644 index 0000000..670cd9a --- /dev/null +++ b/snippets/golang/practices/structured-logging-bad.md @@ -0,0 +1,10 @@ +// โŒ Unstructured log.Printf โ€” hard to parse, search, or alert on +log.Printf("user %s created with email %s in %v", userID, email, latency) + +// โŒ log.Fatal in a library โ€” kills the whole process +func (r *Repo) Connect() { + db, err := sql.Open("postgres", dsn) + if err != nil { + log.Fatal(err) // never use Fatal in libraries + } +} \ No newline at end of file diff --git a/snippets/golang/practices/structured-logging-good.md b/snippets/golang/practices/structured-logging-good.md new file mode 100644 index 0000000..9666342 --- /dev/null +++ b/snippets/golang/practices/structured-logging-good.md @@ -0,0 +1,26 @@ +import "log/slog" + +// Setup: JSON in production, text in development +func NewLogger(env string) *slog.Logger { + if env == "production" { + return slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ + Level: slog.LevelInfo, + })) + } + return slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + Level: slog.LevelDebug, + })) +} + +// Usage: structured key-value pairs, not format strings +logger.Info("user created", + slog.String("user_id", user.ID), + slog.String("email", user.Email), + slog.Duration("latency", time.Since(start)), +) + +// With context for trace propagation +logger.InfoContext(ctx, "request completed", + slog.Int("status", statusCode), + slog.String("path", r.URL.Path), +) \ No newline at end of file diff --git a/snippets/golang/practices/table-driven-tests-good.md b/snippets/golang/practices/table-driven-tests-good.md new file mode 100644 index 0000000..4d8e9e7 --- /dev/null +++ b/snippets/golang/practices/table-driven-tests-good.md @@ -0,0 +1,18 @@ +func TestAdd(t *testing.T) { + tests := []struct { + name string + a, b int + expected int + }{ + {"positive", 2, 3, 5}, + {"negative", -1, -2, -3}, + {"zero", 0, 0, 0}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if got := Add(tc.a, tc.b); got != tc.expected { + t.Errorf("Add(%d,%d) = %d, want %d", tc.a, tc.b, got, tc.expected) + } + }) + } +} \ No newline at end of file diff --git a/snippets/golang/practices/thin-handlers-good.md b/snippets/golang/practices/thin-handlers-good.md new file mode 100644 index 0000000..0f5d8ac --- /dev/null +++ b/snippets/golang/practices/thin-handlers-good.md @@ -0,0 +1,7 @@ +func (h *Handler) CreateUser(c echo.Context) error { + var req CreateUserRequest + if err := c.Bind(&req); err != nil { return echo.ErrBadRequest } + user, err := h.svc.CreateUser(c.Request().Context(), req) + if err != nil { return err } + return c.JSON(http.StatusCreated, user) +} \ No newline at end of file diff --git a/snippets/lenis/cheatsheet.md b/snippets/lenis/cheatsheet.md new file mode 100644 index 0000000..49913e3 --- /dev/null +++ b/snippets/lenis/cheatsheet.md @@ -0,0 +1,67 @@ +# Lenis React - Quick Reference + +## Required CSS + +```css +/* In your global CSS file โ€” or use: import 'lenis/dist/lenis.css' */ +html.lenis, html.lenis body { + height: auto; +} + +.lenis.lenis-smooth { + scroll-behavior: auto !important; +} + +.lenis.lenis-smooth [data-lenis-prevent] { + overscroll-behavior: contain; +} + +.lenis.lenis-stopped { + overflow: hidden; +} + +.lenis.lenis-smooth iframe { + pointer-events: none; +} +``` + +## Preventing Scroll on Nested Elements + +```html + +
...
+ + +
...
+ + +
...
+``` + +## Common Pitfalls + +| Problem | Cause | Fix | +|---|---|---| +| Scroll jumps with GSAP ScrollTrigger | RAF loop conflict | Use `autoRaf: false` + `gsap.ticker.add()` | +| `useLenis` returns undefined | Component outside `` tree | Move provider higher, or use `root` prop | +| Lenis not working in Next.js | Missing `'use client'` | Add directive to provider component | +| Modal/overlay blocks page scroll | Lenis still active | Call `lenis.stop()` on open, `lenis.start()` on close | +| iOS touch feels laggy | `smoothTouch: true` | Set `smoothTouch: false` (default) | +| `scroll-behavior: smooth` conflicts | CSS fighting Lenis | Add `.lenis.lenis-smooth { scroll-behavior: auto !important; }` | +| Anchor links don't work | Lenis intercepting | Lenis handles anchors โ€” use `scrollTo` or `data-lenis-prevent` | + +## lerp Decision Guide + +| Site Type | Recommended lerp | +|---|---| +| Marketing / landing | 0.08 โ€“ 0.12 | +| Creative / portfolio | 0.05 โ€“ 0.08 | +| App / dashboard | 0.1 (default) | + +## Package Import Reference + +| Package | Status | Import | +|---|---|---| +| `lenis` | Current (v1+) | `import { ReactLenis, useLenis } from 'lenis/react'` | +| `@studio-freight/lenis` | Legacy | `import Lenis from '@studio-freight/lenis'` | +| `@studio-freight/react-lenis` | Legacy | `import { ReactLenis } from '@studio-freight/react-lenis'` | \ No newline at end of file diff --git a/snippets/lenis/css/prevent-scroll.md b/snippets/lenis/css/prevent-scroll.md new file mode 100644 index 0000000..7dbf520 --- /dev/null +++ b/snippets/lenis/css/prevent-scroll.md @@ -0,0 +1,16 @@ + + + +
+ +
+ + +
...
+ + +
...
+ + +lenis?.stop() // pause Lenis (e.g. modal open) +lenis?.start() // resume Lenis (e.g. modal close) diff --git a/snippets/lenis/css/required.md b/snippets/lenis/css/required.md new file mode 100644 index 0000000..d1ab045 --- /dev/null +++ b/snippets/lenis/css/required.md @@ -0,0 +1,22 @@ +/* Required Lenis CSS โ€” import via 'lenis/dist/lenis.css' or add manually */ + +html.lenis, +html.lenis body { + height: auto; +} + +.lenis.lenis-smooth { + scroll-behavior: auto !important; +} + +.lenis.lenis-smooth [data-lenis-prevent] { + overscroll-behavior: contain; +} + +.lenis.lenis-stopped { + overflow: hidden; +} + +.lenis.lenis-smooth iframe { + pointer-events: none; +} diff --git a/snippets/lenis/examples/lenis-ref-imperative.md b/snippets/lenis/examples/lenis-ref-imperative.md new file mode 100644 index 0000000..45fd47b --- /dev/null +++ b/snippets/lenis/examples/lenis-ref-imperative.md @@ -0,0 +1,25 @@ +import { useRef, useEffect } from "react"; +import { ReactLenis, type LenisRef } from "lenis/react"; + +export function App() { + const lenisRef = useRef(null); + + useEffect(() => { + const lenis = lenisRef.current?.lenis; + if (!lenis) return; + + // Manually tick when needed (e.g. autoRaf: false) + function raf(time: number) { + lenis.raf(time); + requestAnimationFrame(raf); + } + const rafId = requestAnimationFrame(raf); + return () => cancelAnimationFrame(rafId); + }, []); + + return ( + + {/* ... */} + + ); +} \ No newline at end of file diff --git a/snippets/lenis/examples/react-lenis-container.md b/snippets/lenis/examples/react-lenis-container.md new file mode 100644 index 0000000..2999cc6 --- /dev/null +++ b/snippets/lenis/examples/react-lenis-container.md @@ -0,0 +1,12 @@ +import { ReactLenis } from "lenis/react"; + +export function ScrollContainer({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} \ No newline at end of file diff --git a/snippets/lenis/examples/react-lenis-ref.md b/snippets/lenis/examples/react-lenis-ref.md new file mode 100644 index 0000000..6ee3037 --- /dev/null +++ b/snippets/lenis/examples/react-lenis-ref.md @@ -0,0 +1,12 @@ +import { useRef } from "react"; +import { ReactLenis, type LenisRef } from "lenis/react"; + +export function SmoothLayout({ children }: { children: React.ReactNode }) { + const lenisRef = useRef(null); + + return ( + + {children} + + ); +} \ No newline at end of file diff --git a/snippets/lenis/examples/react-lenis-root.md b/snippets/lenis/examples/react-lenis-root.md new file mode 100644 index 0000000..897f31d --- /dev/null +++ b/snippets/lenis/examples/react-lenis-root.md @@ -0,0 +1,14 @@ +import { ReactLenis } from "lenis/react"; +import "lenis/dist/lenis.css"; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + + {children} + + + + ); +} \ No newline at end of file diff --git a/snippets/lenis/examples/use-lenis-modal.md b/snippets/lenis/examples/use-lenis-modal.md new file mode 100644 index 0000000..1130436 --- /dev/null +++ b/snippets/lenis/examples/use-lenis-modal.md @@ -0,0 +1,12 @@ +import { useLenis } from "lenis/react"; + +export function Modal({ onClose }: { onClose: () => void }) { + const lenis = useLenis(); + + useEffect(() => { + lenis?.stop(); + return () => lenis?.start(); + }, [lenis]); + + return
{/* modal content */}
; +} \ No newline at end of file diff --git a/snippets/lenis/examples/use-lenis-parallax.md b/snippets/lenis/examples/use-lenis-parallax.md new file mode 100644 index 0000000..d561566 --- /dev/null +++ b/snippets/lenis/examples/use-lenis-parallax.md @@ -0,0 +1,18 @@ +import { useRef } from "react"; +import { useLenis } from "lenis/react"; + +export function ParallaxSection() { + const ref = useRef(null); + + useLenis(({ scroll }) => { + if (!ref.current) return; + const offset = scroll * 0.3; + ref.current.style.transform = `translateY(${offset}px)`; + }); + + return ( +
+
+
+ ); +} \ No newline at end of file diff --git a/snippets/lenis/examples/use-lenis-progress.md b/snippets/lenis/examples/use-lenis-progress.md new file mode 100644 index 0000000..66c5757 --- /dev/null +++ b/snippets/lenis/examples/use-lenis-progress.md @@ -0,0 +1,17 @@ +import { useState } from "react"; +import { useLenis } from "lenis/react"; + +export function ScrollProgress() { + const [progress, setProgress] = useState(0); + + useLenis(({ progress }) => { + setProgress(progress); + }); + + return ( +
+ ); +} \ No newline at end of file diff --git a/snippets/lenis/examples/use-lenis-scroll-to.md b/snippets/lenis/examples/use-lenis-scroll-to.md new file mode 100644 index 0000000..dcc6f95 --- /dev/null +++ b/snippets/lenis/examples/use-lenis-scroll-to.md @@ -0,0 +1,12 @@ +import { useLenis } from "lenis/react"; + +export function NavLink({ href, children }: { href: string; children: React.ReactNode }) { + const lenis = useLenis(); + + function handleClick(e: React.MouseEvent) { + e.preventDefault(); + lenis?.scrollTo(href, { duration: 1.2, easing: (t) => 1 - Math.pow(1 - t, 4) }); + } + + return {children}; +} \ No newline at end of file diff --git a/snippets/lenis/options/horizontal.md b/snippets/lenis/options/horizontal.md new file mode 100644 index 0000000..6e8277d --- /dev/null +++ b/snippets/lenis/options/horizontal.md @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/snippets/lenis/options/tuned-marketing.md b/snippets/lenis/options/tuned-marketing.md new file mode 100644 index 0000000..aef28ec --- /dev/null +++ b/snippets/lenis/options/tuned-marketing.md @@ -0,0 +1,10 @@ + 1 - Math.pow(1 - t, 5), + smoothWheel: true, + smoothTouch: false, + }} +> \ No newline at end of file diff --git a/snippets/lenis/patterns/accessibility.md b/snippets/lenis/patterns/accessibility.md new file mode 100644 index 0000000..9c38fe3 --- /dev/null +++ b/snippets/lenis/patterns/accessibility.md @@ -0,0 +1,23 @@ +"use client"; + +import { ReactLenis } from "lenis/react"; +import "lenis/dist/lenis.css"; + +function useReducedMotion() { + if (typeof window === "undefined") return false; + return window.matchMedia("(prefers-reduced-motion: reduce)").matches; +} + +export function AccessibleSmoothScrollProvider({ children }: { children: React.ReactNode }) { + const prefersReducedMotion = useReducedMotion(); + + if (prefersReducedMotion) { + return <>{children}; + } + + return ( + + {children} + + ); +} \ No newline at end of file diff --git a/snippets/lenis/patterns/custom-container.md b/snippets/lenis/patterns/custom-container.md new file mode 100644 index 0000000..d40aeac --- /dev/null +++ b/snippets/lenis/patterns/custom-container.md @@ -0,0 +1,26 @@ +"use client"; + +import { useRef } from "react"; +import { ReactLenis } from "lenis/react"; +import "lenis/dist/lenis.css"; + +export function ScrollPanel({ children }: { children: React.ReactNode }) { + const wrapperRef = useRef(null); + const contentRef = useRef(null); + + return ( + +
+
+ {children} +
+
+
+ ); +} \ No newline at end of file diff --git a/snippets/lenis/patterns/framer-motion-integration.md b/snippets/lenis/patterns/framer-motion-integration.md new file mode 100644 index 0000000..966dc5b --- /dev/null +++ b/snippets/lenis/patterns/framer-motion-integration.md @@ -0,0 +1,35 @@ +"use client"; + +import { useEffect } from "react"; +import { ReactLenis, useLenis } from "lenis/react"; +import "lenis/dist/lenis.css"; +import { frame } from "motion"; + +function FramerSyncEffect() { + const lenis = useLenis(); + + useEffect(() => { + if (!lenis) return; + + function update({ timestamp }: { timestamp: number }) { + lenis.raf(timestamp); + } + + frame.update(update, true); + + return () => { + frame.cancel(update); + }; + }, [lenis]); + + return null; +} + +export function FramerLenisProvider({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + ); +} \ No newline at end of file diff --git a/snippets/lenis/patterns/full-page.md b/snippets/lenis/patterns/full-page.md new file mode 100644 index 0000000..488dcc0 --- /dev/null +++ b/snippets/lenis/patterns/full-page.md @@ -0,0 +1,17 @@ +// app/layout.tsx (Next.js App Router) +"use client"; // Must be client component + +import { ReactLenis } from "lenis/react"; +import "lenis/dist/lenis.css"; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + + {children} + + + + ); +} \ No newline at end of file diff --git a/snippets/lenis/patterns/gsap-integration.md b/snippets/lenis/patterns/gsap-integration.md new file mode 100644 index 0000000..f2c8cf5 --- /dev/null +++ b/snippets/lenis/patterns/gsap-integration.md @@ -0,0 +1,43 @@ +"use client"; + +import { useEffect } from "react"; +import { ReactLenis, useLenis } from "lenis/react"; +import "lenis/dist/lenis.css"; +import gsap from "gsap"; +import ScrollTrigger from "gsap/ScrollTrigger"; + +gsap.registerPlugin(ScrollTrigger); + +function GSAPSyncEffect() { + const lenis = useLenis(); + + useEffect(() => { + if (!lenis) return; + + // Sync Lenis with GSAP ticker + gsap.ticker.add((time) => { + lenis.raf(time * 1000); + }); + + // Disable GSAP's lagSmoothing to prevent conflicts + gsap.ticker.lagSmoothing(0); + + // Update ScrollTrigger on each scroll + lenis.on("scroll", ScrollTrigger.update); + + return () => { + lenis.off("scroll", ScrollTrigger.update); + }; + }, [lenis]); + + return null; +} + +export function GSAPLenisProvider({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + ); +} \ No newline at end of file diff --git a/snippets/lenis/patterns/next-js.md b/snippets/lenis/patterns/next-js.md new file mode 100644 index 0000000..48f41cb --- /dev/null +++ b/snippets/lenis/patterns/next-js.md @@ -0,0 +1,28 @@ +// components/smooth-scroll-provider.tsx +"use client"; + +import { ReactLenis } from "lenis/react"; +import "lenis/dist/lenis.css"; + +export function SmoothScrollProvider({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} + +// app/layout.tsx +import { SmoothScrollProvider } from "@/components/smooth-scroll-provider"; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + + {children} + + + + ); +} \ No newline at end of file diff --git a/snippets/lenis/patterns/scroll-to-nav.md b/snippets/lenis/patterns/scroll-to-nav.md new file mode 100644 index 0000000..02c04ff --- /dev/null +++ b/snippets/lenis/patterns/scroll-to-nav.md @@ -0,0 +1,32 @@ +"use client"; + +import { useLenis } from "lenis/react"; + +interface NavLinkProps { + href: string; + children: React.ReactNode; + offset?: number; + duration?: number; +} + +export function NavLink({ href, children, offset = -80, duration = 1.2 }: NavLinkProps) { + const lenis = useLenis(); + + function handleClick(e: React.MouseEvent) { + e.preventDefault(); + lenis?.scrollTo(href, { + offset, + duration, + easing: (t) => 1 - Math.pow(1 - t, 4), + }); + } + + return ( + + {children} + + ); +} + +// Usage: +// Features \ No newline at end of file diff --git a/snippets/lenis/recipes/back-to-top.md b/snippets/lenis/recipes/back-to-top.md new file mode 100644 index 0000000..7e8a4ce --- /dev/null +++ b/snippets/lenis/recipes/back-to-top.md @@ -0,0 +1,14 @@ +'use client' +import { useLenis } from 'lenis/react' +import { useState } from 'react' + +export function BackToTop() { + const [visible, setVisible] = useState(false) + const lenis = useLenis(({ scroll }) => setVisible(scroll > 400)) + + return visible ? ( + + ) : null +} diff --git a/snippets/lenis/recipes/direction-indicator.md b/snippets/lenis/recipes/direction-indicator.md new file mode 100644 index 0000000..51e005e --- /dev/null +++ b/snippets/lenis/recipes/direction-indicator.md @@ -0,0 +1,21 @@ +'use client' +import { useState } from 'react' +import { useLenis } from 'lenis/react' + +// Scroll event properties: +// scroll โ€” current position (px) +// limit โ€” max scroll (px) +// velocity โ€” scroll speed +// direction โ€” 1 (down) or -1 (up) +// progress โ€” 0 to 1 + +export function DirectionIndicator() { + const [dir, setDir] = useState<'up' | 'down'>('down') + + useLenis(({ direction }) => { + if (direction === 1) setDir('down') + if (direction === -1) setDir('up') + }) + + return
Scrolling: {dir}
+} diff --git a/snippets/lenis/recipes/gsap-complete.md b/snippets/lenis/recipes/gsap-complete.md new file mode 100644 index 0000000..d1b6510 --- /dev/null +++ b/snippets/lenis/recipes/gsap-complete.md @@ -0,0 +1,53 @@ +'use client' +import gsap from 'gsap' +import ScrollTrigger from 'gsap/ScrollTrigger' +import { ReactLenis } from 'lenis/react' +import type { LenisRef } from 'lenis/react' +import { useEffect, useRef } from 'react' + +gsap.registerPlugin(ScrollTrigger) + +export function GSAPScrollProvider({ children }: { children: React.ReactNode }) { + const lenisRef = useRef(null) + + useEffect(() => { + const update = (time: number) => { + lenisRef.current?.lenis?.raf(time * 1000) + } + gsap.ticker.add(update) + gsap.ticker.lagSmoothing(0) + return () => gsap.ticker.remove(update) + }, []) + + return ( + + {children} + + ) +} + +// Usage in a component with ScrollTrigger: +import { useEffect, useRef } from 'react' +import gsap from 'gsap' +import ScrollTrigger from 'gsap/ScrollTrigger' + +export function FadeInSection({ children }: { children: React.ReactNode }) { + const ref = useRef(null) + + useEffect(() => { + const ctx = gsap.context(() => { + gsap.from(ref.current, { + opacity: 0, + y: 60, + duration: 0.8, + scrollTrigger: { + trigger: ref.current, + start: 'top 85%', + }, + }) + }, ref) + return () => ctx.revert() + }, []) + + return
{children}
+} diff --git a/snippets/lenis/recipes/horizontal-scroll-section.md b/snippets/lenis/recipes/horizontal-scroll-section.md new file mode 100644 index 0000000..52af8ad --- /dev/null +++ b/snippets/lenis/recipes/horizontal-scroll-section.md @@ -0,0 +1,29 @@ +'use client' +import { ReactLenis } from 'lenis/react' +import { useRef } from 'react' + +export function HorizontalScroller({ children }: { children: React.ReactNode }) { + const wrapperRef = useRef(null) + const contentRef = useRef(null) + + return ( + +
+
+ {children} +
+
+
+ ) +} diff --git a/snippets/lenis/recipes/parallax-layer.md b/snippets/lenis/recipes/parallax-layer.md new file mode 100644 index 0000000..bf2b47d --- /dev/null +++ b/snippets/lenis/recipes/parallax-layer.md @@ -0,0 +1,21 @@ +'use client' +import { useLenis } from 'lenis/react' +import { useRef } from 'react' + +export function ParallaxLayer({ + speed = 0.5, + children, +}: { + speed?: number + children: React.ReactNode +}) { + const ref = useRef(null) + + useLenis(({ scroll }) => { + if (ref.current) { + ref.current.style.transform = `translateY(${scroll * speed}px)` + } + }) + + return
{children}
+} diff --git a/snippets/lenis/recipes/scroll-locked-modal.md b/snippets/lenis/recipes/scroll-locked-modal.md new file mode 100644 index 0000000..54e0ad9 --- /dev/null +++ b/snippets/lenis/recipes/scroll-locked-modal.md @@ -0,0 +1,25 @@ +'use client' +import { useLenis } from 'lenis/react' +import { useEffect } from 'react' + +export function Modal({ isOpen, onClose, children }: { + isOpen: boolean + onClose: () => void + children: React.ReactNode +}) { + const lenis = useLenis() + + useEffect(() => { + if (isOpen) lenis?.stop() + else lenis?.start() + return () => lenis?.start() // safety cleanup + }, [isOpen, lenis]) + + if (!isOpen) return null + return ( +
+ {children} + +
+ ) +} diff --git a/snippets/lenis/recipes/scroll-progress-bar.md b/snippets/lenis/recipes/scroll-progress-bar.md new file mode 100644 index 0000000..71d1f8d --- /dev/null +++ b/snippets/lenis/recipes/scroll-progress-bar.md @@ -0,0 +1,24 @@ +'use client' +import { useLenis } from 'lenis/react' +import { useState } from 'react' + +export function ScrollProgressBar() { + const [progress, setProgress] = useState(0) + + useLenis(({ progress }) => setProgress(progress)) + + return ( +
+ ) +} diff --git a/snippets/lenis/usage/lenis-options.md b/snippets/lenis/usage/lenis-options.md new file mode 100644 index 0000000..c5ceddd --- /dev/null +++ b/snippets/lenis/usage/lenis-options.md @@ -0,0 +1,13 @@ + 1 - Math.pow(1 - t, 4), + orientation: "vertical", + smoothWheel: true, + smoothTouch: false, + }} +> + {children} + \ No newline at end of file diff --git a/snippets/lenis/usage/lenis-ref.md b/snippets/lenis/usage/lenis-ref.md new file mode 100644 index 0000000..f900c76 --- /dev/null +++ b/snippets/lenis/usage/lenis-ref.md @@ -0,0 +1,16 @@ +import { useRef } from "react"; +import { ReactLenis, type LenisRef } from "lenis/react"; + +export function SmoothLayout({ children }: { children: React.ReactNode }) { + const lenisRef = useRef(null); + + function scrollToSection(id: string) { + lenisRef.current?.lenis?.scrollTo(`#${id}`, { duration: 1.0 }); + } + + return ( + + {children} + + ); +} \ No newline at end of file diff --git a/snippets/lenis/usage/react-lenis.md b/snippets/lenis/usage/react-lenis.md new file mode 100644 index 0000000..4710190 --- /dev/null +++ b/snippets/lenis/usage/react-lenis.md @@ -0,0 +1,15 @@ +// Root layout (Next.js App Router) +import { ReactLenis } from "lenis/react"; +import "lenis/dist/lenis.css"; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + + {children} + + + + ); +} \ No newline at end of file diff --git a/snippets/lenis/usage/use-lenis.md b/snippets/lenis/usage/use-lenis.md new file mode 100644 index 0000000..3d4f16b --- /dev/null +++ b/snippets/lenis/usage/use-lenis.md @@ -0,0 +1,21 @@ +import { useLenis } from "lenis/react"; + +// Read the instance +function MyComponent() { + const lenis = useLenis(); + + function scrollToTop() { + lenis?.scrollTo(0, { duration: 1.2 }); + } + + return ; +} + +// Subscribe to scroll events +function ScrollTracker() { + useLenis(({ scroll, progress, velocity }) => { + console.log("scroll:", scroll, "progress:", progress); + }); + + return null; +} \ No newline at end of file diff --git a/snippets/react/cheatsheet.md b/snippets/react/cheatsheet.md new file mode 100644 index 0000000..ca15964 --- /dev/null +++ b/snippets/react/cheatsheet.md @@ -0,0 +1,88 @@ +# React Pro Coder - Quick Reference + +## Architecture-First Reasoning Order (Step 2) + +Never skip layers. Reason strictly in this order: +1. Responsibilities +2. Invariants (inputs/state/ordering) +3. Dependency direction +4. Module boundaries +5. Public APIs +6. Folder structure +7. Files +8. Functions +9. Syntax + +## Rendering Decision Tree + +``` +Is it SEO-critical? +โ”œโ”€โ”€ Yes โ†’ RSC / SSR / SSG / ISR +โ””โ”€โ”€ No + โ”œโ”€โ”€ Needs interactivity? (state, events, browser APIs) + โ”‚ โ””โ”€โ”€ Yes โ†’ "use client" Client Component + โ””โ”€โ”€ No โ†’ RSC (default) +``` + +## State Hierarchy (Strict Order) + +1. URL state (searchParams/pathname) โ€” shareable, bookmarkable +2. Server state (React Query / SWR / RSC fetch) โ€” cached, deduplicated +3. Local component state โ€” useState / useReducer +4. Shared client state โ€” Zustand (Redux prohibited) +5. Context โ€” injection only (theme, auth tokens, i18n, feature flags) + +## Forbidden Patterns + +| Pattern | Reason | Alternative | +|---|---|---| +| `useEffect` for data fetching | Waterfall, no caching, race conditions | RSC fetch or React Query | +| `useEffect(fn, [])` as componentDidMount | Runs twice in React 18 Strict Mode | RSC async functions, useQuery | +| Prop drilling > 2 levels | Tight coupling, middle components hold data they don't use | Composition (children/slots), context injection, Zustand | +| Context for frequently changing state | Every consumer re-renders on every change | Zustand with slice selectors | +| `any` in TypeScript | Defeats type safety | `unknown` + type narrowing, or define proper types | +| Redux | Verbose boilerplate | Zustand | +| Barrel exports in large codebases | Breaks tree-shaking | Direct imports | + +## Output Contract Checklist (Step 6) + +Every implementation response must include: +- [ ] Task Classification (Feature / Refactor / Bug Fix / Performance / Review) +- [ ] Environment Verification (node version, React version, Next.js version) +- [ ] Assumptions stated explicitly +- [ ] Architecture Decision (rendering + state location + module boundaries) +- [ ] SEO Requirements (if applicable) +- [ ] Public APIs documented +- [ ] Code organized by file paths +- [ ] Tests (Vitest + Testing Library) +- [ ] Negative Doubt Log +- [ ] Risks & Trade-offs + +## Negative Doubt Routine (Step 7) + +After drafting output, verify: +1. Find 5 concrete failure modes + add tests for each +2. Falsify assumptions โ€” what if they are wrong? +3. Enforce invariants with guards/validation +4. Audit dependencies โ€” no circular deps, minimal public surface +5. Challenge simpler alternatives โ€” is there an easier way? +6. Inject at least one test per failure mode +7. Revise + repeat pass +8. Append Negative Doubt Log to response + +Hard stop: if correctness/safety is still uncertain after this routine, refuse to finalize. + +## Environment Gate (Step 0) + +```bash +node -v # >= 18.x +npm ls react # React 18+ +npm ls next # Next.js 14+ if using App Router / RSC +``` + +Defaults (if unspecified): +- Framework: Next.js App Router +- Styling: Tailwind CSS + shadcn/ui +- Icons: lucide-react +- Shared client state: Zustand +- Server state: React Query or SWR (or RSC fetch) \ No newline at end of file diff --git a/snippets/react/patterns/component-template.md b/snippets/react/patterns/component-template.md new file mode 100644 index 0000000..50beedd --- /dev/null +++ b/snippets/react/patterns/component-template.md @@ -0,0 +1,17 @@ +import { type FC } from "react"; +import { cn } from "@/lib/utils"; +// import { Button } from "@/components/ui/button"; +// import { SomeIcon } from "lucide-react"; + +interface Props { + className?: string; + children?: React.ReactNode; +} + +export const ComponentName: FC = ({ className, children }) => { + return ( +
+ {children} +
+ ); +}; \ No newline at end of file diff --git a/snippets/react/patterns/composition-anti.md b/snippets/react/patterns/composition-anti.md new file mode 100644 index 0000000..e58bce6 --- /dev/null +++ b/snippets/react/patterns/composition-anti.md @@ -0,0 +1,5 @@ +// โŒ Prop drilling โ€” user passed through 3 components + + + + diff --git a/snippets/react/patterns/composition-pattern.md b/snippets/react/patterns/composition-pattern.md new file mode 100644 index 0000000..b15918b --- /dev/null +++ b/snippets/react/patterns/composition-pattern.md @@ -0,0 +1,15 @@ +// โœ… Composition โ€” pass JSX as children/slots +function Layout({ sidebar, content }: { sidebar: React.ReactNode; content: React.ReactNode }) { + return ( +
+ +
{content}
+
+ ); +} + +// Usage โ€” no prop drilling +} + content={} +/> \ No newline at end of file diff --git a/snippets/react/patterns/data-fetching-anti.md b/snippets/react/patterns/data-fetching-anti.md new file mode 100644 index 0000000..92c63ce --- /dev/null +++ b/snippets/react/patterns/data-fetching-anti.md @@ -0,0 +1,6 @@ +// โŒ useEffect for fetching +"use client"; +function Component() { + const [data, setData] = useState(null); + useEffect(() => { fetch("/api").then(r => r.json()).then(setData); }, []); +} diff --git a/snippets/react/patterns/data-fetching-rsc.md b/snippets/react/patterns/data-fetching-rsc.md new file mode 100644 index 0000000..d471d13 --- /dev/null +++ b/snippets/react/patterns/data-fetching-rsc.md @@ -0,0 +1,15 @@ +// โœ… Fetch in RSC โ€” no loading state, no useEffect +export default async function Page({ params }: { params: { id: string } }) { + const data = await fetch(`/api/items/${params.id}`, { + next: { revalidate: 60 }, // ISR: revalidate every 60s + }).then(r => r.json()); + return ; +} + +// โœ… Client-side: React Query (not useEffect) +"use client"; +import { useQuery } from "@tanstack/react-query"; +function ClientWidget() { + const { data } = useQuery({ queryKey: ["widget"], queryFn: fetchWidget }); + return
{data?.value}
; +} \ No newline at end of file diff --git a/snippets/react/patterns/nextjs-metadata.md b/snippets/react/patterns/nextjs-metadata.md new file mode 100644 index 0000000..f6b7095 --- /dev/null +++ b/snippets/react/patterns/nextjs-metadata.md @@ -0,0 +1,23 @@ +import type { Metadata } from "next"; + +// Static metadata +export const metadata: Metadata = { + title: "Products", + description: "Browse all products", + openGraph: { title: "Products", type: "website" }, +}; + +// Dynamic metadata +export async function generateMetadata( + { params }: { params: { id: string } } +): Promise { + const product = await getProduct(params.id); + return { + title: product.name, + description: product.description, + openGraph: { + title: product.name, + images: [product.imageUrl], + }, + }; +} \ No newline at end of file diff --git a/snippets/react/patterns/rsc-anti.md b/snippets/react/patterns/rsc-anti.md new file mode 100644 index 0000000..aca7d48 --- /dev/null +++ b/snippets/react/patterns/rsc-anti.md @@ -0,0 +1,5 @@ +// โŒ 'use client' on a component that just renders data +"use client"; +export default function ProductCard({ product }) { + return
{product.name}
; // no interactivity โ€” RSC is fine +} diff --git a/snippets/react/patterns/rsc-default.md b/snippets/react/patterns/rsc-default.md new file mode 100644 index 0000000..df84805 --- /dev/null +++ b/snippets/react/patterns/rsc-default.md @@ -0,0 +1,13 @@ +// โœ… RSC by default โ€” no directive needed +export default async function ProductList() { + const products = await db.query("SELECT * FROM products"); + return
    {products.map(p =>
  • {p.name}
  • )}
; +} + +// โœ… Client only when needed +"use client"; +import { useState } from "react"; +export function Counter() { + const [count, setCount] = useState(0); + return ; +} \ No newline at end of file diff --git a/snippets/react/patterns/state-hierarchy-anti.md b/snippets/react/patterns/state-hierarchy-anti.md new file mode 100644 index 0000000..abf4ef3 --- /dev/null +++ b/snippets/react/patterns/state-hierarchy-anti.md @@ -0,0 +1,2 @@ +// โŒ Context for frequently changing state +const CountContext = createContext(0); // re-renders all consumers on every change diff --git a/snippets/react/patterns/state-hierarchy.md b/snippets/react/patterns/state-hierarchy.md new file mode 100644 index 0000000..949b131 --- /dev/null +++ b/snippets/react/patterns/state-hierarchy.md @@ -0,0 +1,15 @@ +// 1. URL state (searchParams) โ€” shareable, bookmarkable +const searchParams = useSearchParams(); +const sort = searchParams.get("sort") ?? "asc"; + +// 2. Server state โ€” React Query / SWR / RSC fetch +const { data } = useQuery({ queryKey: ["users"], queryFn: fetchUsers }); + +// 3. Local component state +const [open, setOpen] = useState(false); + +// 4. Shared client state โ€” Zustand +const user = useAuthStore((s) => s.user); + +// 5. Context โ€” injection only (theme, auth, i18n, feature flags) +const theme = useContext(ThemeContext); \ No newline at end of file diff --git a/snippets/react/patterns/suspense-boundary.md b/snippets/react/patterns/suspense-boundary.md new file mode 100644 index 0000000..999ff3f --- /dev/null +++ b/snippets/react/patterns/suspense-boundary.md @@ -0,0 +1,12 @@ +import { Suspense } from "react"; +import { ErrorBoundary } from "react-error-boundary"; + +export default function Page() { + return ( + }> + }> + + + + ); +} \ No newline at end of file diff --git a/snippets/react/patterns/zustand-store.md b/snippets/react/patterns/zustand-store.md new file mode 100644 index 0000000..f024419 --- /dev/null +++ b/snippets/react/patterns/zustand-store.md @@ -0,0 +1,24 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; + +interface AuthStore { + user: User | null; + token: string | null; + setUser: (user: User, token: string) => void; + logout: () => void; +} + +export const useAuthStore = create()( + persist( + (set) => ({ + user: null, + token: null, + setUser: (user, token) => set({ user, token }), + logout: () => set({ user: null, token: null }), + }), + { name: "auth-storage" } + ) +); + +// Usage โ€” subscribe only to needed slice (perf) +const user = useAuthStore((s) => s.user); \ No newline at end of file diff --git a/snippets/rust/cheatsheet.md b/snippets/rust/cheatsheet.md new file mode 100644 index 0000000..c1ba772 --- /dev/null +++ b/snippets/rust/cheatsheet.md @@ -0,0 +1,71 @@ +# Rust Best Practices Cheatsheet + +## Borrowing & Ownership +- Prefer `&T` over `.clone()` unless ownership transfer is required +- Use `&str` over `String`, `&[T]` over `Vec` in function parameters +- Small `Copy` types (โ‰ค24 bytes) can be passed by value โ€” no cost +- Use `Cow<'_, T>` when ownership is ambiguous (sometimes needed, sometimes not) +- Pass large types (>512 bytes) by reference โ€” avoid stack copies + +## Error Handling +- Return `Result` for fallible operations; avoid `panic!` in production +- Never use `unwrap()`/`expect()` outside tests +- Use `thiserror` for library errors (callers match on typed errors) +- Use `anyhow` for binary/application errors (context strings only) +- Prefer `?` operator over match chains for error propagation + +## Performance +- Always benchmark with `--release` flag โ€” debug is 10โ€“100x slower +- Run `cargo clippy -- -D clippy::perf` for performance hints +- Avoid cloning in loops โ€” use `.iter()` instead of `.into_iter()` for Copy types +- Prefer iterators over manual loops โ€” avoid intermediate `.collect()` calls +- Keep small types on the stack; heap-allocate recursive or large (>512B) structures +- Profile with `cargo flamegraph` before optimizing โ€” don't guess, measure + +## Linting +Run: `cargo clippy --all-targets --all-features --locked -- -D warnings` + +Key lints: +- `redundant_clone` โ€” unnecessary cloning +- `large_enum_variant` โ€” oversized variants (consider boxing) +- `needless_collect` โ€” premature collection + +Use `#[expect(clippy::lint, reason = "...")]` over `#[allow(...)]` โ€” expect fails if the lint is fixed. + +## Testing +- Name tests descriptively: `process_should_return_error_when_input_empty()` +- One assertion per test when possible +- Use doc tests (`///`) for public API examples โ€” they compile and run with `cargo test` +- Consider `cargo insta` for snapshot testing generated output + +## Generics & Dispatch +- Prefer generics (static dispatch) for performance-critical code โ€” zero runtime cost +- Use `dyn Trait` only when heterogeneous collections are needed or type erasure is required +- Box at API boundaries, not internally โ€” use generics internally, expose `Box` at the public API edge +- Avoid premature boxing: use `struct Renderer` not `struct Renderer { backend: Box }` + +## Type State Pattern +Encode valid states in the type system to catch invalid operations at compile time. +Use `PhantomData` on structs โ€” impl blocks restrict methods to valid states only. +Use only when invalid state transitions are a real risk โ€” simple enums often suffice. + +## Pointers & Thread Safety +| Pointer | Thread-safe? | Use | +|---------|-------------|-----| +| `&T` | Yes | Shared read access | +| `&mut T` | No (exclusive) | Exclusive mutation | +| `Box` | Yes (if T: Send+Sync) | Single-owner heap allocation | +| `Rc` | No | Multiple owners, single thread | +| `Arc` | Yes | Multiple owners, multiple threads | +| `RefCell` | No | Interior mutability, single thread | +| `Mutex` | Yes | Shared mutability across threads | +| `RwLock` | Yes | Multiple readers OR one writer | +| `OnceLock` | Yes | Thread-safe single initialization | + +Common pattern: `Arc>` for shared mutable state across threads. + +## Documentation +- `//` comments explain *why* (safety, workarounds, design rationale) +- `///` doc comments explain *what* and *how* for public APIs +- Every `Note` comment needs a linked issue: `// Note(#42): ...` +- Enable `#![deny(missing_docs)]` for libraries \ No newline at end of file diff --git a/snippets/rust/practices/avoid-clone-in-loops-bad.md b/snippets/rust/practices/avoid-clone-in-loops-bad.md new file mode 100644 index 0000000..cff804b --- /dev/null +++ b/snippets/rust/practices/avoid-clone-in-loops-bad.md @@ -0,0 +1 @@ +let sum: u32 = numbers.clone().into_iter().sum(); // unnecessary clone \ No newline at end of file diff --git a/snippets/rust/practices/avoid-clone-in-loops-good.md b/snippets/rust/practices/avoid-clone-in-loops-good.md new file mode 100644 index 0000000..8505814 --- /dev/null +++ b/snippets/rust/practices/avoid-clone-in-loops-good.md @@ -0,0 +1,2 @@ +let sum: u32 = numbers.iter().sum(); // borrows, no alloc +let upper: Vec<_> = strings.iter().map(|s| s.to_uppercase()).collect(); \ No newline at end of file diff --git a/snippets/rust/practices/benchmark-release-bad.md b/snippets/rust/practices/benchmark-release-bad.md new file mode 100644 index 0000000..8c8d69e --- /dev/null +++ b/snippets/rust/practices/benchmark-release-bad.md @@ -0,0 +1 @@ +cargo build && time ./target/debug/app # debug perf is irrelevant \ No newline at end of file diff --git a/snippets/rust/practices/benchmark-release-good.md b/snippets/rust/practices/benchmark-release-good.md new file mode 100644 index 0000000..761e9eb --- /dev/null +++ b/snippets/rust/practices/benchmark-release-good.md @@ -0,0 +1,2 @@ +cargo bench # uses release profile +cargo build --release && ./target/release/app \ No newline at end of file diff --git a/snippets/rust/practices/borrow-over-clone-bad.md b/snippets/rust/practices/borrow-over-clone-bad.md new file mode 100644 index 0000000..372820f --- /dev/null +++ b/snippets/rust/practices/borrow-over-clone-bad.md @@ -0,0 +1 @@ +fn process(data: Vec) -> usize { data.len() } // forces caller to clone or give up ownership \ No newline at end of file diff --git a/snippets/rust/practices/borrow-over-clone-good.md b/snippets/rust/practices/borrow-over-clone-good.md new file mode 100644 index 0000000..d801846 --- /dev/null +++ b/snippets/rust/practices/borrow-over-clone-good.md @@ -0,0 +1 @@ +fn process(data: &[u8]) -> usize { data.len() } \ No newline at end of file diff --git a/snippets/rust/practices/copy-by-value-good.md b/snippets/rust/practices/copy-by-value-good.md new file mode 100644 index 0000000..f578f86 --- /dev/null +++ b/snippets/rust/practices/copy-by-value-good.md @@ -0,0 +1,2 @@ +fn double(n: u32) -> u32 { n * 2 } // Copy โ€” pass by value +fn point_x(p: Point) -> f32 { p.x } // Point: Copy + small \ No newline at end of file diff --git a/snippets/rust/practices/cow-ambiguous-ownership-good.md b/snippets/rust/practices/cow-ambiguous-ownership-good.md new file mode 100644 index 0000000..42aacae --- /dev/null +++ b/snippets/rust/practices/cow-ambiguous-ownership-good.md @@ -0,0 +1,8 @@ +use std::borrow::Cow; +fn normalize(s: &str) -> Cow<'_, str> { + if s.chars().all(|c| c.is_lowercase()) { + Cow::Borrowed(s) // no allocation when already lowercase + } else { + Cow::Owned(s.to_lowercase()) // allocates only when needed + } +} \ No newline at end of file diff --git a/snippets/rust/practices/descriptive-test-names-bad.md b/snippets/rust/practices/descriptive-test-names-bad.md new file mode 100644 index 0000000..22de546 --- /dev/null +++ b/snippets/rust/practices/descriptive-test-names-bad.md @@ -0,0 +1,5 @@ +#[test] +fn test_parse() { ... } + +#[test] +fn test_login() { ... } \ No newline at end of file diff --git a/snippets/rust/practices/descriptive-test-names-good.md b/snippets/rust/practices/descriptive-test-names-good.md new file mode 100644 index 0000000..9285c1d --- /dev/null +++ b/snippets/rust/practices/descriptive-test-names-good.md @@ -0,0 +1,5 @@ +#[test] +fn parse_should_return_error_when_json_malformed() { ... } + +#[test] +fn login_should_reject_expired_token() { ... } \ No newline at end of file diff --git a/snippets/rust/practices/doc-tests-good.md b/snippets/rust/practices/doc-tests-good.md new file mode 100644 index 0000000..9912af1 --- /dev/null +++ b/snippets/rust/practices/doc-tests-good.md @@ -0,0 +1,7 @@ +/// Adds two numbers. +/// +/// # Examples +/// ``` +/// assert_eq!(mylib::add(2, 3), 5); +/// ``` +pub fn add(a: i32, b: i32) -> i32 { a + b } \ No newline at end of file diff --git a/snippets/rust/practices/expect-over-allow-bad.md b/snippets/rust/practices/expect-over-allow-bad.md new file mode 100644 index 0000000..50a0992 --- /dev/null +++ b/snippets/rust/practices/expect-over-allow-bad.md @@ -0,0 +1,2 @@ +#[allow(clippy::large_enum_variant)] // no reason, never expires +enum MyEnum { Foo(LargeStruct), Bar(u8) } \ No newline at end of file diff --git a/snippets/rust/practices/expect-over-allow-good.md b/snippets/rust/practices/expect-over-allow-good.md new file mode 100644 index 0000000..c0401ce --- /dev/null +++ b/snippets/rust/practices/expect-over-allow-good.md @@ -0,0 +1,2 @@ +#[expect(clippy::large_enum_variant, reason = "Foo variant is the hot path")] +enum MyEnum { Foo(LargeStruct), Bar(u8) } \ No newline at end of file diff --git a/snippets/rust/practices/no-unwrap-in-prod-bad.md b/snippets/rust/practices/no-unwrap-in-prod-bad.md new file mode 100644 index 0000000..869cbd4 --- /dev/null +++ b/snippets/rust/practices/no-unwrap-in-prod-bad.md @@ -0,0 +1 @@ +let value = map.get(&key).unwrap(); // panics in production \ No newline at end of file diff --git a/snippets/rust/practices/no-unwrap-in-prod-good.md b/snippets/rust/practices/no-unwrap-in-prod-good.md new file mode 100644 index 0000000..25617f6 --- /dev/null +++ b/snippets/rust/practices/no-unwrap-in-prod-good.md @@ -0,0 +1 @@ +let value = map.get(&key)?; // returns None/Err to caller \ No newline at end of file diff --git a/snippets/rust/practices/one-assertion-per-test-good.md b/snippets/rust/practices/one-assertion-per-test-good.md new file mode 100644 index 0000000..1a84b82 --- /dev/null +++ b/snippets/rust/practices/one-assertion-per-test-good.md @@ -0,0 +1,2 @@ +#[test] fn returns_correct_value() { assert_eq!(add(2, 3), 5); } +#[test] fn handles_overflow() { assert!(add(i32::MAX, 1).is_err()); } \ No newline at end of file diff --git a/snippets/rust/practices/prefer-iterators-bad.md b/snippets/rust/practices/prefer-iterators-bad.md new file mode 100644 index 0000000..dd73c7a --- /dev/null +++ b/snippets/rust/practices/prefer-iterators-bad.md @@ -0,0 +1,2 @@ +let filtered: Vec<_> = data.iter().filter(|x| x.active).collect(); // alloc +let result: Vec<_> = filtered.iter().map(|x| x.value * 2).collect(); // alloc again \ No newline at end of file diff --git a/snippets/rust/practices/prefer-iterators-good.md b/snippets/rust/practices/prefer-iterators-good.md new file mode 100644 index 0000000..fe802f4 --- /dev/null +++ b/snippets/rust/practices/prefer-iterators-good.md @@ -0,0 +1,5 @@ +let result: Vec<_> = data + .iter() + .filter(|x| x.active) + .map(|x| x.value * 2) + .collect(); // one allocation at the end \ No newline at end of file diff --git a/snippets/rust/practices/result-not-panic-bad.md b/snippets/rust/practices/result-not-panic-bad.md new file mode 100644 index 0000000..5e8f024 --- /dev/null +++ b/snippets/rust/practices/result-not-panic-bad.md @@ -0,0 +1,4 @@ +fn parse_config(path: &str) -> Config { + let text = fs::read_to_string(path).unwrap(); // panics if file missing + toml::from_str(&text).unwrap() +} \ No newline at end of file diff --git a/snippets/rust/practices/result-not-panic-good.md b/snippets/rust/practices/result-not-panic-good.md new file mode 100644 index 0000000..2f869d5 --- /dev/null +++ b/snippets/rust/practices/result-not-panic-good.md @@ -0,0 +1,4 @@ +fn parse_config(path: &str) -> Result { + let text = fs::read_to_string(path)?; + toml::from_str(&text).map_err(ConfigError::Parse) +} \ No newline at end of file diff --git a/snippets/rust/practices/send-sync-bad.md b/snippets/rust/practices/send-sync-bad.md new file mode 100644 index 0000000..3923e3f --- /dev/null +++ b/snippets/rust/practices/send-sync-bad.md @@ -0,0 +1,3 @@ +use std::rc::Rc; +let data = Rc::new(vec![1, 2, 3]); +// thread::spawn(move || ...data...); // compile error: Rc is not Send \ No newline at end of file diff --git a/snippets/rust/practices/send-sync-good.md b/snippets/rust/practices/send-sync-good.md new file mode 100644 index 0000000..fd939bc --- /dev/null +++ b/snippets/rust/practices/send-sync-good.md @@ -0,0 +1,4 @@ +use std::sync::Arc; +let data = Arc::new(vec![1, 2, 3]); +let clone = Arc::clone(&data); +thread::spawn(move || println!("{:?}", clone)); \ No newline at end of file diff --git a/snippets/rust/practices/static-over-dynamic-dispatch-good.md b/snippets/rust/practices/static-over-dynamic-dispatch-good.md new file mode 100644 index 0000000..009986d --- /dev/null +++ b/snippets/rust/practices/static-over-dynamic-dispatch-good.md @@ -0,0 +1,5 @@ +// Static dispatch โ€” compiler generates specialized code +fn process(p: T) { p.run(); } + +// Dynamic dispatch โ€” needed for mixed types +fn run_all(processors: &[Box]) { ... } \ No newline at end of file diff --git a/snippets/rust/practices/str-over-string-bad.md b/snippets/rust/practices/str-over-string-bad.md new file mode 100644 index 0000000..c99e0aa --- /dev/null +++ b/snippets/rust/practices/str-over-string-bad.md @@ -0,0 +1,2 @@ +fn greet(name: String) { println!("Hello, {name}"); } +// Caller: greet("world".to_string()) โ€” unnecessary allocation \ No newline at end of file diff --git a/snippets/rust/practices/str-over-string-good.md b/snippets/rust/practices/str-over-string-good.md new file mode 100644 index 0000000..1618865 --- /dev/null +++ b/snippets/rust/practices/str-over-string-good.md @@ -0,0 +1,2 @@ +fn greet(name: &str) { println!("Hello, {name}"); } +// Caller: greet("world") or greet(&owned_string) \ No newline at end of file diff --git a/snippets/rust/practices/thiserror-vs-anyhow-good.md b/snippets/rust/practices/thiserror-vs-anyhow-good.md new file mode 100644 index 0000000..7009586 --- /dev/null +++ b/snippets/rust/practices/thiserror-vs-anyhow-good.md @@ -0,0 +1,12 @@ +// Library +#[derive(thiserror::Error, Debug)] +pub enum ApiError { + #[error("not found: {0}")] NotFound(String), + #[error("io error")] Io(#[from] std::io::Error), +} + +// Binary/application +fn main() -> anyhow::Result<()> { + let config = load_config()?; // anyhow adds context automatically + Ok(()) +} \ No newline at end of file diff --git a/snippets/rust/practices/type-state-pattern-good.md b/snippets/rust/practices/type-state-pattern-good.md new file mode 100644 index 0000000..bec4421 --- /dev/null +++ b/snippets/rust/practices/type-state-pattern-good.md @@ -0,0 +1,17 @@ +use std::marker::PhantomData; + +struct Connection { addr: String, _state: PhantomData } +struct Disconnected; struct Connected; + +impl Connection { + fn connect(self) -> Connection { + Connection { addr: self.addr, _state: PhantomData } + } +} + +impl Connection { + fn send(&self, data: &[u8]) { /* only possible when connected */ } + fn disconnect(self) -> Connection { + Connection { addr: self.addr, _state: PhantomData } + } +} \ No newline at end of file diff --git a/snippets/ui-ux/cheatsheet.md b/snippets/ui-ux/cheatsheet.md new file mode 100644 index 0000000..3883623 --- /dev/null +++ b/snippets/ui-ux/cheatsheet.md @@ -0,0 +1,51 @@ +# UI/UX Fundamentals Cheatsheet + +## Typography +- Scale: 1.25 (minor third) or 1.333 (perfect fourth) ratio +- Display/H1/H2/H3: fluid with clamp() โ€” body 16px stays fixed +- Large text (24px+): tight line height 1.05โ€“1.2 +- Body text (14โ€“18px): generous line height 1.6โ€“1.75 +- Headings: negative tracking (-0.01 to -0.03em) +- Body: zero tracking โ€” NEVER negative +- Overlines: positive tracking (+0.06 to +0.10em) +- Max 2 font families. Always include fallback: `ui-sans-serif, system-ui, sans-serif` +- Prose max-width: 65ch + +## Color +- Use OKLCH โ€” perceptually uniform, P3 gamut, independent axes +- Commit to warm OR cool neutrals โ€” never mix +- Body text: 4.5:1 contrast (AA). Large text/UI: 3:1 (AA) +- Dark mode: warm charcoal (oklch 0.13โ€“0.15 L), not #000 +- Off-white text (oklch 0.94 L), not #fff +- Status solid variants: L ~0.55 for white text at 4.5:1 +- Always provide solid + soft variants for status colors +- Warm shadows: oklch(0.22 0.006 56 / 0.08) โ€” never rgba(0,0,0,...) + +## Spacing +- 4px grid: 4, 8, 12, 16, 20, 24, 32, 40, 48, 64, 96px +- Space within groups < space between groups (most important rule) +- Section: 96px โ†’ 64px โ†’ 48px (desktop โ†’ tablet โ†’ mobile) +- Card padding: 28โ€“40px โ†’ 20โ€“28px โ†’ 16โ€“20px + +## Elevation (5 levels) +- 0: page bg โ€” 1: inline cards โ€” 2: standard cards +- 3: dropdowns/popovers โ€” 4: modals/dialogs +- Dark mode: no shadows โ€” use progressively lighter bg per level + +## Motion +- Exits faster than entrances (enter 200ms โ†’ exit 150ms) +- ease-out entering, ease-in exiting, ease-in-out repositioning +- NEVER linear easing for UI +- ALWAYS prefers-reduced-motion in @layer base with !important + +## Accessibility +- Touch targets: min 44ร—44px (WCAG), 48ร—48px (recommended) +- Gap between targets: โ‰ฅ8px +- Every interactive element needs visible focus: 2px ring, 2px offset, primary color +- Trap focus in modals, return on close +- Never color as only state indicator โ€” add icons/text + +## Components +- Buttons: sm=36px, md=40px, lg=44px height +- Disabled: exactly 0.5 opacity + pointer-events: none +- Z-index scale: dropdown=1000, sticky=1010, modal=1050, tooltip=1070, toast=1080 \ No newline at end of file diff --git a/snippets/ui-ux/components/badge.md b/snippets/ui-ux/components/badge.md new file mode 100644 index 0000000..b0574cd --- /dev/null +++ b/snippets/ui-ux/components/badge.md @@ -0,0 +1,12 @@ +/* Badge sizes */ +.badge-sm { height: 20px; padding: 0 6px; font-size: 11px; } +.badge-md { height: 24px; padding: 0 8px; font-size: 12px; } + +/* Badge variants (always include both solid and soft) */ +.badge-success { background: var(--color-success); color: #fff; } +.badge-success-soft { background: var(--color-success-soft); color: var(--color-success); } +.badge-warning { background: var(--color-warning); color: var(--color-warning-fg); } +.badge-warning-soft { background: var(--color-warning-soft); color: var(--color-warning-dark); } + +/* Shape */ +.badge { border-radius: 9999px; font-weight: 600; } \ No newline at end of file diff --git a/snippets/ui-ux/components/button.md b/snippets/ui-ux/components/button.md new file mode 100644 index 0000000..1370149 --- /dev/null +++ b/snippets/ui-ux/components/button.md @@ -0,0 +1,8 @@ +/* Button sizes */ +.btn-sm { height: 36px; padding: 0 12px; font-size: 14px; } +.btn-md { height: 40px; padding: 0 16px; font-size: 14px; } +.btn-lg { height: 44px; padding: 0 20px; font-size: 16px; } + +/* States */ +.btn:disabled { opacity: 0.5; pointer-events: none; } +.btn:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; } diff --git a/snippets/ui-ux/components/card.md b/snippets/ui-ux/components/card.md new file mode 100644 index 0000000..4afd296 --- /dev/null +++ b/snippets/ui-ux/components/card.md @@ -0,0 +1,19 @@ +/* Card variants */ +.card { + background: var(--color-surface); + border-radius: var(--radius-card, 0.5rem); + padding: clamp(1.25rem, 2vw, 1.75rem); +} + +.card-interactive { + cursor: pointer; + transition: box-shadow 150ms ease-out, transform 150ms ease-out; +} +.card-interactive:hover { + box-shadow: 0 4px 12px oklch(0.30 0.02 60 / 0.12); + transform: translateY(-1px); +} +.card-interactive:focus-visible { + outline: 2px solid var(--color-primary); + outline-offset: 2px; +} \ No newline at end of file diff --git a/snippets/ui-ux/components/form-input.md b/snippets/ui-ux/components/form-input.md new file mode 100644 index 0000000..e6217fa --- /dev/null +++ b/snippets/ui-ux/components/form-input.md @@ -0,0 +1,31 @@ +/* Input sizes */ +.input-sm { height: 36px; padding: 0 10px; font-size: 14px; } +.input-md { height: 40px; padding: 0 12px; font-size: 14px; } +.input-lg { height: 44px; padding: 0 14px; font-size: 16px; } + +/* States */ +.input:focus-visible { + outline: none; + border-color: var(--color-primary); + box-shadow: 0 0 0 2px var(--color-primary) / 0.2; +} +.input-error { + border-color: var(--color-error); +} +.input-error:focus-visible { + box-shadow: 0 0 0 2px var(--color-error) / 0.2; +} +.input:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Error message */ +.input-error-msg { + display: flex; + align-items: center; + gap: 4px; + color: var(--color-error); + font-size: 13px; + margin-top: 4px; +} \ No newline at end of file diff --git a/snippets/ui-ux/principles/4px-grid.md b/snippets/ui-ux/principles/4px-grid.md new file mode 100644 index 0000000..4ed0be5 --- /dev/null +++ b/snippets/ui-ux/principles/4px-grid.md @@ -0,0 +1,7 @@ +/* 4px grid โ€” all spacing must be multiples of 4 */ +/* 4px, 8px, 12px, 16px, 20px, 24px, 32px, 40px, 48px, 64px, 96px */ + +/* In Tailwind v4 */ +@theme { + --spacing: 0.25rem; /* 4px base โ€” p-1=4px, p-4=16px, p-8=32px */ +} \ No newline at end of file diff --git a/snippets/ui-ux/principles/5-elevation-levels.md b/snippets/ui-ux/principles/5-elevation-levels.md new file mode 100644 index 0000000..9eef4bc --- /dev/null +++ b/snippets/ui-ux/principles/5-elevation-levels.md @@ -0,0 +1,7 @@ +:root { + --surface-0: var(--color-bg); + --surface-1: oklch(from var(--color-bg) calc(l + 0.03) c h); + --surface-2: oklch(from var(--color-bg) calc(l + 0.06) c h); + --surface-3: oklch(from var(--color-bg) calc(l + 0.09) c h); + --surface-4: oklch(from var(--color-bg) calc(l + 0.12) c h); +} diff --git a/snippets/ui-ux/principles/dark-mode.md b/snippets/ui-ux/principles/dark-mode.md new file mode 100644 index 0000000..32099e9 --- /dev/null +++ b/snippets/ui-ux/principles/dark-mode.md @@ -0,0 +1,6 @@ +.dark { + --color-bg: oklch(0.13 0.008 265); /* warm charcoal */ + --color-text: oklch(0.94 0.008 265); /* off-white */ + /* surface-1 */ --color-surface: oklch(0.16 0.008 265); + /* surface-2 */ --color-card: oklch(0.19 0.008 265); +} diff --git a/snippets/ui-ux/principles/easing-rules.md b/snippets/ui-ux/principles/easing-rules.md new file mode 100644 index 0000000..4aedfc0 --- /dev/null +++ b/snippets/ui-ux/principles/easing-rules.md @@ -0,0 +1,3 @@ +/* Enter */ transition: opacity 200ms cubic-bezier(0,0,0.2,1); /* ease-out */ +/* Exit */ transition: opacity 150ms cubic-bezier(0.4,0,1,1); /* ease-in */ +/* Move */ transition: transform 300ms cubic-bezier(0.4,0,0.2,1); /* ease-in-out */ diff --git a/snippets/ui-ux/principles/focus-management.md b/snippets/ui-ux/principles/focus-management.md new file mode 100644 index 0000000..12d3e2e --- /dev/null +++ b/snippets/ui-ux/principles/focus-management.md @@ -0,0 +1,4 @@ +:focus-visible { + outline: 2px solid var(--color-primary); + outline-offset: 2px; +} diff --git a/snippets/ui-ux/principles/mobile-first.md b/snippets/ui-ux/principles/mobile-first.md new file mode 100644 index 0000000..508b6c3 --- /dev/null +++ b/snippets/ui-ux/principles/mobile-first.md @@ -0,0 +1,3 @@ +/* Mobile first */ +.card { padding: 1rem; } +@media (min-width: 768px) { .card { padding: 1.75rem; } } diff --git a/snippets/ui-ux/principles/oklch-color.md b/snippets/ui-ux/principles/oklch-color.md new file mode 100644 index 0000000..442b57d --- /dev/null +++ b/snippets/ui-ux/principles/oklch-color.md @@ -0,0 +1,4 @@ +--color-brand-500: oklch(0.62 0.14 291); /* dusty lavender */ +/* Darken: */ oklch(0.54 0.14 291) +/* Desaturate: */ oklch(0.62 0.06 291) +/* Shift warm: */ oklch(0.62 0.14 60) \ No newline at end of file diff --git a/snippets/ui-ux/principles/prose-width.md b/snippets/ui-ux/principles/prose-width.md new file mode 100644 index 0000000..15c03df --- /dev/null +++ b/snippets/ui-ux/principles/prose-width.md @@ -0,0 +1 @@ +.prose { max-width: 65ch; } \ No newline at end of file diff --git a/snippets/ui-ux/principles/reduced-motion.md b/snippets/ui-ux/principles/reduced-motion.md new file mode 100644 index 0000000..ac8a769 --- /dev/null +++ b/snippets/ui-ux/principles/reduced-motion.md @@ -0,0 +1,8 @@ +@layer base { + @media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } + } +} diff --git a/snippets/ui-ux/principles/semantic-status-colors.md b/snippets/ui-ux/principles/semantic-status-colors.md new file mode 100644 index 0000000..96d983c --- /dev/null +++ b/snippets/ui-ux/principles/semantic-status-colors.md @@ -0,0 +1,4 @@ +--color-success: oklch(0.55 0.15 145); /* solid โ€” white text OK */ +--color-success-soft: oklch(0.96 0.04 145); /* tinted bg */ +--color-warning: oklch(0.72 0.15 80); /* solid โ€” dark text only */ +--color-warning-soft: oklch(0.97 0.04 80); diff --git a/snippets/ui-ux/principles/touch-targets.md b/snippets/ui-ux/principles/touch-targets.md new file mode 100644 index 0000000..ff74e1c --- /dev/null +++ b/snippets/ui-ux/principles/touch-targets.md @@ -0,0 +1,6 @@ +/* Hit area expansion */ +.btn-small::after { + content: ''; + position: absolute; + inset: -8px; +} diff --git a/snippets/ui-ux/principles/type-scale.md b/snippets/ui-ux/principles/type-scale.md new file mode 100644 index 0000000..46c6af3 --- /dev/null +++ b/snippets/ui-ux/principles/type-scale.md @@ -0,0 +1,4 @@ +/* fluid headings, fixed body */ +font-size: clamp(2.5rem, 4vw, 3.5rem); /* h1 */ +font-size: clamp(1.75rem, 3vw, 2.5rem); /* h2 */ +font-size: 1rem; /* body โ€” fixed */ diff --git a/snippets/ui-ux/principles/warm-shadows.md b/snippets/ui-ux/principles/warm-shadows.md new file mode 100644 index 0000000..903fea0 --- /dev/null +++ b/snippets/ui-ux/principles/warm-shadows.md @@ -0,0 +1,5 @@ +/* โœ… Warm shadow */ +box-shadow: 0 2px 8px oklch(0.22 0.006 56 / 0.08); + +/* โŒ Cold shadow */ +box-shadow: 0 2px 8px rgba(0,0,0,0.08); diff --git a/snippets/ui-ux/principles/warm-vs-cool.md b/snippets/ui-ux/principles/warm-vs-cool.md new file mode 100644 index 0000000..dc690a4 --- /dev/null +++ b/snippets/ui-ux/principles/warm-vs-cool.md @@ -0,0 +1,10 @@ +/* Warm neutrals โ€” inviting, premium */ +--color-bg: oklch(0.98 0.008 60); /* warm off-white, H=60 */ +--color-border: oklch(0.92 0.008 60); /* warm border */ + +/* Cool neutrals โ€” technical, corporate */ +--color-bg: oklch(0.98 0.002 240); /* cool white, H=240 */ +--color-border: oklch(0.92 0.002 240); /* cool border */ + +/* NEVER mix: */ +/* background: oklch(0.98 0.008 60) + border: oklch(0.92 0.002 240) โ† incoherent */ \ No newline at end of file diff --git a/snippets/ui-ux/principles/wcag-contrast.md b/snippets/ui-ux/principles/wcag-contrast.md new file mode 100644 index 0000000..553366d --- /dev/null +++ b/snippets/ui-ux/principles/wcag-contrast.md @@ -0,0 +1,8 @@ +/* Contrast requirements */ +/* Body text (< 18px): 4.5:1 โ€” AA */ +/* Large text (โ‰ฅ 18px bold or โ‰ฅ 24px): 3:1 โ€” AA */ +/* UI components (borders, icons): 3:1 โ€” AA */ +/* Enhanced body: 7:1 โ€” AAA */ + +/* Fix failing status colors: reduce L in OKLCH */ +/* oklch(0.63 0.15 145) fails โ†’ oklch(0.55 0.15 145) passes 4.5:1 with white */ \ No newline at end of file diff --git a/snippets/ui-ux/references/elevation-table.md b/snippets/ui-ux/references/elevation-table.md new file mode 100644 index 0000000..732c46c --- /dev/null +++ b/snippets/ui-ux/references/elevation-table.md @@ -0,0 +1,18 @@ +# Elevation Levels + +| Level | Name | Use | Dark mode | +|-------|------|-----|-----------| +| 0 | Flat | Page background | Darkest bg | +| 1 | Subtle | Inline cards, zebra rows | Slightly lighter | +| 2 | Raised | Standard cards | Lighter still | +| 3 | Elevated | Dropdowns, popovers | More lighter | +| 4 | Floating | Modals, dialogs | Lightest card bg | + +## Key rules +- Each level MUST be visually distinguishable โ€” not just via borders +- Dark mode: shadows invisible โ€” use progressively lighter bg-color per level +- Surfaces nest: page โ†’ card โ†’ inset panel โ†’ floating popover + +## Shadow warmth formula +box-shadow: 0 2px 8px oklch(0.22 0.006 56 / 0.08) +Never: box-shadow: 0 2px 8px rgba(0,0,0,0.08) \ No newline at end of file diff --git a/snippets/ui-ux/references/motion-table.md b/snippets/ui-ux/references/motion-table.md new file mode 100644 index 0000000..2565c0f --- /dev/null +++ b/snippets/ui-ux/references/motion-table.md @@ -0,0 +1,22 @@ +# Motion Reference + +## Duration +| Duration | Use | +|----------|-----| +| 0ms | Instant state changes | +| 100โ€“150ms | Hover, focus, toggles | +| 200ms | Default transitions | +| 300ms | Panel open/close | +| 400โ€“500ms | Complex animations | + +Exits faster than entrances. Enter 200ms โ†’ Exit 150ms. + +## Easing +| Easing | Use | +|--------|-----| +| ease-out `cubic-bezier(0,0,0.2,1)` | Entering (appearing) | +| ease-in `cubic-bezier(0.4,0,1,1)` | Exiting (closing) | +| ease-in-out `cubic-bezier(0.4,0,0.2,1)` | Repositioning (staying visible) | +| spring/bounce | Celebration, playful feedback only | + +NEVER use linear easing for UI motion. \ No newline at end of file diff --git a/snippets/ui-ux/references/spacing-table.md b/snippets/ui-ux/references/spacing-table.md new file mode 100644 index 0000000..8a89b3e --- /dev/null +++ b/snippets/ui-ux/references/spacing-table.md @@ -0,0 +1,26 @@ +# Spacing Scale (4px grid) + +| Value | px | Use | +|-------|----|-----| +| 1 | 4px | icon+label gap, tight inline | +| 2 | 8px | related element gap | +| 3 | 12px | close grouped elements | +| 4 | 16px | standard component gap | +| 5 | 20px | card internal gap | +| 6 | 24px | card padding (sm), grid gap | +| 7 | 28px | card padding (md) | +| 8 | 32px | grid gap (lg) | +| 10 | 40px | section gap | +| 12 | 48px | section padding (mobile) | +| 16 | 64px | section separation | +| 24 | 96px | major section padding | + +## Spacing = hierarchy rule +Space within groups < space between groups + +## Responsive values +| Context | Desktop | Tablet | Mobile | +|---------|---------|--------|--------| +| Section padding | 96px | 64px | 48px | +| Card padding | 28โ€“40px | 20โ€“28px | 16โ€“20px | +| Grid gap | 32โ€“48px | 24โ€“32px | 16โ€“24px | \ No newline at end of file diff --git a/snippets/ui-ux/references/type-scale-table.md b/snippets/ui-ux/references/type-scale-table.md new file mode 100644 index 0000000..bb4ac41 --- /dev/null +++ b/snippets/ui-ux/references/type-scale-table.md @@ -0,0 +1,25 @@ +# Type Scale Reference + +| Role | Size | Weight | Line height | Tracking | +|------|------|--------|-------------|----------| +| Display | 48โ€“72px fluid | 800 | 1.05โ€“1.1 | -0.03em | +| H1 | 40โ€“56px fluid | 700 | 1.1 | -0.02em | +| H2 | 28โ€“40px fluid | 700 | 1.15 | -0.02em | +| H3 | 20โ€“24px fluid | 600 | 1.3 | -0.01em | +| Subtitle | 16โ€“20px fluid | 500 | 1.5 | 0 | +| Body | 16px fixed | 400 | 1.75 | 0 | +| Body small | 14px fixed | 400 | 1.6 | 0 | +| Caption | 13px fixed | 500 | 1.5 | 0 | +| Overline | 12px fixed | 600 | 1.4 | +0.10em | + +## Line height rules +- Large text (24px+): tight (1.05โ€“1.2) +- Body text (14โ€“18px): generous (1.6โ€“1.75) +- Small text (12โ€“13px): moderate (1.4โ€“1.5) +- Rule: as font size decreases, line height ratio increases + +## Tracking rules +- Headings: negative (-0.01 to -0.03em) +- Body: zero (default kerning) +- Overlines/caps: positive (+0.06 to +0.10em) +- NEVER track body text negatively \ No newline at end of file diff --git a/snippets/ui-ux/references/wcag-table.md b/snippets/ui-ux/references/wcag-table.md new file mode 100644 index 0000000..b69ca74 --- /dev/null +++ b/snippets/ui-ux/references/wcag-table.md @@ -0,0 +1,25 @@ +# WCAG Contrast Requirements + +| Context | Minimum ratio | Level | +|---------|--------------|-------| +| Body text (< 18px) | 4.5:1 | AA | +| Large text (โ‰ฅ 18px bold or โ‰ฅ 24px) | 3:1 | AA | +| UI components (borders, icons) | 3:1 | AA | +| Enhanced body text | 7:1 | AAA | +| Enhanced large text | 4.5:1 | AAA | + +## Design token pairs to audit +| Pair | Target | +|------|--------| +| Body text on page bg | 4.5:1 AA | +| Muted text on page bg | 4.5:1 AA | +| Primary on bg (links) | 4.5:1 AA | +| White on primary (buttons) | 4.5:1 AA | +| White on destructive/success/info | 4.5:1 AA | +| Warning-fg on warning | 3:1 AA-large | +| Border vs background | โ‰ฅ1.1:1 visible | +| Focus ring vs surface | โ‰ฅ3:1 WCAG 2.4.7 | + +## Fix failing colors +Reduce L in OKLCH while keeping C and H unchanged: +oklch(0.63 0.15 145) โ†’ oklch(0.55 0.15 145) \ No newline at end of file diff --git a/src/index.ts b/src/index.ts old mode 100644 new mode 100755 index e69ee88..2adcb27 --- a/src/index.ts +++ b/src/index.ts @@ -5,13 +5,30 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" import { loadPlugins } from "./registry.js"; import { reactflowPlugin } from "./plugins/reactflow/index.js"; import { motionPlugin } from "./plugins/motion/index.js"; +import { lenisPlugin } from "./plugins/lenis/index.js"; +import { reactPlugin } from "./plugins/react/index.js"; +import { echoPlugin } from "./plugins/echo/index.js"; +import { golangPlugin } from "./plugins/golang/index.js"; +import { rustPlugin } from "./plugins/rust/index.js"; +import { designTokensPlugin } from "./plugins/design-tokens/index.js"; +import { uiUxPlugin } from "./plugins/ui-ux/index.js"; const server = new McpServer({ name: "unified-mcp", version: "1.0.0", }); -loadPlugins(server, [reactflowPlugin, motionPlugin]); +loadPlugins(server, [ + reactflowPlugin, + motionPlugin, + lenisPlugin, + reactPlugin, + echoPlugin, + golangPlugin, + rustPlugin, + designTokensPlugin, + uiUxPlugin, +]); async function main() { const transport = new StdioServerTransport(); diff --git a/src/plugins/design-tokens/data.ts b/src/plugins/design-tokens/data.ts new file mode 100644 index 0000000..da5a4f2 --- /dev/null +++ b/src/plugins/design-tokens/data.ts @@ -0,0 +1,587 @@ +import { snippet } from "./loader.js"; + +export interface TokenCategory { + name: string; + description: string; + layer: "primitive" | "semantic" | "domain"; + cssExample: string; + tailwindExample?: string; + rules: string[]; + gotchas: string[]; +} + +export interface RampStop { + stop: number; + oklch: string; + role: string; + lightMode: string; + darkMode?: string; +} + +export interface ColorRamp { + name: string; + description: string; + stops: RampStop[]; +} + +export interface TokenProcedure { + step: number; + title: string; + description: string; + code: string; + rules: string[]; + gotchas: string[]; +} + +// --------------------------------------------------------------------------- +// TOKEN CATEGORIES +// --------------------------------------------------------------------------- + +export const TOKEN_CATEGORIES: TokenCategory[] = [ + { + name: "colors", + description: + "OKLCH-based color system using three-layer architecture: @theme primitives โ†’ :root/:dark semantics โ†’ domain tokens. Role-based naming prevents color-name coupling.", + layer: "semantic", + cssExample: snippet("categories/colors.md"), + tailwindExample: ` +
+ +

Muted text

+
`, + rules: [ + "Always use role-based names (--color-brand-500), never hue names (--color-violet-500)", + "Three-layer architecture: @theme primitives โ†’ :root/:dark semantics โ†’ @theme inline utilities", + "Dark mode uses oklch charcoal backgrounds (L 0.15-0.25), not pure black", + "Status colors (success/error/warning) need L ~0.55 for 4.5:1 contrast with white foreground", + "Brand color at 500 is the default for light mode; 400 for dark mode primary", + "@theme inline is REQUIRED to expose CSS custom properties as Tailwind utilities", + "Commit to one neutral temperature (warm OR cool) โ€” never mix warm bg with cool borders", + ], + gotchas: [ + "@theme inline vs @theme: inline bridges runtime-swappable CSS vars; plain @theme defines static compile-time primitives. Use @theme for color ramp primitives, @theme inline to expose semantic tokens as utilities.", + "Status colors (success green, error red) typically need L reduced from 0.63 to 0.55 for 4.5:1 contrast with white โ€” run a contrast audit after finalizing the palette.", + "Dark mode elevation: shadows are invisible on dark backgrounds. Use progressively lighter bg-color per elevation level instead of box-shadow.", + "After renaming ramps, grep for stale hue-based names (e.g., old 'violet-500' references) in component files.", + "oklch(0.62 0.14 291) and oklch(0.62 0.14 291 / 0.5) are different values โ€” alpha must be explicit in oklch.", + "Pure black (#000) and pure white (#fff) look harsh. Use near-black (oklch 0.10-0.15) and near-white (oklch 0.97-0.99) instead.", + ], + }, + { + name: "spacing", + description: + "4px base grid system with named semantic tokens. The --spacing variable in Tailwind v4 is the base multiplier (0.25rem = 4px), not an individual token.", + layer: "semantic", + cssExample: snippet("categories/spacing.md"), + tailwindExample: ` +
+
+
+
+
+ Label +
+
+
+
+
`, + rules: [ + "--spacing in Tailwind v4 is the BASE MULTIPLIER (0.25rem), not individual token overrides", + "ALL spacing values must be multiples of 4px (0.25rem increments)", + "Space within groups < space between groups (single most important spacing rule)", + "Named semantic tokens (--spacing-card, --spacing-section-y) auto-generate Tailwind utilities", + "Use semantic names based on context/role, not pixel values (--spacing-card not --spacing-28)", + "Mobile section-x: 1.5rem (24px); desktop: 3rem (48px) via responsive override", + ], + gotchas: [ + "--spacing in Tailwind v4 sets the multiplier for ALL numeric spacing utilities (p-4 = 4 ร— 0.25rem = 1rem). Overriding it changes EVERY spacing utility across the project.", + "Named tokens like --spacing-card generate class p-card, but ONLY if defined in @theme or :root with Tailwind v4's CSS variable scanning. Verify the class exists before shipping.", + "Do not use arbitrary pixel values outside the 4px grid (e.g., 7px, 13px, 22px) โ€” they break visual rhythm.", + "section-x padding should be responsive: smaller on mobile (1.5rem), larger on desktop (3-4rem). A single static value often looks wrong on one breakpoint.", + ], + }, + { + name: "typography", + description: + "Fluid type scale using clamp() for display/heading sizes, fixed 16px body. Role-based tokens with strict line-height and tracking rules.", + layer: "semantic", + cssExample: snippet("categories/typography.md"), + tailwindExample: ` +
+

Category Label

+

Page Heading

+

+ A clear subtitle sentence introducing the content. +

+

Body paragraph text at 16px with generous line height.

+
`, + rules: [ + "Large text (24px+) requires tight line height (1.05โ€“1.2); body requires generous (1.6โ€“1.75)", + "As font size decreases, line height must increase", + "Headings use negative tracking (-0.01 to -0.03em); body uses zero tracking; overlines use positive (+0.06 to +0.10em)", + "NEVER apply negative tracking to body text โ€” it reduces readability", + "Body text is FIXED at 16px โ€” never use fluid clamp() for body", + "Maximum 2 font families per project; mono only for code, badges, terminal output", + "Prose max-width: 65ch for optimal reading line length", + "Type scale ratios: minor third (1.25), perfect fourth (1.333), or augmented fourth (1.414)", + ], + gotchas: [ + "Tight line-height on body text (e.g., 1.2) is a critical readability bug. Always use 1.6+ for paragraph text.", + "clamp() fluid scaling on body text causes reflow and unpredictable sizing on resize โ€” keep body sizes fixed.", + "Mixing font-weight 700 heading with font-weight 400 body in the same font variable file requires a variable font or separate font loads.", + "letter-spacing in em is relative to font-size, so -0.03em on display (72px) = -2.16px โ€” verify visually at each size.", + "overline text-transform: uppercase with positive tracking looks correct; without uppercase it looks odd.", + ], + }, + { + name: "component-sizing", + description: + "Standardized component size scales for buttons, inputs, icons, and avatars. Includes WCAG touch target minimums.", + layer: "semantic", + cssExample: snippet("categories/component-sizing.md"), + tailwindExample: ` + +`, + rules: [ + "All interactive elements on mobile must meet 44ร—44px touch target minimum (WCAG 2.5.5)", + "Prefer 48px touch targets for comfortable use (2.5.5 AAA equivalent)", + "Gap between adjacent touch targets must be โ‰ฅ 8px to prevent mis-taps", + "Button md (40px) is the default for desktop; lg (44px) for mobile-first UIs", + "Icon size inside button should be 1 step smaller than button text size", + "Avatars in lists use sm (32px); standalone profile use lg/xl (48โ€“64px)", + ], + gotchas: [ + "A 28px button without a larger hit area (via padding or pseudo-element) fails WCAG 2.5.5 on touch devices.", + "Icon-only buttons require an explicit aria-label AND a visible tooltip โ€” an icon alone is not accessible.", + "Disabled buttons should still meet minimum size requirements even when opacity: 0.5.", + ], + }, + { + name: "border-radius", + description: + "Consistent border radius scale from sharp (0) to pill. Semantic tokens for component contexts.", + layer: "primitive", + cssExample: snippet("categories/border-radius.md"), + tailwindExample: `
Card
+ +Badge`, + rules: [ + "Keep radius consistent within a design system โ€” all cards same radius, all buttons same radius", + "Border radius should scale proportionally with component size (small badges use sm, large modals use xl)", + "Pill (radius-full) is for badges, toggles, and avatar images โ€” not general cards", + "Inputs typically use smaller radius (sm/md) than cards (lg/xl) for visual differentiation", + ], + gotchas: [ + "Mixing large (24px) and small (4px) radii in adjacent components looks inconsistent. Pick 1โ€“2 radius values per component tier.", + "border-radius on a container does not clip overflowing children by default โ€” add overflow: hidden if children need clipping.", + "Very large radius (radius-3xl on small components) can make them look bubble-like and unpolished.", + ], + }, + { + name: "shadows", + description: + "5-level elevation shadow scale using oklch-tinted warm shadows. Dark mode uses bg-color elevation instead of box shadows.", + layer: "semantic", + cssExample: snippet("categories/shadows-elevation.md"), + tailwindExample: ` +
Card
+ + +
+ Elevated card in dark mode +
+ + +
Modal
`, + rules: [ + "Always use oklch-tinted warm shadows โ€” never rgba(0,0,0) black shadows", + "5 elevation levels: flat(none) / subtle(xs-sm) / raised(md) / elevated(lg) / floating(xl-2xl)", + "Dark mode: shadows are invisible โ€” use progressively lighter bg-color per elevation level instead", + "Every elevation level must be visually distinguishable from adjacent levels", + "Focus ring shadow uses 2px solid ring at 2px offset from element edge", + "Modal/overlay uses shadow-2xl; dropdown uses shadow-lg; tooltip uses shadow-md", + ], + gotchas: [ + "rgba(0,0,0,0.1) shadows look cold and blue-grey on warm backgrounds. Use oklch-tinted shadows that complement your warm neutrals.", + "In dark mode, box-shadow with dark backgrounds makes no visual difference. Forgetting to implement bg-color elevation in dark mode results in a flat, no-depth dark UI.", + "Multiple box-shadow layers (comma-separated) are additive. Test on both dark and light backgrounds before shipping.", + "shadow-focus must contrast with both the element background and the page background โ€” test on white AND colored button variants.", + ], + }, + { + name: "motion", + description: + "Duration and easing token system for consistent UI animation. Always includes prefers-reduced-motion override.", + layer: "primitive", + cssExample: snippet("categories/motion.md"), + tailwindExample: ` + + + +
+ Sidebar panel +
`, + rules: [ + "ALWAYS implement prefers-reduced-motion in @layer base with !important", + "Hover/focus/toggles: 100โ€“150ms (duration-fast)", + "Default transitions (color, bg, border): 200ms (duration-normal)", + "Panel open/close: 300ms (duration-slow)", + "Complex animations: 400โ€“500ms (duration-slower)", + "Exits should be faster than entrances โ€” user-initiated dismissal expects immediacy", + "ease-out for entering elements, ease-in for exiting, ease-in-out for repositioning", + "NEVER use linear easing for UI transitions (only for progress bars and loaders)", + ], + gotchas: [ + "Forgetting prefers-reduced-motion is a WCAG 2.3.3 violation. It must be in @layer base with !important to override inline styles.", + "ease-in on enter makes the animation feel slow to start โ€” always use ease-out for entering elements.", + "Long exit durations (300ms+) make the UI feel sluggish because users are waiting to interact with the next state.", + "CSS transition shorthand 'all' captures every property change, including layout โ€” can cause jank. Explicitly list only the properties you want to animate.", + ], + }, + { + name: "z-index", + description: + "Structured z-index scale to prevent stacking context chaos. Named semantic layers for every UI level.", + layer: "primitive", + cssExample: snippet("categories/z-index.md"), + tailwindExample: `
+ Navigation +
+ +
+
+ Modal +
`, + rules: [ + "NEVER use arbitrary z-index values (999, 9999) โ€” always use named scale tokens", + "Z-index only works on positioned elements (position: relative/absolute/fixed/sticky)", + "Z-index is scoped to the stacking context โ€” a child cannot exceed its parent's stacking context", + "Sticky header should be below modals (z-sticky < z-modal)", + "Toast notifications should be above everything except z-max", + "Document every new z-index usage โ€” stacking context bugs are very hard to debug", + ], + gotchas: [ + "transform, opacity < 1, filter, and will-change create new stacking contexts โ€” a z-index on a child inside one of these is trapped within that context.", + "z-index: 9999 on a component inside a transform: parent will still be below the parent's stacking context order.", + "Libraries like Radix UI and Headless UI have their own z-index values (often 50-9999). Check their defaults before setting your own values.", + "Two modals open simultaneously will stack by DOM order unless z-index is incremented dynamically.", + ], + }, + { + name: "opacity", + description: + "Semantic opacity scale for disabled states, overlays, glass effects, and ghost UI elements.", + layer: "primitive", + cssExample: snippet("categories/opacity.md"), + tailwindExample: ` + + +
+ + +
+ Glass card +
`, + rules: [ + "Disabled state: exactly 0.5 opacity (--opacity-disabled) โ€” do not use 0.3 or 0.7", + "pointer-events: none must accompany opacity: 0.5 on disabled elements", + "Glass morphism requires backdrop-filter: blur() + semi-transparent background", + "Opacity-based text muting (0.65) is acceptable; prefer semantic color tokens where possible", + "Never use opacity < 0.35 for interactive elements โ€” they become invisible to low-vision users", + ], + gotchas: [ + "opacity: 0 is different from visibility: hidden and display: none. opacity: 0 elements still receive pointer events and occupy layout space.", + "backdrop-filter is not supported in all contexts โ€” it requires the element to not have overflow: hidden on an ancestor. Test on Safari.", + "Using opacity on a parent element reduces opacity of ALL children (including text) โ€” for bg-only opacity, use an rgba/oklch background color with alpha channel instead.", + "Disabled state with just opacity: 0.5 and no pointer-events: none still allows clicks โ€” a common security/UX bug.", + ], + }, + { + name: "density", + description: + "Density mode system for compact/default/comfortable UI via CSS class overrides on the root element.", + layer: "domain", + cssExample: snippet("categories/density.md"), + tailwindExample: `// React: density context provider +const DensityProvider = ({ density, children }) => ( +
+ {children} +
+); + +// All spacing/sizing tokens automatically adjust inside the wrapper + + {/* Compact row heights, tighter padding */} +`, + rules: [ + "Apply density class to the outermost wrapper โ€” all child tokens cascade automatically", + "Compact mode (0.75ร—): data-dense UIs like tables, admin panels, dashboards", + "Default mode (1ร—): standard application interfaces", + "Comfortable mode (1.125ร—): onboarding flows, marketing pages, reading UIs", + "Touch targets must NEVER go below 44px even in compact mode โ€” handle separately", + "Only override the tokens that change; inherit everything else from default", + ], + gotchas: [ + "Compact density that reduces button height below 44px violates WCAG 2.5.5 on touch devices โ€” either cap the minimum or apply density only on desktop breakpoints.", + "Font sizes should change minimally between density modes (ยฑ1โ€“2px) โ€” the spacing/padding difference should drive the perception of density.", + "Density mode on a nested component (not root) can cause unexpected token cascading if inner components define their own token values.", + ], + }, +]; + +// --------------------------------------------------------------------------- +// COLOR RAMPS +// --------------------------------------------------------------------------- + +export const COLOR_RAMPS: ColorRamp[] = [ + { + name: "brand", + description: + "Primary brand color ramp (purple-violet, H=291). Used for CTAs, interactive elements, and brand identity.", + stops: [ + { stop: 50, oklch: "oklch(0.97 0.02 291)", role: "wash/tint", lightMode: "Background tints, highlight bands", darkMode: "Not used in dark mode" }, + { stop: 100, oklch: "oklch(0.94 0.04 291)", role: "subtle background", lightMode: "Hover state backgrounds, selected item bg", darkMode: "Very dark tinted overlays" }, + { stop: 200, oklch: "oklch(0.88 0.06 291)", role: "hover background", lightMode: "Pressed state backgrounds, focus rings", darkMode: "Dark border accents" }, + { stop: 300, oklch: "oklch(0.78 0.09 291)", role: "active state", lightMode: "Active state bg, icon fill in light mode", darkMode: "Brand text on dark bg (subtle)" }, + { stop: 400, oklch: "oklch(0.70 0.12 291)", role: "dark mode primary", lightMode: "Strong icon fill", darkMode: "Primary CTA text, links, active indicators" }, + { stop: 500, oklch: "oklch(0.62 0.14 291)", role: "default brand", lightMode: "Primary button bg, links, active indicators" }, + { stop: 600, oklch: "oklch(0.54 0.14 291)", role: "hover", lightMode: "Hover state for brand-500 interactive elements" }, + { stop: 700, oklch: "oklch(0.46 0.13 291)", role: "dark text", lightMode: "Brand-colored text on light bg", darkMode: "Brand-colored borders" }, + { stop: 800, oklch: "oklch(0.38 0.11 291)", role: "dark borders", lightMode: "Strong borders, dividers", darkMode: "Borders on dark elevated surfaces" }, + { stop: 900, oklch: "oklch(0.28 0.08 291)", role: "text light mode", lightMode: "Brand-tinted text, headings with brand tone" }, + { stop: 950, oklch: "oklch(0.18 0.05 291)", role: "high emphasis", lightMode: "Highest emphasis headings, near-black brand" }, + ], + }, + { + name: "neutral", + description: + "Warm neutral ramp (H=60-80, low chroma). Used for backgrounds, borders, text, and surfaces. Commit to warm OR cool โ€” never mix.", + stops: [ + { stop: 50, oklch: "oklch(0.98 0.008 60)", role: "page background", lightMode: "Main page background (warm off-white)", darkMode: "Not used" }, + { stop: 100, oklch: "oklch(0.96 0.008 60)", role: "subtle bg", lightMode: "Subtle backgrounds, zebra rows, code blocks", darkMode: "Not used" }, + { stop: 200, oklch: "oklch(0.92 0.008 60)", role: "border", lightMode: "Default borders, dividers", darkMode: "Not typically used" }, + { stop: 300, oklch: "oklch(0.84 0.008 60)", role: "strong border", lightMode: "Strong borders, input borders", darkMode: "Subtle borders" }, + { stop: 400, oklch: "oklch(0.72 0.008 60)", role: "muted text", lightMode: "Placeholder text, disabled text", darkMode: "Strong borders, icons" }, + { stop: 500, oklch: "oklch(0.58 0.010 60)", role: "secondary text", lightMode: "Secondary/muted text, captions", darkMode: "Secondary text" }, + { stop: 600, oklch: "oklch(0.46 0.010 60)", role: "tertiary text", lightMode: "Tertiary labels", darkMode: "Body text" }, + { stop: 700, oklch: "oklch(0.35 0.010 60)", role: "strong text", lightMode: "Strong secondary text", darkMode: "Heading text" }, + { stop: 800, oklch: "oklch(0.26 0.010 60)", role: "body text", lightMode: "Body copy, high-readability text", darkMode: "Primary text" }, + { stop: 900, oklch: "oklch(0.18 0.008 60)", role: "heading text", lightMode: "Headings, high contrast", darkMode: "Highest emphasis text" }, + { stop: 950, oklch: "oklch(0.12 0.005 60)", role: "near-black", lightMode: "Maximum contrast, decorative dark elements", darkMode: "Not used" }, + ], + }, + { + name: "pop", + description: + "Accent/pop color ramp (orange-amber, H=50). Used sparingly for callouts, highlights, and energy accents.", + stops: [ + { stop: 50, oklch: "oklch(0.97 0.025 50)", role: "wash", lightMode: "Warm highlight backgrounds, callout tints" }, + { stop: 100, oklch: "oklch(0.94 0.045 50)", role: "subtle bg", lightMode: "Callout backgrounds, warning backgrounds" }, + { stop: 200, oklch: "oklch(0.88 0.075 50)", role: "hover bg", lightMode: "Pressed states for pop-colored elements" }, + { stop: 300, oklch: "oklch(0.80 0.110 50)", role: "active state", lightMode: "Active indicators, selected states" }, + { stop: 400, oklch: "oklch(0.72 0.145 50)", role: "dark mode accent", lightMode: "Strong icon fill", darkMode: "Accent CTAs, highlights in dark mode" }, + { stop: 500, oklch: "oklch(0.65 0.165 50)", role: "default pop", lightMode: "Accent buttons, callout borders, highlights" }, + { stop: 600, oklch: "oklch(0.57 0.155 50)", role: "hover", lightMode: "Hover state for pop-500 elements" }, + { stop: 700, oklch: "oklch(0.48 0.140 50)", role: "dark text", lightMode: "Pop-colored text on light bg" }, + { stop: 800, oklch: "oklch(0.38 0.110 50)", role: "borders", lightMode: "Strong accent borders" }, + { stop: 900, oklch: "oklch(0.28 0.070 50)", role: "text", lightMode: "Pop-tinted text, warm headings" }, + { stop: 950, oklch: "oklch(0.18 0.040 50)", role: "near-black", lightMode: "Maximum emphasis with pop warmth" }, + ], + }, +]; + +// --------------------------------------------------------------------------- +// BUILD PROCEDURES +// --------------------------------------------------------------------------- + +export const TOKEN_PROCEDURES: TokenProcedure[] = [ + { + step: 1, + title: "Define color ramp primitives in @theme", + description: "Create 11-stop OKLCH ramps for brand, neutral, and pop colors. These are static compile-time values.", + code: snippet("procedures/step-1-colors.md"), + rules: [ + "Use @theme (not :root) for primitive ramps โ€” they become Tailwind static utilities", + "Hue angle (H) must be consistent across all stops in a ramp", + "Chroma (C) peaks around 400-600, decreases toward 50 and 950", + "Lightness (L) must be monotonically decreasing from 50 to 950", + ], + gotchas: [ + "@theme values are static โ€” they cannot reference CSS custom properties or be overridden at runtime by JavaScript.", + "If you change the H value after building components, all color utilities change across the app. Lock the hue angle early.", + ], + }, + { + step: 2, + title: "Map semantic tokens in :root and .dark", + description: "Create role-based semantic tokens that reference primitives. These are the tokens your components actually use.", + code: snippet("procedures/step-2-spacing.md"), + rules: [ + "Components must only reference semantic tokens, never primitive ramp values directly", + "Dark mode requires bg-color elevation (lighter bg per level) since shadows are invisible", + "Status color L should be ~0.55 for 4.5:1 contrast with white foreground", + "Dark mode primary shifts from brand-500 (light) to brand-400 (dark) for perceptual brightness parity", + ], + gotchas: [ + "Status colors at L=0.63 (typical green/red) fail 4.5:1 AA with white. Always run contrast check after defining status colors.", + "Do not use hex values for semantic tokens โ€” use oklch primitives so the color stays in the defined color space.", + ], + }, + { + step: 3, + title: "Bridge to Tailwind v4 utilities with @theme inline", + description: "Use @theme inline to expose runtime-swappable CSS custom properties as Tailwind utility classes.", + code: snippet("procedures/step-3-typography.md"), + rules: [ + "@theme inline is read at runtime โ€” it reflects CSS custom property values dynamically", + "Plain @theme is static โ€” values are baked in at build time", + "Use @theme inline ONLY for semantic tokens; use plain @theme for primitive ramps", + "Name mapping: --color-X in @theme inline โ†’ bg-X, text-X, border-X Tailwind classes", + ], + gotchas: [ + "If you put runtime-swappable vars in plain @theme (not inline), dark mode will NOT work โ€” the values won't update when .dark class is toggled.", + "@theme inline does not accept hardcoded values โ€” it should only map CSS var references.", + ], + }, + { + step: 4, + title: "Define spacing system", + description: "Set the 4px base multiplier and create named semantic spacing tokens.", + code: snippet("procedures/step-4-component-sizing.md"), + rules: [ + "--spacing is the global multiplier โ€” changing it scales ALL numeric spacing utilities", + "Named tokens auto-generate Tailwind utilities: p-card, py-section-y, gap-grid-cards", + "Always follow 4px grid: 0.25rem, 0.5rem, 0.75rem, 1rem, 1.25rem, 1.5rem, 1.75rem...", + ], + gotchas: [ + "--spacing is NOT an individual token for a specific spacing value. It is the base MULTIPLIER that affects p-1, p-2, p-4, etc. Overriding it changes every numeric spacing utility in the project.", + ], + }, + { + step: 5, + title: "Set up typography scale", + description: "Define fluid heading sizes with clamp(), fixed body sizes, and line-height/tracking tokens.", + code: snippet("procedures/step-5-remaining.md"), + rules: [ + "Body text (16px) is NEVER fluid โ€” use fixed rem values", + "Headings MUST have tight line-height (1.05โ€“1.2), not body line-height (1.6+)", + "Negative tracking on body text is a readability error", + ], + gotchas: [ + "Applying clamp() to body text causes font-size to change on window resize, causing reflow and jarring user experience.", + ], + }, + { + step: 6, + title: "Define shadows and elevation", + description: "Build the 5-level elevation shadow system using warm oklch-tinted shadows.", + code: snippet("procedures/step-6-accessibility.md"), + rules: ["Use oklch-tinted shadows, never rgba(0,0,0)", "Dark mode disables all shadows, uses bg-color elevation instead"], + gotchas: ["rgba(0,0,0) shadows look cold on warm backgrounds. The warm hue tint (H=60) makes shadows feel natural."], + }, + { + step: 7, + title: "Define motion and z-index tokens", + description: "Add duration, easing, and z-index scale with prefers-reduced-motion.", + code: snippet("procedures/step-7-validation.md"), + rules: ["prefers-reduced-motion MUST be in @layer base with !important", "Never use arbitrary z-index values"], + gotchas: ["Forgetting prefers-reduced-motion is a WCAG 2.3.3 violation."], + }, + { + step: 8, + title: "Run contrast audit and verify token usage", + description: "After building the full token system, run a contrast audit on all color token pairs.", + code: snippet("procedures/step-8-deliverables.md"), + rules: [ + "Run contrast audit AFTER finalizing the palette โ€” before building components", + "Status colors often need L adjusted to ~0.55 for AA compliance with white foreground", + "Grep for stale hue-name references after renaming ramps", + ], + gotchas: [ + "WCAG contrast is calculated on final rendered colors. If CSS vars are not resolving correctly in dark mode, contrast tools won't catch it โ€” test with a browser extension on the live page.", + ], + }, +]; + +// --------------------------------------------------------------------------- +// SEARCH HELPER +// --------------------------------------------------------------------------- + +export function searchTokens(query: string): { category?: TokenCategory; ramp?: ColorRamp; procedure?: TokenProcedure }[] { + const q = query.toLowerCase(); + const results: { category?: TokenCategory; ramp?: ColorRamp; procedure?: TokenProcedure }[] = []; + + for (const cat of TOKEN_CATEGORIES) { + if ( + cat.name.toLowerCase().includes(q) || + cat.description.toLowerCase().includes(q) || + cat.rules.some((r) => r.toLowerCase().includes(q)) || + cat.gotchas.some((g) => g.toLowerCase().includes(q)) || + cat.cssExample.toLowerCase().includes(q) + ) { + results.push({ category: cat }); + } + } + + for (const ramp of COLOR_RAMPS) { + if ( + ramp.name.toLowerCase().includes(q) || + ramp.description.toLowerCase().includes(q) || + ramp.stops.some((s) => s.role.toLowerCase().includes(q) || s.lightMode.toLowerCase().includes(q)) + ) { + results.push({ ramp }); + } + } + + for (const proc of TOKEN_PROCEDURES) { + if ( + proc.title.toLowerCase().includes(q) || + proc.description.toLowerCase().includes(q) || + proc.code.toLowerCase().includes(q) || + proc.rules.some((r) => r.toLowerCase().includes(q)) || + proc.gotchas.some((g) => g.toLowerCase().includes(q)) + ) { + results.push({ procedure: proc }); + } + } + + return results; +} + +export function getCategoryByName(name: string): TokenCategory | undefined { + return TOKEN_CATEGORIES.find((c) => c.name.toLowerCase() === name.toLowerCase()); +} + +export function getRampByName(name: string): ColorRamp | undefined { + return COLOR_RAMPS.find((r) => r.name.toLowerCase() === name.toLowerCase()); +} + +export function getProcedureByStep(step: number): TokenProcedure | undefined { + return TOKEN_PROCEDURES.find((p) => p.step === step); +} + +export function getAllGotchas(): { source: string; gotcha: string }[] { + const all: { source: string; gotcha: string }[] = []; + for (const cat of TOKEN_CATEGORIES) { + for (const gotcha of cat.gotchas) { + all.push({ source: cat.name, gotcha }); + } + } + for (const proc of TOKEN_PROCEDURES) { + for (const gotcha of proc.gotchas) { + all.push({ source: `Step ${proc.step}: ${proc.title}`, gotcha }); + } + } + return all; +} diff --git a/src/plugins/design-tokens/index.ts b/src/plugins/design-tokens/index.ts new file mode 100644 index 0000000..b9f7c71 --- /dev/null +++ b/src/plugins/design-tokens/index.ts @@ -0,0 +1,24 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { Plugin } from "../../registry.js"; +import { register as listCategories } from "./tools/list-categories.js"; +import { register as getCategory } from "./tools/get-category.js"; +import { register as getColorRamp } from "./tools/get-color-ramp.js"; +import { register as getProcedure } from "./tools/get-procedure.js"; +import { register as search } from "./tools/search.js"; +import { register as getGotchas } from "./tools/get-gotchas.js"; +import { register as generate } from "./tools/generate.js"; + +function register(server: McpServer): void { + listCategories(server); + getCategory(server); + getColorRamp(server); + getProcedure(server); + search(server); + getGotchas(server); + generate(server); +} + +export const designTokensPlugin: Plugin = { + name: "design-tokens", + register, +}; diff --git a/src/plugins/design-tokens/loader.ts b/src/plugins/design-tokens/loader.ts new file mode 100644 index 0000000..fad0438 --- /dev/null +++ b/src/plugins/design-tokens/loader.ts @@ -0,0 +1,2 @@ +import { createSnippetLoader } from "../../shared/loader-factory.js"; +export const snippet = createSnippetLoader("design-tokens"); diff --git a/src/plugins/design-tokens/tools/generate.ts b/src/plugins/design-tokens/tools/generate.ts new file mode 100644 index 0000000..4df3936 --- /dev/null +++ b/src/plugins/design-tokens/tools/generate.ts @@ -0,0 +1,54 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { snippet } from "../loader.js"; + +const TEMPLATE_COLORS = snippet("templates/colors-tailwind-v4.md"); +const TEMPLATE_SPACING = snippet("templates/spacing.md"); +const TEMPLATE_TYPOGRAPHY = snippet("templates/typography.md"); +const TEMPLATE_MOTION = snippet("templates/motion.md"); + +export function register(server: McpServer): void { + server.tool( + "design_tokens_generate", + "Generate CSS token scaffolding from a description. Returns ready-to-use CSS custom properties.", + { + description: z.string().describe( + "What to generate (e.g. 'dark mode color tokens', 'spacing system', 'typography scale', 'motion tokens', 'complete token system')" + ), + framework: z.enum(["css", "tailwind-v4"]).optional() + .describe("Output format (default: tailwind-v4)"), + }, + async ({ description }) => { + const desc = description.toLowerCase(); + + const parts: string[] = []; + + if (desc.includes("color") || desc.includes("dark") || desc.includes("complete") || desc.includes("full")) { + parts.push(TEMPLATE_COLORS); + } + + if (desc.includes("spacing") || desc.includes("complete") || desc.includes("full")) { + parts.push(TEMPLATE_SPACING); + } + + if (desc.includes("typography") || desc.includes("type") || desc.includes("complete") || desc.includes("full")) { + parts.push(TEMPLATE_TYPOGRAPHY); + } + + if (desc.includes("motion") || desc.includes("animation") || desc.includes("complete") || desc.includes("full")) { + parts.push(TEMPLATE_MOTION); + } + + if (parts.length === 0) { + return { + content: [{ type: "text", text: `No template matched "${description}". Try: colors, spacing, typography, motion, complete` }], + }; + } + + const code = parts.join("\n\n"); + return { + content: [{ type: "text", text: `\`\`\`css\n${code}\n\`\`\`\n\nCustomize the OKLCH values to match your brand palette. Run a contrast audit after finalizing colors.` }], + }; + } + ); +} diff --git a/src/plugins/design-tokens/tools/get-category.ts b/src/plugins/design-tokens/tools/get-category.ts new file mode 100644 index 0000000..6e07b58 --- /dev/null +++ b/src/plugins/design-tokens/tools/get-category.ts @@ -0,0 +1,43 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { TOKEN_CATEGORIES, getCategoryByName } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "design_tokens_get_category", + "Get full details for a design token category including CSS examples, rules, and gotchas", + { + name: z.string().describe( + "Category name: colors, spacing, typography, component-sizing, border-radius, shadows-elevation, motion, z-index, opacity, density" + ), + }, + async ({ name }) => { + const cat = getCategoryByName(name); + if (!cat) { + const available = TOKEN_CATEGORIES.map((c) => c.name).join(", "); + return { + content: [{ type: "text", text: `Category "${name}" not found.\n\nAvailable: ${available}` }], + isError: true, + }; + } + + let text = `# ${cat.name} tokens\n\n`; + text += `**Layer:** ${cat.layer}\n\n`; + text += `${cat.description}\n\n`; + + text += `## CSS Example\n\`\`\`css\n${cat.cssExample}\n\`\`\`\n\n`; + + if (cat.tailwindExample) { + text += `## Tailwind v4 Usage\n\`\`\`css\n${cat.tailwindExample}\n\`\`\`\n\n`; + } + + text += `## Rules\n`; + for (const rule of cat.rules) text += `- ${rule}\n`; + + text += `\n## Gotchas\n`; + for (const gotcha of cat.gotchas) text += `- ${gotcha}\n`; + + return { content: [{ type: "text", text }] }; + } + ); +} diff --git a/src/plugins/design-tokens/tools/get-color-ramp.ts b/src/plugins/design-tokens/tools/get-color-ramp.ts new file mode 100644 index 0000000..03e6831 --- /dev/null +++ b/src/plugins/design-tokens/tools/get-color-ramp.ts @@ -0,0 +1,41 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { COLOR_RAMPS, getRampByName } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "design_tokens_get_color_ramp", + "Get a color ramp (brand/neutral/pop) with all 11 OKLCH stops, semantic roles, and light/dark mode usage", + { + name: z.enum(["brand", "neutral", "pop"]).describe("Ramp name"), + }, + async ({ name }) => { + const ramp = getRampByName(name); + if (!ramp) { + return { + content: [{ type: "text", text: `Ramp "${name}" not found. Available: ${COLOR_RAMPS.map((r) => r.name).join(", ")}` }], + isError: true, + }; + } + + let text = `# ${ramp.name} color ramp\n\n${ramp.description}\n\n`; + text += "Use **role-based naming** (`brand-500`), never hue names (`violet-500`) โ€” prevents codebase-wide renames when palette changes.\n\n"; + + text += "## Stops\n\n"; + text += "| Stop | OKLCH | Role | Light Mode | Dark Mode |\n"; + text += "|------|-------|------|------------|----------|\n"; + for (const stop of ramp.stops) { + text += `| ${stop.stop} | \`${stop.oklch}\` | ${stop.role} | ${stop.lightMode} | ${stop.darkMode ?? "โ€”"} |\n`; + } + + text += "\n## CSS\n\`\`\`css\n@theme {\n"; + for (const stop of ramp.stops) { + text += ` --color-${name}-${stop.stop}: ${stop.oklch};\n`; + } + text += "}\n\`\`\`\n\n"; + text += "**Contrast fix:** If status colors fail 4.5:1 with white text, reduce OKLCH Lightness (L 0.63 โ†’ 0.55) โ€” keep C and H unchanged."; + + return { content: [{ type: "text", text }] }; + } + ); +} diff --git a/src/plugins/design-tokens/tools/get-gotchas.ts b/src/plugins/design-tokens/tools/get-gotchas.ts new file mode 100644 index 0000000..8257bfe --- /dev/null +++ b/src/plugins/design-tokens/tools/get-gotchas.ts @@ -0,0 +1,29 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { getAllGotchas } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "design_tokens_get_gotchas", + "List all common design token mistakes and fixes across all categories", + {}, + async () => { + const gotchas = getAllGotchas(); + + let text = "# Design Token Gotchas\n\nCommon mistakes that break token systems:\n\n"; + + const bySource: Record = {}; + for (const { source, gotcha } of gotchas) { + if (!bySource[source]) bySource[source] = []; + bySource[source].push(gotcha); + } + + for (const [source, items] of Object.entries(bySource)) { + text += `## ${source}\n`; + for (const item of items) text += `- ${item}\n`; + text += "\n"; + } + + return { content: [{ type: "text", text }] }; + } + ); +} diff --git a/src/plugins/design-tokens/tools/get-procedure.ts b/src/plugins/design-tokens/tools/get-procedure.ts new file mode 100644 index 0000000..e24b745 --- /dev/null +++ b/src/plugins/design-tokens/tools/get-procedure.ts @@ -0,0 +1,40 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { TOKEN_PROCEDURES, getProcedureByStep } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "design_tokens_get_procedure", + "Get the step-by-step token system build procedure. Steps 1-8 cover the full production workflow.", + { + step: z.number().min(1).max(8).optional() + .describe("Step number (1-8). Omit to get all steps."), + }, + async ({ step }) => { + if (step !== undefined) { + const proc = getProcedureByStep(step); + if (!proc) { + return { + content: [{ type: "text", text: `Step ${step} not found. Valid steps: 1-${TOKEN_PROCEDURES.length}` }], + isError: true, + }; + } + let text = `# Step ${proc.step}: ${proc.title}\n\n${proc.description}\n\n`; + text += `## Code\n\`\`\`css\n${proc.code}\n\`\`\`\n\n`; + text += `## Rules\n`; + for (const rule of proc.rules) text += `- ${rule}\n`; + text += `\n## Gotchas\n`; + for (const gotcha of proc.gotchas) text += `- ${gotcha}\n`; + return { content: [{ type: "text", text }] }; + } + + let text = "# Design Token Build Procedure\n\n"; + text += "A complete token system = 10 categories. Follow in order.\n\n"; + for (const proc of TOKEN_PROCEDURES) { + text += `## Step ${proc.step}: ${proc.title}\n${proc.description}\n\n`; + } + text += "\nCall `design_tokens_get_procedure` with a step number for full code + rules."; + return { content: [{ type: "text", text }] }; + } + ); +} diff --git a/src/plugins/design-tokens/tools/list-categories.ts b/src/plugins/design-tokens/tools/list-categories.ts new file mode 100644 index 0000000..324ebbb --- /dev/null +++ b/src/plugins/design-tokens/tools/list-categories.ts @@ -0,0 +1,42 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { TOKEN_CATEGORIES } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "design_tokens_list_categories", + "List all 10 design token categories with descriptions and architecture layer", + { + layer: z.enum(["all", "primitive", "semantic", "domain"]).optional() + .describe("Filter by token layer"), + }, + async ({ layer }) => { + const cats = layer && layer !== "all" + ? TOKEN_CATEGORIES.filter((c) => c.layer === layer) + : TOKEN_CATEGORIES; + + let text = "# Design Token Categories\n\n"; + text += "A production token system covers 10 categories. Missing any = incomplete system.\n\n"; + text += "## Three-layer architecture\n"; + text += "- **@theme primitives** โ†’ raw values (ramp stops)\n"; + text += "- **:root/.dark semantics** โ†’ role-based mappings\n"; + text += "- **Domain tokens** โ†’ app-specific (viz, editor, etc.)\n\n"; + + const byLayer: Record = {}; + for (const cat of cats) { + if (!byLayer[cat.layer]) byLayer[cat.layer] = []; + byLayer[cat.layer].push(cat); + } + + for (const [layerName, items] of Object.entries(byLayer)) { + text += `## Layer: ${layerName}\n`; + for (const cat of items) { + text += `### ${cat.name}\n${cat.description}\n\n`; + } + } + + text += `\n**Total:** ${cats.length} categories`; + return { content: [{ type: "text", text }] }; + } + ); +} diff --git a/src/plugins/design-tokens/tools/search.ts b/src/plugins/design-tokens/tools/search.ts new file mode 100644 index 0000000..3959e94 --- /dev/null +++ b/src/plugins/design-tokens/tools/search.ts @@ -0,0 +1,37 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { searchTokens } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "design_tokens_search", + "Search design token documentation by keyword across categories, ramps, and procedures", + { + query: z.string().describe("Search query (e.g. 'dark mode', 'spacing', 'oklch', 'contrast', 'motion')"), + }, + async ({ query }) => { + const results = searchTokens(query); + if (results.length === 0) { + return { + content: [{ type: "text", text: `No results for "${query}". Try: colors, spacing, typography, motion, elevation, z-index, opacity, density, contrast, oklch, tailwind` }], + }; + } + + let text = `# Search results for "${query}"\n\nFound ${results.length} result(s):\n\n`; + for (const result of results) { + if (result.category) { + text += `## Category: ${result.category.name}\n${result.category.description}\n`; + text += `Layer: ${result.category.layer}\n\n`; + } + if (result.ramp) { + text += `## Color Ramp: ${result.ramp.name}\n${result.ramp.description}\n\n`; + } + if (result.procedure) { + text += `## Procedure Step ${result.procedure.step}: ${result.procedure.title}\n${result.procedure.description}\n\n`; + } + text += "---\n\n"; + } + return { content: [{ type: "text", text }] }; + } + ); +} diff --git a/src/plugins/echo/data.ts b/src/plugins/echo/data.ts new file mode 100644 index 0000000..fb8ce82 --- /dev/null +++ b/src/plugins/echo/data.ts @@ -0,0 +1,501 @@ +import { snippet } from "./loader.js"; + +export const RECIPE_CATEGORIES = [ + "crud", + "websocket", + "sse", + "auth", + "file", + "tls", + "middleware", + "proxy", + "streaming", + "graceful-shutdown", + "http2", + "testing", + "routing", +] as const; + +export type RecipeCategory = (typeof RECIPE_CATEGORIES)[number]; + +export interface Recipe { + name: string; + category: RecipeCategory; + description: string; + when: string; + code: string; + gotchas?: string[]; + relatedRecipes?: string[]; +} + +export interface MiddlewareRef { + name: string; + purpose: string; + usage: string; + code: string; + order?: string; +} + +// --------------------------------------------------------------------------- +// RECIPES +// --------------------------------------------------------------------------- + +export const RECIPES: Recipe[] = [ + { + name: "hello-world", + category: "routing", + description: "Minimal Echo server with a single GET route", + when: "Starting a new Echo project or verifying your setup", + code: snippet("recipes/hello-world.md"), + gotchas: [ + "e.Logger.Fatal calls os.Exit on error โ€” use it only in main", + "echo.New() returns a configured instance; always use this, not raw http.Server", + ], + relatedRecipes: ["crud-api", "middleware-chain", "graceful-shutdown"], + }, + { + name: "crud-api", + category: "crud", + description: "Full CRUD REST API with JSON binding and echo.Context", + when: "Building a resource-oriented REST API with JSON payloads", + code: snippet("recipes/crud-api.md"), + gotchas: [ + "c.Bind() reads Body only once โ€” do not call it twice", + "Return echo.NewHTTPError for client errors; Echo renders these as JSON automatically", + "Validate() requires setting e.Validator โ€” wire up a validator like go-playground/validator", + "PathParamsBinder is the idiomatic way to parse typed path params", + ], + relatedRecipes: ["hello-world", "middleware-chain", "route-groups"], + }, + { + name: "websocket", + category: "websocket", + description: "WebSocket upgrade with read/write loop and proper cleanup", + when: "Building real-time bidirectional communication (chat, live updates, collaborative tools)", + code: snippet("recipes/websocket.md"), + gotchas: [ + "Always defer ws.Close() immediately after upgrade", + "WebSocket handler owns the loop โ€” it blocks until the connection closes", + "echo.Context is NOT goroutine-safe; capture needed values before spawning goroutines", + "gorilla/websocket is preferred in production over golang.org/x/net/websocket", + "Set CheckOrigin to validate the request origin in production โ€” never blindly return true", + ], + relatedRecipes: ["sse", "graceful-shutdown"], + }, + { + name: "sse", + category: "sse", + description: "Server-Sent Events with Flush(), content-type header, and client disconnect via context.Done()", + when: "One-way server-to-client streaming (live logs, dashboards, notifications) without WebSocket overhead", + code: snippet("recipes/sse.md"), + gotchas: [ + "Flush() is REQUIRED after every write โ€” without it data buffers and never reaches the client", + "SSE format: 'data: \\n\\n' โ€” double newline terminates the event", + "Always watch context.Done() to detect client disconnect and stop the loop", + "Disable Gzip middleware on SSE routes โ€” compression prevents proper streaming", + "SSE is unidirectional (server โ†’ client); use WebSocket for bidirectional", + "Nginx/proxies may buffer SSE; add 'X-Accel-Buffering: no' header when behind nginx", + ], + relatedRecipes: ["websocket", "streaming-response"], + }, + { + name: "jwt-auth", + category: "auth", + description: "JWT middleware setup, token generation, and protected routes", + when: "Stateless authentication for REST APIs or microservices", + code: snippet("recipes/jwt-auth.md"), + gotchas: [ + "Always validate alg, iss, aud, and exp when verifying tokens", + "Use echojwt package (not echo's built-in deprecated JWT) for v4+", + "jwtSecret must be at least 32 bytes; load from environment variable in production", + "Never log or return the raw token in error responses", + "Use bcrypt/argon2 for password comparison โ€” never plain string equality in production", + ], + relatedRecipes: ["crud-api", "middleware-chain", "route-groups"], + }, + { + name: "cors", + category: "middleware", + description: "CORS middleware with configuration for allowed origins, methods, and headers", + when: "Your API is called from a browser on a different domain", + code: snippet("recipes/cors.md"), + gotchas: [ + "CORS middleware must be registered BEFORE any route handlers", + "AllowCredentials: true requires explicit origins โ€” wildcard '*' is rejected by browsers", + "OPTIONS preflight requests must return 200 โ€” Echo handles this automatically with CORS middleware", + "Do not set both AllowOrigins: ['*'] and AllowCredentials: true โ€” browsers reject this", + ], + relatedRecipes: ["middleware-chain", "jwt-auth"], + }, + { + name: "graceful-shutdown", + category: "graceful-shutdown", + description: "Signal handling with e.Shutdown(ctx) and configurable timeout", + when: "Production deployments where in-flight requests must complete before shutdown", + code: snippet("recipes/graceful-shutdown.md"), + gotchas: [ + "e.Start() must run in a goroutine; otherwise signal handling never executes", + "Check for http.ErrServerClosed โ€” it is the normal shutdown error, not a real failure", + "Timeout should be > your slowest expected request; 10โ€“30s is typical", + "signal.Notify requires a buffered channel (size 1) to avoid missing signals", + "In Kubernetes, configure terminationGracePeriodSeconds > your shutdown timeout", + ], + relatedRecipes: ["hello-world", "middleware-chain"], + }, + { + name: "file-upload", + category: "file", + description: "Multipart file upload with c.FormFile(), validation, and disk save", + when: "Accepting user-uploaded files (images, documents, etc.)", + code: snippet("recipes/file-upload.md"), + gotchas: [ + "Always use filepath.Base() to sanitize filenames โ€” path traversal attacks use '../' sequences", + "Set BodyLimit middleware AND validate file.Size โ€” both layers of defense", + "Use a UUID or hash as the stored filename; never trust the client-provided name for storage paths", + "Close src before dst to ensure all data is flushed", + "Consider storing to object storage (S3/GCS) instead of local disk in production", + ], + relatedRecipes: ["file-download", "middleware-chain"], + }, + { + name: "file-download", + category: "file", + description: "File download with c.Attachment() (download prompt) and c.Inline() (browser display)", + when: "Serving files to clients, either as downloads or for inline rendering", + code: snippet("recipes/file-download.md"), + gotchas: [ + "Never use user-supplied filenames directly in file paths โ€” always validate against an allowlist or database", + "c.Attachment() triggers a download dialog; c.Inline() lets the browser display it", + "c.File() and c.Static() do not set Content-Disposition โ€” use Attachment/Inline for explicit control", + "Set appropriate Cache-Control headers for static assets", + ], + relatedRecipes: ["file-upload"], + }, + { + name: "auto-tls", + category: "tls", + description: "Automatic TLS with Let's Encrypt via AutoTLSManager", + when: "Production server that needs HTTPS without manual certificate management", + code: snippet("recipes/auto-tls.md"), + gotchas: [ + "Requires ports 80 and 443 to be open โ€” Let's Encrypt uses HTTP-01 challenge on port 80", + "HostWhitelist is REQUIRED to prevent certificate issuance for arbitrary domains", + "Cache directory must be writable and persistent across restarts", + "Rate limits apply โ€” don't restart the server repeatedly during testing; use Let's Encrypt staging", + ], + relatedRecipes: ["http2", "graceful-shutdown"], + }, + { + name: "http2", + category: "http2", + description: "HTTP/2 with manual TLS certificate and server push", + when: "Performance-critical serving where HTTP/2 multiplexing reduces latency", + code: snippet("recipes/http2.md"), + gotchas: [ + "HTTP/2 in browsers requires TLS โ€” plain-text h2c is only for trusted internal networks", + "Server Push is deprecated in Chrome 106+ and removed in many browsers; prefer preload hints", + "Use golang.org/x/net/http2 to explicitly configure HTTP/2 settings", + "Generate self-signed certs for development: openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes", + ], + relatedRecipes: ["auto-tls", "graceful-shutdown"], + }, + { + name: "middleware-chain", + category: "middleware", + description: "Logger โ†’ Recover โ†’ Auth โ†’ Custom middleware chain and why order matters", + when: "Understanding the execution model of Echo middleware and setting up a production chain", + code: snippet("recipes/middleware-chain.md"), + gotchas: [ + "Logger BEFORE Recover: if Recover is first, panics are caught before Logger sees the request complete", + "e.Use() registers global middleware; group.Use() registers only for that group", + "e.Pre() runs middleware before the router โ€” useful for HTTPS redirect, trailing slash removal", + "Middleware wraps handlers like an onion โ€” request flows inward, response flows outward", + "echo.Context is NOT goroutine-safe โ€” never pass it to a goroutine; copy values first", + ], + relatedRecipes: ["hello-world", "cors", "jwt-auth"], + }, + { + name: "reverse-proxy", + category: "proxy", + description: "Reverse proxy setup with load balancing to backend targets", + when: "Fronting backend services, implementing API gateway patterns, or load balancing", + code: snippet("recipes/reverse-proxy.md"), + gotchas: [ + "ProxyWithConfig with RoundRobinBalancer handles load balancing automatically", + "Set appropriate timeouts on the proxy to avoid hanging connections", + "Strip or rewrite path prefixes as needed using middleware.Rewrite", + "X-Forwarded-For and X-Real-IP headers should be passed through to backends", + ], + relatedRecipes: ["middleware-chain", "graceful-shutdown"], + }, + { + name: "streaming-response", + category: "streaming", + description: "Chunked streaming response with explicit Flush() for large or live data", + when: "Streaming large files, live log tailing, or progressive data delivery", + code: snippet("recipes/streaming-response.md"), + gotchas: [ + "Flush() is MANDATORY โ€” without it the entire response buffers until the handler returns", + "Do NOT use Gzip middleware on streaming routes โ€” it buffers the entire response", + "WriteHeader() must be called once before writing body โ€” subsequent calls are no-ops", + "Check context.Done() in long-running streams to detect client disconnect", + ], + relatedRecipes: ["sse", "websocket"], + }, + { + name: "route-groups", + category: "routing", + description: "e.Group() for resource grouping, API versioning, and scoped middleware", + when: "Organizing routes by version, resource, or auth requirement", + code: snippet("recipes/route-groups.md"), + gotchas: [ + "Group middleware only applies to routes registered on that group โ€” not the parent", + "Groups can be nested: v1.Group('/users') creates /v1/users prefix", + "Middleware registered with group.Use() runs after global middleware from e.Use()", + "Route parameters in group prefix are accessible in handlers via c.Param()", + ], + relatedRecipes: ["middleware-chain", "jwt-auth", "crud-api"], + }, + { + name: "timeout", + category: "middleware", + description: "Request timeout middleware with context cancellation", + when: "Enforcing maximum request duration to protect against slow clients or upstream hangs", + code: snippet("recipes/timeout.md"), + gotchas: [ + "Always check context.Done() in handlers โ€” TimeoutMiddleware cancels the context, not the goroutine", + "Set timeout > your slowest expected operation but < your load balancer's timeout", + "Different routes may need different timeouts โ€” apply per-group instead of globally", + ], + relatedRecipes: ["middleware-chain", "graceful-shutdown"], + }, + { + name: "embed-resources", + category: "file", + description: "Embed static assets into the binary with //go:embed and serve via http.FS", + when: "Shipping a self-contained binary with bundled frontend assets, templates, or static files", + code: snippet("recipes/embed-resources.md"), + gotchas: [ + "The //go:embed directive must be in the same package as the variable it annotates", + "fs.Sub() strips the top-level directory prefix from the embedded FS", + "Embedded files are read-only โ€” you cannot write to them at runtime", + "Binary size grows with embedded assets โ€” use only for small/medium asset sets", + ], + relatedRecipes: ["file-download", "hello-world"], + }, + { + name: "subdomain-routing", + category: "routing", + description: "Route requests by subdomain using e.Host() with static or wildcard subdomains", + when: "Multi-tenant apps, API/admin separation, or white-label subdomain routing", + code: snippet("recipes/subdomain-routing.md"), + gotchas: [ + "e.Host() requires the Host header to match โ€” test with /etc/hosts entries locally", + "Wildcard subdomain capture uses :subdomain in the host pattern", + "Ensure DNS wildcard records (*.example.com) are configured in production", + "Host-based routing runs before path routing โ€” combine with group middleware for auth", + ], + relatedRecipes: ["route-groups", "middleware-chain"], + }, + { + name: "jsonp", + category: "routing", + description: "JSONP response for legacy cross-domain requests using c.JSONP()", + when: "Supporting legacy clients or environments where CORS is not available", + code: snippet("recipes/jsonp.md"), + gotchas: [ + "JSONP is a legacy technique โ€” prefer CORS for all modern use cases", + "Always validate the callback parameter โ€” never reflect arbitrary input to avoid XSS", + "JSONP only supports GET requests โ€” cannot be used for POST/PUT/DELETE", + ], + relatedRecipes: ["cors", "crud-api"], + }, +]; + +// --------------------------------------------------------------------------- +// MIDDLEWARE CATALOG +// --------------------------------------------------------------------------- + +export const MIDDLEWARE: MiddlewareRef[] = [ + { + name: "Logger", + purpose: "Logs request method, path, status, latency, and bytes", + usage: "e.Use(middleware.Logger())", + code: snippet("middleware/logger.md"), + order: "FIRST โ€” must be before Recover to log all requests including panics", + }, + { + name: "Recover", + purpose: "Catches panics, logs the stack trace, and returns HTTP 500", + usage: "e.Use(middleware.Recover())", + code: snippet("middleware/recover.md"), + order: "SECOND โ€” after Logger, before all other middleware", + }, + { + name: "CORS", + purpose: "Sets Cross-Origin Resource Sharing headers", + usage: "e.Use(middleware.CORSWithConfig(...))", + code: snippet("middleware/cors.md"), + order: "Before auth middleware so OPTIONS preflight requests pass through", + }, + { + name: "JWT", + purpose: "Validates JWT Bearer tokens and sets claims on context", + usage: "group.Use(echojwt.WithConfig(...))", + code: snippet("middleware/jwt.md"), + order: "After CORS, on protected route groups only", + }, + { + name: "CSRF", + purpose: "Protects against Cross-Site Request Forgery attacks", + usage: "e.Use(middleware.CSRF())", + code: snippet("middleware/csrf.md"), + order: "After session middleware", + }, + { + name: "RateLimiter", + purpose: "Limits request rate per IP using in-memory store", + usage: "e.Use(middleware.RateLimiter(...))", + code: snippet("middleware/rate-limiter.md"), + order: "Early in chain โ€” before auth to prevent brute force", + }, + { + name: "BasicAuth", + purpose: "HTTP Basic Authentication", + usage: "group.Use(middleware.BasicAuth(...))", + code: snippet("middleware/basic-auth.md"), + }, + { + name: "KeyAuth", + purpose: "Validates API keys from header, query, or cookie", + usage: "e.Use(middleware.KeyAuth(...))", + code: snippet("middleware/key-auth.md"), + }, + { + name: "Gzip", + purpose: "Compresses responses with gzip encoding", + usage: "e.Use(middleware.Gzip())", + code: snippet("middleware/gzip.md"), + order: "Do NOT use on SSE or streaming routes โ€” it buffers the entire response", + }, + { + name: "Secure", + purpose: "Sets security headers (HSTS, XSS protection, content type nosniff, etc.)", + usage: "e.Use(middleware.Secure())", + code: snippet("middleware/secure.md"), + }, + { + name: "BodyLimit", + purpose: "Limits request body size to prevent abuse", + usage: "e.Use(middleware.BodyLimit('2M'))", + code: snippet("middleware/body-limit.md"), + order: "Early in chain, before body reading middleware", + }, + { + name: "RequestID", + purpose: "Attaches a unique X-Request-ID to each request for tracing", + usage: "e.Use(middleware.RequestID())", + code: snippet("middleware/request-id.md"), + }, + { + name: "Timeout", + purpose: "Cancels requests that exceed a time limit", + usage: "e.Use(middleware.TimeoutWithConfig(...))", + code: snippet("middleware/timeout.md"), + }, +]; + +// --------------------------------------------------------------------------- +// DECISION MATRIX +// --------------------------------------------------------------------------- + +export interface DecisionEntry { + need: string; + pattern: string; + recipe: string; + notes: string; +} + +export const DECISION_MATRIX: DecisionEntry[] = [ + { need: "REST API", pattern: "CRUD handlers with JSON", recipe: "crud-api", notes: "Thin handlers, validate with go-playground/validator" }, + { need: "Real-time bidirectional", pattern: "WebSocket", recipe: "websocket", notes: "gorilla/websocket preferred; always defer ws.Close()" }, + { need: "Live push notifications", pattern: "Server-Sent Events", recipe: "sse", notes: "Simpler than WebSocket; unidirectional only; always Flush()" }, + { need: "Authentication", pattern: "JWT middleware", recipe: "jwt-auth", notes: "Use echojwt package; validate alg+iss+aud+exp" }, + { need: "File upload", pattern: "Multipart form", recipe: "file-upload", notes: "Always sanitize filename; use BodyLimit middleware" }, + { need: "File download", pattern: "c.Attachment() or c.Inline()", recipe: "file-download", notes: "Never use user-supplied path directly" }, + { need: "HTTPS", pattern: "AutoTLS or manual TLS", recipe: "auto-tls", notes: "Use auto-tls for Let's Encrypt; manual TLS for http2" }, + { need: "API versioning", pattern: "Route groups", recipe: "route-groups", notes: "e.Group('/v1'), e.Group('/v2') with separate middleware" }, + { need: "Proxy / API gateway", pattern: "Proxy middleware", recipe: "reverse-proxy", notes: "Built-in ProxyMiddleware with RoundRobinBalancer" }, + { need: "Zero-downtime deploy", pattern: "Graceful shutdown", recipe: "graceful-shutdown", notes: "Signal + e.Shutdown(ctx) with 10โ€“30s timeout" }, + { need: "Large response streaming", pattern: "Chunked with Flush()", recipe: "streaming-response", notes: "Disable Gzip on streaming routes" }, + { need: "Cross-origin requests", pattern: "CORS middleware", recipe: "cors", notes: "Register before all routes; AllowCredentials needs explicit origins" }, + { need: "Request timeout", pattern: "Timeout middleware", recipe: "timeout", notes: "Apply per-group for fine-grained control" }, + { need: "Embedded assets", pattern: "go:embed + http.FS", recipe: "embed-resources", notes: "Self-contained binary; read-only; watch binary size" }, + { need: "Multi-tenant subdomains", pattern: "e.Host() routing", recipe: "subdomain-routing", notes: "Requires wildcard DNS; combine with group middleware" }, + { need: "Legacy cross-domain", pattern: "JSONP", recipe: "jsonp", notes: "Avoid in new code; use CORS instead" }, +]; + +// --------------------------------------------------------------------------- +// HELPERS +// --------------------------------------------------------------------------- + +export function getRecipeByName(name: string): Recipe | undefined { + return RECIPES.find((r) => r.name.toLowerCase() === name.toLowerCase()); +} + +export function getRecipesByCategory(category: RecipeCategory): Recipe[] { + return RECIPES.filter((r) => r.category === category); +} + +export function getMiddlewareByName(name: string): MiddlewareRef | undefined { + return MIDDLEWARE.find((m) => m.name.toLowerCase() === name.toLowerCase()); +} + +export function searchRecipes(query: string): Recipe[] { + const q = query.toLowerCase(); + return RECIPES.filter( + (r) => + r.name.includes(q) || + r.description.toLowerCase().includes(q) || + r.when.toLowerCase().includes(q) || + r.code.toLowerCase().includes(q) || + r.gotchas?.some((g) => g.toLowerCase().includes(q)) || + r.category.includes(q) + ); +} + +export function searchMiddleware(query: string): MiddlewareRef[] { + const q = query.toLowerCase(); + return MIDDLEWARE.filter( + (m) => + m.name.toLowerCase().includes(q) || + m.purpose.toLowerCase().includes(q) || + m.usage.toLowerCase().includes(q) + ); +} + +export function formatRecipe(r: Recipe): string { + let out = `# ${r.name}\n\n`; + out += `**Category:** ${r.category}\n`; + out += `**Description:** ${r.description}\n`; + out += `**Use when:** ${r.when}\n\n`; + out += `## Code\n\n\`\`\`go\n${r.code}\n\`\`\`\n\n`; + if (r.gotchas && r.gotchas.length > 0) { + out += `## Gotchas\n\n`; + for (const g of r.gotchas) out += `- ${g}\n`; + out += "\n"; + } + if (r.relatedRecipes && r.relatedRecipes.length > 0) { + out += `## Related Recipes\n\n${r.relatedRecipes.join(", ")}\n`; + } + return out; +} + +export function formatMiddleware(m: MiddlewareRef): string { + let out = `# ${m.name} Middleware\n\n`; + out += `**Purpose:** ${m.purpose}\n`; + out += `**Usage:** \`${m.usage}\`\n`; + if (m.order) out += `**Order:** ${m.order}\n`; + out += `\n## Config Example\n\n\`\`\`go\n${m.code}\n\`\`\`\n`; + return out; +} diff --git a/src/plugins/echo/index.ts b/src/plugins/echo/index.ts new file mode 100644 index 0000000..bad41ae --- /dev/null +++ b/src/plugins/echo/index.ts @@ -0,0 +1,19 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { Plugin } from "../../registry.js"; +import { register as listRecipes } from "./tools/list-recipes.js"; +import { register as getRecipe } from "./tools/get-recipe.js"; +import { register as listMiddleware } from "./tools/list-middleware.js"; +import { register as getMiddleware } from "./tools/get-middleware.js"; +import { register as searchDocs } from "./tools/search-docs.js"; +import { register as decisionMatrix } from "./tools/decision-matrix.js"; + +function register(server: McpServer): void { + listRecipes(server); + getRecipe(server); + listMiddleware(server); + getMiddleware(server); + searchDocs(server); + decisionMatrix(server); +} + +export const echoPlugin: Plugin = { name: "echo", register }; diff --git a/src/plugins/echo/loader.ts b/src/plugins/echo/loader.ts new file mode 100644 index 0000000..e369de8 --- /dev/null +++ b/src/plugins/echo/loader.ts @@ -0,0 +1,3 @@ +import { createSnippetLoader } from "../../shared/loader-factory.js"; + +export const snippet = createSnippetLoader("echo"); diff --git a/src/plugins/echo/tools/decision-matrix.ts b/src/plugins/echo/tools/decision-matrix.ts new file mode 100644 index 0000000..62c12d9 --- /dev/null +++ b/src/plugins/echo/tools/decision-matrix.ts @@ -0,0 +1,54 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { DECISION_MATRIX } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "echo_decision_matrix", + "Given a need or requirement, get the recommended Echo pattern and recipe to use.", + { + need: z.string().optional().describe("What you need to build (e.g., 'real-time', 'auth', 'file upload', 'REST API')"), + }, + async ({ need }) => { + if (!need) { + let text = "# Echo Framework โ€” Decision Matrix\n\n"; + text += "| Need | Pattern | Recipe | Notes |\n"; + text += "|------|---------|--------|-------|\n"; + for (const d of DECISION_MATRIX) { + text += `| ${d.need} | ${d.pattern} | \`${d.recipe}\` | ${d.notes} |\n`; + } + return { content: [{ type: "text", text }] }; + } + + const q = need.toLowerCase(); + const matches = DECISION_MATRIX.filter( + (d) => + d.need.toLowerCase().includes(q) || + d.pattern.toLowerCase().includes(q) || + d.notes.toLowerCase().includes(q) + ); + + if (matches.length === 0) { + let text = `No direct match for "${need}".\n\n`; + text += "# Full Decision Matrix\n\n"; + text += "| Need | Pattern | Recipe |\n"; + text += "|------|---------|--------|\n"; + for (const d of DECISION_MATRIX) { + text += `| ${d.need} | ${d.pattern} | \`${d.recipe}\` |\n`; + } + return { content: [{ type: "text", text }] }; + } + + let text = `# Echo Pattern for: "${need}"\n\n`; + for (const d of matches) { + text += `## ${d.pattern}\n\n`; + text += `**Need:** ${d.need}\n`; + text += `**Recipe:** \`${d.recipe}\`\n`; + text += `**Notes:** ${d.notes}\n\n`; + text += `Run \`echo_get_recipe\` with name \`${d.recipe}\` for full working code.\n\n`; + } + + return { content: [{ type: "text", text }] }; + } + ); +} diff --git a/src/plugins/echo/tools/get-middleware.ts b/src/plugins/echo/tools/get-middleware.ts new file mode 100644 index 0000000..590b04b --- /dev/null +++ b/src/plugins/echo/tools/get-middleware.ts @@ -0,0 +1,30 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { MIDDLEWARE, getMiddlewareByName, searchMiddleware, formatMiddleware } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "echo_get_middleware", + "Get detailed config and usage for a specific Echo middleware.", + { + name: z.string().describe("Middleware name (e.g., 'Logger', 'JWT', 'CORS', 'RateLimiter', 'Gzip', 'CSRF')"), + }, + async ({ name }) => { + const mw = getMiddlewareByName(name); + if (!mw) { + const suggestions = searchMiddleware(name).map((m) => m.name); + const allNames = MIDDLEWARE.map((m) => m.name).join(", "); + return { + content: [ + { + type: "text", + text: `Middleware "${name}" not found.${suggestions.length ? ` Did you mean: ${suggestions.slice(0, 3).join(", ")}?` : ""}\n\nAll middleware: ${allNames}`, + }, + ], + isError: true, + }; + } + return { content: [{ type: "text", text: formatMiddleware(mw) }] }; + } + ); +} diff --git a/src/plugins/echo/tools/get-recipe.ts b/src/plugins/echo/tools/get-recipe.ts new file mode 100644 index 0000000..fe0fadd --- /dev/null +++ b/src/plugins/echo/tools/get-recipe.ts @@ -0,0 +1,30 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { RECIPES, getRecipeByName, searchRecipes, formatRecipe } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "echo_get_recipe", + "Get a specific Echo framework recipe with full working Go code, gotchas, and related recipes.", + { + name: z.string().describe("Recipe name (e.g., 'crud-api', 'websocket', 'sse', 'jwt-auth', 'graceful-shutdown')"), + }, + async ({ name }) => { + const recipe = getRecipeByName(name); + if (!recipe) { + const suggestions = searchRecipes(name).map((r) => r.name); + const allNames = RECIPES.map((r) => r.name).join(", "); + return { + content: [ + { + type: "text", + text: `Recipe "${name}" not found.${suggestions.length ? ` Did you mean: ${suggestions.slice(0, 3).join(", ")}?` : ""}\n\nAll recipes: ${allNames}`, + }, + ], + isError: true, + }; + } + return { content: [{ type: "text", text: formatRecipe(recipe) }] }; + } + ); +} diff --git a/src/plugins/echo/tools/list-middleware.ts b/src/plugins/echo/tools/list-middleware.ts new file mode 100644 index 0000000..2de8798 --- /dev/null +++ b/src/plugins/echo/tools/list-middleware.ts @@ -0,0 +1,35 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { MIDDLEWARE } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "echo_list_middleware", + "List all available Echo framework middleware with their purpose and recommended chain order.", + {}, + async () => { + let text = "# Echo Framework โ€” Middleware Catalog\n\n"; + text += `Total: ${MIDDLEWARE.length} middleware\n\n`; + + text += "## Recommended Middleware Order\n\n"; + text += "1. Logger โ€” log all requests (must be FIRST)\n"; + text += "2. Recover โ€” catch panics\n"; + text += "3. RequestID โ€” attach trace ID\n"; + text += "4. CORS โ€” before auth (preflight must pass)\n"; + text += "5. RateLimiter โ€” early rejection\n"; + text += "6. Auth (JWT / BasicAuth / KeyAuth)\n"; + text += "7. Custom business middleware\n\n"; + + text += "## Available Middleware\n\n"; + for (const m of MIDDLEWARE) { + text += `### ${m.name}\n`; + text += `**Purpose:** ${m.purpose}\n`; + text += `**Usage:** \`${m.usage}\`\n`; + if (m.order) text += `**Order note:** ${m.order}\n`; + text += "\n"; + } + + text += "Use echo_get_middleware to get full config examples."; + return { content: [{ type: "text", text }] }; + } + ); +} diff --git a/src/plugins/echo/tools/list-recipes.ts b/src/plugins/echo/tools/list-recipes.ts new file mode 100644 index 0000000..e18351b --- /dev/null +++ b/src/plugins/echo/tools/list-recipes.ts @@ -0,0 +1,34 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { RECIPES, RECIPE_CATEGORIES } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "echo_list_recipes", + "List all Go Echo framework recipes. Optionally filter by category.", + { + category: z.enum(RECIPE_CATEGORIES).optional().describe("Filter by recipe category"), + }, + async ({ category }) => { + const list = category ? RECIPES.filter((r) => r.category === category) : RECIPES; + + const grouped: Record = {}; + for (const r of list) { + if (!grouped[r.category]) grouped[r.category] = []; + grouped[r.category].push(`${r.name} โ€” ${r.description}`); + } + + let text = "# Echo Framework โ€” Recipes\n\n"; + text += `Total: ${list.length} recipes\n\n`; + for (const [cat, items] of Object.entries(grouped)) { + text += `## ${cat}\n`; + for (const item of items) text += `- ${item}\n`; + text += "\n"; + } + text += `\nCategories: ${RECIPE_CATEGORIES.join(", ")}\n`; + text += `\nUse echo_get_recipe to fetch a recipe with full working Go code.`; + + return { content: [{ type: "text", text }] }; + } + ); +} diff --git a/src/plugins/echo/tools/search-docs.ts b/src/plugins/echo/tools/search-docs.ts new file mode 100644 index 0000000..abce20d --- /dev/null +++ b/src/plugins/echo/tools/search-docs.ts @@ -0,0 +1,55 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { searchRecipes, searchMiddleware, RECIPE_CATEGORIES } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "echo_search_docs", + "Search Echo framework recipes and middleware by keyword.", + { + query: z.string().describe("Search query (e.g., 'websocket', 'auth', 'file upload', 'graceful', 'flush')"), + }, + async ({ query }) => { + const recipes = searchRecipes(query); + const middleware = searchMiddleware(query); + + if (recipes.length === 0 && middleware.length === 0) { + return { + content: [ + { + type: "text", + text: `No results for "${query}".\n\nTry broader terms. Available categories: ${RECIPE_CATEGORIES.join(", ")}`, + }, + ], + }; + } + + let text = `# Echo Search: "${query}"\n\n`; + + if (recipes.length > 0) { + text += `## Recipes (${recipes.length})\n\n`; + for (const r of recipes) { + text += `### ${r.name} [${r.category}]\n`; + text += `${r.description}\n`; + text += `**Use when:** ${r.when}\n`; + if (r.gotchas && r.gotchas.length > 0) { + text += `**Key gotcha:** ${r.gotchas[0]}\n`; + } + text += "\n"; + } + } + + if (middleware.length > 0) { + text += `## Middleware (${middleware.length})\n\n`; + for (const m of middleware) { + text += `### ${m.name}\n`; + text += `${m.purpose}\n`; + text += `Usage: \`${m.usage}\`\n\n`; + } + } + + text += "Use echo_get_recipe or echo_get_middleware for full details with code."; + return { content: [{ type: "text", text }] }; + } + ); +} diff --git a/src/plugins/golang/data.ts b/src/plugins/golang/data.ts new file mode 100644 index 0000000..c7b4d43 --- /dev/null +++ b/src/plugins/golang/data.ts @@ -0,0 +1,326 @@ +import { snippet } from "./loader.js"; + +export interface BestPractice { + name: string; + topic: Topic; + priority: "P0" | "P1"; + rule: string; + reason: string; + good?: string; + bad?: string; +} + +export interface DesignPattern { + name: string; + category: "creational" | "structural" | "behavioral" | "concurrency"; + goApproach: string; + when: string; + code: string; + antiPattern?: string; + oopEquivalent?: string; +} + +export const TOPICS = [ + "fundamentals", "architecture", "error-handling", "concurrency", + "api-server", "database", "config", "logging", "security", "testing", +] as const; +export type Topic = (typeof TOPICS)[number]; + +// --------------------------------------------------------------------------- +// BEST PRACTICES +// --------------------------------------------------------------------------- + +export const BEST_PRACTICES: BestPractice[] = [ + // FUNDAMENTALS + { + name: "naming-conventions", + topic: "fundamentals", + priority: "P0", + rule: "camelCase for unexported, PascalCase for exported. Short, descriptive names.", + reason: "Go convention. Exported names are part of the package API.", + good: snippet("practices/naming-conventions-good.md"), + bad: snippet("practices/naming-conventions-bad.md"), + }, + { + name: "small-interfaces", + topic: "fundamentals", + priority: "P0", + rule: "Keep interfaces small (1-2 methods). Define at consumer, not producer.", + reason: "Go proverb: the bigger the interface, the weaker the abstraction. Consumer-side definition enables decoupling without circular deps.", + good: snippet("practices/small-interfaces-good.md"), + bad: snippet("practices/small-interfaces-bad.md"), + }, + { + name: "constructor-pattern", + topic: "fundamentals", + priority: "P0", + rule: "Use NewType() constructor functions for structs requiring validation or defaults.", + reason: "Prevents zero-value misuse. Validation at construction, not at every use site.", + good: snippet("practices/constructor-pattern-good.md"), + }, + // ERROR HANDLING + { + name: "error-wrapping", + topic: "error-handling", + priority: "P0", + rule: "Always wrap errors with context: fmt.Errorf(\"context: %w\", err)", + reason: "Provides call stack context without expensive stack traces. %w enables errors.Is/As.", + good: snippet("practices/error-wrapping-good.md"), + bad: snippet("practices/error-wrapping-bad.md"), + }, + { + name: "handle-once", + topic: "error-handling", + priority: "P0", + rule: "Handle errors once: Log OR Return, never both.", + reason: "Logging and returning causes duplicate error messages at every stack level.", + bad: snippet("practices/handle-once-bad.md"), + good: snippet("practices/handle-once-good.md"), + }, + { + name: "errors-is-as", + topic: "error-handling", + priority: "P0", + rule: "Use errors.Is() for sentinel errors, errors.As() for error types.", + reason: "Works correctly with wrapped errors (%w). Direct == comparison breaks wrapping.", + good: snippet("practices/errors-is-as-good.md"), + bad: snippet("practices/errors-is-as-bad.md"), + }, + // CONCURRENCY + { + name: "context-first-param", + topic: "concurrency", + priority: "P0", + rule: "Pass context.Context as the first parameter to every function that does I/O.", + reason: "Enables cancellation propagation and deadline enforcement across service boundaries.", + good: snippet("practices/context-first-param-good.md"), + bad: snippet("practices/context-first-param-bad.md"), + }, + { + name: "goroutine-lifecycle", + topic: "concurrency", + priority: "P0", + rule: "Never start a goroutine without knowing how it stops.", + reason: "Goroutine leaks exhaust memory. Every goroutine needs a clear exit condition.", + good: snippet("practices/goroutine-lifecycle-good.md"), + bad: snippet("practices/goroutine-lifecycle-bad.md"), + }, + { + name: "errgroup", + topic: "concurrency", + priority: "P0", + rule: "Use errgroup over WaitGroup when goroutines can fail.", + reason: "errgroup propagates the first error and cancels the context for all siblings.", + good: snippet("practices/errgroup-good.md"), + bad: snippet("practices/errgroup-bad.md"), + }, + // SECURITY + { + name: "crypto-rand", + topic: "security", + priority: "P0", + rule: "ALWAYS use crypto/rand for security-sensitive randomness. Never math/rand.", + reason: "math/rand is deterministic and predictable. crypto/rand uses OS entropy.", + good: snippet("practices/crypto-rand-good.md"), + bad: snippet("practices/crypto-rand-bad.md"), + }, + { + name: "parameterized-queries", + topic: "security", + priority: "P0", + rule: "Always use parameterized queries. Never string concatenation for SQL.", + reason: "SQL injection is a critical vulnerability. String concatenation is always wrong.", + good: snippet("practices/parameterized-queries-good.md"), + bad: snippet("practices/parameterized-queries-bad.md"), + }, + // API SERVER + { + name: "graceful-shutdown", + topic: "api-server", + priority: "P0", + rule: "MUST implement graceful shutdown with signal handling.", + reason: "Without graceful shutdown, in-flight requests are aborted on deploy/restart.", + good: snippet("practices/graceful-shutdown-good.md"), + }, + { + name: "thin-handlers", + topic: "api-server", + priority: "P0", + rule: "Keep handlers thin: parse โ†’ call service โ†’ respond. No business logic in handlers.", + reason: "Business logic in handlers is untestable and non-reusable.", + good: snippet("practices/thin-handlers-good.md"), + }, + // TESTING + { + name: "table-driven-tests", + topic: "testing", + priority: "P0", + rule: "Use table-driven test pattern for all test cases.", + reason: "DRY, readable, easy to add cases, works well with t.Run.", + good: snippet("practices/table-driven-tests-good.md"), + }, + // DATABASE + { + name: "database-repository", + topic: "database", + priority: "P0", + rule: "Use the repository pattern with interfaces. Always use QueryContext/ExecContext with context. Always defer rows.Close().", + reason: "Repository pattern decouples business logic from database implementation. Context enables cancellation. Forgetting rows.Close() causes connection leaks.", + good: snippet("practices/database-repository-good.md"), + bad: snippet("practices/database-repository-bad.md"), + }, + // CONFIG + { + name: "config-env-vars", + topic: "config", + priority: "P1", + rule: "Load all config from environment variables into a typed struct at startup. Validate on startup. Never scatter os.Getenv() calls.", + reason: "Centralized validation prevents silent misconfiguration. 12-factor compliance. Typed struct makes config injectable and testable.", + good: snippet("practices/config-env-vars-good.md"), + bad: snippet("practices/config-env-vars-bad.md"), + }, + // LOGGING + { + name: "structured-logging", + topic: "logging", + priority: "P1", + rule: "Use log/slog with structured key-value pairs. JSON in production, text in development. Never log.Fatal() in libraries.", + reason: "Structured logs are machine-parseable and alertable. log.Fatal() in libraries kills the process without allowing cleanup.", + good: snippet("practices/structured-logging-good.md"), + bad: snippet("practices/structured-logging-bad.md"), + }, + // TOOLING + { + name: "golangci-lint", + topic: "fundamentals", + priority: "P1", + rule: "Configure golangci-lint with errcheck, gosec, staticcheck, bodyclose, and noctx. Run in CI.", + reason: "Automated linting catches error handling gaps, security issues, and resource leaks before code review.", + good: snippet("practices/golangci-lint-good.md"), + }, +]; + +// --------------------------------------------------------------------------- +// DESIGN PATTERNS +// --------------------------------------------------------------------------- + +export const DESIGN_PATTERNS: DesignPattern[] = [ + { + name: "functional-options", + category: "creational", + goApproach: "Variadic options functions instead of config structs or builder chains", + when: "Constructor with 5+ optional parameters", + oopEquivalent: "Builder pattern", + code: snippet("patterns/functional-options.md"), + }, + { + name: "adapter", + category: "structural", + goApproach: "Wrapper struct that implements an interface using a different underlying type", + when: "Integrating external libraries or legacy code behind a clean interface", + oopEquivalent: "Adapter / Wrapper", + code: snippet("patterns/adapter.md"), + }, + { + name: "middleware-decorator", + category: "structural", + goApproach: "Higher-order functions wrapping handlers โ€” the standard HTTP middleware pattern", + when: "Adding cross-cutting behavior (logging, auth, rate limiting) without modifying handlers", + oopEquivalent: "Decorator pattern", + code: snippet("patterns/middleware-decorator.md"), + }, + { + name: "worker-pool", + category: "concurrency", + goApproach: "Bounded goroutine pool using buffered channel as semaphore", + when: "Parallel work with bounded concurrency (CPU/network limits)", + code: snippet("patterns/worker-pool.md"), + }, + { + name: "pipeline", + category: "concurrency", + goApproach: "Chain of goroutines connected by channels โ€” each stage transforms the stream", + when: "Stage-by-stage stream processing (ETL, data transformation)", + code: snippet("patterns/pipeline.md"), + }, + { + name: "consumer-side-interface", + category: "structural", + goApproach: "Define interfaces in the consuming package, not the implementing package", + when: "Always โ€” this is the idiomatic Go approach to dependency management", + code: snippet("patterns/consumer-side-interface.md"), + antiPattern: snippet("patterns/consumer-side-interface-anti.md"), + }, + { + name: "strategy", + category: "behavioral", + goApproach: "Interface injection โ€” pass the algorithm as a function or interface", + when: "Multiple algorithms that can be swapped at runtime", + oopEquivalent: "Strategy pattern", + code: snippet("patterns/strategy.md"), + }, + { + name: "fan-out-fan-in", + category: "concurrency", + goApproach: "Distribute work across N goroutines (fan-out), then merge results into one channel (fan-in)", + when: "Parallel independent work items with result collection", + code: snippet("patterns/fan-out-fan-in.md"), + }, + { + name: "observer", + category: "behavioral", + goApproach: "Channel-based event bus โ€” subscribers receive from buffered channels", + when: "Decoupling event producers from consumers without shared state", + oopEquivalent: "Observer / Pub-Sub pattern", + code: snippet("patterns/observer.md"), + }, + { + name: "command", + category: "behavioral", + goApproach: "Function closures as commands โ€” submitted to a worker channel for execution", + when: "Job queues, undo stacks, deferred execution, task pipelines", + oopEquivalent: "Command pattern", + code: snippet("patterns/command.md"), + }, +]; + +// --------------------------------------------------------------------------- +// ANTI-PATTERNS +// --------------------------------------------------------------------------- + +export const ANTI_PATTERNS = [ + { name: "global-mutable-state", description: "Global vars for dependency storage", fix: "Dependency injection โ€” pass deps to constructors" }, + { name: "ignoring-errors", description: "_ = someFunc() or no error check", fix: "Always handle: return, log, or wrap" }, + { name: "business-logic-in-handlers", description: "DB queries, computations in HTTP handlers", fix: "Thin handlers โ€” delegate to service layer" }, + { name: "sql-string-concat", description: "\"SELECT * WHERE id = \" + id", fix: "Always use parameterized queries ($1, ?, ?)" }, + { name: "math-rand-security", description: "math/rand for tokens/secrets", fix: "crypto/rand always for security-sensitive values" }, + { name: "goroutine-leak", description: "Goroutine with no exit condition", fix: "Always pass ctx, select on ctx.Done()" }, + { name: "missing-context", description: "Functions doing I/O without ctx parameter", fix: "First param is always context.Context for I/O" }, + { name: "not-closing-rows", description: "db.QueryContext without defer rows.Close()", fix: "Always defer rows.Close() immediately after query" }, + { name: "log-and-return", description: "log.Printf(err) then return err", fix: "Choose one: return (let caller log) or log (don't return)" }, + { name: "sleep-in-tests", description: "time.Sleep(100*ms) waiting for async", fix: "Use channels, select, or sync primitives" }, +]; + +// --------------------------------------------------------------------------- +// HELPERS +// --------------------------------------------------------------------------- + +export function getPracticesByTopic(topic: Topic): BestPractice[] { + return BEST_PRACTICES.filter((p) => p.topic === topic); +} + +export function getPatternsByCategory(cat: string): DesignPattern[] { + return DESIGN_PATTERNS.filter((p) => p.category === cat); +} + +export function searchAll(query: string): { practices: BestPractice[]; patterns: DesignPattern[] } { + const q = query.toLowerCase(); + return { + practices: BEST_PRACTICES.filter( + (p) => p.name.includes(q) || p.rule.toLowerCase().includes(q) || p.topic.includes(q) + ), + patterns: DESIGN_PATTERNS.filter( + (p) => p.name.includes(q) || p.goApproach.toLowerCase().includes(q) || p.category.includes(q) + ), + }; +} diff --git a/src/plugins/golang/index.ts b/src/plugins/golang/index.ts new file mode 100644 index 0000000..2dafd4e --- /dev/null +++ b/src/plugins/golang/index.ts @@ -0,0 +1,22 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { Plugin } from "../../registry.js"; +import { register as listPractices } from "./tools/list-practices.js"; +import { register as getPractice } from "./tools/get-practice.js"; +import { register as listPatterns } from "./tools/list-patterns.js"; +import { register as getPattern } from "./tools/get-pattern.js"; +import { register as getAntipatterns } from "./tools/get-antipatterns.js"; +import { register as searchDocs } from "./tools/search-docs.js"; + +function register(server: McpServer): void { + listPractices(server); + getPractice(server); + listPatterns(server); + getPattern(server); + getAntipatterns(server); + searchDocs(server); +} + +export const golangPlugin: Plugin = { + name: "golang", + register, +}; diff --git a/src/plugins/golang/loader.ts b/src/plugins/golang/loader.ts new file mode 100644 index 0000000..5f1ac08 --- /dev/null +++ b/src/plugins/golang/loader.ts @@ -0,0 +1,3 @@ +import { createSnippetLoader } from "../../shared/loader-factory.js"; +export const snippet = createSnippetLoader("golang"); + diff --git a/src/plugins/golang/tools/get-antipatterns.ts b/src/plugins/golang/tools/get-antipatterns.ts new file mode 100644 index 0000000..f8f8af6 --- /dev/null +++ b/src/plugins/golang/tools/get-antipatterns.ts @@ -0,0 +1,17 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { ANTI_PATTERNS } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "golang_get_antipatterns", + "List all Go anti-patterns to avoid", + {}, + async () => { + let text = "# Go Anti-patterns\n\nThings that look fine but cause bugs, leaks, or security issues:\n\n"; + for (const ap of ANTI_PATTERNS) { + text += `## โŒ ${ap.name}\n**Problem:** ${ap.description}\n**Fix:** ${ap.fix}\n\n`; + } + return { content: [{ type: "text", text }] }; + } + ); +} diff --git a/src/plugins/golang/tools/get-pattern.ts b/src/plugins/golang/tools/get-pattern.ts new file mode 100644 index 0000000..e17dd28 --- /dev/null +++ b/src/plugins/golang/tools/get-pattern.ts @@ -0,0 +1,30 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { DESIGN_PATTERNS } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "golang_get_pattern", + "Get a Go design pattern with idiomatic code", + { + name: z.string().describe("Pattern name (e.g. 'functional-options', 'worker-pool', 'pipeline', 'middleware-decorator', 'consumer-side-interface', 'strategy', 'adapter')"), + }, + async ({ name }) => { + const pattern = DESIGN_PATTERNS.find((p) => p.name.toLowerCase() === name.toLowerCase()); + if (!pattern) { + return { + content: [{ type: "text", text: `Pattern "${name}" not found.\n\nAvailable: ${DESIGN_PATTERNS.map((p) => p.name).join(", ")}` }], + isError: true, + }; + } + + let text = `# ${pattern.name} [${pattern.category}]\n\n`; + if (pattern.oopEquivalent) text += `*OOP equivalent: ${pattern.oopEquivalent}*\n\n`; + text += `**Go approach:** ${pattern.goApproach}\n\n`; + text += `**When to use:** ${pattern.when}\n\n`; + text += `## Code\n\`\`\`go\n${pattern.code}\n\`\`\`\n`; + if (pattern.antiPattern) text += `\n## Anti-pattern\n\`\`\`go\n${pattern.antiPattern}\n\`\`\`\n`; + return { content: [{ type: "text", text }] }; + } + ); +} diff --git a/src/plugins/golang/tools/get-practice.ts b/src/plugins/golang/tools/get-practice.ts new file mode 100644 index 0000000..79c44d4 --- /dev/null +++ b/src/plugins/golang/tools/get-practice.ts @@ -0,0 +1,29 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { BEST_PRACTICES } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "golang_get_practice", + "Get a Go best practice with good/bad code examples", + { + name: z.string().describe("Practice name (e.g. 'error-wrapping', 'goroutine-lifecycle', 'crypto-rand', 'table-driven-tests', 'thin-handlers')"), + }, + async ({ name }) => { + const practice = BEST_PRACTICES.find((p) => p.name.toLowerCase() === name.toLowerCase()); + if (!practice) { + return { + content: [{ type: "text", text: `Practice "${name}" not found.\n\nAvailable: ${BEST_PRACTICES.map((p) => p.name).join(", ")}` }], + isError: true, + }; + } + + let text = `# ${practice.name} [${practice.topic}] โ€” ${practice.priority}\n\n`; + text += `**Rule:** ${practice.rule}\n\n`; + text += `**Why:** ${practice.reason}\n\n`; + if (practice.good) text += `## โœ… Good\n\`\`\`go\n${practice.good}\n\`\`\`\n\n`; + if (practice.bad) text += `## โŒ Bad\n\`\`\`go\n${practice.bad}\n\`\`\`\n`; + return { content: [{ type: "text", text }] }; + } + ); +} diff --git a/src/plugins/golang/tools/list-patterns.ts b/src/plugins/golang/tools/list-patterns.ts new file mode 100644 index 0000000..a56bb64 --- /dev/null +++ b/src/plugins/golang/tools/list-patterns.ts @@ -0,0 +1,36 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { DESIGN_PATTERNS, getPatternsByCategory } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "golang_list_patterns", + "List Go design patterns by category (creational, structural, behavioral, concurrency)", + { + category: z.enum(["all", "creational", "structural", "behavioral", "concurrency"]).optional(), + }, + async ({ category }) => { + const list = category && category !== "all" + ? getPatternsByCategory(category) + : DESIGN_PATTERNS; + + const grouped: Record = {}; + for (const p of list) { + if (!grouped[p.category]) grouped[p.category] = []; + grouped[p.category].push(p); + } + + let text = "# Go Design Patterns\n\nGo uses composition + interfaces, not inheritance.\n\n"; + for (const [cat, items] of Object.entries(grouped)) { + text += `## ${cat}\n`; + for (const p of items) { + text += `- **${p.name}** โ€” ${p.goApproach.split(".")[0]}`; + if (p.oopEquivalent) text += ` *(OOP: ${p.oopEquivalent})*`; + text += "\n"; + } + text += "\n"; + } + return { content: [{ type: "text", text }] }; + } + ); +} diff --git a/src/plugins/golang/tools/list-practices.ts b/src/plugins/golang/tools/list-practices.ts new file mode 100644 index 0000000..dddc5bc --- /dev/null +++ b/src/plugins/golang/tools/list-practices.ts @@ -0,0 +1,33 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { BEST_PRACTICES, TOPICS, getPracticesByTopic } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "golang_list_practices", + "List Go best practices by topic and priority (P0=critical, P1=standard)", + { + topic: z.enum(["all", ...TOPICS]).optional(), + priority: z.enum(["P0", "P1"]).optional(), + }, + async ({ topic, priority }) => { + let list = topic && topic !== "all" ? getPracticesByTopic(topic as any) : BEST_PRACTICES; + if (priority) list = list.filter((p) => p.priority === priority); + + const grouped: Record = {}; + for (const p of list) { + if (!grouped[p.topic]) grouped[p.topic] = []; + grouped[p.topic].push(p); + } + + let text = "# Go Best Practices\n\n**P0** = critical (bugs/security if violated). **P1** = standard (maintainability).\n\n"; + for (const [t, items] of Object.entries(grouped)) { + text += `## ${t}\n`; + for (const p of items) text += `- [${p.priority}] **${p.name}** โ€” ${p.rule}\n`; + text += "\n"; + } + text += `\n**Total:** ${list.length} practices`; + return { content: [{ type: "text", text }] }; + } + ); +} diff --git a/src/plugins/golang/tools/search-docs.ts b/src/plugins/golang/tools/search-docs.ts new file mode 100644 index 0000000..356ae9a --- /dev/null +++ b/src/plugins/golang/tools/search-docs.ts @@ -0,0 +1,31 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { searchAll } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "golang_search_docs", + "Search Go best practices and design patterns by keyword", + { + query: z.string().describe("Search query (e.g. 'error', 'goroutine', 'interface', 'testing', 'context', 'security')"), + }, + async ({ query }) => { + const { practices, patterns } = searchAll(query); + if (!practices.length && !patterns.length) { + return { content: [{ type: "text", text: `No results for "${query}". Try: error, goroutine, interface, context, security, testing, concurrency` }] }; + } + + let text = `# Go Search: "${query}"\n\n`; + if (practices.length) { + text += `## Best Practices (${practices.length})\n`; + for (const p of practices) text += `- [${p.priority}] **${p.name}** [${p.topic}] โ€” ${p.rule}\n`; + text += "\n"; + } + if (patterns.length) { + text += `## Design Patterns (${patterns.length})\n`; + for (const p of patterns) text += `- **${p.name}** [${p.category}] โ€” ${p.goApproach.split(".")[0]}\n`; + } + return { content: [{ type: "text", text }] }; + } + ); +} diff --git a/src/plugins/lenis/data.ts b/src/plugins/lenis/data.ts new file mode 100644 index 0000000..255ffa7 --- /dev/null +++ b/src/plugins/lenis/data.ts @@ -0,0 +1,463 @@ +import { snippet } from "./loader.js"; + +export interface ApiEntry { + name: string; + kind: ApiKind; + description: string; + importPath: string; + props?: PropEntry[]; + returns?: string; + usage: string; + examples: Example[]; + tips?: string[]; + relatedApis?: string[]; +} + +export interface PropEntry { + name: string; + type: string; + description: string; + default?: string; +} + +export interface Example { + title: string; + code: string; + description?: string; + category: string; +} + +export interface Pattern { + name: string; + description: string; + code: string; + tips?: string[]; +} + +export const API_KINDS = ["component", "hook", "type", "utility"] as const; +export type ApiKind = (typeof API_KINDS)[number]; + +export function capitalize(s: string): string { + return s.charAt(0).toUpperCase() + s.slice(1); +} + +export function formatApiReference(api: ApiEntry): string { + let out = `# ${api.name}\n\n`; + out += `**Kind:** ${api.kind}\n`; + out += `**Import:** \`${api.importPath}\`\n\n`; + out += `${api.description}\n\n`; + + if (api.returns) { + out += `**Returns:** \`${api.returns}\`\n\n`; + } + + out += `## Usage\n\n\`\`\`tsx\n${api.usage}\n\`\`\`\n\n`; + + if (api.props && api.props.length > 0) { + out += `## Props / Options\n\n`; + for (const p of api.props) { + out += `- **${p.name}**: \`${p.type}\`${p.default ? ` (default: \`${p.default}\`)` : ""} โ€” ${p.description}\n`; + } + out += "\n"; + } + + if (api.examples.length > 0) { + out += `## Examples\n\n`; + for (const ex of api.examples) { + out += `### ${ex.title}\n`; + if (ex.description) out += `${ex.description}\n\n`; + out += `\`\`\`tsx\n${ex.code}\n\`\`\`\n\n`; + } + } + + if (api.tips && api.tips.length > 0) { + out += `## Tips\n\n`; + for (const tip of api.tips) out += `- ${tip}\n`; + out += "\n"; + } + + if (api.relatedApis && api.relatedApis.length > 0) { + out += `**Related:** ${api.relatedApis.join(", ")}\n`; + } + + return out; +} + +export interface SearchResult { + api: ApiEntry; + matchingExamples: Example[]; +} + +export function searchApis(query: string): SearchResult[] { + const q = query.toLowerCase(); + const results: SearchResult[] = []; + for (const api of ALL_APIS) { + const nameMatch = api.name.toLowerCase().includes(q); + const descMatch = api.description.toLowerCase().includes(q); + const matchingExamples = api.examples.filter( + (ex) => + ex.title.toLowerCase().includes(q) || + ex.category.toLowerCase().includes(q) || + ex.code.toLowerCase().includes(q), + ); + if (nameMatch || descMatch || matchingExamples.length > 0) { + results.push({ api, matchingExamples }); + } + } + return results; +} + +export function getApiByName(name: string): ApiEntry | undefined { + const n = name.toLowerCase(); + return ALL_APIS.find((a) => a.name.toLowerCase() === n); +} + +// --------------------------------------------------------------------------- +// COMPONENTS +// --------------------------------------------------------------------------- + +const reactLenis: ApiEntry = { + name: "ReactLenis", + kind: "component", + description: + "The core Lenis provider component. Wraps your app (or a scroll container) and sets up the smooth scroll context. When used with root={true}, it attaches to the window scroll. Without root, it creates a container-scoped scroller.", + importPath: 'import { ReactLenis } from "lenis/react"', + props: [ + { + name: "root", + type: "boolean", + description: "Attach Lenis to the window/document scroll instead of a container element. Use this in your root layout.", + default: "false", + }, + { + name: "options", + type: "LenisOptions", + description: "Configuration object passed directly to the Lenis instance. See LenisOptions for all available fields.", + }, + { + name: "ref", + type: "React.Ref", + description: "Ref forwarded to expose the Lenis instance and wrapper DOM element. Shape: { lenis: Lenis | undefined, wrapper: HTMLElement }.", + }, + { + name: "autoRaf", + type: "boolean", + description: "Automatically drive Lenis via its internal requestAnimationFrame loop. Set false when integrating with GSAP ticker or Framer Motion's frame scheduler.", + default: "true", + }, + { + name: "className", + type: "string", + description: "CSS class applied to the outer wrapper div (only relevant when root={false}).", + }, + { + name: "children", + type: "React.ReactNode", + description: "The content to be scroll-wrapped.", + }, + ], + usage: snippet("usage/react-lenis.md"), + examples: [ + { + title: "Root layout setup", + category: "setup", + code: snippet("examples/react-lenis-root.md"), + }, + { + title: "Container scroll (non-root)", + category: "setup", + code: snippet("examples/react-lenis-container.md"), + }, + { + title: "Accessing the Lenis instance via ref", + category: "setup", + code: snippet("examples/react-lenis-ref.md"), + }, + ], + tips: [ + "Always import 'lenis/dist/lenis.css' โ€” without it, the scroll container loses its overflow styles and the scroller breaks visibly.", + "In Next.js App Router, ReactLenis must be in a 'use client' file or wrapped in a client component โ€” it uses refs and effects internally.", + "root={true} delegates to the window scroll. root={false} (default) creates a div-based scroller โ€” you need to set height/overflow on the parent.", + "autoRaf defaults to true. If you integrate GSAP or Framer Motion, set autoRaf={false} and tick manually to avoid double-frame rendering.", + ], + relatedApis: ["useLenis", "LenisRef", "LenisOptions"], +}; + +// --------------------------------------------------------------------------- +// HOOKS +// --------------------------------------------------------------------------- + +const useLenis: ApiEntry = { + name: "useLenis", + kind: "hook", + description: + "Returns the nearest Lenis instance from context. Accepts an optional scroll callback that fires on every scroll frame with the Lenis instance as argument. Must be used inside a ReactLenis provider; returns undefined if no provider is found.", + importPath: 'import { useLenis } from "lenis/react"', + props: [ + { + name: "callback", + type: "(lenis: Lenis) => void", + description: "Optional. Fired on every scroll frame. Use for scroll-linked animations or reading scroll position.", + }, + { + name: "deps", + type: "React.DependencyList", + description: "Optional. Dependency array for the scroll callback, like useEffect deps.", + default: "[]", + }, + { + name: "priority", + type: "number", + description: "Optional. Callback execution order when multiple useLenis calls are active. Lower = earlier.", + default: "0", + }, + ], + returns: "Lenis | undefined", + usage: snippet("usage/use-lenis.md"), + examples: [ + { + title: "Scroll to element", + category: "navigation", + code: snippet("examples/use-lenis-scroll-to.md"), + }, + { + title: "Scroll progress tracker", + category: "scroll", + code: snippet("examples/use-lenis-progress.md"), + }, + { + title: "Scroll-linked parallax", + category: "scroll", + code: snippet("examples/use-lenis-parallax.md"), + }, + { + title: "Stop/start scrolling", + category: "control", + code: snippet("examples/use-lenis-modal.md"), + }, + ], + tips: [ + "useLenis() outside a ReactLenis provider returns undefined โ€” always guard with optional chaining: lenis?.scrollTo(...).", + "The scroll callback fires every frame when scrolling โ€” avoid expensive operations or setState directly inside it. Use refs for DOM mutation or debounce for state.", + "lenis.scrollTo() accepts a CSS selector string, HTMLElement, or number (pixel offset). Passing 0 scrolls to top.", + "lenis.stop() and lenis.start() are the correct way to lock scroll (e.g. modals). Do NOT manipulate overflow on body manually.", + ], + relatedApis: ["ReactLenis", "LenisRef"], +}; + +// --------------------------------------------------------------------------- +// TYPES +// --------------------------------------------------------------------------- + +const lenisRef: ApiEntry = { + name: "LenisRef", + kind: "type", + description: + "The shape of the ref forwarded by ReactLenis. Use this when you need to imperatively access the Lenis instance or its wrapper DOM element outside of the useLenis hook.", + importPath: 'import type { LenisRef } from "lenis/react"', + props: [ + { + name: "lenis", + type: "Lenis | undefined", + description: "The active Lenis instance. May be undefined before the component mounts.", + }, + { + name: "wrapper", + type: "HTMLElement", + description: "The outer wrapper DOM element that Lenis is attached to.", + }, + ], + usage: snippet("usage/lenis-ref.md"), + examples: [ + { + title: "Imperative scroll from outside context", + category: "setup", + code: snippet("examples/lenis-ref-imperative.md"), + }, + ], + tips: [ + "LenisRef.lenis is undefined until ReactLenis mounts โ€” always check for its existence before calling methods.", + "Prefer useLenis() over LenisRef for components inside the provider tree. LenisRef is mainly for components that render the provider itself.", + ], + relatedApis: ["ReactLenis", "useLenis"], +}; + +const lenisOptions: ApiEntry = { + name: "LenisOptions", + kind: "type", + description: + "Configuration object passed to ReactLenis via the options prop. Controls scroll physics, orientation, and behavior. All fields are optional.", + importPath: 'import type { LenisOptions } from "lenis"', + props: [ + { + name: "lerp", + type: "number", + description: "Linear interpolation factor. Controls how quickly scroll catches up to the target. Lower = smoother but slower.", + default: "0.1", + }, + { + name: "duration", + type: "number", + description: "Duration (in seconds) for scroll animations triggered programmatically. Overrides lerp when set.", + }, + { + name: "easing", + type: "(t: number) => number", + description: "Easing function for programmatic scrolls. Receives t in [0,1] and returns eased value.", + default: "(t) => Math.min(1, 1.001 - Math.pow(2, -10 * t))", + }, + { + name: "orientation", + type: "'vertical' | 'horizontal'", + description: "Scroll axis.", + default: "'vertical'", + }, + { + name: "gestureOrientation", + type: "'vertical' | 'horizontal' | 'both'", + description: "Which gesture axes to capture for scroll.", + default: "'vertical'", + }, + { + name: "smoothWheel", + type: "boolean", + description: "Enable smooth scrolling for mouse wheel events.", + default: "true", + }, + { + name: "smoothTouch", + type: "boolean", + description: "Enable smooth scrolling for touch (mobile) events. Can feel laggy on low-end iOS devices.", + default: "false", + }, + { + name: "touchMultiplier", + type: "number", + description: "Multiplier applied to touch scroll delta.", + default: "2", + }, + { + name: "infinite", + type: "boolean", + description: "Enable infinite scroll โ€” wraps around when reaching start or end.", + default: "false", + }, + { + name: "autoRaf", + type: "boolean", + description: "Automatically run Lenis via its internal RAF loop. Set false when integrating with GSAP ticker or Framer Motion.", + default: "true", + }, + { + name: "wrapper", + type: "HTMLElement | Window", + description: "The scroll container element. Defaults to window when root={true}.", + }, + { + name: "content", + type: "HTMLElement", + description: "The inner content element. Used for non-root (container) scroll.", + }, + ], + usage: snippet("usage/lenis-options.md"), + examples: [ + { + title: "Tuned scroll feel for a marketing site", + category: "options", + code: snippet("options/tuned-marketing.md"), + }, + { + title: "Horizontal scroll options", + category: "options", + code: snippet("options/horizontal.md"), + }, + ], + tips: [ + "lerp: 0.1 is the default โ€” values closer to 0 are smoother but feel slower. 0.05-0.15 is the practical range.", + "Do NOT set smoothTouch: true on production sites targeting iOS โ€” it introduces perceivable input lag.", + "duration and lerp are mutually exclusive; duration-based scrolling uses an easing function while lerp uses linear interpolation per frame.", + "infinite: true works best with a content height that is a multiple of the viewport โ€” otherwise it jumps.", + ], + relatedApis: ["ReactLenis"], +}; + +// --------------------------------------------------------------------------- +// ALL APIS EXPORT +// --------------------------------------------------------------------------- + +export const ALL_APIS: ApiEntry[] = [reactLenis, useLenis, lenisRef, lenisOptions]; + +// --------------------------------------------------------------------------- +// PATTERNS +// --------------------------------------------------------------------------- + +export const PATTERNS: Record = { + "full-page": { + name: "full-page", + description: "Standard root layout setup โ€” ReactLenis wraps the entire app for full-page smooth scrolling.", + code: snippet("patterns/full-page.md"), + tips: [ + "root={true} is required for full-page scroll โ€” without it, Lenis creates an overflow:hidden container.", + "The CSS import is mandatory โ€” skip it and the layout breaks.", + ], + }, + "next-js": { + name: "next-js", + description: "Next.js App Router pattern using a dedicated SmoothScrollProvider client component to wrap the layout.", + code: snippet("patterns/next-js.md"), + tips: [ + "Keep app/layout.tsx as a Server Component โ€” extract the 'use client' directive into SmoothScrollProvider.", + "This preserves RSC boundaries and avoids unnecessarily client-rendering the entire layout.", + ], + }, + "gsap-integration": { + name: "gsap-integration", + description: "Integrate Lenis with GSAP ScrollTrigger by disabling autoRaf and driving Lenis from GSAP's ticker.", + code: snippet("patterns/gsap-integration.md"), + tips: [ + "autoRaf: false is required โ€” if GSAP and Lenis both run their own RAF loops, scroll updates fire twice per frame causing desync.", + "gsap.ticker.lagSmoothing(0) prevents GSAP from adjusting delta time which would cause Lenis to stutter after tab switches.", + "lenis.raf() expects milliseconds โ€” multiply GSAP time (seconds) by 1000.", + ], + }, + "framer-motion-integration": { + name: "framer-motion-integration", + description: "Integrate Lenis with Framer Motion by disabling autoRaf and syncing via frame.update.", + code: snippet("patterns/framer-motion-integration.md"), + tips: [ + "Use frame from 'motion' (not 'framer-motion') โ€” this is the Framer Motion v11+ low-level scheduler.", + "frame.update(fn, true) schedules the update to run on every frame. The second argument (true) enables loop mode.", + "autoRaf: false is mandatory โ€” same reasoning as with GSAP, prevent double-ticking.", + ], + }, + "custom-container": { + name: "custom-container", + description: "Scoped scroll container using wrapper and content refs for non-window smooth scroll.", + code: snippet("patterns/custom-container.md"), + tips: [ + "The wrapper element needs overflow: hidden and a fixed height for container scroll to work.", + "The content element is the scrollable inner div โ€” it should grow naturally with its children.", + "This pattern is useful for split-panel layouts, side drawers, or modal scroll areas.", + ], + }, + "accessibility": { + name: "accessibility", + description: "Respect prefers-reduced-motion by disabling smooth scrolling for users who prefer it.", + code: snippet("patterns/accessibility.md"), + tips: [ + "Never force smooth scrolling on users who have opted out via prefers-reduced-motion.", + "When skipping ReactLenis, native scroll is used โ€” no polyfill needed.", + "For a hook-based approach, use the useReducedMotion hook from Framer Motion or write your own with a useEffect + matchMedia listener.", + ], + }, + "scroll-to-nav": { + name: "scroll-to-nav", + description: "Navigation link that uses lenis.scrollTo() for smooth in-page anchor navigation.", + code: snippet("patterns/scroll-to-nav.md"), + tips: [ + "offset compensates for sticky headers โ€” pass a negative value equal to the header height.", + "lenis.scrollTo() accepts a CSS selector ('#section'), HTMLElement, or pixel number.", + "For Next.js App Router, use usePathname to detect route changes and reset scroll position.", + ], + }, +}; diff --git a/src/plugins/lenis/index.ts b/src/plugins/lenis/index.ts new file mode 100644 index 0000000..e28b539 --- /dev/null +++ b/src/plugins/lenis/index.ts @@ -0,0 +1,22 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { Plugin } from "../../registry.js"; +import { register as listApis } from "./tools/list-apis.js"; +import { register as getApi } from "./tools/get-api.js"; +import { register as searchDocs } from "./tools/search-docs.js"; +import { register as getPattern } from "./tools/get-pattern.js"; +import { register as generateSetup } from "./tools/generate-setup.js"; +import { register as cheatsheet } from "./tools/cheatsheet.js"; + +function register(server: McpServer): void { + listApis(server); + getApi(server); + searchDocs(server); + getPattern(server); + generateSetup(server); + cheatsheet(server); +} + +export const lenisPlugin: Plugin = { + name: "lenis", + register, +}; diff --git a/src/plugins/lenis/loader.ts b/src/plugins/lenis/loader.ts new file mode 100644 index 0000000..78dc426 --- /dev/null +++ b/src/plugins/lenis/loader.ts @@ -0,0 +1,3 @@ +import { createSnippetLoader } from "../../shared/loader-factory.js"; + +export const snippet = createSnippetLoader("lenis"); diff --git a/src/plugins/lenis/tools/cheatsheet.ts b/src/plugins/lenis/tools/cheatsheet.ts new file mode 100644 index 0000000..38b170e --- /dev/null +++ b/src/plugins/lenis/tools/cheatsheet.ts @@ -0,0 +1,117 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; + +export function register(server: McpServer): void { + server.resource( + "lenis-cheatsheet", + "lenis://react/cheatsheet", + { description: "Lenis React quick reference cheatsheet", mimeType: "text/markdown" }, + async () => { + const text = `# Lenis React โ€” Cheatsheet + +## Install +\`\`\`bash +npm install lenis +\`\`\` + +## Required CSS +\`\`\`tsx +import "lenis/dist/lenis.css"; // NEVER skip this +\`\`\` + +## Import +\`\`\`tsx +import { ReactLenis, useLenis } from "lenis/react"; +import type { LenisRef, LenisOptions } from "lenis/react"; +\`\`\` + +## Quick Patterns + +### Basic full-page setup +\`\`\`tsx +// Must be 'use client' in Next.js + + {children} + +\`\`\` + +### Next.js App Router (keep layout.tsx as RSC) +\`\`\`tsx +// components/smooth-scroll-provider.tsx +"use client"; +export function SmoothScrollProvider({ children }) { + return {children}; +} +// app/layout.tsx (Server Component) +{children} +\`\`\` + +### useLenis โ€” scroll callback +\`\`\`tsx +useLenis(({ scroll, progress, velocity }) => { + // fires every frame while scrolling +}); +\`\`\` + +### useLenis โ€” scroll to element +\`\`\`tsx +const lenis = useLenis(); +lenis?.scrollTo("#section", { offset: -80, duration: 1.2 }); +\`\`\` + +### Stop / start scroll (e.g. modal open) +\`\`\`tsx +const lenis = useLenis(); +lenis?.stop(); // lock +lenis?.start(); // unlock +\`\`\` + +### GSAP integration (autoRaf: false) +\`\`\`tsx + + ... + +// In a child component: +const lenis = useLenis(); +useEffect(() => { + if (!lenis) return; + gsap.ticker.add((time) => lenis.raf(time * 1000)); + gsap.ticker.lagSmoothing(0); + lenis.on("scroll", ScrollTrigger.update); + return () => lenis.off("scroll", ScrollTrigger.update); +}, [lenis]); +\`\`\` + +### Framer Motion integration (autoRaf: false) +\`\`\`tsx +import { frame } from "motion"; +const lenis = useLenis(); +useEffect(() => { + if (!lenis) return; + const update = ({ timestamp }) => lenis.raf(timestamp); + frame.update(update, true); + return () => frame.cancel(update); +}, [lenis]); +\`\`\` + +## Key LenisOptions +| Option | Default | Notes | +|---|---|---| +| lerp | 0.1 | Smoothing factor. Lower = smoother. 0.05โ€“0.15 range | +| duration | โ€” | For programmatic scrolls. Overrides lerp | +| orientation | vertical | 'vertical' or 'horizontal' | +| smoothWheel | true | Mouse wheel smooth scroll | +| smoothTouch | false | Touch smooth scroll โ€” avoid on iOS | +| autoRaf | true | Set false for GSAP/Framer sync | +| infinite | false | Infinite loop scroll | + +## Common Pitfalls +- Missing \`import "lenis/dist/lenis.css"\` โ€” scroll breaks visually +- Using \`autoRaf: true\` with GSAP ticker โ€” causes desync / double frames +- Calling \`useLenis()\` outside \`\` โ€” returns undefined, crashes +- Missing \`"use client"\` in Next.js App Router โ€” Lenis uses refs and effects +- \`smoothTouch: true\` on iOS โ€” perceivable input lag on low-end devices +`; + return { contents: [{ uri: "lenis://react/cheatsheet", mimeType: "text/markdown", text }] }; + }, + ); +} diff --git a/src/plugins/lenis/tools/generate-setup.ts b/src/plugins/lenis/tools/generate-setup.ts new file mode 100644 index 0000000..fb57806 --- /dev/null +++ b/src/plugins/lenis/tools/generate-setup.ts @@ -0,0 +1,281 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; + +export function register(server: McpServer): void { + server.tool( + "lenis_generate_setup", + "Generate complete Lenis setup code from a natural-language description. Handles Next.js, GSAP, Framer Motion, basic React, and custom container scenarios.", + { + description: z + .string() + .describe( + "Describe your setup (e.g., 'next.js app router with gsap scrolltrigger', 'basic react spa', 'framer motion integration', 'next.js with accessibility support', 'horizontal scroll container')", + ), + }, + async ({ description }) => { + const desc = description.toLowerCase(); + + const isNextJs = desc.includes("next") || desc.includes("app router") || desc.includes("app dir"); + const isGSAP = desc.includes("gsap") || desc.includes("scroll trigger") || desc.includes("scrolltrigger"); + const isFramer = desc.includes("framer") || desc.includes("motion"); + const isHorizontal = desc.includes("horizontal"); + const isAccessibility = desc.includes("a11y") || desc.includes("accessibility") || desc.includes("reduced motion"); + const isContainer = desc.includes("container") || desc.includes("panel") || desc.includes("section"); + + let code = ""; + let notes = ""; + + if (isGSAP && isNextJs) { + code = `// components/lenis-gsap-provider.tsx +"use client"; + +import { useEffect } from "react"; +import { ReactLenis, useLenis } from "lenis/react"; +import "lenis/dist/lenis.css"; +import gsap from "gsap"; +import ScrollTrigger from "gsap/ScrollTrigger"; + +gsap.registerPlugin(ScrollTrigger); + +function GSAPSync() { + const lenis = useLenis(); + + useEffect(() => { + if (!lenis) return; + gsap.ticker.add((time) => lenis.raf(time * 1000)); + gsap.ticker.lagSmoothing(0); + lenis.on("scroll", ScrollTrigger.update); + return () => lenis.off("scroll", ScrollTrigger.update); + }, [lenis]); + + return null; +} + +export function LenisGSAPProvider({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + ); +} + +// app/layout.tsx +import { LenisGSAPProvider } from "@/components/lenis-gsap-provider"; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + + ); +}`; + notes = "- autoRaf: false is critical โ€” prevents Lenis and GSAP from each running their own RAF loop\n- gsap.ticker.lagSmoothing(0) prevents stutter after tab switches\n- lenis.raf() expects ms โ€” multiply GSAP seconds by 1000"; + } else if (isGSAP) { + code = `// lenis-gsap-setup.tsx +"use client"; + +import { useEffect } from "react"; +import { ReactLenis, useLenis } from "lenis/react"; +import "lenis/dist/lenis.css"; +import gsap from "gsap"; +import ScrollTrigger from "gsap/ScrollTrigger"; + +gsap.registerPlugin(ScrollTrigger); + +function GSAPSync() { + const lenis = useLenis(); + useEffect(() => { + if (!lenis) return; + gsap.ticker.add((time) => lenis.raf(time * 1000)); + gsap.ticker.lagSmoothing(0); + lenis.on("scroll", ScrollTrigger.update); + return () => lenis.off("scroll", ScrollTrigger.update); + }, [lenis]); + return null; +} + +export function App() { + return ( + + + {/* your app */} + + ); +}`; + notes = "- Set autoRaf: false to prevent double-frame rendering with GSAP\n- Multiply GSAP ticker time (seconds) by 1000 for lenis.raf()"; + } else if (isFramer) { + code = `// components/lenis-framer-provider.tsx +"use client"; + +import { useEffect } from "react"; +import { ReactLenis, useLenis } from "lenis/react"; +import "lenis/dist/lenis.css"; +import { frame } from "motion"; + +function FramerSync() { + const lenis = useLenis(); + useEffect(() => { + if (!lenis) return; + const update = ({ timestamp }: { timestamp: number }) => lenis.raf(timestamp); + frame.update(update, true); + return () => frame.cancel(update); + }, [lenis]); + return null; +} + +export function LenisFramerProvider({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + ); +}${isNextJs ? ` + +// app/layout.tsx +import { LenisFramerProvider } from "@/components/lenis-framer-provider"; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + + ); +}` : ""}`; + notes = "- Import frame from 'motion' (not 'framer-motion') โ€” this is the v11+ low-level scheduler\n- frame.update(fn, true) loops on every frame\n- autoRaf: false prevents duplicate ticking"; + } else if (isHorizontal) { + code = `// components/horizontal-scroll.tsx +"use client"; + +import { ReactLenis } from "lenis/react"; +import "lenis/dist/lenis.css"; + +export function HorizontalScroll({ children }: { children: React.ReactNode }) { + return ( + +
+ {children} +
+
+ ); +}`; + notes = "- orientation: 'horizontal' tells Lenis which axis to scroll\n- gestureOrientation: 'both' captures both wheel and touch gestures\n- The inner div should use flex-nowrap to allow horizontal overflow"; + } else if (isAccessibility) { + code = `// components/smooth-scroll-provider.tsx +"use client"; + +import { ReactLenis } from "lenis/react"; +import "lenis/dist/lenis.css"; + +function useReducedMotion(): boolean { + if (typeof window === "undefined") return false; + return window.matchMedia("(prefers-reduced-motion: reduce)").matches; +} + +export function SmoothScrollProvider({ children }: { children: React.ReactNode }) { + const reduced = useReducedMotion(); + + if (reduced) return <>{children}; + + return ( + + {children} + + ); +}${isNextJs ? ` + +// app/layout.tsx +import { SmoothScrollProvider } from "@/components/smooth-scroll-provider"; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + + ); +}` : ""}`; + notes = "- Skipping ReactLenis falls back to native scroll โ€” no extra setup needed\n- WCAG 2.1 SC 2.3.3 (AAA): smooth scrolling must respect prefers-reduced-motion"; + } else if (isNextJs) { + code = `// components/smooth-scroll-provider.tsx +"use client"; + +import { ReactLenis } from "lenis/react"; +import "lenis/dist/lenis.css"; + +export function SmoothScrollProvider({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} + +// app/layout.tsx +import { SmoothScrollProvider } from "@/components/smooth-scroll-provider"; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + + {children} + + + + ); +}`; + notes = "- Keep app/layout.tsx as a Server Component โ€” extract 'use client' into SmoothScrollProvider\n- This preserves RSC boundaries and server rendering for child pages\n- The CSS import in the client component is required"; + } else if (isContainer) { + code = `// components/scroll-panel.tsx +"use client"; + +import { useRef } from "react"; +import { ReactLenis } from "lenis/react"; +import "lenis/dist/lenis.css"; + +export function ScrollPanel({ children }: { children: React.ReactNode }) { + return ( + +
+ {children} +
+
+ ); +}`; + notes = "- ReactLenis without root creates a scoped container scroll\n- The wrapper needs a fixed height for the scroll to work\n- Use wrapper/content refs via LenisOptions for full control"; + } else { + code = `// App.tsx โ€” basic React SPA setup +import { ReactLenis } from "lenis/react"; +import "lenis/dist/lenis.css"; + +export function App() { + return ( + +
+ {/* your content */} +
+
+ ); +}`; + notes = "- root={true} attaches Lenis to the window scroll\n- lerp: 0.1 is the recommended starting value โ€” tune between 0.05 (smoother) and 0.15 (snappier)\n- The CSS import is required โ€” it sets up the scroll container styles"; + } + + const text = `# Lenis Setup: ${description}\n\n\`\`\`tsx\n${code}\n\`\`\`\n\n## Notes\n\n${notes}`; + return { content: [{ type: "text", text }] }; + }, + ); +} diff --git a/src/plugins/lenis/tools/get-api.ts b/src/plugins/lenis/tools/get-api.ts new file mode 100644 index 0000000..6b5e850 --- /dev/null +++ b/src/plugins/lenis/tools/get-api.ts @@ -0,0 +1,29 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { ALL_APIS, searchApis, getApiByName, formatApiReference } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "lenis_get_api", + "Get detailed API reference for a specific Lenis API โ€” props, options, usage examples, and tips.", + { + name: z.string().describe("API name (e.g., 'ReactLenis', 'useLenis', 'LenisRef', 'LenisOptions')"), + }, + async ({ name }) => { + const api = getApiByName(name); + if (!api) { + const suggestions = searchApis(name).map((r) => r.api.name); + return { + content: [ + { + type: "text", + text: `API "${name}" not found.${suggestions.length ? ` Did you mean: ${suggestions.join(", ")}?` : ""}\n\nAvailable APIs: ${ALL_APIS.map((a) => a.name).join(", ")}`, + }, + ], + isError: true, + }; + } + return { content: [{ type: "text", text: formatApiReference(api) }] }; + }, + ); +} diff --git a/src/plugins/lenis/tools/get-pattern.ts b/src/plugins/lenis/tools/get-pattern.ts new file mode 100644 index 0000000..4c706f5 --- /dev/null +++ b/src/plugins/lenis/tools/get-pattern.ts @@ -0,0 +1,46 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { PATTERNS } from "../data.js"; + +const PATTERN_NAMES = Object.keys(PATTERNS) as [string, ...string[]]; + +export function register(server: McpServer): void { + server.tool( + "lenis_get_pattern", + "Get a complete Lenis integration pattern with full production-ready code. Covers Next.js setup, GSAP integration, Framer Motion sync, custom containers, accessibility, and navigation.", + { + name: z + .enum(PATTERN_NAMES) + .describe( + "Pattern name: full-page | next-js | gsap-integration | framer-motion-integration | custom-container | accessibility | scroll-to-nav", + ), + }, + async ({ name }) => { + const pattern = PATTERNS[name]; + if (!pattern) { + return { + content: [ + { + type: "text", + text: `Pattern "${name}" not found.\n\nAvailable patterns:\n${Object.entries(PATTERNS) + .map(([k, p]) => `- ${k}: ${p.description}`) + .join("\n")}`, + }, + ], + isError: true, + }; + } + + let text = `# Lenis Pattern: ${pattern.name}\n\n`; + text += `${pattern.description}\n\n`; + text += `## Code\n\n\`\`\`tsx\n${pattern.code}\n\`\`\`\n\n`; + + if (pattern.tips && pattern.tips.length > 0) { + text += `## Key Notes\n\n`; + for (const tip of pattern.tips) text += `- ${tip}\n`; + } + + return { content: [{ type: "text", text }] }; + }, + ); +} diff --git a/src/plugins/lenis/tools/list-apis.ts b/src/plugins/lenis/tools/list-apis.ts new file mode 100644 index 0000000..20f1b2e --- /dev/null +++ b/src/plugins/lenis/tools/list-apis.ts @@ -0,0 +1,34 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { ALL_APIS, API_KINDS, capitalize } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "lenis_list_apis", + "List all Lenis smooth scroll APIs โ€” ReactLenis component, useLenis hook, LenisRef and LenisOptions types.", + { + kind: z.enum(["all", ...API_KINDS]).optional().describe("Filter by API kind: component, hook, type, utility"), + }, + async ({ kind }) => { + const apis = kind && kind !== "all" + ? ALL_APIS.filter((a) => a.kind === kind) + : ALL_APIS; + + const grouped: Record = {}; + for (const api of apis) { + const k = api.kind; + if (!grouped[k]) grouped[k] = []; + grouped[k].push(`${api.name} โ€” ${api.description.split(".")[0]}`); + } + + let text = "# Lenis React โ€” API Reference\n\n"; + text += `Import from \`"lenis/react"\` (types from \`"lenis"\`)\n\n`; + for (const [k, items] of Object.entries(grouped)) { + text += `## ${capitalize(k)}s\n`; + for (const item of items) text += `- ${item}\n`; + text += "\n"; + } + return { content: [{ type: "text", text }] }; + }, + ); +} diff --git a/src/plugins/lenis/tools/search-docs.ts b/src/plugins/lenis/tools/search-docs.ts new file mode 100644 index 0000000..018e20f --- /dev/null +++ b/src/plugins/lenis/tools/search-docs.ts @@ -0,0 +1,61 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { searchApis, PATTERNS } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "lenis_search_docs", + "Search Lenis documentation by keyword. Searches API names, descriptions, code examples, and integration patterns.", + { + query: z.string().describe("Search query (e.g., 'gsap', 'smooth scroll', 'scrollTo', 'next.js', 'options', 'ref')"), + }, + async ({ query }) => { + const q = query.toLowerCase(); + const apiResults = searchApis(query); + + const matchingPatterns = Object.entries(PATTERNS).filter( + ([key, pattern]) => + key.includes(q) || + pattern.name.toLowerCase().includes(q) || + pattern.description.toLowerCase().includes(q) || + pattern.code.toLowerCase().includes(q), + ); + + if (apiResults.length === 0 && matchingPatterns.length === 0) { + return { + content: [ + { + type: "text", + text: `No results for "${query}".\n\nAvailable APIs: ReactLenis, useLenis, LenisRef, LenisOptions\nAvailable patterns: ${Object.keys(PATTERNS).join(", ")}`, + }, + ], + }; + } + + let text = `# Lenis search: "${query}"\n\n`; + + if (apiResults.length > 0) { + text += `## APIs (${apiResults.length})\n\n`; + for (const { api, matchingExamples } of apiResults) { + text += `### ${api.name} (${api.kind})\n${api.description}\n**Import:** \`${api.importPath}\`\n\n`; + if (matchingExamples.length > 0) { + text += "**Relevant examples:**\n"; + for (const ex of matchingExamples) { + text += `#### ${ex.title}\n\`\`\`tsx\n${ex.code}\n\`\`\`\n\n`; + } + } + text += "---\n\n"; + } + } + + if (matchingPatterns.length > 0) { + text += `## Patterns (${matchingPatterns.length})\n\n`; + for (const [key, pattern] of matchingPatterns) { + text += `### ${pattern.name}\n${pattern.description}\n\nUse \`lenis_get_pattern\` with name="${key}" for full code.\n\n`; + } + } + + return { content: [{ type: "text", text }] }; + }, + ); +} diff --git a/src/plugins/react/data.ts b/src/plugins/react/data.ts new file mode 100644 index 0000000..dcfc36c --- /dev/null +++ b/src/plugins/react/data.ts @@ -0,0 +1,186 @@ +import { snippet } from "./loader.js"; + +export interface Pattern { + name: string; + category: PatternCategory; + description: string; + when: string; + code: string; + antiPattern?: string; + tips?: string[]; +} + +export interface Constraint { + name: string; + rule: string; + reason: string; + example?: string; +} + +export const PATTERN_CATEGORIES = [ + "rendering", "state", "data-fetching", "routing", + "performance", "testing", "architecture", +] as const; +export type PatternCategory = (typeof PATTERN_CATEGORIES)[number]; + +// --------------------------------------------------------------------------- +// PATTERNS +// --------------------------------------------------------------------------- + +export const PATTERNS: Pattern[] = [ + { + name: "rsc-default", + category: "rendering", + description: "Server Components are the default. Use 'use client' only when interactivity is required.", + when: "Every new component. Decide server vs client first.", + code: snippet("patterns/rsc-default.md"), + antiPattern: snippet("patterns/rsc-anti.md"), + tips: [ + "SEO-critical content โ†’ RSC/SSR/SSG/ISR", + "Non-SEO + interactive โ†’ Client Component", + "Fetching data โ†’ always RSC unless client-side only", + ], + }, + { + name: "state-hierarchy", + category: "state", + description: "Strict state placement order: URL โ†’ server โ†’ local โ†’ Zustand โ†’ Context (injection only).", + when: "Deciding where to put any new piece of state.", + code: snippet("patterns/state-hierarchy.md"), + antiPattern: snippet("patterns/state-hierarchy-anti.md"), + tips: [ + "Ask: can this live in the URL? If yes โ†’ URL state", + "Context is for injection (stable values), not reactive state", + "Redux is banned โ€” Zustand only for shared client state", + ], + }, + { + name: "data-fetching-rsc", + category: "data-fetching", + description: "Fetch in Server Components. Never useEffect for data fetching.", + when: "Any data that can be fetched at request time.", + code: snippet("patterns/data-fetching-rsc.md"), + antiPattern: snippet("patterns/data-fetching-anti.md"), + }, + { + name: "zustand-store", + category: "state", + description: "Zustand for shared client state. Slice pattern for large stores.", + when: "State shared across multiple client components that isn't URL or server state.", + code: snippet("patterns/zustand-store.md"), + tips: ["Never use Redux", "Slice selectors prevent unnecessary re-renders", "persist middleware for auth/settings"], + }, + { + name: "suspense-boundary", + category: "rendering", + description: "Suspense for async RSC children. Error boundaries for error states.", + when: "Any page with async data in RSC children.", + code: snippet("patterns/suspense-boundary.md"), + tips: ["Skeleton over spinner for layout-stable loading", "ErrorBoundary wraps Suspense"], + }, + { + name: "nextjs-metadata", + category: "routing", + description: "generateMetadata for dynamic SEO in Next.js App Router.", + when: "Every page that needs SEO (title, description, OG).", + code: snippet("patterns/nextjs-metadata.md"), + }, + { + name: "composition-pattern", + category: "architecture", + description: "Avoid prop drilling >2 levels. Use composition or context injection.", + when: "Props being passed through >2 intermediate components.", + code: snippet("patterns/composition-pattern.md"), + antiPattern: snippet("patterns/composition-anti.md"), + }, + { + name: "component-template", + category: "architecture", + description: "Standard component scaffold with TypeScript + Tailwind + shadcn/ui + lucide-react.", + when: "Creating any new React component.", + code: snippet("patterns/component-template.md"), + tips: [ + "Always use cn() for conditional classNames", + "Prefer named exports over default exports for components", + "Use FC type annotation", + ], + }, +]; + +// --------------------------------------------------------------------------- +// CONSTRAINTS +// --------------------------------------------------------------------------- + +export const CONSTRAINTS: Constraint[] = [ + { + name: "no-useeffect-fetch", + rule: "Never useEffect for data fetching", + reason: "Causes waterfall requests, no caching, race conditions, flash of loading state", + example: "Use RSC for server data. Use React Query/SWR for client-side data.", + }, + { + name: "no-redux", + rule: "No Redux โ€” Zustand only for shared client state", + reason: "Redux is verbose boilerplate. Zustand achieves the same with 10% of the code.", + example: "create() from zustand with persist middleware", + }, + { + name: "no-any-typescript", + rule: "No `any` in TypeScript", + reason: "Defeats the purpose of TypeScript. Use unknown + type narrowing, or define the proper type.", + example: "unknown + type guard, or generate types from API schema", + }, + { + name: "no-prop-drilling", + rule: "No prop drilling more than 2 levels deep", + reason: "Creates tight coupling. Middle components know about data they don't use.", + example: "Composition (children/slots), context injection, or Zustand", + }, + { + name: "no-context-for-state", + rule: "No Context for frequently changing state", + reason: "Every consumer re-renders on every context change, even if they only use one field", + example: "Zustand for state, Context only for: theme, auth tokens, i18n, feature flags", + }, + { + name: "no-barrel-exports", + rule: "Avoid barrel exports (index.ts re-exports) in large codebases", + reason: "Breaks tree-shaking โ€” bundler must include everything from the barrel", + example: "Import directly: import { Button } from '@/components/ui/button'", + }, + { + name: "server-first", + rule: "Default to RSC. Only add 'use client' when interactivity is required", + reason: "RSC reduces JS bundle size, improves LCP, enables streaming, better SEO", + example: "Forms with server actions, data display โ†’ RSC. Dropdowns, modals โ†’ client", + }, + { + name: "no-useeffect-mount", + rule: "No useEffect(fn, []) as componentDidMount substitute", + reason: "In React 18 Strict Mode, effects run twice. Use RSC data fetching or React Query.", + example: "RSC async functions, useQuery with enabled: true", + }, +]; + +// --------------------------------------------------------------------------- +// HELPERS +// --------------------------------------------------------------------------- + +export function getPatternsByCategory(category: PatternCategory): Pattern[] { + return PATTERNS.filter((p) => p.category === category); +} + +export function getPatternByName(name: string): Pattern | undefined { + return PATTERNS.find((p) => p.name.toLowerCase() === name.toLowerCase()); +} + +export function searchPatterns(query: string): Pattern[] { + const q = query.toLowerCase(); + return PATTERNS.filter( + (p) => + p.name.toLowerCase().includes(q) || + p.description.toLowerCase().includes(q) || + p.category.toLowerCase().includes(q) || + p.when.toLowerCase().includes(q) + ); +} diff --git a/src/plugins/react/index.ts b/src/plugins/react/index.ts new file mode 100644 index 0000000..2c2e1d3 --- /dev/null +++ b/src/plugins/react/index.ts @@ -0,0 +1,18 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { Plugin } from "../../registry.js"; +import { register as listPatterns } from "./tools/list-patterns.js"; +import { register as getPattern } from "./tools/get-pattern.js"; +import { register as getConstraints } from "./tools/get-constraints.js"; +import { register as searchDocs } from "./tools/search-docs.js"; + +function register(server: McpServer): void { + listPatterns(server); + getPattern(server); + getConstraints(server); + searchDocs(server); +} + +export const reactPlugin: Plugin = { + name: "react", + register, +}; diff --git a/src/plugins/react/loader.ts b/src/plugins/react/loader.ts new file mode 100644 index 0000000..40394ed --- /dev/null +++ b/src/plugins/react/loader.ts @@ -0,0 +1,3 @@ +import { createSnippetLoader } from "../../shared/loader-factory.js"; + +export const snippet = createSnippetLoader("react"); diff --git a/src/plugins/react/tools/get-constraints.ts b/src/plugins/react/tools/get-constraints.ts new file mode 100644 index 0000000..102683e --- /dev/null +++ b/src/plugins/react/tools/get-constraints.ts @@ -0,0 +1,22 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { CONSTRAINTS } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "react_get_constraints", + "List all forbidden React/Next.js patterns and their reasons", + {}, + async () => { + let text = "# React/Next.js Constraints (Hard Rules)\n\n"; + text += "Violating these causes bugs, performance issues, or maintenance problems.\n\n"; + for (const c of CONSTRAINTS) { + text += `## ${c.name}\n`; + text += `**Rule:** ${c.rule}\n`; + text += `**Why:** ${c.reason}\n`; + if (c.example) text += `**Instead:** ${c.example}\n`; + text += "\n"; + } + return { content: [{ type: "text", text }] }; + } + ); +} diff --git a/src/plugins/react/tools/get-pattern.ts b/src/plugins/react/tools/get-pattern.ts new file mode 100644 index 0000000..8975cc9 --- /dev/null +++ b/src/plugins/react/tools/get-pattern.ts @@ -0,0 +1,39 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { PATTERNS, getPatternByName } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "react_get_pattern", + "Get a React/Next.js pattern with full code example and anti-pattern", + { + name: z.string().describe("Pattern name (e.g. 'rsc-default', 'state-hierarchy', 'zustand-store', 'suspense-boundary', 'nextjs-metadata', 'composition-pattern', 'component-template')"), + }, + async ({ name }) => { + const pattern = getPatternByName(name); + if (!pattern) { + const available = PATTERNS.map((p) => p.name).join(", "); + return { + content: [{ type: "text", text: `Pattern "${name}" not found.\n\nAvailable: ${available}` }], + isError: true, + }; + } + + let text = `# ${pattern.name} [${pattern.category}]\n\n`; + text += `${pattern.description}\n\n`; + text += `**When to use:** ${pattern.when}\n\n`; + text += `## Code\n\`\`\`tsx\n${pattern.code}\n\`\`\`\n\n`; + + if (pattern.antiPattern) { + text += `## Anti-pattern (avoid)\n\`\`\`tsx\n${pattern.antiPattern}\n\`\`\`\n\n`; + } + + if (pattern.tips?.length) { + text += `## Tips\n`; + for (const tip of pattern.tips) text += `- ${tip}\n`; + } + + return { content: [{ type: "text", text }] }; + } + ); +} diff --git a/src/plugins/react/tools/list-patterns.ts b/src/plugins/react/tools/list-patterns.ts new file mode 100644 index 0000000..96d15eb --- /dev/null +++ b/src/plugins/react/tools/list-patterns.ts @@ -0,0 +1,35 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { PATTERNS, PATTERN_CATEGORIES, getPatternsByCategory } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "react_list_patterns", + "List all React/Next.js patterns by category", + { + category: z.enum(["all", ...PATTERN_CATEGORIES]).optional(), + }, + async ({ category }) => { + const list = category && category !== "all" + ? getPatternsByCategory(category as any) + : PATTERNS; + + const grouped: Record = {}; + for (const p of list) { + if (!grouped[p.category]) grouped[p.category] = []; + grouped[p.category].push(p); + } + + let text = "# React/Next.js Patterns (SDE-3 level)\n\n"; + for (const [cat, items] of Object.entries(grouped)) { + text += `## ${cat}\n`; + for (const p of items) { + text += `- **${p.name}** โ€” ${p.description}\n`; + } + text += "\n"; + } + text += `\n**Total:** ${list.length} patterns`; + return { content: [{ type: "text", text }] }; + } + ); +} diff --git a/src/plugins/react/tools/search-docs.ts b/src/plugins/react/tools/search-docs.ts new file mode 100644 index 0000000..31a5495 --- /dev/null +++ b/src/plugins/react/tools/search-docs.ts @@ -0,0 +1,45 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { searchPatterns, CONSTRAINTS } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "react_search_docs", + "Search React/Next.js patterns and constraints by keyword", + { + query: z.string().describe("Search query (e.g. 'server component', 'state', 'fetch', 'zustand', 'SEO')"), + }, + async ({ query }) => { + const patterns = searchPatterns(query); + const q = query.toLowerCase(); + const constraints = CONSTRAINTS.filter( + (c) => + c.name.toLowerCase().includes(q) || + c.rule.toLowerCase().includes(q) || + c.reason.toLowerCase().includes(q) + ); + + if (!patterns.length && !constraints.length) { + return { + content: [{ type: "text", text: `No results for "${query}". Try: server component, state, fetch, zustand, SEO, metadata, suspense` }], + }; + } + + let text = `# Search: "${query}"\n\n`; + if (patterns.length) { + text += `## Patterns (${patterns.length})\n`; + for (const p of patterns) { + text += `- **${p.name}** [${p.category}] โ€” ${p.description}\n`; + } + text += "\n"; + } + if (constraints.length) { + text += `## Constraints (${constraints.length})\n`; + for (const c of constraints) { + text += `- **${c.name}** โ€” ${c.rule}\n`; + } + } + return { content: [{ type: "text", text }] }; + } + ); +} diff --git a/src/plugins/rust/data.ts b/src/plugins/rust/data.ts new file mode 100644 index 0000000..764d46c --- /dev/null +++ b/src/plugins/rust/data.ts @@ -0,0 +1,205 @@ +import { snippet } from "./loader.js"; + +export interface BestPractice { + name: string; + chapter: Chapter; + rule: string; + reason: string; + good?: string; + bad?: string; + tips?: string[]; +} + +export const CHAPTERS = [ + "coding-styles", "clippy", "performance", "error-handling", + "testing", "generics", "type-state", "documentation", "pointers", +] as const; +export type Chapter = (typeof CHAPTERS)[number]; + +// --------------------------------------------------------------------------- +// BEST PRACTICES +// --------------------------------------------------------------------------- + +export const BEST_PRACTICES: BestPractice[] = [ + // CODING STYLES + { + name: "borrow-over-clone", + chapter: "coding-styles", + rule: "Prefer &T over .clone() unless ownership transfer is required.", + reason: "Cloning allocates heap memory unnecessarily. References are zero-cost.", + good: snippet("practices/borrow-over-clone-good.md"), + bad: snippet("practices/borrow-over-clone-bad.md"), + }, + { + name: "str-over-string", + chapter: "coding-styles", + rule: "Use &str over String, &[T] over Vec in function parameters.", + reason: "&str accepts both &String and string literals. More flexible and zero-copy.", + good: snippet("practices/str-over-string-good.md"), + bad: snippet("practices/str-over-string-bad.md"), + }, + { + name: "copy-by-value", + chapter: "coding-styles", + rule: "Small Copy types (โ‰ค24 bytes) can be passed by value โ€” no need for &T.", + reason: "Copying small types (u32, bool, small structs) is as fast or faster than a reference.", + good: snippet("practices/copy-by-value-good.md"), + }, + { + name: "cow-ambiguous-ownership", + chapter: "coding-styles", + rule: "Use Cow<'_, T> when ownership is sometimes required and sometimes not.", + reason: "Avoids always cloning (wasteful) or always borrowing (restrictive).", + good: snippet("practices/cow-ambiguous-ownership-good.md"), + }, + + // ERROR HANDLING + { + name: "result-not-panic", + chapter: "error-handling", + rule: "Return Result for fallible operations. Never panic! in production code.", + reason: "panic! unwinds the stack and kills the thread. Use it only for programmer errors.", + good: snippet("practices/result-not-panic-good.md"), + bad: snippet("practices/result-not-panic-bad.md"), + }, + { + name: "no-unwrap-in-prod", + chapter: "error-handling", + rule: "Never use unwrap() or expect() outside of tests.", + reason: "Both panic on None/Err. Use ? operator or proper error handling.", + good: snippet("practices/no-unwrap-in-prod-good.md"), + bad: snippet("practices/no-unwrap-in-prod-bad.md"), + tips: ["expect() is slightly better than unwrap() (message on panic), but still banned in prod"], + }, + { + name: "thiserror-vs-anyhow", + chapter: "error-handling", + rule: "thiserror for library errors, anyhow for binary/application errors.", + reason: "Libraries need typed errors (callers match on them). Binaries just need context strings.", + good: snippet("practices/thiserror-vs-anyhow-good.md"), + }, + + // PERFORMANCE + { + name: "benchmark-release", + chapter: "performance", + rule: "Always benchmark with --release flag. Debug builds are 10-100x slower.", + reason: "Debug builds disable optimizations. Benchmarks without --release are meaningless.", + good: snippet("practices/benchmark-release-good.md"), + bad: snippet("practices/benchmark-release-bad.md"), + }, + { + name: "avoid-clone-in-loops", + chapter: "performance", + rule: "Avoid cloning in loops. Use .iter() instead of .into_iter() for Copy types.", + reason: "Cloning in a loop = N allocations. References or Copy types are zero-cost.", + good: snippet("practices/avoid-clone-in-loops-good.md"), + bad: snippet("practices/avoid-clone-in-loops-bad.md"), + }, + { + name: "prefer-iterators", + chapter: "performance", + rule: "Prefer iterator chains over manual loops. Avoid premature .collect().", + reason: "Iterators are lazy โ€” they don't allocate until consumed. collect() is the allocation point.", + good: snippet("practices/prefer-iterators-good.md"), + bad: snippet("practices/prefer-iterators-bad.md"), + }, + + // CLIPPY + { + name: "clippy-command", + chapter: "clippy", + rule: "Run: cargo clippy --all-targets --all-features --locked -- -D warnings", + reason: "Catches common mistakes, redundant code, and performance issues automatically.", + tips: [ + "Add to CI to enforce on every PR", + "Key lints: redundant_clone, large_enum_variant, needless_collect", + "Use #[expect(clippy::lint)] with justification comment, not #[allow(...)]", + ], + }, + { + name: "expect-over-allow", + chapter: "clippy", + rule: "Use #[expect(clippy::lint_name)] over #[allow(...)]. Add a justification comment.", + reason: "#[expect] fails if the warning no longer fires (lint was fixed). #[allow] silently rots.", + good: snippet("practices/expect-over-allow-good.md"), + bad: snippet("practices/expect-over-allow-bad.md"), + }, + + // TESTING + { + name: "descriptive-test-names", + chapter: "testing", + rule: "Name tests descriptively: process_should_return_error_when_input_empty()", + reason: "Test names are documentation. Vague names like test_process() don't explain what's tested.", + good: snippet("practices/descriptive-test-names-good.md"), + bad: snippet("practices/descriptive-test-names-bad.md"), + }, + { + name: "one-assertion-per-test", + chapter: "testing", + rule: "One assertion per test when possible.", + reason: "Multiple assertions in one test: first failure hides the rest. Separate tests give clearer failures.", + good: snippet("practices/one-assertion-per-test-good.md"), + }, + { + name: "doc-tests", + chapter: "documentation", + rule: "Use doc tests (///) for public API usage examples. They run with cargo test.", + reason: "Doc tests are the only examples guaranteed to stay correct โ€” they compile and run.", + good: snippet("practices/doc-tests-good.md"), + }, + + // GENERICS + { + name: "static-over-dynamic-dispatch", + chapter: "generics", + rule: "Prefer generics (static dispatch) for performance-critical code. Use dyn Trait for heterogeneous collections.", + reason: "Generics monomorphize at compile time โ€” zero runtime cost. dyn Trait has vtable overhead.", + good: snippet("practices/static-over-dynamic-dispatch-good.md"), + }, + + // TYPE STATE + { + name: "type-state-pattern", + chapter: "type-state", + rule: "Encode valid state transitions in the type system using PhantomData.", + reason: "Catches invalid operations at compile time, not runtime. Zero cost abstraction.", + good: snippet("practices/type-state-pattern-good.md"), + tips: ["Use when invalid state transitions are a real risk", "Don't over-engineer โ€” simple enums often suffice"], + }, + + // POINTERS + { + name: "send-sync", + chapter: "pointers", + rule: "Understand Send + Sync before sharing data across threads.", + reason: "Send = safe to move to another thread. Sync = safe to share reference across threads. Wrong use = data races.", + tips: [ + "Arc: shared ownership across threads (T: Send + Sync)", + "Mutex: interior mutability for T: Send", + "Rc and RefCell are NOT thread-safe โ€” local only", + ], + good: snippet("practices/send-sync-good.md"), + bad: snippet("practices/send-sync-bad.md"), + }, +]; + +// --------------------------------------------------------------------------- +// HELPERS +// --------------------------------------------------------------------------- + +export function getPracticesByChapter(chapter: Chapter): BestPractice[] { + return BEST_PRACTICES.filter((p) => p.chapter === chapter); +} + +export function searchPractices(query: string): BestPractice[] { + const q = query.toLowerCase(); + return BEST_PRACTICES.filter( + (p) => + p.name.toLowerCase().includes(q) || + p.rule.toLowerCase().includes(q) || + p.chapter.toLowerCase().includes(q) || + (p.reason?.toLowerCase().includes(q) ?? false) + ); +} diff --git a/src/plugins/rust/index.ts b/src/plugins/rust/index.ts new file mode 100644 index 0000000..1f521bc --- /dev/null +++ b/src/plugins/rust/index.ts @@ -0,0 +1,18 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { Plugin } from "../../registry.js"; +import { register as listPractices } from "./tools/list-practices.js"; +import { register as getPractice } from "./tools/get-practice.js"; +import { register as searchDocs } from "./tools/search-docs.js"; +import { register as cheatsheet } from "./tools/cheatsheet.js"; + +function register(server: McpServer): void { + listPractices(server); + getPractice(server); + searchDocs(server); + cheatsheet(server); +} + +export const rustPlugin: Plugin = { + name: "rust", + register, +}; diff --git a/src/plugins/rust/loader.ts b/src/plugins/rust/loader.ts new file mode 100644 index 0000000..1f703a5 --- /dev/null +++ b/src/plugins/rust/loader.ts @@ -0,0 +1,2 @@ +import { createSnippetLoader } from "../../shared/loader-factory.js"; +export const snippet = createSnippetLoader("rust"); diff --git a/src/plugins/rust/tools/cheatsheet.ts b/src/plugins/rust/tools/cheatsheet.ts new file mode 100644 index 0000000..5857c76 --- /dev/null +++ b/src/plugins/rust/tools/cheatsheet.ts @@ -0,0 +1,15 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { snippet } from "../loader.js"; + +const CHEATSHEET = snippet("cheatsheet.md"); + +export function register(server: McpServer): void { + server.tool( + "rust_cheatsheet", + "Quick Rust reference: ownership rules, error handling, clippy commands, and key patterns", + {}, + async () => { + return { content: [{ type: "text", text: CHEATSHEET }] }; + } + ); +} diff --git a/src/plugins/rust/tools/get-practice.ts b/src/plugins/rust/tools/get-practice.ts new file mode 100644 index 0000000..5808cf3 --- /dev/null +++ b/src/plugins/rust/tools/get-practice.ts @@ -0,0 +1,33 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { BEST_PRACTICES } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "rust_get_practice", + "Get a Rust best practice with code examples", + { + name: z.string().describe("Practice name (e.g. 'borrow-over-clone', 'result-not-panic', 'thiserror-vs-anyhow', 'type-state-pattern', 'clippy-command')"), + }, + async ({ name }) => { + const practice = BEST_PRACTICES.find((p) => p.name.toLowerCase() === name.toLowerCase()); + if (!practice) { + return { + content: [{ type: "text", text: `Practice "${name}" not found.\n\nAvailable: ${BEST_PRACTICES.map((p) => p.name).join(", ")}` }], + isError: true, + }; + } + + let text = `# ${practice.name} [${practice.chapter}]\n\n`; + text += `**Rule:** ${practice.rule}\n\n`; + text += `**Why:** ${practice.reason}\n\n`; + if (practice.good) text += `## โœ… Good\n\`\`\`rust\n${practice.good}\n\`\`\`\n\n`; + if (practice.bad) text += `## โŒ Bad\n\`\`\`rust\n${practice.bad}\n\`\`\`\n\n`; + if (practice.tips?.length) { + text += `## Tips\n`; + for (const tip of practice.tips) text += `- ${tip}\n`; + } + return { content: [{ type: "text", text }] }; + } + ); +} diff --git a/src/plugins/rust/tools/list-practices.ts b/src/plugins/rust/tools/list-practices.ts new file mode 100644 index 0000000..14fe7ae --- /dev/null +++ b/src/plugins/rust/tools/list-practices.ts @@ -0,0 +1,33 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { BEST_PRACTICES, CHAPTERS, getPracticesByChapter } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "rust_list_practices", + "List Rust best practices by chapter", + { + chapter: z.enum(["all", ...CHAPTERS]).optional(), + }, + async ({ chapter }) => { + const list = chapter && chapter !== "all" + ? getPracticesByChapter(chapter as any) + : BEST_PRACTICES; + + const grouped: Record = {}; + for (const p of list) { + if (!grouped[p.chapter]) grouped[p.chapter] = []; + grouped[p.chapter].push(p); + } + + let text = "# Rust Best Practices (Apollo GraphQL Handbook)\n\n"; + for (const [ch, items] of Object.entries(grouped)) { + text += `## Chapter: ${ch}\n`; + for (const p of items) text += `- **${p.name}** โ€” ${p.rule}\n`; + text += "\n"; + } + text += `\n**Total:** ${list.length} practices`; + return { content: [{ type: "text", text }] }; + } + ); +} diff --git a/src/plugins/rust/tools/search-docs.ts b/src/plugins/rust/tools/search-docs.ts new file mode 100644 index 0000000..3398154 --- /dev/null +++ b/src/plugins/rust/tools/search-docs.ts @@ -0,0 +1,24 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { searchPractices } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "rust_search_docs", + "Search Rust best practices by keyword", + { + query: z.string().describe("Search query (e.g. 'ownership', 'error handling', 'performance', 'clippy', 'testing', 'async')"), + }, + async ({ query }) => { + const results = searchPractices(query); + if (!results.length) { + return { content: [{ type: "text", text: `No results for "${query}". Try: ownership, clone, error, panic, performance, clippy, testing, generic, thread` }] }; + } + let text = `# Rust Search: "${query}"\n\nFound ${results.length} practice(s):\n\n`; + for (const p of results) { + text += `## ${p.name} [${p.chapter}]\n${p.rule}\n\n`; + } + return { content: [{ type: "text", text }] }; + } + ); +} diff --git a/src/plugins/ui-ux/data.ts b/src/plugins/ui-ux/data.ts new file mode 100644 index 0000000..0479da5 --- /dev/null +++ b/src/plugins/ui-ux/data.ts @@ -0,0 +1,385 @@ +import { snippet } from "./loader.js"; + +export interface Principle { + name: string; + domain: Domain; + rule: string; + detail: string; + examples?: string[]; + antiPatterns?: string[]; + cssExample?: string; +} + +export interface ComponentPattern { + name: string; + variants?: string[]; + states: string[]; + rules: string[]; + code?: string; +} + +export interface Checklist { + domain: Domain; + items: ChecklistItem[]; +} + +export interface ChecklistItem { + label: string; + detail: string; + critical: boolean; +} + +export const DOMAINS = [ + "typography", "color", "spacing", "elevation", + "motion", "accessibility", "responsive", "components", +] as const; +export type Domain = (typeof DOMAINS)[number]; + +// --------------------------------------------------------------------------- +// PRINCIPLES +// --------------------------------------------------------------------------- + +export const PRINCIPLES: Principle[] = [ + // TYPOGRAPHY + { + name: "type-scale", + domain: "typography", + rule: "Use mathematical ratios for type scale, not arbitrary sizes.", + detail: "Common ratios: 1.25 (Major Third), 1.333 (Perfect Fourth), 1.414 (Augmented Fourth). Recommended web scale: Display 48-72px, H1 40-56px, H2 28-40px, H3 20-24px, Subtitle 16-20px, Body 16px, Body-sm 14px, Caption 13px, Overline 12px.", + cssExample: snippet("principles/type-scale.md"), + antiPatterns: ["Random px values with no ratio", "Fluid body text (causes reflow)", "More than 2 type families"], + }, + { + name: "line-height-rules", + domain: "typography", + rule: "Line height ratio increases as font size decreases.", + detail: "Large text 24px+: tight (1.05โ€“1.2) โ€” built-in optical spacing. Body text 14โ€“18px: generous (1.6โ€“1.75) โ€” reading comfort. Small text 12โ€“13px: moderate (1.4โ€“1.5).", + antiPatterns: ["Same line height for headings and body", "Line height below 1.4 for body text"], + }, + { + name: "tracking-rules", + domain: "typography", + rule: "Headings: negative tracking. Body: zero. Overlines: positive.", + detail: "Headings -0.01 to -0.03em (improves density at large sizes). Body: zero (default kerning is optimal). Overlines/caps: +0.06 to +0.10em (spreads uppercase for legibility). NEVER track body text negatively.", + antiPatterns: ["Negative tracking on body text (kills readability)", "Tracking headings positively"], + }, + { + name: "font-pairing", + domain: "typography", + rule: "Max 2 font families. Sans + mono is safest for apps.", + detail: "Mono only for: code, technical values, badges, terminal output. Always include fallback stack: `ui-sans-serif, system-ui, sans-serif`.", + antiPatterns: ["3+ font families", "Decorative fonts for body text", "Missing fallback stack"], + }, + { + name: "prose-width", + domain: "typography", + rule: "Max prose width: 65ch (~600px). Beyond this, eye tracking degrades.", + detail: "Apply `max-width: 65ch` to all body text containers. This is not the page width โ€” cards and components can be wider.", + cssExample: snippet("principles/prose-width.md"), + antiPatterns: ["Full-width paragraphs", "Applying 65ch to the whole layout"], + }, + + // COLOR + { + name: "oklch-color-space", + domain: "color", + rule: "Use OKLCH for new projects โ€” perceptually uniform, P3 gamut, Tailwind v4 native.", + detail: "oklch(L C H): L=Lightness 0โ€“1, C=Chroma 0โ€“0.4, H=Hue 0โ€“360ยฐ. Each axis is independent. To darken: reduce L. To desaturate: reduce C. To shift hue: adjust H only.", + cssExample: snippet("principles/oklch-color.md"), + antiPatterns: ["Mixing HSL and OKLCH", "Using rgb() for brand colors in new projects"], + }, + { + name: "warm-vs-cool-neutrals", + domain: "color", + rule: "Commit to warm OR cool neutrals โ€” never mix.", + detail: "Warm (C 0.005โ€“0.015, H 50โ€“80ยฐ): inviting, premium. Cool (C ~0, H 220โ€“240ยฐ): technical, corporate. Mixing warm backgrounds with cool borders breaks visual coherence.", + antiPatterns: ["Warm bg with cool border", "Switching neutral tone across components"], + }, + { + name: "wcag-contrast", + domain: "color", + rule: "Body text: 4.5:1 (AA). Large text โ‰ฅ18px bold or โ‰ฅ24px: 3:1 (AA). UI components: 3:1.", + detail: "AAA (enhanced): 7:1 for body, 4.5:1 for large. Fix: reduce OKLCH Lightness (L) of status colors from ~0.63 to ~0.55. Keep C and H unchanged.", + examples: ["Run: npm install wcag-contrast", "Online: https://webaim.org/resources/contrastchecker/"], + antiPatterns: ["Testing only in light mode", "Assuming brand colors pass without checking"], + }, + { + name: "dark-mode-principles", + domain: "color", + rule: "Dark mode: redesign, don't invert. Warm charcoal, not black.", + detail: "Background: oklch(0.13 0.008 265) โ€” warm charcoal, not #000. Text: oklch(0.94 0.008 265) โ€” off-white, not #fff. Higher elevation = lighter bg (shadows invisible on dark). Reduce saturation slightly (vivid on dark = neon). Primary accents brighten: brand-600 โ†’ brand-400.", + cssExample: snippet("principles/dark-mode.md"), + antiPatterns: ["Pure black background (#000)", "Pure white text (#fff) on dark", "Inverting light mode colors directly"], + }, + { + name: "semantic-status-colors", + domain: "color", + rule: "Always provide solid + soft variant for status colors (success/error/warning/info).", + detail: "Solid: passes 4.5:1 with white text (L ~0.55). Soft: light tinted bg + dark text for non-critical contexts. Warning uses dark text (not white) because amber is too light.", + cssExample: snippet("principles/semantic-status-colors.md"), + antiPatterns: ["White text on warning (fails contrast)", "Same color for solid and soft"], + }, + + // SPACING + { + name: "4px-grid", + domain: "spacing", + rule: "All spacing = multiples of 4px: 4, 8, 12, 16, 20, 24, 32, 40, 48, 64, 96px.", + detail: "Provides enough granularity without pixel-pushing. In Tailwind v4, `--spacing: 0.25rem` sets the base and all utilities derive from it automatically.", + antiPatterns: ["6px, 10px, 14px values", "Mixing 4px and 8px grids"], + }, + { + name: "spacing-hierarchy", + domain: "spacing", + rule: "Space within groups < space between groups. This is the single most important spatial rule.", + detail: "Tight (4โ€“8px): related elements (icon + label). Medium (12โ€“24px): grouped (card padding). Loose (32โ€“64px): separated (grid gaps). Section (64โ€“96px): major divisions.", + antiPatterns: ["Same gap between items and between sections", "Large internal padding with small section gaps"], + }, + + // ELEVATION + { + name: "5-elevation-levels", + domain: "elevation", + rule: "5 surface levels. Each must be visually distinguishable via bg color, not just borders.", + detail: "Level 0: flat (page bg). Level 1: subtle (inline cards). Level 2: raised (standard cards). Level 3: elevated (dropdowns, popovers). Level 4: floating (modals, dialogs). In dark mode: no shadows โ€” use progressively lighter bg colors.", + cssExample: snippet("principles/5-elevation-levels.md"), + antiPatterns: ["Borders as the only elevation signal", "Shadows on dark backgrounds"], + }, + { + name: "warm-shadows", + domain: "elevation", + rule: "Shadows must be warm-tinted (oklch), never rgba(0,0,0,...).", + detail: "Pure black shadows look harsh and disconnected from warm UIs. Tint shadows with a warm hue at very low opacity.", + cssExample: snippet("principles/warm-shadows.md"), + antiPatterns: ["rgba(0,0,0,...) shadows in warm UI", "High-opacity shadows (>0.2)"], + }, + + // MOTION + { + name: "duration-rules", + domain: "motion", + rule: "Exits faster than entrances. Users initiated the exit โ€” they expect immediacy.", + detail: "0ms: instant state changes. 100โ€“150ms: hover/focus/toggles. 200ms: default transitions. 300ms: panel open/close. 400โ€“500ms: complex animations. Exits: subtract 50โ€“100ms from enter duration.", + antiPatterns: ["Same duration for enter and exit", "Transitions longer than 500ms in UI (feels sluggish)"], + }, + { + name: "easing-rules", + domain: "motion", + rule: "ease-out for entering. ease-in for exiting. ease-in-out for repositioning. NEVER linear.", + detail: "ease-out: elements appearing โ€” starts fast, settles naturally. ease-in: elements leaving โ€” starts gently, ends decisively. ease-in-out: elements moving to new position. Spring/bounce: playful feedback only.", + cssExample: snippet("principles/easing-rules.md"), + antiPatterns: ["Linear easing for any UI motion", "Bounce/spring for serious/professional contexts"], + }, + { + name: "reduced-motion", + domain: "motion", + rule: "ALWAYS implement prefers-reduced-motion. Not optional.", + detail: "Place in @layer base with !important. Applies to all elements and pseudos. Some users have vestibular disorders โ€” motion can cause nausea.", + cssExample: snippet("principles/reduced-motion.md"), + antiPatterns: ["Missing prefers-reduced-motion", "Only disabling some animations"], + }, + + // ACCESSIBILITY + { + name: "touch-targets", + domain: "accessibility", + rule: "Touch targets: min 44ร—44px (WCAG), recommended 48ร—48px. Gap โ‰ฅ 8px between targets.", + detail: "Visual size โ‰  touch target. A 34px button can have a 44px hit area via padding or ::after pseudo-element. Gap prevents accidental taps on adjacent targets.", + cssExample: snippet("principles/touch-targets.md"), + antiPatterns: ["< 44px touch targets on mobile", "Adjacent buttons with no gap"], + }, + { + name: "focus-management", + domain: "accessibility", + rule: "Every interactive element MUST have visible focus indicator: 2px ring, 2px offset, primary color.", + detail: "Trap focus in modals. Return focus to trigger on close. Never `outline: none` without a custom focus style.", + cssExample: snippet("principles/focus-management.md"), + antiPatterns: ["outline: none without custom focus", "Focus styles with < 3:1 contrast", "No focus trap in modals"], + }, + { + name: "color-not-only-indicator", + domain: "accessibility", + rule: "Never use color as the ONLY state indicator โ€” add icons, text, or patterns.", + detail: "8% of men have color vision deficiency. Error states need both red color AND an error icon or text. Links need underline OR other visual differentiation beyond color.", + antiPatterns: ["Red border as sole error indicator", "Links distinguished only by color"], + }, + + // RESPONSIVE + { + name: "breakpoints", + domain: "responsive", + rule: "Content-driven breakpoints, not device-driven.", + detail: "Common breakpoints: sm 640px, md 768px, lg 1024px, xl 1280px, 2xl 1536px. But add a breakpoint where your content breaks โ€” not where a device exists.", + antiPatterns: ["Breakpoints at arbitrary device widths", "More than 5 breakpoints"], + }, + { + name: "mobile-first", + domain: "responsive", + rule: "Write mobile styles first, override with min-width queries.", + detail: "Mobile-first produces smaller CSS. Most traffic is mobile. Progressive enhancement > graceful degradation.", + cssExample: snippet("principles/mobile-first.md"), + antiPatterns: ["Desktop-first with max-width overrides", "Separate mobile stylesheet"], + }, +]; + +// --------------------------------------------------------------------------- +// COMPONENT PATTERNS +// --------------------------------------------------------------------------- + +export const COMPONENT_PATTERNS: ComponentPattern[] = [ + { + name: "button", + variants: ["primary", "secondary", "ghost", "destructive", "link"], + states: ["default", "hover", "active", "focus", "disabled", "loading"], + rules: [ + "Min height 36px (sm), 40px (md), 44px (lg)", + "Disabled: opacity 0.5, pointer-events none, aria-disabled", + "Loading: show spinner, keep original width to prevent layout shift", + "Touch target min 44px โ€” use padding to expand if needed", + "Primary: solid brand bg. Secondary: outlined. Ghost: transparent. Destructive: error color.", + ], + code: snippet("components/button.md"), + }, + { + name: "card", + variants: ["default", "elevated", "outlined", "interactive"], + states: ["default", "hover", "focus", "selected"], + rules: [ + "Padding 20โ€“28px (sm screens), 28โ€“40px (lg screens)", + "Interactive cards need focus ring and cursor: pointer", + "Use surface color, not bg color โ€” for elevation distinction", + "Border radius: 8px (sm), 12px (md), 16px (lg)", + ], + }, + { + name: "badge", + variants: ["default", "success", "error", "warning", "info", "outline"], + states: ["default"], + rules: [ + "Font: monospace or semi-bold sans-serif", + "Height: 20px (sm), 24px (md)", + "Padding: 0 6px (sm), 0 8px (md)", + "Always include solid + soft variants per status", + "Never use color alone โ€” include semantic text or icon", + ], + }, + { + name: "form-input", + variants: ["text", "textarea", "select", "checkbox", "radio"], + states: ["default", "hover", "focus", "error", "disabled", "readonly"], + rules: [ + "Height: 36px (sm), 40px (md), 44px (lg)", + "Error: red border + error icon + error text below (never color alone)", + "Placeholder: muted color (not same as label)", + "Label always visible (never placeholder-as-label)", + "Focus: 2px ring at --color-primary", + ], + }, +]; + +// --------------------------------------------------------------------------- +// CHECKLISTS +// --------------------------------------------------------------------------- + +export const CHECKLISTS: Checklist[] = [ + { + domain: "typography", + items: [ + { label: "Type scale uses mathematical ratio", detail: "1.25 or 1.333 ratio between steps", critical: true }, + { label: "Heading line heights are tight (1.1)", detail: "Not body line height (1.75)", critical: true }, + { label: "Body line height generous (1.6โ€“1.75)", detail: "Reading comfort at 16px", critical: true }, + { label: "No negative tracking on body text", detail: "Only headings get negative tracking", critical: true }, + { label: "Prose max-width: 65ch", detail: "Applied to content containers, not page", critical: false }, + { label: "Max 2 font families", detail: "Sans + mono for apps", critical: false }, + { label: "Fallback font stack present", detail: "ui-sans-serif, system-ui, sans-serif", critical: false }, + { label: "Fluid type uses clamp()", detail: "Headings only, body is fixed 16px", critical: false }, + ], + }, + { + domain: "color", + items: [ + { label: "Body text 4.5:1 contrast (AA)", detail: "Test every text+bg combination", critical: true }, + { label: "UI components 3:1 contrast", detail: "Borders, icons, input outlines", critical: true }, + { label: "No pure black (#000)", detail: "Use dark neutral e.g. #1C1917", critical: true }, + { label: "No pure white (#FFF) for bg", detail: "Use tinted white e.g. #FAF8F5", critical: false }, + { label: "Warm/cool neutrals committed", detail: "Never mix warm bg with cool borders", critical: true }, + { label: "Status colors have soft variant", detail: "Solid + soft for success/error/warning/info", critical: false }, + { label: "Dark mode redesigned, not inverted", detail: "Warm charcoal, off-white text", critical: true }, + ], + }, + { + domain: "accessibility", + items: [ + { label: "Touch targets โ‰ฅ 44ร—44px", detail: "All interactive elements on mobile", critical: true }, + { label: "Focus indicators on all interactive elements", detail: "2px ring, 2px offset, primary color", critical: true }, + { label: "Focus trapped in modals", detail: "Tab stays within modal, returned on close", critical: true }, + { label: "prefers-reduced-motion implemented", detail: "In @layer base with !important", critical: true }, + { label: "Color not sole state indicator", detail: "Error needs icon/text, not just red", critical: true }, + { label: "All images have alt text", detail: "Decorative: alt=''", critical: true }, + ], + }, + { + domain: "motion", + items: [ + { label: "prefers-reduced-motion: reduce", detail: "All animations disabled", critical: true }, + { label: "Exits faster than entrances", detail: "Enter 200ms โ†’ exit 150ms", critical: false }, + { label: "No linear easing", detail: "ease-out for entering, ease-in for exiting", critical: false }, + { label: "No transitions > 500ms", detail: "Feels sluggish", critical: false }, + { label: "Spring/bounce limited to playful contexts", detail: "Not in professional/serious UIs", critical: false }, + ], + }, +]; + +// --------------------------------------------------------------------------- +// GOTCHAS +// --------------------------------------------------------------------------- + +export const GOTCHAS: { domain: Domain; gotcha: string; fix: string }[] = [ + { domain: "typography", gotcha: "Heading line heights match body (1.75)", fix: "Headings need tight line height: 1.05โ€“1.2" }, + { domain: "typography", gotcha: "Negative tracking on body text", fix: "Body tracking = 0. Only headings go negative." }, + { domain: "typography", gotcha: "More than 2 font families", fix: "Stick to Sans + Mono. Decorative fonts for headings only." }, + { domain: "color", gotcha: "Pure black (#000) for text", fix: "Use oklch(0.12 0.005 60) or similar warm dark neutral" }, + { domain: "color", gotcha: "Pure white (#FFF) background", fix: "Use oklch(0.98 0.004 60) โ€” tinted white" }, + { domain: "color", gotcha: "Warm backgrounds with cool borders", fix: "Commit to one neutral temperature throughout" }, + { domain: "color", gotcha: "rgba(0,0,0,...) shadows", fix: "Use oklch-tinted shadows: oklch(0.22 0.006 56 / 0.08)" }, + { domain: "elevation", gotcha: "Shadows as only elevation signal in dark mode", fix: "Shadows invisible on dark. Use progressively lighter bg per level." }, + { domain: "motion", gotcha: "Missing prefers-reduced-motion", fix: "Add to @layer base with !important on * and pseudos" }, + { domain: "motion", gotcha: "Linear easing for UI motion", fix: "ease-out entering, ease-in exiting, ease-in-out repositioning" }, + { domain: "accessibility", gotcha: "outline: none with no custom focus style", fix: "Always replace with visible 2px ring" }, + { domain: "accessibility", gotcha: "Color as only error indicator", fix: "Add error icon and text message alongside red color" }, + { domain: "components", gotcha: "Z-index values: 999, 9999, 99999", fix: "Define scale: dropdown=1000, modal=1050, tooltip=1070, toast=1080" }, + { domain: "components", gotcha: "Disabled opacity not 0.5", fix: "Less = unreadable. More = doesn't look disabled. Exactly 0.5." }, +]; + +// --------------------------------------------------------------------------- +// HELPERS +// --------------------------------------------------------------------------- + +export function getPrinciplesByDomain(domain: Domain): Principle[] { + return PRINCIPLES.filter((p) => p.domain === domain); +} + +export function searchPrinciples(query: string): Principle[] { + const q = query.toLowerCase(); + return PRINCIPLES.filter( + (p) => + p.name.toLowerCase().includes(q) || + p.rule.toLowerCase().includes(q) || + p.detail.toLowerCase().includes(q) || + p.domain.toLowerCase().includes(q) + ); +} + +export function getComponentPatternByName(name: string): ComponentPattern | undefined { + return COMPONENT_PATTERNS.find( + (c) => c.name.toLowerCase() === name.toLowerCase() + ); +} + +export function getChecklistByDomain(domain: Domain): Checklist | undefined { + return CHECKLISTS.find((c) => c.domain === domain); +} + +export function getAllGotchas(domain?: Domain): { domain: Domain; gotcha: string; fix: string }[] { + return domain ? GOTCHAS.filter((g) => g.domain === domain) : GOTCHAS; +} diff --git a/src/plugins/ui-ux/index.ts b/src/plugins/ui-ux/index.ts new file mode 100644 index 0000000..e7443a1 --- /dev/null +++ b/src/plugins/ui-ux/index.ts @@ -0,0 +1,22 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { Plugin } from "../../registry.js"; +import { register as listPrinciples } from "./tools/list-principles.js"; +import { register as getPrinciple } from "./tools/get-principle.js"; +import { register as getComponentPattern } from "./tools/get-component-pattern.js"; +import { register as getChecklist } from "./tools/get-checklist.js"; +import { register as search } from "./tools/search.js"; +import { register as getGotchas } from "./tools/get-gotchas.js"; + +function register(server: McpServer): void { + listPrinciples(server); + getPrinciple(server); + getComponentPattern(server); + getChecklist(server); + search(server); + getGotchas(server); +} + +export const uiUxPlugin: Plugin = { + name: "ui-ux", + register, +}; diff --git a/src/plugins/ui-ux/loader.ts b/src/plugins/ui-ux/loader.ts new file mode 100644 index 0000000..c605a0a --- /dev/null +++ b/src/plugins/ui-ux/loader.ts @@ -0,0 +1,2 @@ +import { createSnippetLoader } from "../../shared/loader-factory.js"; +export const snippet = createSnippetLoader("ui-ux"); diff --git a/src/plugins/ui-ux/tools/get-checklist.ts b/src/plugins/ui-ux/tools/get-checklist.ts new file mode 100644 index 0000000..fb77379 --- /dev/null +++ b/src/plugins/ui-ux/tools/get-checklist.ts @@ -0,0 +1,43 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { DOMAINS, getChecklistByDomain } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "ui_ux_get_checklist", + "Get quality checklist for a UI/UX domain before shipping", + { + domain: z.enum(DOMAINS).describe("Domain: typography, color, accessibility, motion, etc."), + }, + async ({ domain }) => { + const checklist = getChecklistByDomain(domain); + if (!checklist) { + return { + content: [{ type: "text", text: `No checklist for domain "${domain}". Available: ${DOMAINS.join(", ")}` }], + isError: true, + }; + } + + let text = `# ${domain} checklist\n\n`; + const critical = checklist.items.filter((i) => i.critical); + const standard = checklist.items.filter((i) => !i.critical); + + if (critical.length) { + text += `## Critical (must pass)\n`; + for (const item of critical) { + text += `- [ ] **${item.label}** โ€” ${item.detail}\n`; + } + text += "\n"; + } + + if (standard.length) { + text += `## Standard\n`; + for (const item of standard) { + text += `- [ ] ${item.label} โ€” ${item.detail}\n`; + } + } + + return { content: [{ type: "text", text }] }; + } + ); +} diff --git a/src/plugins/ui-ux/tools/get-component-pattern.ts b/src/plugins/ui-ux/tools/get-component-pattern.ts new file mode 100644 index 0000000..189ea47 --- /dev/null +++ b/src/plugins/ui-ux/tools/get-component-pattern.ts @@ -0,0 +1,39 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { COMPONENT_PATTERNS, getComponentPatternByName } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "ui_ux_get_component_pattern", + "Get component pattern spec including variants, states, and sizing rules", + { + name: z.string().describe("Component name: button, card, badge, form-input"), + }, + async ({ name }) => { + const pattern = getComponentPatternByName(name); + if (!pattern) { + const available = COMPONENT_PATTERNS.map((c) => c.name).join(", "); + return { + content: [{ type: "text", text: `Component "${name}" not found.\n\nAvailable: ${available}` }], + isError: true, + }; + } + + let text = `# ${pattern.name} component pattern\n\n`; + + if (pattern.variants?.length) { + text += `**Variants:** ${pattern.variants.join(", ")}\n`; + } + text += `**States:** ${pattern.states.join(", ")}\n\n`; + + text += `## Rules\n`; + for (const rule of pattern.rules) text += `- ${rule}\n`; + + if (pattern.code) { + text += `\n## CSS\n\`\`\`css\n${pattern.code}\n\`\`\`\n`; + } + + return { content: [{ type: "text", text }] }; + } + ); +} diff --git a/src/plugins/ui-ux/tools/get-gotchas.ts b/src/plugins/ui-ux/tools/get-gotchas.ts new file mode 100644 index 0000000..0f07e2d --- /dev/null +++ b/src/plugins/ui-ux/tools/get-gotchas.ts @@ -0,0 +1,36 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { DOMAINS, getAllGotchas } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "ui_ux_get_gotchas", + "List all common UI/UX mistakes and fixes, optionally filtered by domain", + { + domain: z.enum(["all", ...DOMAINS]).optional().describe("Filter by domain"), + }, + async ({ domain }) => { + const gotchas = domain && domain !== "all" + ? getAllGotchas(domain as any) + : getAllGotchas(); + + let text = "# UI/UX Gotchas\n\nCommon mistakes that break polished interfaces:\n\n"; + + const byDomain: Record = {}; + for (const g of gotchas) { + if (!byDomain[g.domain]) byDomain[g.domain] = []; + byDomain[g.domain].push(g); + } + + for (const [dom, items] of Object.entries(byDomain)) { + text += `## ${dom}\n`; + for (const item of items) { + text += `- โŒ **${item.gotcha}**\n โœ… ${item.fix}\n`; + } + text += "\n"; + } + + return { content: [{ type: "text", text }] }; + } + ); +} diff --git a/src/plugins/ui-ux/tools/get-principle.ts b/src/plugins/ui-ux/tools/get-principle.ts new file mode 100644 index 0000000..45594ef --- /dev/null +++ b/src/plugins/ui-ux/tools/get-principle.ts @@ -0,0 +1,47 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { PRINCIPLES } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "ui_ux_get_principle", + "Get full details for a UI/UX principle including examples, anti-patterns, and CSS examples", + { + name: z.string().describe("Principle name (e.g. 'type-scale', 'wcag-contrast', 'dark-mode-principles', 'touch-targets', 'easing-rules')"), + }, + async ({ name }) => { + const principle = PRINCIPLES.find( + (p) => p.name.toLowerCase() === name.toLowerCase() + ); + + if (!principle) { + const available = PRINCIPLES.map((p) => p.name).join(", "); + return { + content: [{ type: "text", text: `Principle "${name}" not found.\n\nAvailable: ${available}` }], + isError: true, + }; + } + + let text = `# ${principle.name} [${principle.domain}]\n\n`; + text += `**Rule:** ${principle.rule}\n\n`; + text += `${principle.detail}\n\n`; + + if (principle.cssExample) { + text += `## CSS Example\n\`\`\`css\n${principle.cssExample}\n\`\`\`\n\n`; + } + + if (principle.examples?.length) { + text += `## Examples\n`; + for (const ex of principle.examples) text += `- ${ex}\n`; + text += "\n"; + } + + if (principle.antiPatterns?.length) { + text += `## Anti-patterns (avoid)\n`; + for (const ap of principle.antiPatterns) text += `- โŒ ${ap}\n`; + } + + return { content: [{ type: "text", text }] }; + } + ); +} diff --git a/src/plugins/ui-ux/tools/list-principles.ts b/src/plugins/ui-ux/tools/list-principles.ts new file mode 100644 index 0000000..748e2d4 --- /dev/null +++ b/src/plugins/ui-ux/tools/list-principles.ts @@ -0,0 +1,35 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { PRINCIPLES, DOMAINS, getPrinciplesByDomain } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "ui_ux_list_principles", + "List all UI/UX principles by domain (typography, color, spacing, elevation, motion, accessibility, responsive, components)", + { + domain: z.enum(["all", ...DOMAINS]).optional().describe("Filter by domain"), + }, + async ({ domain }) => { + const list = domain && domain !== "all" + ? getPrinciplesByDomain(domain as any) + : PRINCIPLES; + + const grouped: Record = {}; + for (const p of list) { + if (!grouped[p.domain]) grouped[p.domain] = []; + grouped[p.domain].push(p); + } + + let text = "# UI/UX Principles\n\n"; + for (const [dom, items] of Object.entries(grouped)) { + text += `## ${dom} (${items.length})\n`; + for (const p of items) { + text += `- **${p.name}** โ€” ${p.rule}\n`; + } + text += "\n"; + } + text += `\n**Total:** ${list.length} principles across ${Object.keys(grouped).length} domains`; + return { content: [{ type: "text", text }] }; + } + ); +} diff --git a/src/plugins/ui-ux/tools/search.ts b/src/plugins/ui-ux/tools/search.ts new file mode 100644 index 0000000..516b1bf --- /dev/null +++ b/src/plugins/ui-ux/tools/search.ts @@ -0,0 +1,47 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { searchPrinciples, COMPONENT_PATTERNS } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "ui_ux_search", + "Search UI/UX principles and component patterns by keyword", + { + query: z.string().describe("Search query (e.g. 'contrast', 'dark mode', 'spacing', 'focus', 'button')"), + }, + async ({ query }) => { + const principles = searchPrinciples(query); + const q = query.toLowerCase(); + const components = COMPONENT_PATTERNS.filter( + (c) => + c.name.includes(q) || + c.rules.some((r) => r.toLowerCase().includes(q)) || + c.states.some((s) => s.toLowerCase().includes(q)) + ); + + if (!principles.length && !components.length) { + return { + content: [{ type: "text", text: `No results for "${query}". Try: contrast, dark mode, spacing, focus, motion, typography, button, card` }], + }; + } + + let text = `# Search results for "${query}"\n\n`; + + if (principles.length) { + text += `## Principles (${principles.length})\n`; + for (const p of principles) { + text += `### ${p.name} [${p.domain}]\n${p.rule}\n\n`; + } + } + + if (components.length) { + text += `## Component Patterns (${components.length})\n`; + for (const c of components) { + text += `### ${c.name}\nVariants: ${c.variants?.join(", ") ?? "โ€”"} | States: ${c.states.join(", ")}\n\n`; + } + } + + return { content: [{ type: "text", text }] }; + } + ); +} diff --git a/src/registry.ts b/src/registry.ts old mode 100644 new mode 100755 diff --git a/src/shared/loader-factory.ts b/src/shared/loader-factory.ts old mode 100644 new mode 100755 From d5e6860c7fb98cd70621006615c58a01cd342091 Mon Sep 17 00:00:00 2001 From: Kailas Mahavarkar <66670953+KailasMahavarkar@users.noreply.github.com> Date: Wed, 8 Apr 2026 20:22:21 +0530 Subject: [PATCH 02/65] docs: fix badges - flat-square style, remove broken dynamic badges, drop Go logo Co-Authored-By: Claude Sonnet 4.6 --- README.md | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 16aa234..7afadcc 100755 --- a/README.md +++ b/README.md @@ -5,27 +5,23 @@ **One MCP server. Every library your AI needs. Zero conflicts.**

- CI - License - Stars - TypeScript - MCP compatible + MIT + Stars + TypeScript + MCP + 9 plugins

- - React Flow v12 - Motion v12 - Lenis - React 19 - Tailwind v4 -

-

- - Echo Go - Go - Rust - UI/UX + React Flow + Motion + Lenis + React + Tailwind + Echo + Go + Rust + UI/UX


From 256353c171be6ae2eb9fb7b61f10073dc4798f59 Mon Sep 17 00:00:00 2001 From: Kailas Mahavarkar <66670953+KailasMahavarkar@users.noreply.github.com> Date: Wed, 8 Apr 2026 20:22:56 +0530 Subject: [PATCH 03/65] docs: remove CI badge from README --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 7afadcc..4c9f6bc 100755 --- a/README.md +++ b/README.md @@ -5,7 +5,6 @@ **One MCP server. Every library your AI needs. Zero conflicts.**

- CI MIT Stars TypeScript From 42ed3c805a5537664217ec5e24e7f98016d02ce8 Mon Sep 17 00:00:00 2001 From: Kailas Mahavarkar <66670953+KailasMahavarkar@users.noreply.github.com> Date: Wed, 8 Apr 2026 23:05:14 +0530 Subject: [PATCH 04/65] feat: migrate unified-skill into unified-mcp repo --- README.md | 10 +- SKILL.md | 449 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 456 insertions(+), 3 deletions(-) create mode 100755 SKILL.md diff --git a/README.md b/README.md index 4c9f6bc..e7bcad1 100755 --- a/README.md +++ b/README.md @@ -32,11 +32,15 @@ --- -## ๐Ÿค Companion +## ๐Ÿค AI Skill Included -This server pairs with **[unified-skill](https://github.com/orkait/unified-skill)** - a Claude Code skill that teaches your AI assistant *when and how* to use these tools. The skill handles judgment and gotchas; this server handles the data. +This repository includes `SKILL.md` - a Claude Code skill that teaches your AI assistant *when and how* to use these tools. The skill handles judgment and gotchas; the MCP server handles the data. -Install both to get the full experience. +To use the skill, clone this repository into your Claude Code skills directory: + +```bash +git clone https://github.com/orkait/unified-mcp.git ~/.claude/skills/unified-mcp +``` --- diff --git a/SKILL.md b/SKILL.md new file mode 100755 index 0000000..69bb9a9 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,449 @@ +--- +name: unified-skill +description: >- + Build flow-based UIs with React Flow v12, animate with Motion for React v12, + add smooth scroll with Lenis, build with React 19 and Next.js App Router, + write Go APIs with Echo framework, apply Go best practices and design patterns, + write idiomatic Rust, build design token systems with Tailwind v4 and OKLCH, + and apply UI/UX design principles for typography, color, spacing, elevation, + motion, and accessibility. +metadata: + author: claude + version: "2.0.0" + license: MIT +triggers: + - react flow + - nodes and edges + - flow diagram + - workflow builder + - pipeline editor + - DAG editor + - canvas UI + - node-based editor + - draggable nodes + - xyflow + - motion + - framer-motion + - animate + - animation + - AnimatePresence + - exit animation + - layout animation + - whileHover + - whileTap + - whileInView + - spring + - variants + - motion value + - useScroll + - lenis + - smooth scroll + - scroll animation + - react component + - server component + - RSC + - next.js + - app router + - zustand + - echo framework + - golang + - go api + - go server + - rust + - ownership + - borrow checker + - design tokens + - tailwind v4 + - oklch + - color ramp + - token system + - ui design + - ux principles + - typography scale + - color contrast + - wcag +activation: + mode: fuzzy + priority: high +--- + +# unified-mcp Skill + +This skill relies on [unified-mcp](https://github.com/orkait/unified-mcp). Always use the MCP tools below for all API details, props, code examples, patterns, and transitions. This skill covers judgment, gotchas, and decisions only - unified-mcp provides all actual data. + +--- + +## React Flow - MCP Tools + +| Tool | What it returns | +|------|----------------| +| `reactflow_list_apis` | All 56 APIs grouped by kind (component/hook/utility/type) | +| `reactflow_get_api("Name")` | Full props table, usage snippet, code examples, tips | +| `reactflow_get_pattern("name")` | Complete implementation code for enterprise patterns | +| `reactflow_get_template("name")` | Production-ready TSX starter | +| `reactflow_get_examples("category")` | Curated code examples filtered by category | +| `reactflow_get_migration_guide` | v11 โ†’ v12 breaking changes with before/after diffs | +| `reactflow_generate_flow("description")` | Ready-to-use TSX from a prose description | +| `reactflow_search_docs("keyword")` | Full-text search across all docs | + +Patterns: zustand-store, undo-redo, drag-and-drop, auto-layout-dagre, auto-layout-elk, context-menu, copy-paste, save-restore, prevent-cycles, keyboard-shortcuts, performance, dark-mode, ssr, subflows, edge-reconnection, custom-connection-line, auto-layout-on-mount + +--- + +## React Flow - Critical Rules + +- nodeTypes and edgeTypes must be defined outside components - never inline, causes remount on every render +- memo() all custom nodes and edges - they re-render aggressively otherwise +- node.measured.width/height is read-only - set by the library, do not set it yourself +- deleteElements() is async - returns Promise with deletedNodes and deletedEdges +- onBeforeDelete must be async - return Promise false to cancel, Promise true to confirm +- Hooks outside ReactFlow must be wrapped in ReactFlowProvider +- getNode() and getEdge() return undefined (not null) when not found - v12 change +- useUpdateNodeInternals() - call after adding or removing handles programmatically +- Attribution - use proOptions hideAttribution on free tier + +--- + +## React Flow - Decisions + +State management: single component use useNodesState and useEdgesState; shared across components use Zustand via reactflow_get_pattern zustand-store; undo/redo needed use Zustand with zundo via reactflow_get_pattern undo-redo + +Layout: tree or left-right hierarchy use dagre via reactflow_get_pattern auto-layout-dagre; complex graphs nested ports use ELK via reactflow_get_pattern auto-layout-elk; on first render use useNodesInitialized with useEffect via reactflow_get_pattern auto-layout-on-mount + +Custom nodes: always memo() with useCallback on handlers; use useNodeId() inside node to avoid prop drilling; use useNodesData(ids) to subscribe to specific node data; template via reactflow_get_template custom-node + +Custom edges: destructure 5 values edgePath labelX labelY offsetX offsetY from path utils; complex labels use EdgeLabelRenderer (renders in DOM not SVG); template via reactflow_get_template custom-edge + +Connection validation: prevent cycles via reactflow_get_pattern prevent-cycles; restrict handle types via isValidConnection prop on Handle or ReactFlow + +Groups and subflows: parentId with extent parent and expandParent true on child nodes; add zIndexMode auto on ReactFlow for correct z-ordering + +Performance: hide off-screen elements via onlyRenderVisibleElements prop; batch node updates via setNodes updater not repeated updateNode calls; full guide via reactflow_get_pattern performance + +--- + +## Motion for React - MCP Tools + +| Tool | What it returns | +|------|----------------| +| `motion_list_apis` | All 32 APIs grouped by kind (component/hook/function/utility) | +| `motion_get_api("Name")` | Full props table, usage snippet, code examples, tips | +| `motion_get_examples("category")` | Curated code examples filtered by category | +| `motion_get_transitions` | Complete transition reference: spring presets, tween, inertia, orchestration | +| `motion_generate_animation("description")` | Ready-to-use TSX animation from a prose description | +| `motion_search_docs("keyword")` | Full-text search across all docs | + +Example categories: animation, gestures, scroll, layout, exit, drag, hover, svg, transitions, variants, keyframes, spring, reorder, performance + +--- + +## Motion for React - Critical Rules + +- Import from motion/react not framer-motion; framer-motion has been replaced by motion +- AnimatePresence goes outside the conditional - wrap the show/component expression, not the inner element +- exit requires AnimatePresence parent - without it, exit animations are silently ignored +- AnimatePresence direct children need unique key props +- MotionValues are NOT React state - do not read them in render; subscribe with useMotionValueEvent +- AnimatePresence mode popLayout - direct custom component children must accept ref as a prop (React 19) +- useReducedMotion() - always respect for accessibility +- stagger() - import from motion/react, use inside variant transition.delayChildren +- useScroll container vs target - container is a scrollable element; target is tracked within the container + +--- + +## Motion for React - Decisions + +Basic animation: simple one-off use initial and animate props; reusable states use variants; imperative control use useAnimate + +Exit animations: always use AnimatePresence with exit prop and unique key; old page exits before new enters use mode wait; keep siblings in layout during exit use mode popLayout + +Layout animations: single element reflow use layout prop; cross-component shared transition use layoutId; sync siblings use LayoutGroup wrapper + +Scroll-linked: progress bar or parallax use useScroll with useTransform; enter viewport use whileInView or useInView + +Physics and spring: bouncy feel use transition type spring; smooth follow use useSpring with a motionValue; presets via motion_get_transitions + +Gestures: hover and tap states use whileHover and whileTap; drag use drag prop with dragConstraints; custom drag trigger use useDragControls with dragListener false + +Staggered lists: use variants on container and children; set delayChildren stagger 0.3 in parent variant transition + +Performance: animate transform and opacity only - avoid width height top left which triggers layout + +--- + +## Lenis - MCP Tools + +| Tool | What it returns | +|------|----------------| +| `lenis_list_apis` | All Lenis APIs - options, methods, events | +| `lenis_get_api("name")` | Full API reference with usage snippet | +| `lenis_get_pattern("name")` | Integration pattern with full code | +| `lenis_generate_setup("description")` | Complete Lenis setup from a description | +| `lenis_cheatsheet` | Required CSS, data-lenis-prevent usage, pitfalls | +| `lenis_search_docs("keyword")` | Full-text search across all Lenis docs | + +Patterns: full-page, next-js, gsap-integration, framer-motion-integration, custom-container, accessibility, scroll-to-nav + +Recipes: scroll-progress-bar, back-to-top, horizontal-scroll-section, scroll-locked-modal, parallax-layer, direction-indicator, gsap-complete + +--- + +## Lenis - Critical Rules + +- Required CSS must be imported - import 'lenis/dist/lenis.css' or add manually; without it scroll behavior breaks +- html.lenis and html.lenis body must have height: auto - do not set fixed height on these +- autoRaf false when using GSAP - use gsap.ticker.add to drive Lenis RAF; do not let both run simultaneously +- gsap.ticker.lagSmoothing(0) - required when integrating GSAP to prevent jank on tab refocus +- data-lenis-prevent on scrollable containers inside Lenis - modals, sidebars, code blocks with overflow scroll +- lenis.stop() when modal opens, lenis.start() when it closes - call via useLenis hook or ref +- Do not use requestAnimationFrame directly with Lenis - banned per project rules; use gsap.ticker or autoRaf + +--- + +## Lenis - Decisions + +Root vs container: full page smooth scroll use ReactLenis with root prop; scoped smooth scroll inside a div use ReactLenis without root and pass a container ref + +GSAP integration: use lenis_get_pattern gsap-integration; set autoRaf false and drive via gsap.ticker.add + +Framer Motion integration: use lenis_get_pattern framer-motion-integration; they coexist without conflict + +Preventing scroll on nested elements: use data-lenis-prevent for all overrides; lenis_cheatsheet has the full attribute reference + +--- + +## React + Next.js - MCP Tools + +| Tool | What it returns | +|------|----------------| +| `react_list_patterns` | All patterns with categories | +| `react_get_pattern("name")` | Full pattern: code, anti-pattern, tips | +| `react_get_constraints` | Hard rules and banned patterns | +| `react_search_docs("keyword")` | Search across patterns and rules | + +--- + +## React + Next.js - Critical Rules + +- Server Components are the default - use 'use client' only when interactivity is required +- Never useEffect for data fetching - fetch in RSC or use React Query / SWR in client components +- Redux is banned - Zustand only for shared client state +- Context is for injection only (theme, auth, i18n) - not for frequently changing state +- URL state first (searchParams) before any other state mechanism +- generateMetadata must be async and in the same file as the page component + +--- + +## React + Next.js - Decisions + +State placement order: URL state (searchParams) โ†’ server state (RSC/React Query) โ†’ local useState โ†’ Zustand โ†’ Context injection + +Component type: SEO-critical or data-only use RSC; needs onClick, useState, useEffect, or browser APIs use 'use client' + +Data fetching: request-time data use RSC with async/await fetch; client-side only or real-time use React Query or SWR; never useEffect with fetch + +Sharing state: two sibling client components use Zustand slice; deep component tree with stable values use Context; URL-serializable use searchParams + +--- + +## Echo (Go) - MCP Tools + +| Tool | What it returns | +|------|----------------| +| `echo_list_recipes` | All 19 recipes by category | +| `echo_get_recipe("name")` | Full recipe with complete runnable code | +| `echo_list_middleware` | All 13 middleware with purpose and order guidance | +| `echo_get_middleware("name")` | Full middleware reference with usage and gotchas | +| `echo_decision_matrix` | When to use what - Echo vs stdlib vs alternatives | +| `echo_search_docs("keyword")` | Full-text search across all recipes and middleware | + +Recipes: hello-world, crud-api, jwt-auth, websocket, sse, file-upload, file-download, graceful-shutdown, middleware-chain, cors, route-groups, http2, auto-tls, reverse-proxy, streaming-response, embed-resources, timeout, subdomain-routing, jsonp + +--- + +## Echo (Go) - Critical Rules + +- Graceful shutdown is mandatory - always implement signal handling; aborts in-flight requests without it +- Never string concatenate SQL - always use parameterized queries; SQL injection is critical +- c.Bind(&req) before accessing request body - do not read c.Request().Body directly +- Gzip middleware must NOT be used on SSE or streaming routes - it buffers the full response +- JWT middleware runs before the handler - do not re-validate the token inside the handler +- e.Logger.Fatal() on startup errors only - never in request handlers +- Always return the error from handlers - do not swallow errors with _ + +--- + +## Echo (Go) - Decisions + +Middleware order: Recover โ†’ Logger โ†’ RequestID โ†’ CORS โ†’ RateLimit โ†’ Auth โ†’ business handlers + +Route grouping: shared prefix use e.Group(); shared middleware use group.Use(); per-route middleware pass as variadic args to GET/POST + +Authentication: JWT use echo_get_middleware JWT; API keys use echo_get_middleware KeyAuth; basic auth use echo_get_middleware BasicAuth + +Timeouts: per-handler use echo_get_middleware Timeout; global use server.ReadTimeout and WriteTimeout on http.Server + +--- + +## Golang - MCP Tools + +| Tool | What it returns | +|------|----------------| +| `golang_list_practices` | All 18 best practices by topic | +| `golang_get_practice("name")` | Full practice: rule, reason, good/bad code | +| `golang_list_patterns` | All 10 design patterns by category | +| `golang_get_pattern("name")` | Full pattern with Go-idiomatic implementation | +| `golang_get_antipatterns` | Common Go mistakes and fixes | +| `golang_search_docs("keyword")` | Search across practices and patterns | + +Practice topics: fundamentals, error-handling, concurrency, api-server, database, config, logging, security, testing + +Pattern categories: creational (functional-options), structural (adapter, middleware-decorator, consumer-side-interface), behavioral (strategy, observer, command), concurrency (worker-pool, pipeline, fan-out-fan-in) + +--- + +## Golang - Critical Rules + +- context.Context is always the first parameter for any function doing I/O - never store in struct +- Always wrap errors with fmt.Errorf("context: %w", err) - bare return err loses the call stack +- Handle errors once: log OR return, never both - duplicate log entries pollute production logs +- Never use math/rand for security-sensitive values - always crypto/rand +- Never string-concatenate SQL queries - always parameterized queries +- Never start a goroutine without knowing how it stops - always pass ctx and select on ctx.Done() +- Use errors.Is() and errors.As() not == or type assertion - direct comparison breaks with wrapped errors +- Always defer rows.Close() immediately after a successful query + +--- + +## Golang - Decisions + +Error handling: sentinel errors use errors.Is(); typed errors use errors.As(); new error types use golang_get_practice errors-is-as + +Concurrency: goroutines that can fail use errgroup not WaitGroup; bounded parallel work use worker-pool pattern; streaming pipeline use pipeline pattern; details via golang_get_pattern + +Interfaces: define in the consumer package not the producer; keep to 1-2 methods; details via golang_get_practice small-interfaces and golang_get_pattern consumer-side-interface + +Constructor: any struct needing validation or defaults use NewType() pattern; never expose zero-value-misuse structs + +Dependency injection: pass deps to constructors, never global vars; functional options for 5+ optional params via golang_get_pattern functional-options + +--- + +## Rust - MCP Tools + +| Tool | What it returns | +|------|----------------| +| `rust_list_practices` | All 18 best practices by topic | +| `rust_get_practice("name")` | Full practice: rule, reason, good/bad examples | +| `rust_search_docs("keyword")` | Search across all practices | +| `rust_cheatsheet` | Ownership rules, pointer type table, performance tips | + +--- + +## Rust - Critical Rules + +- Borrow over clone - pass &T not T unless you actually need ownership +- Never unwrap() in production code - use ? operator with proper error types +- Use thiserror for library errors, anyhow for application errors - never Box in libraries +- Prefer iterators over manual loops - map, filter, fold compose and avoid bounds-check overhead +- &str over String for function parameters - more flexible, avoids forced allocation +- Send + Sync must be explicit for types crossing thread boundaries - derive or impl carefully +- Clone inside a loop is a red flag - restructure to borrow instead + +--- + +## Rust - Decisions + +Ownership ambiguity: shared ownership use Rc (single-thread) or Arc (multi-thread); ambiguous ownership use Cow<'a, T> via rust_get_practice cow-ambiguous-ownership + +Error handling: library crate use thiserror with derive macros; application binary use anyhow with context(); never panic in library code + +String handling: function parameter accepting string use &str; storing in struct use String; path handling use &Path not &str + +Performance: profile before optimizing; benchmark in --release only; borrow instead of clone in hot paths; static dispatch over dyn Trait when the type set is known + +--- + +## Design Tokens - MCP Tools + +| Tool | What it returns | +|------|----------------| +| `design_tokens_list_categories` | All 10 token categories with descriptions | +| `design_tokens_get_category("name")` | Full CSS + rules + gotchas for a token category | +| `design_tokens_get_color_ramp("name")` | Color ramp: stops, oklch values, semantic roles | +| `design_tokens_get_procedure(step)` | Step-by-step token build procedures (8 steps) | +| `design_tokens_get_gotchas` | All gotchas across every category and procedure | +| `design_tokens_generate("description")` | Complete Tailwind v4 token file from a palette description | +| `design_tokens_search("keyword")` | Search across all categories, ramps, and procedures | + +Token categories: colors, spacing, typography, component-sizing, border-radius, shadows-elevation, motion, z-index, opacity, grid-layout + +--- + +## Design Tokens - Critical Rules + +- @theme for primitives (static, compile-time) and @theme inline for semantic tokens (runtime-swappable, dark mode works) +- Never put runtime-swappable vars in plain @theme - dark mode will not work +- Three-layer architecture: @theme primitives โ†’ :root/:dark semantics โ†’ @theme inline utilities +- All spacing values must be multiples of 4px - never 6px, 10px, 14px, etc. +- --spacing in Tailwind v4 is the BASE MULTIPLIER (0.25rem) not an individual token - changing it scales all utilities +- Shadows must be oklch-tinted not rgba(0,0,0) - warm shadows on warm UIs +- Dark mode: disable all shadows, use progressively lighter bg-color per elevation level instead +- Status colors need L ~0.55 for 4.5:1 contrast with white foreground - run contrast audit after finalizing + +--- + +## Design Tokens - Decisions + +Building from scratch: follow the 8-step procedure via design_tokens_get_procedure; start with color primitives, then semantics, then @theme inline + +Color space: always OKLCH for new projects; to darken reduce L only; to desaturate reduce C only; hue stays consistent across a ramp + +Neutral temperature: commit to warm (H 50-80ยฐ) or cool (H 220-240ยฐ) - never mix; warm bg with cool borders breaks visual coherence + +Tailwind integration: use design_tokens_get_category colors for the full three-layer template; @theme inline maps CSS vars to bg-X text-X border-X utilities at runtime + +--- + +## UI/UX Principles - MCP Tools + +| Tool | What it returns | +|------|----------------| +| `ui_ux_list_principles` | All principles by domain | +| `ui_ux_get_principle("name")` | Full principle: rule, detail, CSS example, anti-patterns | +| `ui_ux_get_component_pattern("name")` | Component spec: variants, states, sizing rules, CSS | +| `ui_ux_get_checklist("domain")` | Pre-ship checklist per domain | +| `ui_ux_get_gotchas` | All common UI mistakes and their fixes | +| `ui_ux_search("keyword")` | Search across principles, patterns, and gotchas | + +Domains: typography, color, spacing, elevation, motion, accessibility, responsive, components + +Component patterns: button, card, badge, form-input + +--- + +## UI/UX - Critical Rules + +- Touch targets minimum 44x44px (WCAG 2.5.5) - visual size does not equal hit area; expand with padding or ::after +- prefers-reduced-motion is mandatory - place in @layer base with !important on * and pseudos +- Never outline: none without a custom focus style - keyboard users become lost +- Never color as the only state indicator - error needs icon + text alongside red color +- Body text line height minimum 1.6 - tight line height on body is a critical readability bug +- Pure black (#000) and pure white (#fff) are banned - use near-black and near-white oklch values +- Heading line height must be tight (1.05-1.2) - body line height (1.75) on headings looks wrong + +--- + +## UI/UX - Decisions + +Typography scale: use mathematical ratio (1.25 or 1.333); fluid headings with clamp(), fixed 16px body; details via ui_ux_get_principle type-scale + +Color system: OKLCH for new projects; warm or cool neutrals, never mixed; status colors need solid + soft variant; details via ui_ux_get_principle oklch-color-space + +Spacing: 4px grid, all values multiples of 4; space within groups smaller than space between groups; details via ui_ux_get_principle 4px-grid + +Elevation: 5 levels distinguished by bg-color not just borders; dark mode uses lighter bg per level, not shadows; details via ui_ux_get_principle 5-elevation-levels + +Motion: exits faster than entrances (subtract 50-100ms); ease-out entering, ease-in exiting, ease-in-out repositioning; never linear; details via ui_ux_get_principle duration-rules + +Pre-ship: run ui_ux_get_checklist for each domain before shipping a new component or page From eace6de408531f3335bc0ce2ad20901d0fa27bd2 Mon Sep 17 00:00:00 2001 From: Kailas Mahavarkar <66670953+KailasMahavarkar@users.noreply.github.com> Date: Wed, 8 Apr 2026 23:16:11 +0530 Subject: [PATCH 05/65] chore: rename project to hyperstack --- README.md | 34 +++++++++++++++++----------------- SKILL.md | 6 +++--- package-lock.json | 6 +++--- package.json | 4 ++-- scripts/start-mcp.sh | 4 ++-- src/index.ts | 2 +- 6 files changed, 28 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index e7bcad1..81564b4 100755 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@

-# unified-mcp +# hyperstack **One MCP server. Every library your AI needs. Zero conflicts.**

MIT - Stars + Stars TypeScript MCP 9 plugins @@ -39,7 +39,7 @@ This repository includes `SKILL.md` - a Claude Code skill that teaches your AI a To use the skill, clone this repository into your Claude Code skills directory: ```bash -git clone https://github.com/orkait/unified-mcp.git ~/.claude/skills/unified-mcp +git clone https://github.com/orkait/hyperstack.git ~/.claude/skills/hyperstack ``` --- @@ -253,10 +253,10 @@ git clone https://github.com/orkait/unified-mcp.git ~/.claude/skills/unified-mcp Build once, reuse forever. The wrapper script keeps **one** named container alive and runs each MCP session inside it via `docker exec` - no duplicate containers, no matter how many AI sessions are open. ```bash -git clone https://github.com/orkait/unified-mcp.git -cd unified-mcp +git clone https://github.com/orkait/hyperstack.git +cd hyperstack npm install && npm run build -docker build -t unified-mcp . +docker build -t hyperstack . ``` Add to your MCP config: @@ -267,8 +267,8 @@ Add to your MCP config: ```json { "mcpServers": { - "unified-mcp": { - "command": "/absolute/path/to/unified-mcp/scripts/start-mcp.sh" + "hyperstack": { + "command": "/absolute/path/to/hyperstack/scripts/start-mcp.sh" } } } @@ -282,8 +282,8 @@ Add to your MCP config: ```json { "mcpServers": { - "unified-mcp": { - "command": "/absolute/path/to/unified-mcp/scripts/start-mcp.sh" + "hyperstack": { + "command": "/absolute/path/to/hyperstack/scripts/start-mcp.sh" } } } @@ -296,17 +296,17 @@ Add to your MCP config: ### ๐Ÿ“ฆ Without Docker (Node directly) ```bash -git clone https://github.com/orkait/unified-mcp.git -cd unified-mcp +git clone https://github.com/orkait/hyperstack.git +cd hyperstack npm install && npm run build ``` ```json { "mcpServers": { - "unified-mcp": { + "hyperstack": { "command": "node", - "args": ["/absolute/path/to/unified-mcp/dist/index.js"] + "args": ["/absolute/path/to/hyperstack/dist/index.js"] } } } @@ -318,7 +318,7 @@ npm install && npm run build Running a separate MCP server per library means one Docker container per server at startup. Two libraries = two containers. Ten libraries = ten containers - every session. -`unified-mcp` runs everything in **one process**. All plugins share the same server, same connection, same container. +`hyperstack` runs everything in **one process**. All plugins share the same server, same connection, same container. Tool names are namespaced per plugin (`reactflow_list_apis` vs `motion_list_apis`) so there are zero naming conflicts - the LLM always knows which library a tool belongs to. @@ -341,8 +341,8 @@ Tool names are namespaced per plugin (`reactflow_list_apis` vs `motion_list_apis 3. Rebuild and redeploy: ```bash npm run build - docker build -t unified-mcp . - docker rm -f unified-mcp-daemon # next session recreates it + docker build -t hyperstack . + docker rm -f hyperstack-daemon # next session recreates it ``` No changes to your MCP config required. diff --git a/SKILL.md b/SKILL.md index 69bb9a9..133e646 100755 --- a/SKILL.md +++ b/SKILL.md @@ -1,5 +1,5 @@ --- -name: unified-skill +name: hyperstack description: >- Build flow-based UIs with React Flow v12, animate with Motion for React v12, add smooth scroll with Lenis, build with React 19 and Next.js App Router, @@ -67,9 +67,9 @@ activation: priority: high --- -# unified-mcp Skill +# hyperstack Skill -This skill relies on [unified-mcp](https://github.com/orkait/unified-mcp). Always use the MCP tools below for all API details, props, code examples, patterns, and transitions. This skill covers judgment, gotchas, and decisions only - unified-mcp provides all actual data. +This skill relies on [hyperstack](https://github.com/orkait/hyperstack). Always use the MCP tools below for all API details, props, code examples, patterns, and transitions. This skill covers judgment, gotchas, and decisions only - hyperstack provides all actual data. --- diff --git a/package-lock.json b/package-lock.json index 43c4be8..a725a08 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "@orkait-ai/unified-mcp", + "name": "@orkait-ai/hyperstack", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@orkait-ai/unified-mcp", + "name": "@orkait-ai/hyperstack", "version": "1.0.0", "license": "MIT", "dependencies": { @@ -13,7 +13,7 @@ "zod": "^3.23.0" }, "bin": { - "unified-mcp": "dist/index.js" + "hyperstack": "dist/index.js" }, "devDependencies": { "@types/node": "^20.0.0", diff --git a/package.json b/package.json index 1c11063..068c9f4 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { - "name": "@orkait-ai/unified-mcp", + "name": "@orkait-ai/hyperstack", "version": "1.0.0", "description": "Unified MCP server for frontend libraries: React Flow, Motion for React, and more", "main": "dist/index.js", "bin": { - "unified-mcp": "dist/index.js" + "hyperstack": "dist/index.js" }, "type": "module", "scripts": { diff --git a/scripts/start-mcp.sh b/scripts/start-mcp.sh index a500d34..e8795d7 100755 --- a/scripts/start-mcp.sh +++ b/scripts/start-mcp.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash set -euo pipefail -CONTAINER="unified-mcp-daemon" -IMAGE="unified-mcp" +CONTAINER="hyperstack-daemon" +IMAGE="hyperstack" LOCK_FILE="/tmp/${CONTAINER}.lock" # Use a file lock so concurrent session startups don't race to create the container. diff --git a/src/index.ts b/src/index.ts index 2adcb27..e9e7a4e 100755 --- a/src/index.ts +++ b/src/index.ts @@ -14,7 +14,7 @@ import { designTokensPlugin } from "./plugins/design-tokens/index.js"; import { uiUxPlugin } from "./plugins/ui-ux/index.js"; const server = new McpServer({ - name: "unified-mcp", + name: "hyperstack", version: "1.0.0", }); From 702a2b3e16eed90c61612ad2af7e53b694c70f80 Mon Sep 17 00:00:00 2001 From: Kailas Mahavarkar <66670953+KailasMahavarkar@users.noreply.github.com> Date: Wed, 8 Apr 2026 23:37:38 +0530 Subject: [PATCH 06/65] refactor: encapsulate snippets inside plugins and remove dist compilation --- Dockerfile | 7 +- README.md | 41 +- package-lock.json | 543 ++++++++++++++++++ package.json | 15 +- scripts/start-mcp.sh | 2 +- src/plugins/design-tokens/data.ts | 36 +- .../snippets/categories/border-radius.txt | 0 .../snippets/categories/colors.txt | 0 .../snippets/categories/component-sizing.txt | 0 .../snippets/categories/density.txt | 0 .../snippets/categories/motion.txt | 0 .../snippets/categories/opacity.txt | 0 .../snippets/categories/shadows-elevation.txt | 0 .../snippets/categories/spacing.txt | 0 .../snippets/categories/typography.txt | 0 .../snippets/categories/z-index.txt | 0 .../snippets/procedures/step-1-colors.txt | 0 .../snippets/procedures/step-2-spacing.txt | 0 .../snippets/procedures/step-3-typography.txt | 0 .../procedures/step-4-component-sizing.txt | 0 .../snippets/procedures/step-5-remaining.txt | 0 .../procedures/step-6-accessibility.txt | 0 .../snippets/procedures/step-7-validation.txt | 0 .../procedures/step-8-deliverables.txt | 0 .../snippets/references/color-roles.txt | 0 .../snippets/references/token-checklist.txt | 0 .../snippets/templates/colors-tailwind-v4.txt | 0 .../snippets/templates/motion.txt | 0 .../snippets/templates/spacing.txt | 0 .../snippets/templates/typography.txt | 0 src/plugins/design-tokens/tools/generate.ts | 8 +- src/plugins/echo/data.ts | 64 +-- .../plugins/echo/snippets/cheatsheet.txt | 0 .../echo/snippets/middleware/basic-auth.txt | 0 .../echo/snippets/middleware/body-limit.txt | 0 .../plugins/echo/snippets/middleware/cors.txt | 0 .../plugins/echo/snippets/middleware/csrf.txt | 0 .../plugins/echo/snippets/middleware/gzip.txt | 0 .../plugins/echo/snippets/middleware/jwt.txt | 0 .../echo/snippets/middleware/key-auth.txt | 0 .../echo/snippets/middleware/logger.txt | 0 .../echo/snippets/middleware/rate-limiter.txt | 0 .../echo/snippets/middleware/recover.txt | 0 .../echo/snippets/middleware/request-id.txt | 0 .../echo/snippets/middleware/secure.txt | 0 .../echo/snippets/middleware/timeout.txt | 0 .../echo/snippets/recipes/auto-tls.txt | 0 .../plugins/echo/snippets/recipes/cors.txt | 0 .../echo/snippets/recipes/crud-api.txt | 0 .../echo/snippets/recipes/embed-resources.txt | 0 .../echo/snippets/recipes/file-download.txt | 0 .../echo/snippets/recipes/file-upload.txt | 0 .../snippets/recipes/graceful-shutdown.txt | 0 .../echo/snippets/recipes/hello-world.txt | 0 .../plugins/echo/snippets/recipes/http2.txt | 0 .../plugins/echo/snippets/recipes/jsonp.txt | 0 .../echo/snippets/recipes/jwt-auth.txt | 0 .../snippets/recipes/middleware-chain.txt | 0 .../echo/snippets/recipes/reverse-proxy.txt | 0 .../echo/snippets/recipes/route-groups.txt | 0 .../plugins/echo/snippets/recipes/sse.txt | 0 .../snippets/recipes/streaming-response.txt | 0 .../snippets/recipes/subdomain-routing.txt | 0 .../plugins/echo/snippets/recipes/timeout.txt | 0 .../echo/snippets/recipes/websocket.txt | 0 src/plugins/golang/data.ts | 84 +-- .../plugins/golang/snippets/cheatsheet.txt | 0 .../golang/snippets/patterns/adapter.txt | 0 .../golang/snippets/patterns/command.txt | 0 .../patterns/consumer-side-interface-anti.txt | 0 .../patterns/consumer-side-interface.txt | 0 .../snippets/patterns/fan-out-fan-in.txt | 0 .../snippets/patterns/functional-options.txt | 0 .../patterns/middleware-decorator.txt | 0 .../golang/snippets/patterns/observer.txt | 0 .../golang/snippets/patterns/pipeline.txt | 0 .../golang/snippets/patterns/strategy.txt | 0 .../golang/snippets/patterns/worker-pool.txt | 0 .../practices/config-env-vars-bad.txt | 0 .../practices/config-env-vars-good.txt | 0 .../practices/constructor-pattern-good.txt | 0 .../practices/context-first-param-bad.txt | 0 .../practices/context-first-param-good.txt | 0 .../snippets/practices/crypto-rand-bad.txt | 0 .../snippets/practices/crypto-rand-good.txt | 0 .../practices/database-repository-bad.txt | 0 .../practices/database-repository-good.txt | 0 .../snippets/practices/errgroup-bad.txt | 0 .../snippets/practices/errgroup-good.txt | 0 .../snippets/practices/error-wrapping-bad.txt | 0 .../practices/error-wrapping-good.txt | 0 .../snippets/practices/errors-is-as-bad.txt | 0 .../snippets/practices/errors-is-as-good.txt | 0 .../snippets/practices/golangci-lint-good.txt | 0 .../practices/goroutine-lifecycle-bad.txt | 0 .../practices/goroutine-lifecycle-good.txt | 0 .../practices/graceful-shutdown-good.txt | 0 .../snippets/practices/handle-once-bad.txt | 0 .../snippets/practices/handle-once-good.txt | 0 .../practices/naming-conventions-bad.txt | 0 .../practices/naming-conventions-good.txt | 0 .../practices/parameterized-queries-bad.txt | 0 .../practices/parameterized-queries-good.txt | 0 .../practices/small-interfaces-bad.txt | 0 .../practices/small-interfaces-good.txt | 0 .../practices/structured-logging-bad.txt | 0 .../practices/structured-logging-good.txt | 0 .../practices/table-driven-tests-good.txt | 0 .../snippets/practices/thin-handlers-good.txt | 0 src/plugins/lenis/data.ts | 42 +- .../plugins/lenis/snippets/cheatsheet.txt | 0 .../lenis/snippets/css/prevent-scroll.txt | 0 .../plugins/lenis/snippets/css/required.txt | 0 .../examples/lenis-ref-imperative.txt | 0 .../examples/react-lenis-container.txt | 0 .../snippets/examples/react-lenis-ref.txt | 0 .../snippets/examples/react-lenis-root.txt | 0 .../snippets/examples/use-lenis-modal.txt | 0 .../snippets/examples/use-lenis-parallax.txt | 0 .../snippets/examples/use-lenis-progress.txt | 0 .../snippets/examples/use-lenis-scroll-to.txt | 0 .../lenis/snippets/options/horizontal.txt | 0 .../snippets/options/tuned-marketing.txt | 0 .../lenis/snippets/patterns/accessibility.txt | 0 .../snippets/patterns/custom-container.txt | 0 .../patterns/framer-motion-integration.txt | 0 .../lenis/snippets/patterns/full-page.txt | 0 .../snippets/patterns/gsap-integration.txt | 0 .../lenis/snippets/patterns/next-js.txt | 0 .../lenis/snippets/patterns/scroll-to-nav.txt | 0 .../lenis/snippets/recipes/back-to-top.txt | 0 .../snippets/recipes/direction-indicator.txt | 0 .../lenis/snippets/recipes/gsap-complete.txt | 0 .../recipes/horizontal-scroll-section.txt | 0 .../lenis/snippets/recipes/parallax-layer.txt | 0 .../snippets/recipes/scroll-locked-modal.txt | 0 .../snippets/recipes/scroll-progress-bar.txt | 0 .../lenis/snippets/usage/lenis-options.txt | 0 .../lenis/snippets/usage/lenis-ref.txt | 0 .../lenis/snippets/usage/react-lenis.txt | 0 .../lenis/snippets/usage/use-lenis.txt | 0 src/plugins/motion/data.ts | 158 ++--- .../modal-with-exit-animation.txt | 0 .../page-transitions-with-wait-mode.txt | 0 .../synchronized-accordion-items.txt | 0 .../LazyMotion/async-feature-loading.txt | 0 .../MotionConfig/global-spring-transition.txt | 0 .../MotionConfig/respect-reduced-motion.txt | 0 .../reorderable-list-with-exit-animations.txt | 0 .../animate/timeline-with-sequencing.txt | 0 .../hover/standalone-hover-with-react-ref.txt | 0 .../animate-counter-without-re-renders.txt | 0 .../examples/motion/animate-css-variables.txt | 0 .../examples/motion/basic-fade-in.txt | 0 .../examples/motion/drag-with-constraints.txt | 0 .../motion/dynamic-variants-with-custom.txt | 0 .../examples/motion/hover-and-tap.txt | 0 .../snippets/examples/motion/keyframes.txt | 0 .../examples/motion/layout-animation.txt | 0 .../scroll-image-reveal-with-clippath.txt | 0 .../motion/scroll-triggered-entrance.txt | 0 .../motion/shared-layout-with-layoutid.txt | 0 .../examples/motion/snap-to-grid-drag.txt | 0 .../examples/motion/svg-line-drawing.txt | 0 .../examples/motion/svg-path-morphing.txt | 0 .../motion/variants-with-orchestration.txt | 0 ...card-keyframe-start-from-current-value.txt | 0 .../stagger/staggered-list-with-easing.txt | 0 .../useAnimate/animation-sequence.txt | 0 .../exit-animation-with-usepresence.txt | 0 .../useAnimate/staggered-list-entrance.txt | 0 .../useAnimationFrame/continuous-rotation.txt | 0 .../useCycle/toggle-animation-state.txt | 0 .../useDragControls/custom-drag-handle.txt | 0 .../trigger-animation-when-in-view.txt | 0 .../useMotionTemplate/dynamic-gradient.txt | 0 .../useMotionValue/track-drag-position.txt | 0 .../detect-scroll-direction.txt | 0 .../pause-video-when-tab-hidden.txt | 0 .../useScroll/element-reveal-on-scroll.txt | 0 .../useScroll/horizontal-scroll-section.txt | 0 .../useScroll/scroll-progress-bar.txt | 0 .../examples/useSpring/mouse-follower.txt | 0 .../useSpring/smooth-scroll-tracking.txt | 0 .../examples/useTime/perpetual-rotation.txt | 0 .../useTransform/parallax-scroll-effect.txt | 0 .../scroll-linked-color-change.txt | 0 .../velocity-based-skew-on-drag.txt | 0 .../plugins/motion/snippets/transitions.txt | 0 .../motion/snippets/usage/AnimatePresence.txt | 0 .../motion/snippets/usage/LayoutGroup.txt | 0 .../motion/snippets/usage/LazyMotion.txt | 0 .../motion/snippets/usage/MotionConfig.txt | 0 .../motion/snippets/usage/Reorder.Group.txt | 0 .../motion/snippets/usage/Reorder.Item.txt | 0 .../plugins/motion/snippets/usage/animate.txt | 0 .../plugins/motion/snippets/usage/hover.txt | 0 .../plugins/motion/snippets/usage/inView.txt | 0 .../plugins/motion/snippets/usage/motion.txt | 0 .../plugins/motion/snippets/usage/press.txt | 0 .../plugins/motion/snippets/usage/scroll.txt | 0 .../plugins/motion/snippets/usage/stagger.txt | 0 .../motion/snippets/usage/useAnimate.txt | 0 .../snippets/usage/useAnimationFrame.txt | 0 .../motion/snippets/usage/useCycle.txt | 0 .../motion/snippets/usage/useDragControls.txt | 0 .../motion/snippets/usage/useInView.txt | 0 .../motion/snippets/usage/useIsPresent.txt | 0 .../snippets/usage/useMotionTemplate.txt | 0 .../motion/snippets/usage/useMotionValue.txt | 0 .../snippets/usage/useMotionValueEvent.txt | 0 .../motion/snippets/usage/usePageInView.txt | 0 .../motion/snippets/usage/usePresence.txt | 0 .../motion/snippets/usage/usePresenceData.txt | 0 .../snippets/usage/useReducedMotion.txt | 0 .../motion/snippets/usage/useScroll.txt | 0 .../motion/snippets/usage/useSpring.txt | 0 .../plugins/motion/snippets/usage/useTime.txt | 0 .../motion/snippets/usage/useTransform.txt | 0 .../motion/snippets/usage/useVelocity.txt | 0 .../motion/snippets/usage/useWillChange.txt | 0 src/plugins/react/data.ts | 24 +- .../plugins/react/snippets/cheatsheet.txt | 0 .../snippets/patterns/component-template.txt | 0 .../snippets/patterns/composition-anti.txt | 0 .../snippets/patterns/composition-pattern.txt | 0 .../snippets/patterns/data-fetching-anti.txt | 0 .../snippets/patterns/data-fetching-rsc.txt | 0 .../snippets/patterns/nextjs-metadata.txt | 0 .../react/snippets/patterns/rsc-anti.txt | 0 .../react/snippets/patterns/rsc-default.txt | 0 .../patterns/state-hierarchy-anti.txt | 0 .../snippets/patterns/state-hierarchy.txt | 0 .../snippets/patterns/suspense-boundary.txt | 0 .../react/snippets/patterns/zustand-store.txt | 0 src/plugins/reactflow/data/api-types.ts | 16 +- src/plugins/reactflow/data/components.ts | 48 +- src/plugins/reactflow/data/hooks.ts | 50 +- src/plugins/reactflow/data/migration.ts | 2 +- src/plugins/reactflow/data/patterns.ts | 34 +- src/plugins/reactflow/data/templates.ts | 6 +- src/plugins/reactflow/data/utilities.ts | 32 +- .../Background/cross-pattern-background.txt | 0 .../custom-control-with-layout-button.txt | 0 .../Controls/custom-control-button.txt | 0 .../edge-with-delete-button.txt | 0 .../examples/Handle/multiple-handles.txt | 0 .../examples/Node/typed-custom-node-data.txt | 0 .../resizable-node-with-handles.txt | 0 .../ReactFlow/controlled-flow-zustand.txt | 0 .../examples/ReactFlow/uncontrolled-flow.txt | 0 .../reconnectEdge/edge-reconnection.txt | 0 .../colorize-handle-during-connection.txt | 0 .../display-connected-node-data.txt | 0 .../auto-layout-on-mount.txt | 0 .../useNodesState/minimal-controlled-flow.txt | 0 .../useReactFlow/add-node-on-button-click.txt | 0 .../useReactFlow/delete-selected-elements.txt | 0 .../plugins/reactflow/snippets/migration.txt | 0 .../snippets/patterns/auto-layout-dagre.txt | 0 .../snippets/patterns/auto-layout-elk.txt | 0 .../patterns/auto-layout-on-mount.txt | 0 .../snippets/patterns/context-menu.txt | 0 .../snippets/patterns/copy-paste.txt | 0 .../patterns/custom-connection-line.txt | 0 .../reactflow/snippets/patterns/dark-mode.txt | 0 .../snippets/patterns/drag-and-drop.txt | 0 .../snippets/patterns/edge-reconnection.txt | 0 .../snippets/patterns/keyboard-shortcuts.txt | 0 .../snippets/patterns/performance.txt | 0 .../snippets/patterns/prevent-cycles.txt | 0 .../snippets/patterns/save-restore.txt | 0 .../reactflow/snippets/patterns/ssr.txt | 0 .../reactflow/snippets/patterns/subflows.txt | 0 .../reactflow/snippets/patterns/undo-redo.txt | 0 .../snippets/patterns/zustand-store.txt | 0 .../snippets/templates/custom-edge.txt | 0 .../snippets/templates/custom-node.txt | 0 .../snippets/templates/zustand-store.txt | 0 .../reactflow/snippets/usage/Background.txt | 0 .../reactflow/snippets/usage/BaseEdge.txt | 0 .../reactflow/snippets/usage/Connection.txt | 0 .../snippets/usage/ControlButton.txt | 0 .../reactflow/snippets/usage/Controls.txt | 0 .../plugins/reactflow/snippets/usage/Edge.txt | 0 .../snippets/usage/EdgeLabelRenderer.txt | 0 .../reactflow/snippets/usage/EdgeProps.txt | 0 .../reactflow/snippets/usage/EdgeText.txt | 0 .../reactflow/snippets/usage/EdgeToolbar.txt | 0 .../reactflow/snippets/usage/Handle.txt | 0 .../reactflow/snippets/usage/MiniMap.txt | 0 .../plugins/reactflow/snippets/usage/Node.txt | 0 .../reactflow/snippets/usage/NodeProps.txt | 0 .../snippets/usage/NodeResizeControl.txt | 0 .../reactflow/snippets/usage/NodeResizer.txt | 0 .../reactflow/snippets/usage/NodeToolbar.txt | 0 .../reactflow/snippets/usage/Panel.txt | 0 .../reactflow/snippets/usage/ReactFlow.txt | 0 .../snippets/usage/ReactFlowInstance.txt | 0 .../snippets/usage/ReactFlowProvider.txt | 0 .../reactflow/snippets/usage/Viewport.txt | 0 .../snippets/usage/ViewportPortal.txt | 0 .../reactflow/snippets/usage/addEdge.txt | 0 .../snippets/usage/applyEdgeChanges.txt | 0 .../snippets/usage/applyNodeChanges.txt | 0 .../snippets/usage/getBezierPath.txt | 0 .../snippets/usage/getConnectedEdges.txt | 0 .../reactflow/snippets/usage/getIncomers.txt | 0 .../snippets/usage/getNodesBounds.txt | 0 .../reactflow/snippets/usage/getOutgoers.txt | 0 .../snippets/usage/getSimpleBezierPath.txt | 0 .../snippets/usage/getSmoothStepPath.txt | 0 .../snippets/usage/getStraightPath.txt | 0 .../snippets/usage/getViewportForBounds.txt | 0 .../reactflow/snippets/usage/isEdge.txt | 0 .../reactflow/snippets/usage/isNode.txt | 0 .../snippets/usage/reconnectEdge.txt | 0 .../snippets/usage/useConnection.txt | 0 .../reactflow/snippets/usage/useEdges.txt | 0 .../snippets/usage/useEdgesState.txt | 0 .../snippets/usage/useHandleConnections.txt | 0 .../snippets/usage/useInternalNode.txt | 0 .../reactflow/snippets/usage/useKeyPress.txt | 0 .../snippets/usage/useNodeConnections.txt | 0 .../reactflow/snippets/usage/useNodeId.txt | 0 .../reactflow/snippets/usage/useNodes.txt | 0 .../reactflow/snippets/usage/useNodesData.txt | 0 .../snippets/usage/useNodesInitialized.txt | 0 .../snippets/usage/useNodesState.txt | 0 .../snippets/usage/useOnSelectionChange.txt | 0 .../snippets/usage/useOnViewportChange.txt | 0 .../reactflow/snippets/usage/useReactFlow.txt | 0 .../reactflow/snippets/usage/useStore.txt | 0 .../reactflow/snippets/usage/useStoreApi.txt | 0 .../snippets/usage/useUpdateNodeInternals.txt | 0 .../reactflow/snippets/usage/useViewport.txt | 0 src/plugins/rust/data.ts | 54 +- .../plugins/rust/snippets/cheatsheet.txt | 0 .../practices/avoid-clone-in-loops-bad.txt | 0 .../practices/avoid-clone-in-loops-good.txt | 0 .../practices/benchmark-release-bad.txt | 0 .../practices/benchmark-release-good.txt | 0 .../practices/borrow-over-clone-bad.txt | 0 .../practices/borrow-over-clone-good.txt | 0 .../snippets/practices/copy-by-value-good.txt | 0 .../cow-ambiguous-ownership-good.txt | 0 .../practices/descriptive-test-names-bad.txt | 0 .../practices/descriptive-test-names-good.txt | 0 .../snippets/practices/doc-tests-good.txt | 0 .../practices/expect-over-allow-bad.txt | 0 .../practices/expect-over-allow-good.txt | 0 .../practices/no-unwrap-in-prod-bad.txt | 0 .../practices/no-unwrap-in-prod-good.txt | 0 .../practices/one-assertion-per-test-good.txt | 0 .../practices/prefer-iterators-bad.txt | 0 .../practices/prefer-iterators-good.txt | 0 .../practices/result-not-panic-bad.txt | 0 .../practices/result-not-panic-good.txt | 0 .../rust/snippets/practices/send-sync-bad.txt | 0 .../snippets/practices/send-sync-good.txt | 0 .../static-over-dynamic-dispatch-good.txt | 0 .../practices/str-over-string-bad.txt | 0 .../practices/str-over-string-good.txt | 0 .../practices/thiserror-vs-anyhow-good.txt | 0 .../practices/type-state-pattern-good.txt | 0 src/plugins/rust/tools/cheatsheet.ts | 2 +- src/plugins/ui-ux/data.ts | 26 +- .../plugins/ui-ux/snippets/cheatsheet.txt | 0 .../ui-ux/snippets/components/badge.txt | 0 .../ui-ux/snippets/components/button.txt | 0 .../ui-ux/snippets/components/card.txt | 0 .../ui-ux/snippets/components/form-input.txt | 0 .../ui-ux/snippets/principles/4px-grid.txt | 0 .../principles/5-elevation-levels.txt | 0 .../ui-ux/snippets/principles/dark-mode.txt | 0 .../snippets/principles/easing-rules.txt | 0 .../snippets/principles/focus-management.txt | 0 .../snippets/principles/mobile-first.txt | 0 .../ui-ux/snippets/principles/oklch-color.txt | 0 .../ui-ux/snippets/principles/prose-width.txt | 0 .../snippets/principles/reduced-motion.txt | 0 .../principles/semantic-status-colors.txt | 0 .../snippets/principles/touch-targets.txt | 0 .../ui-ux/snippets/principles/type-scale.txt | 0 .../snippets/principles/warm-shadows.txt | 0 .../snippets/principles/warm-vs-cool.txt | 0 .../snippets/principles/wcag-contrast.txt | 0 .../snippets/references/elevation-table.txt | 0 .../snippets/references/motion-table.txt | 0 .../snippets/references/spacing-table.txt | 0 .../snippets/references/type-scale-table.txt | 0 .../ui-ux/snippets/references/wcag-table.txt | 0 src/shared/loader-factory.ts | 5 +- summary.md | 123 ++++ 394 files changed, 1045 insertions(+), 377 deletions(-) rename snippets/design-tokens/categories/border-radius.md => src/plugins/design-tokens/snippets/categories/border-radius.txt (100%) rename snippets/design-tokens/categories/colors.md => src/plugins/design-tokens/snippets/categories/colors.txt (100%) rename snippets/design-tokens/categories/component-sizing.md => src/plugins/design-tokens/snippets/categories/component-sizing.txt (100%) rename snippets/design-tokens/categories/density.md => src/plugins/design-tokens/snippets/categories/density.txt (100%) rename snippets/design-tokens/categories/motion.md => src/plugins/design-tokens/snippets/categories/motion.txt (100%) rename snippets/design-tokens/categories/opacity.md => src/plugins/design-tokens/snippets/categories/opacity.txt (100%) rename snippets/design-tokens/categories/shadows-elevation.md => src/plugins/design-tokens/snippets/categories/shadows-elevation.txt (100%) rename snippets/design-tokens/categories/spacing.md => src/plugins/design-tokens/snippets/categories/spacing.txt (100%) rename snippets/design-tokens/categories/typography.md => src/plugins/design-tokens/snippets/categories/typography.txt (100%) rename snippets/design-tokens/categories/z-index.md => src/plugins/design-tokens/snippets/categories/z-index.txt (100%) rename snippets/design-tokens/procedures/step-1-colors.md => src/plugins/design-tokens/snippets/procedures/step-1-colors.txt (100%) rename snippets/design-tokens/procedures/step-2-spacing.md => src/plugins/design-tokens/snippets/procedures/step-2-spacing.txt (100%) rename snippets/design-tokens/procedures/step-3-typography.md => src/plugins/design-tokens/snippets/procedures/step-3-typography.txt (100%) rename snippets/design-tokens/procedures/step-4-component-sizing.md => src/plugins/design-tokens/snippets/procedures/step-4-component-sizing.txt (100%) rename snippets/design-tokens/procedures/step-5-remaining.md => src/plugins/design-tokens/snippets/procedures/step-5-remaining.txt (100%) rename snippets/design-tokens/procedures/step-6-accessibility.md => src/plugins/design-tokens/snippets/procedures/step-6-accessibility.txt (100%) rename snippets/design-tokens/procedures/step-7-validation.md => src/plugins/design-tokens/snippets/procedures/step-7-validation.txt (100%) rename snippets/design-tokens/procedures/step-8-deliverables.md => src/plugins/design-tokens/snippets/procedures/step-8-deliverables.txt (100%) rename snippets/design-tokens/references/color-roles.md => src/plugins/design-tokens/snippets/references/color-roles.txt (100%) rename snippets/design-tokens/references/token-checklist.md => src/plugins/design-tokens/snippets/references/token-checklist.txt (100%) rename snippets/design-tokens/templates/colors-tailwind-v4.md => src/plugins/design-tokens/snippets/templates/colors-tailwind-v4.txt (100%) rename snippets/design-tokens/templates/motion.md => src/plugins/design-tokens/snippets/templates/motion.txt (100%) rename snippets/design-tokens/templates/spacing.md => src/plugins/design-tokens/snippets/templates/spacing.txt (100%) rename snippets/design-tokens/templates/typography.md => src/plugins/design-tokens/snippets/templates/typography.txt (100%) rename snippets/echo/cheatsheet.md => src/plugins/echo/snippets/cheatsheet.txt (100%) rename snippets/echo/middleware/basic-auth.md => src/plugins/echo/snippets/middleware/basic-auth.txt (100%) rename snippets/echo/middleware/body-limit.md => src/plugins/echo/snippets/middleware/body-limit.txt (100%) rename snippets/echo/middleware/cors.md => src/plugins/echo/snippets/middleware/cors.txt (100%) rename snippets/echo/middleware/csrf.md => src/plugins/echo/snippets/middleware/csrf.txt (100%) rename snippets/echo/middleware/gzip.md => src/plugins/echo/snippets/middleware/gzip.txt (100%) rename snippets/echo/middleware/jwt.md => src/plugins/echo/snippets/middleware/jwt.txt (100%) rename snippets/echo/middleware/key-auth.md => src/plugins/echo/snippets/middleware/key-auth.txt (100%) rename snippets/echo/middleware/logger.md => src/plugins/echo/snippets/middleware/logger.txt (100%) rename snippets/echo/middleware/rate-limiter.md => src/plugins/echo/snippets/middleware/rate-limiter.txt (100%) rename snippets/echo/middleware/recover.md => src/plugins/echo/snippets/middleware/recover.txt (100%) rename snippets/echo/middleware/request-id.md => src/plugins/echo/snippets/middleware/request-id.txt (100%) rename snippets/echo/middleware/secure.md => src/plugins/echo/snippets/middleware/secure.txt (100%) rename snippets/echo/middleware/timeout.md => src/plugins/echo/snippets/middleware/timeout.txt (100%) rename snippets/echo/recipes/auto-tls.md => src/plugins/echo/snippets/recipes/auto-tls.txt (100%) rename snippets/echo/recipes/cors.md => src/plugins/echo/snippets/recipes/cors.txt (100%) rename snippets/echo/recipes/crud-api.md => src/plugins/echo/snippets/recipes/crud-api.txt (100%) rename snippets/echo/recipes/embed-resources.md => src/plugins/echo/snippets/recipes/embed-resources.txt (100%) rename snippets/echo/recipes/file-download.md => src/plugins/echo/snippets/recipes/file-download.txt (100%) rename snippets/echo/recipes/file-upload.md => src/plugins/echo/snippets/recipes/file-upload.txt (100%) rename snippets/echo/recipes/graceful-shutdown.md => src/plugins/echo/snippets/recipes/graceful-shutdown.txt (100%) rename snippets/echo/recipes/hello-world.md => src/plugins/echo/snippets/recipes/hello-world.txt (100%) rename snippets/echo/recipes/http2.md => src/plugins/echo/snippets/recipes/http2.txt (100%) rename snippets/echo/recipes/jsonp.md => src/plugins/echo/snippets/recipes/jsonp.txt (100%) rename snippets/echo/recipes/jwt-auth.md => src/plugins/echo/snippets/recipes/jwt-auth.txt (100%) rename snippets/echo/recipes/middleware-chain.md => src/plugins/echo/snippets/recipes/middleware-chain.txt (100%) rename snippets/echo/recipes/reverse-proxy.md => src/plugins/echo/snippets/recipes/reverse-proxy.txt (100%) rename snippets/echo/recipes/route-groups.md => src/plugins/echo/snippets/recipes/route-groups.txt (100%) rename snippets/echo/recipes/sse.md => src/plugins/echo/snippets/recipes/sse.txt (100%) rename snippets/echo/recipes/streaming-response.md => src/plugins/echo/snippets/recipes/streaming-response.txt (100%) rename snippets/echo/recipes/subdomain-routing.md => src/plugins/echo/snippets/recipes/subdomain-routing.txt (100%) rename snippets/echo/recipes/timeout.md => src/plugins/echo/snippets/recipes/timeout.txt (100%) rename snippets/echo/recipes/websocket.md => src/plugins/echo/snippets/recipes/websocket.txt (100%) rename snippets/golang/cheatsheet.md => src/plugins/golang/snippets/cheatsheet.txt (100%) rename snippets/golang/patterns/adapter.md => src/plugins/golang/snippets/patterns/adapter.txt (100%) rename snippets/golang/patterns/command.md => src/plugins/golang/snippets/patterns/command.txt (100%) rename snippets/golang/patterns/consumer-side-interface-anti.md => src/plugins/golang/snippets/patterns/consumer-side-interface-anti.txt (100%) rename snippets/golang/patterns/consumer-side-interface.md => src/plugins/golang/snippets/patterns/consumer-side-interface.txt (100%) rename snippets/golang/patterns/fan-out-fan-in.md => src/plugins/golang/snippets/patterns/fan-out-fan-in.txt (100%) rename snippets/golang/patterns/functional-options.md => src/plugins/golang/snippets/patterns/functional-options.txt (100%) rename snippets/golang/patterns/middleware-decorator.md => src/plugins/golang/snippets/patterns/middleware-decorator.txt (100%) rename snippets/golang/patterns/observer.md => src/plugins/golang/snippets/patterns/observer.txt (100%) rename snippets/golang/patterns/pipeline.md => src/plugins/golang/snippets/patterns/pipeline.txt (100%) rename snippets/golang/patterns/strategy.md => src/plugins/golang/snippets/patterns/strategy.txt (100%) rename snippets/golang/patterns/worker-pool.md => src/plugins/golang/snippets/patterns/worker-pool.txt (100%) rename snippets/golang/practices/config-env-vars-bad.md => src/plugins/golang/snippets/practices/config-env-vars-bad.txt (100%) rename snippets/golang/practices/config-env-vars-good.md => src/plugins/golang/snippets/practices/config-env-vars-good.txt (100%) rename snippets/golang/practices/constructor-pattern-good.md => src/plugins/golang/snippets/practices/constructor-pattern-good.txt (100%) rename snippets/golang/practices/context-first-param-bad.md => src/plugins/golang/snippets/practices/context-first-param-bad.txt (100%) rename snippets/golang/practices/context-first-param-good.md => src/plugins/golang/snippets/practices/context-first-param-good.txt (100%) rename snippets/golang/practices/crypto-rand-bad.md => src/plugins/golang/snippets/practices/crypto-rand-bad.txt (100%) rename snippets/golang/practices/crypto-rand-good.md => src/plugins/golang/snippets/practices/crypto-rand-good.txt (100%) rename snippets/golang/practices/database-repository-bad.md => src/plugins/golang/snippets/practices/database-repository-bad.txt (100%) rename snippets/golang/practices/database-repository-good.md => src/plugins/golang/snippets/practices/database-repository-good.txt (100%) rename snippets/golang/practices/errgroup-bad.md => src/plugins/golang/snippets/practices/errgroup-bad.txt (100%) rename snippets/golang/practices/errgroup-good.md => src/plugins/golang/snippets/practices/errgroup-good.txt (100%) rename snippets/golang/practices/error-wrapping-bad.md => src/plugins/golang/snippets/practices/error-wrapping-bad.txt (100%) rename snippets/golang/practices/error-wrapping-good.md => src/plugins/golang/snippets/practices/error-wrapping-good.txt (100%) rename snippets/golang/practices/errors-is-as-bad.md => src/plugins/golang/snippets/practices/errors-is-as-bad.txt (100%) rename snippets/golang/practices/errors-is-as-good.md => src/plugins/golang/snippets/practices/errors-is-as-good.txt (100%) rename snippets/golang/practices/golangci-lint-good.md => src/plugins/golang/snippets/practices/golangci-lint-good.txt (100%) rename snippets/golang/practices/goroutine-lifecycle-bad.md => src/plugins/golang/snippets/practices/goroutine-lifecycle-bad.txt (100%) rename snippets/golang/practices/goroutine-lifecycle-good.md => src/plugins/golang/snippets/practices/goroutine-lifecycle-good.txt (100%) rename snippets/golang/practices/graceful-shutdown-good.md => src/plugins/golang/snippets/practices/graceful-shutdown-good.txt (100%) rename snippets/golang/practices/handle-once-bad.md => src/plugins/golang/snippets/practices/handle-once-bad.txt (100%) rename snippets/golang/practices/handle-once-good.md => src/plugins/golang/snippets/practices/handle-once-good.txt (100%) rename snippets/golang/practices/naming-conventions-bad.md => src/plugins/golang/snippets/practices/naming-conventions-bad.txt (100%) rename snippets/golang/practices/naming-conventions-good.md => src/plugins/golang/snippets/practices/naming-conventions-good.txt (100%) rename snippets/golang/practices/parameterized-queries-bad.md => src/plugins/golang/snippets/practices/parameterized-queries-bad.txt (100%) rename snippets/golang/practices/parameterized-queries-good.md => src/plugins/golang/snippets/practices/parameterized-queries-good.txt (100%) rename snippets/golang/practices/small-interfaces-bad.md => src/plugins/golang/snippets/practices/small-interfaces-bad.txt (100%) rename snippets/golang/practices/small-interfaces-good.md => src/plugins/golang/snippets/practices/small-interfaces-good.txt (100%) rename snippets/golang/practices/structured-logging-bad.md => src/plugins/golang/snippets/practices/structured-logging-bad.txt (100%) rename snippets/golang/practices/structured-logging-good.md => src/plugins/golang/snippets/practices/structured-logging-good.txt (100%) rename snippets/golang/practices/table-driven-tests-good.md => src/plugins/golang/snippets/practices/table-driven-tests-good.txt (100%) rename snippets/golang/practices/thin-handlers-good.md => src/plugins/golang/snippets/practices/thin-handlers-good.txt (100%) rename snippets/lenis/cheatsheet.md => src/plugins/lenis/snippets/cheatsheet.txt (100%) rename snippets/lenis/css/prevent-scroll.md => src/plugins/lenis/snippets/css/prevent-scroll.txt (100%) rename snippets/lenis/css/required.md => src/plugins/lenis/snippets/css/required.txt (100%) rename snippets/lenis/examples/lenis-ref-imperative.md => src/plugins/lenis/snippets/examples/lenis-ref-imperative.txt (100%) rename snippets/lenis/examples/react-lenis-container.md => src/plugins/lenis/snippets/examples/react-lenis-container.txt (100%) rename snippets/lenis/examples/react-lenis-ref.md => src/plugins/lenis/snippets/examples/react-lenis-ref.txt (100%) rename snippets/lenis/examples/react-lenis-root.md => src/plugins/lenis/snippets/examples/react-lenis-root.txt (100%) rename snippets/lenis/examples/use-lenis-modal.md => src/plugins/lenis/snippets/examples/use-lenis-modal.txt (100%) rename snippets/lenis/examples/use-lenis-parallax.md => src/plugins/lenis/snippets/examples/use-lenis-parallax.txt (100%) rename snippets/lenis/examples/use-lenis-progress.md => src/plugins/lenis/snippets/examples/use-lenis-progress.txt (100%) rename snippets/lenis/examples/use-lenis-scroll-to.md => src/plugins/lenis/snippets/examples/use-lenis-scroll-to.txt (100%) rename snippets/lenis/options/horizontal.md => src/plugins/lenis/snippets/options/horizontal.txt (100%) rename snippets/lenis/options/tuned-marketing.md => src/plugins/lenis/snippets/options/tuned-marketing.txt (100%) rename snippets/lenis/patterns/accessibility.md => src/plugins/lenis/snippets/patterns/accessibility.txt (100%) rename snippets/lenis/patterns/custom-container.md => src/plugins/lenis/snippets/patterns/custom-container.txt (100%) rename snippets/lenis/patterns/framer-motion-integration.md => src/plugins/lenis/snippets/patterns/framer-motion-integration.txt (100%) rename snippets/lenis/patterns/full-page.md => src/plugins/lenis/snippets/patterns/full-page.txt (100%) rename snippets/lenis/patterns/gsap-integration.md => src/plugins/lenis/snippets/patterns/gsap-integration.txt (100%) rename snippets/lenis/patterns/next-js.md => src/plugins/lenis/snippets/patterns/next-js.txt (100%) rename snippets/lenis/patterns/scroll-to-nav.md => src/plugins/lenis/snippets/patterns/scroll-to-nav.txt (100%) rename snippets/lenis/recipes/back-to-top.md => src/plugins/lenis/snippets/recipes/back-to-top.txt (100%) rename snippets/lenis/recipes/direction-indicator.md => src/plugins/lenis/snippets/recipes/direction-indicator.txt (100%) rename snippets/lenis/recipes/gsap-complete.md => src/plugins/lenis/snippets/recipes/gsap-complete.txt (100%) rename snippets/lenis/recipes/horizontal-scroll-section.md => src/plugins/lenis/snippets/recipes/horizontal-scroll-section.txt (100%) rename snippets/lenis/recipes/parallax-layer.md => src/plugins/lenis/snippets/recipes/parallax-layer.txt (100%) rename snippets/lenis/recipes/scroll-locked-modal.md => src/plugins/lenis/snippets/recipes/scroll-locked-modal.txt (100%) rename snippets/lenis/recipes/scroll-progress-bar.md => src/plugins/lenis/snippets/recipes/scroll-progress-bar.txt (100%) rename snippets/lenis/usage/lenis-options.md => src/plugins/lenis/snippets/usage/lenis-options.txt (100%) rename snippets/lenis/usage/lenis-ref.md => src/plugins/lenis/snippets/usage/lenis-ref.txt (100%) rename snippets/lenis/usage/react-lenis.md => src/plugins/lenis/snippets/usage/react-lenis.txt (100%) rename snippets/lenis/usage/use-lenis.md => src/plugins/lenis/snippets/usage/use-lenis.txt (100%) rename snippets/motion/examples/AnimatePresence/modal-with-exit-animation.md => src/plugins/motion/snippets/examples/AnimatePresence/modal-with-exit-animation.txt (100%) rename snippets/motion/examples/AnimatePresence/page-transitions-with-wait-mode.md => src/plugins/motion/snippets/examples/AnimatePresence/page-transitions-with-wait-mode.txt (100%) rename snippets/motion/examples/LayoutGroup/synchronized-accordion-items.md => src/plugins/motion/snippets/examples/LayoutGroup/synchronized-accordion-items.txt (100%) rename snippets/motion/examples/LazyMotion/async-feature-loading.md => src/plugins/motion/snippets/examples/LazyMotion/async-feature-loading.txt (100%) rename snippets/motion/examples/MotionConfig/global-spring-transition.md => src/plugins/motion/snippets/examples/MotionConfig/global-spring-transition.txt (100%) rename snippets/motion/examples/MotionConfig/respect-reduced-motion.md => src/plugins/motion/snippets/examples/MotionConfig/respect-reduced-motion.txt (100%) rename snippets/motion/examples/Reorder.Group/reorderable-list-with-exit-animations.md => src/plugins/motion/snippets/examples/Reorder.Group/reorderable-list-with-exit-animations.txt (100%) rename snippets/motion/examples/animate/timeline-with-sequencing.md => src/plugins/motion/snippets/examples/animate/timeline-with-sequencing.txt (100%) rename snippets/motion/examples/hover/standalone-hover-with-react-ref.md => src/plugins/motion/snippets/examples/hover/standalone-hover-with-react-ref.txt (100%) rename snippets/motion/examples/motion/animate-counter-without-re-renders.md => src/plugins/motion/snippets/examples/motion/animate-counter-without-re-renders.txt (100%) rename snippets/motion/examples/motion/animate-css-variables.md => src/plugins/motion/snippets/examples/motion/animate-css-variables.txt (100%) rename snippets/motion/examples/motion/basic-fade-in.md => src/plugins/motion/snippets/examples/motion/basic-fade-in.txt (100%) rename snippets/motion/examples/motion/drag-with-constraints.md => src/plugins/motion/snippets/examples/motion/drag-with-constraints.txt (100%) rename snippets/motion/examples/motion/dynamic-variants-with-custom.md => src/plugins/motion/snippets/examples/motion/dynamic-variants-with-custom.txt (100%) rename snippets/motion/examples/motion/hover-and-tap.md => src/plugins/motion/snippets/examples/motion/hover-and-tap.txt (100%) rename snippets/motion/examples/motion/keyframes.md => src/plugins/motion/snippets/examples/motion/keyframes.txt (100%) rename snippets/motion/examples/motion/layout-animation.md => src/plugins/motion/snippets/examples/motion/layout-animation.txt (100%) rename snippets/motion/examples/motion/scroll-image-reveal-with-clippath.md => src/plugins/motion/snippets/examples/motion/scroll-image-reveal-with-clippath.txt (100%) rename snippets/motion/examples/motion/scroll-triggered-entrance.md => src/plugins/motion/snippets/examples/motion/scroll-triggered-entrance.txt (100%) rename snippets/motion/examples/motion/shared-layout-with-layoutid.md => src/plugins/motion/snippets/examples/motion/shared-layout-with-layoutid.txt (100%) rename snippets/motion/examples/motion/snap-to-grid-drag.md => src/plugins/motion/snippets/examples/motion/snap-to-grid-drag.txt (100%) rename snippets/motion/examples/motion/svg-line-drawing.md => src/plugins/motion/snippets/examples/motion/svg-line-drawing.txt (100%) rename snippets/motion/examples/motion/svg-path-morphing.md => src/plugins/motion/snippets/examples/motion/svg-path-morphing.txt (100%) rename snippets/motion/examples/motion/variants-with-orchestration.md => src/plugins/motion/snippets/examples/motion/variants-with-orchestration.txt (100%) rename snippets/motion/examples/motion/wildcard-keyframe-start-from-current-value.md => src/plugins/motion/snippets/examples/motion/wildcard-keyframe-start-from-current-value.txt (100%) rename snippets/motion/examples/stagger/staggered-list-with-easing.md => src/plugins/motion/snippets/examples/stagger/staggered-list-with-easing.txt (100%) rename snippets/motion/examples/useAnimate/animation-sequence.md => src/plugins/motion/snippets/examples/useAnimate/animation-sequence.txt (100%) rename snippets/motion/examples/useAnimate/exit-animation-with-usepresence.md => src/plugins/motion/snippets/examples/useAnimate/exit-animation-with-usepresence.txt (100%) rename snippets/motion/examples/useAnimate/staggered-list-entrance.md => src/plugins/motion/snippets/examples/useAnimate/staggered-list-entrance.txt (100%) rename snippets/motion/examples/useAnimationFrame/continuous-rotation.md => src/plugins/motion/snippets/examples/useAnimationFrame/continuous-rotation.txt (100%) rename snippets/motion/examples/useCycle/toggle-animation-state.md => src/plugins/motion/snippets/examples/useCycle/toggle-animation-state.txt (100%) rename snippets/motion/examples/useDragControls/custom-drag-handle.md => src/plugins/motion/snippets/examples/useDragControls/custom-drag-handle.txt (100%) rename snippets/motion/examples/useInView/trigger-animation-when-in-view.md => src/plugins/motion/snippets/examples/useInView/trigger-animation-when-in-view.txt (100%) rename snippets/motion/examples/useMotionTemplate/dynamic-gradient.md => src/plugins/motion/snippets/examples/useMotionTemplate/dynamic-gradient.txt (100%) rename snippets/motion/examples/useMotionValue/track-drag-position.md => src/plugins/motion/snippets/examples/useMotionValue/track-drag-position.txt (100%) rename snippets/motion/examples/useMotionValueEvent/detect-scroll-direction.md => src/plugins/motion/snippets/examples/useMotionValueEvent/detect-scroll-direction.txt (100%) rename snippets/motion/examples/usePageInView/pause-video-when-tab-hidden.md => src/plugins/motion/snippets/examples/usePageInView/pause-video-when-tab-hidden.txt (100%) rename snippets/motion/examples/useScroll/element-reveal-on-scroll.md => src/plugins/motion/snippets/examples/useScroll/element-reveal-on-scroll.txt (100%) rename snippets/motion/examples/useScroll/horizontal-scroll-section.md => src/plugins/motion/snippets/examples/useScroll/horizontal-scroll-section.txt (100%) rename snippets/motion/examples/useScroll/scroll-progress-bar.md => src/plugins/motion/snippets/examples/useScroll/scroll-progress-bar.txt (100%) rename snippets/motion/examples/useSpring/mouse-follower.md => src/plugins/motion/snippets/examples/useSpring/mouse-follower.txt (100%) rename snippets/motion/examples/useSpring/smooth-scroll-tracking.md => src/plugins/motion/snippets/examples/useSpring/smooth-scroll-tracking.txt (100%) rename snippets/motion/examples/useTime/perpetual-rotation.md => src/plugins/motion/snippets/examples/useTime/perpetual-rotation.txt (100%) rename snippets/motion/examples/useTransform/parallax-scroll-effect.md => src/plugins/motion/snippets/examples/useTransform/parallax-scroll-effect.txt (100%) rename snippets/motion/examples/useTransform/scroll-linked-color-change.md => src/plugins/motion/snippets/examples/useTransform/scroll-linked-color-change.txt (100%) rename snippets/motion/examples/useVelocity/velocity-based-skew-on-drag.md => src/plugins/motion/snippets/examples/useVelocity/velocity-based-skew-on-drag.txt (100%) rename snippets/motion/transitions.md => src/plugins/motion/snippets/transitions.txt (100%) rename snippets/motion/usage/AnimatePresence.md => src/plugins/motion/snippets/usage/AnimatePresence.txt (100%) rename snippets/motion/usage/LayoutGroup.md => src/plugins/motion/snippets/usage/LayoutGroup.txt (100%) rename snippets/motion/usage/LazyMotion.md => src/plugins/motion/snippets/usage/LazyMotion.txt (100%) rename snippets/motion/usage/MotionConfig.md => src/plugins/motion/snippets/usage/MotionConfig.txt (100%) rename snippets/motion/usage/Reorder.Group.md => src/plugins/motion/snippets/usage/Reorder.Group.txt (100%) rename snippets/motion/usage/Reorder.Item.md => src/plugins/motion/snippets/usage/Reorder.Item.txt (100%) rename snippets/motion/usage/animate.md => src/plugins/motion/snippets/usage/animate.txt (100%) rename snippets/motion/usage/hover.md => src/plugins/motion/snippets/usage/hover.txt (100%) rename snippets/motion/usage/inView.md => src/plugins/motion/snippets/usage/inView.txt (100%) rename snippets/motion/usage/motion.md => src/plugins/motion/snippets/usage/motion.txt (100%) rename snippets/motion/usage/press.md => src/plugins/motion/snippets/usage/press.txt (100%) rename snippets/motion/usage/scroll.md => src/plugins/motion/snippets/usage/scroll.txt (100%) rename snippets/motion/usage/stagger.md => src/plugins/motion/snippets/usage/stagger.txt (100%) rename snippets/motion/usage/useAnimate.md => src/plugins/motion/snippets/usage/useAnimate.txt (100%) rename snippets/motion/usage/useAnimationFrame.md => src/plugins/motion/snippets/usage/useAnimationFrame.txt (100%) rename snippets/motion/usage/useCycle.md => src/plugins/motion/snippets/usage/useCycle.txt (100%) rename snippets/motion/usage/useDragControls.md => src/plugins/motion/snippets/usage/useDragControls.txt (100%) rename snippets/motion/usage/useInView.md => src/plugins/motion/snippets/usage/useInView.txt (100%) rename snippets/motion/usage/useIsPresent.md => src/plugins/motion/snippets/usage/useIsPresent.txt (100%) rename snippets/motion/usage/useMotionTemplate.md => src/plugins/motion/snippets/usage/useMotionTemplate.txt (100%) rename snippets/motion/usage/useMotionValue.md => src/plugins/motion/snippets/usage/useMotionValue.txt (100%) rename snippets/motion/usage/useMotionValueEvent.md => src/plugins/motion/snippets/usage/useMotionValueEvent.txt (100%) rename snippets/motion/usage/usePageInView.md => src/plugins/motion/snippets/usage/usePageInView.txt (100%) rename snippets/motion/usage/usePresence.md => src/plugins/motion/snippets/usage/usePresence.txt (100%) rename snippets/motion/usage/usePresenceData.md => src/plugins/motion/snippets/usage/usePresenceData.txt (100%) rename snippets/motion/usage/useReducedMotion.md => src/plugins/motion/snippets/usage/useReducedMotion.txt (100%) rename snippets/motion/usage/useScroll.md => src/plugins/motion/snippets/usage/useScroll.txt (100%) rename snippets/motion/usage/useSpring.md => src/plugins/motion/snippets/usage/useSpring.txt (100%) rename snippets/motion/usage/useTime.md => src/plugins/motion/snippets/usage/useTime.txt (100%) rename snippets/motion/usage/useTransform.md => src/plugins/motion/snippets/usage/useTransform.txt (100%) rename snippets/motion/usage/useVelocity.md => src/plugins/motion/snippets/usage/useVelocity.txt (100%) rename snippets/motion/usage/useWillChange.md => src/plugins/motion/snippets/usage/useWillChange.txt (100%) rename snippets/react/cheatsheet.md => src/plugins/react/snippets/cheatsheet.txt (100%) rename snippets/react/patterns/component-template.md => src/plugins/react/snippets/patterns/component-template.txt (100%) rename snippets/react/patterns/composition-anti.md => src/plugins/react/snippets/patterns/composition-anti.txt (100%) rename snippets/react/patterns/composition-pattern.md => src/plugins/react/snippets/patterns/composition-pattern.txt (100%) rename snippets/react/patterns/data-fetching-anti.md => src/plugins/react/snippets/patterns/data-fetching-anti.txt (100%) rename snippets/react/patterns/data-fetching-rsc.md => src/plugins/react/snippets/patterns/data-fetching-rsc.txt (100%) rename snippets/react/patterns/nextjs-metadata.md => src/plugins/react/snippets/patterns/nextjs-metadata.txt (100%) rename snippets/react/patterns/rsc-anti.md => src/plugins/react/snippets/patterns/rsc-anti.txt (100%) rename snippets/react/patterns/rsc-default.md => src/plugins/react/snippets/patterns/rsc-default.txt (100%) rename snippets/react/patterns/state-hierarchy-anti.md => src/plugins/react/snippets/patterns/state-hierarchy-anti.txt (100%) rename snippets/react/patterns/state-hierarchy.md => src/plugins/react/snippets/patterns/state-hierarchy.txt (100%) rename snippets/react/patterns/suspense-boundary.md => src/plugins/react/snippets/patterns/suspense-boundary.txt (100%) rename snippets/react/patterns/zustand-store.md => src/plugins/react/snippets/patterns/zustand-store.txt (100%) rename snippets/reactflow/examples/Background/cross-pattern-background.md => src/plugins/reactflow/snippets/examples/Background/cross-pattern-background.txt (100%) rename snippets/reactflow/examples/ControlButton/custom-control-with-layout-button.md => src/plugins/reactflow/snippets/examples/ControlButton/custom-control-with-layout-button.txt (100%) rename snippets/reactflow/examples/Controls/custom-control-button.md => src/plugins/reactflow/snippets/examples/Controls/custom-control-button.txt (100%) rename snippets/reactflow/examples/EdgeLabelRenderer/edge-with-delete-button.md => src/plugins/reactflow/snippets/examples/EdgeLabelRenderer/edge-with-delete-button.txt (100%) rename snippets/reactflow/examples/Handle/multiple-handles.md => src/plugins/reactflow/snippets/examples/Handle/multiple-handles.txt (100%) rename snippets/reactflow/examples/Node/typed-custom-node-data.md => src/plugins/reactflow/snippets/examples/Node/typed-custom-node-data.txt (100%) rename snippets/reactflow/examples/NodeResizer/resizable-node-with-handles.md => src/plugins/reactflow/snippets/examples/NodeResizer/resizable-node-with-handles.txt (100%) rename snippets/reactflow/examples/ReactFlow/controlled-flow-zustand.md => src/plugins/reactflow/snippets/examples/ReactFlow/controlled-flow-zustand.txt (100%) rename snippets/reactflow/examples/ReactFlow/uncontrolled-flow.md => src/plugins/reactflow/snippets/examples/ReactFlow/uncontrolled-flow.txt (100%) rename snippets/reactflow/examples/reconnectEdge/edge-reconnection.md => src/plugins/reactflow/snippets/examples/reconnectEdge/edge-reconnection.txt (100%) rename snippets/reactflow/examples/useConnection/colorize-handle-during-connection.md => src/plugins/reactflow/snippets/examples/useConnection/colorize-handle-during-connection.txt (100%) rename snippets/reactflow/examples/useNodesData/display-connected-node-data.md => src/plugins/reactflow/snippets/examples/useNodesData/display-connected-node-data.txt (100%) rename snippets/reactflow/examples/useNodesInitialized/auto-layout-on-mount.md => src/plugins/reactflow/snippets/examples/useNodesInitialized/auto-layout-on-mount.txt (100%) rename snippets/reactflow/examples/useNodesState/minimal-controlled-flow.md => src/plugins/reactflow/snippets/examples/useNodesState/minimal-controlled-flow.txt (100%) rename snippets/reactflow/examples/useReactFlow/add-node-on-button-click.md => src/plugins/reactflow/snippets/examples/useReactFlow/add-node-on-button-click.txt (100%) rename snippets/reactflow/examples/useReactFlow/delete-selected-elements.md => src/plugins/reactflow/snippets/examples/useReactFlow/delete-selected-elements.txt (100%) rename snippets/reactflow/migration.md => src/plugins/reactflow/snippets/migration.txt (100%) rename snippets/reactflow/patterns/auto-layout-dagre.md => src/plugins/reactflow/snippets/patterns/auto-layout-dagre.txt (100%) rename snippets/reactflow/patterns/auto-layout-elk.md => src/plugins/reactflow/snippets/patterns/auto-layout-elk.txt (100%) rename snippets/reactflow/patterns/auto-layout-on-mount.md => src/plugins/reactflow/snippets/patterns/auto-layout-on-mount.txt (100%) rename snippets/reactflow/patterns/context-menu.md => src/plugins/reactflow/snippets/patterns/context-menu.txt (100%) rename snippets/reactflow/patterns/copy-paste.md => src/plugins/reactflow/snippets/patterns/copy-paste.txt (100%) rename snippets/reactflow/patterns/custom-connection-line.md => src/plugins/reactflow/snippets/patterns/custom-connection-line.txt (100%) rename snippets/reactflow/patterns/dark-mode.md => src/plugins/reactflow/snippets/patterns/dark-mode.txt (100%) rename snippets/reactflow/patterns/drag-and-drop.md => src/plugins/reactflow/snippets/patterns/drag-and-drop.txt (100%) rename snippets/reactflow/patterns/edge-reconnection.md => src/plugins/reactflow/snippets/patterns/edge-reconnection.txt (100%) rename snippets/reactflow/patterns/keyboard-shortcuts.md => src/plugins/reactflow/snippets/patterns/keyboard-shortcuts.txt (100%) rename snippets/reactflow/patterns/performance.md => src/plugins/reactflow/snippets/patterns/performance.txt (100%) rename snippets/reactflow/patterns/prevent-cycles.md => src/plugins/reactflow/snippets/patterns/prevent-cycles.txt (100%) rename snippets/reactflow/patterns/save-restore.md => src/plugins/reactflow/snippets/patterns/save-restore.txt (100%) rename snippets/reactflow/patterns/ssr.md => src/plugins/reactflow/snippets/patterns/ssr.txt (100%) rename snippets/reactflow/patterns/subflows.md => src/plugins/reactflow/snippets/patterns/subflows.txt (100%) rename snippets/reactflow/patterns/undo-redo.md => src/plugins/reactflow/snippets/patterns/undo-redo.txt (100%) rename snippets/reactflow/patterns/zustand-store.md => src/plugins/reactflow/snippets/patterns/zustand-store.txt (100%) rename snippets/reactflow/templates/custom-edge.md => src/plugins/reactflow/snippets/templates/custom-edge.txt (100%) rename snippets/reactflow/templates/custom-node.md => src/plugins/reactflow/snippets/templates/custom-node.txt (100%) rename snippets/reactflow/templates/zustand-store.md => src/plugins/reactflow/snippets/templates/zustand-store.txt (100%) rename snippets/reactflow/usage/Background.md => src/plugins/reactflow/snippets/usage/Background.txt (100%) rename snippets/reactflow/usage/BaseEdge.md => src/plugins/reactflow/snippets/usage/BaseEdge.txt (100%) rename snippets/reactflow/usage/Connection.md => src/plugins/reactflow/snippets/usage/Connection.txt (100%) rename snippets/reactflow/usage/ControlButton.md => src/plugins/reactflow/snippets/usage/ControlButton.txt (100%) rename snippets/reactflow/usage/Controls.md => src/plugins/reactflow/snippets/usage/Controls.txt (100%) rename snippets/reactflow/usage/Edge.md => src/plugins/reactflow/snippets/usage/Edge.txt (100%) rename snippets/reactflow/usage/EdgeLabelRenderer.md => src/plugins/reactflow/snippets/usage/EdgeLabelRenderer.txt (100%) rename snippets/reactflow/usage/EdgeProps.md => src/plugins/reactflow/snippets/usage/EdgeProps.txt (100%) rename snippets/reactflow/usage/EdgeText.md => src/plugins/reactflow/snippets/usage/EdgeText.txt (100%) rename snippets/reactflow/usage/EdgeToolbar.md => src/plugins/reactflow/snippets/usage/EdgeToolbar.txt (100%) rename snippets/reactflow/usage/Handle.md => src/plugins/reactflow/snippets/usage/Handle.txt (100%) rename snippets/reactflow/usage/MiniMap.md => src/plugins/reactflow/snippets/usage/MiniMap.txt (100%) rename snippets/reactflow/usage/Node.md => src/plugins/reactflow/snippets/usage/Node.txt (100%) rename snippets/reactflow/usage/NodeProps.md => src/plugins/reactflow/snippets/usage/NodeProps.txt (100%) rename snippets/reactflow/usage/NodeResizeControl.md => src/plugins/reactflow/snippets/usage/NodeResizeControl.txt (100%) rename snippets/reactflow/usage/NodeResizer.md => src/plugins/reactflow/snippets/usage/NodeResizer.txt (100%) rename snippets/reactflow/usage/NodeToolbar.md => src/plugins/reactflow/snippets/usage/NodeToolbar.txt (100%) rename snippets/reactflow/usage/Panel.md => src/plugins/reactflow/snippets/usage/Panel.txt (100%) rename snippets/reactflow/usage/ReactFlow.md => src/plugins/reactflow/snippets/usage/ReactFlow.txt (100%) rename snippets/reactflow/usage/ReactFlowInstance.md => src/plugins/reactflow/snippets/usage/ReactFlowInstance.txt (100%) rename snippets/reactflow/usage/ReactFlowProvider.md => src/plugins/reactflow/snippets/usage/ReactFlowProvider.txt (100%) rename snippets/reactflow/usage/Viewport.md => src/plugins/reactflow/snippets/usage/Viewport.txt (100%) rename snippets/reactflow/usage/ViewportPortal.md => src/plugins/reactflow/snippets/usage/ViewportPortal.txt (100%) rename snippets/reactflow/usage/addEdge.md => src/plugins/reactflow/snippets/usage/addEdge.txt (100%) rename snippets/reactflow/usage/applyEdgeChanges.md => src/plugins/reactflow/snippets/usage/applyEdgeChanges.txt (100%) rename snippets/reactflow/usage/applyNodeChanges.md => src/plugins/reactflow/snippets/usage/applyNodeChanges.txt (100%) rename snippets/reactflow/usage/getBezierPath.md => src/plugins/reactflow/snippets/usage/getBezierPath.txt (100%) rename snippets/reactflow/usage/getConnectedEdges.md => src/plugins/reactflow/snippets/usage/getConnectedEdges.txt (100%) rename snippets/reactflow/usage/getIncomers.md => src/plugins/reactflow/snippets/usage/getIncomers.txt (100%) rename snippets/reactflow/usage/getNodesBounds.md => src/plugins/reactflow/snippets/usage/getNodesBounds.txt (100%) rename snippets/reactflow/usage/getOutgoers.md => src/plugins/reactflow/snippets/usage/getOutgoers.txt (100%) rename snippets/reactflow/usage/getSimpleBezierPath.md => src/plugins/reactflow/snippets/usage/getSimpleBezierPath.txt (100%) rename snippets/reactflow/usage/getSmoothStepPath.md => src/plugins/reactflow/snippets/usage/getSmoothStepPath.txt (100%) rename snippets/reactflow/usage/getStraightPath.md => src/plugins/reactflow/snippets/usage/getStraightPath.txt (100%) rename snippets/reactflow/usage/getViewportForBounds.md => src/plugins/reactflow/snippets/usage/getViewportForBounds.txt (100%) rename snippets/reactflow/usage/isEdge.md => src/plugins/reactflow/snippets/usage/isEdge.txt (100%) rename snippets/reactflow/usage/isNode.md => src/plugins/reactflow/snippets/usage/isNode.txt (100%) rename snippets/reactflow/usage/reconnectEdge.md => src/plugins/reactflow/snippets/usage/reconnectEdge.txt (100%) rename snippets/reactflow/usage/useConnection.md => src/plugins/reactflow/snippets/usage/useConnection.txt (100%) rename snippets/reactflow/usage/useEdges.md => src/plugins/reactflow/snippets/usage/useEdges.txt (100%) rename snippets/reactflow/usage/useEdgesState.md => src/plugins/reactflow/snippets/usage/useEdgesState.txt (100%) rename snippets/reactflow/usage/useHandleConnections.md => src/plugins/reactflow/snippets/usage/useHandleConnections.txt (100%) rename snippets/reactflow/usage/useInternalNode.md => src/plugins/reactflow/snippets/usage/useInternalNode.txt (100%) rename snippets/reactflow/usage/useKeyPress.md => src/plugins/reactflow/snippets/usage/useKeyPress.txt (100%) rename snippets/reactflow/usage/useNodeConnections.md => src/plugins/reactflow/snippets/usage/useNodeConnections.txt (100%) rename snippets/reactflow/usage/useNodeId.md => src/plugins/reactflow/snippets/usage/useNodeId.txt (100%) rename snippets/reactflow/usage/useNodes.md => src/plugins/reactflow/snippets/usage/useNodes.txt (100%) rename snippets/reactflow/usage/useNodesData.md => src/plugins/reactflow/snippets/usage/useNodesData.txt (100%) rename snippets/reactflow/usage/useNodesInitialized.md => src/plugins/reactflow/snippets/usage/useNodesInitialized.txt (100%) rename snippets/reactflow/usage/useNodesState.md => src/plugins/reactflow/snippets/usage/useNodesState.txt (100%) rename snippets/reactflow/usage/useOnSelectionChange.md => src/plugins/reactflow/snippets/usage/useOnSelectionChange.txt (100%) rename snippets/reactflow/usage/useOnViewportChange.md => src/plugins/reactflow/snippets/usage/useOnViewportChange.txt (100%) rename snippets/reactflow/usage/useReactFlow.md => src/plugins/reactflow/snippets/usage/useReactFlow.txt (100%) rename snippets/reactflow/usage/useStore.md => src/plugins/reactflow/snippets/usage/useStore.txt (100%) rename snippets/reactflow/usage/useStoreApi.md => src/plugins/reactflow/snippets/usage/useStoreApi.txt (100%) rename snippets/reactflow/usage/useUpdateNodeInternals.md => src/plugins/reactflow/snippets/usage/useUpdateNodeInternals.txt (100%) rename snippets/reactflow/usage/useViewport.md => src/plugins/reactflow/snippets/usage/useViewport.txt (100%) rename snippets/rust/cheatsheet.md => src/plugins/rust/snippets/cheatsheet.txt (100%) rename snippets/rust/practices/avoid-clone-in-loops-bad.md => src/plugins/rust/snippets/practices/avoid-clone-in-loops-bad.txt (100%) rename snippets/rust/practices/avoid-clone-in-loops-good.md => src/plugins/rust/snippets/practices/avoid-clone-in-loops-good.txt (100%) rename snippets/rust/practices/benchmark-release-bad.md => src/plugins/rust/snippets/practices/benchmark-release-bad.txt (100%) rename snippets/rust/practices/benchmark-release-good.md => src/plugins/rust/snippets/practices/benchmark-release-good.txt (100%) rename snippets/rust/practices/borrow-over-clone-bad.md => src/plugins/rust/snippets/practices/borrow-over-clone-bad.txt (100%) rename snippets/rust/practices/borrow-over-clone-good.md => src/plugins/rust/snippets/practices/borrow-over-clone-good.txt (100%) rename snippets/rust/practices/copy-by-value-good.md => src/plugins/rust/snippets/practices/copy-by-value-good.txt (100%) rename snippets/rust/practices/cow-ambiguous-ownership-good.md => src/plugins/rust/snippets/practices/cow-ambiguous-ownership-good.txt (100%) rename snippets/rust/practices/descriptive-test-names-bad.md => src/plugins/rust/snippets/practices/descriptive-test-names-bad.txt (100%) rename snippets/rust/practices/descriptive-test-names-good.md => src/plugins/rust/snippets/practices/descriptive-test-names-good.txt (100%) rename snippets/rust/practices/doc-tests-good.md => src/plugins/rust/snippets/practices/doc-tests-good.txt (100%) rename snippets/rust/practices/expect-over-allow-bad.md => src/plugins/rust/snippets/practices/expect-over-allow-bad.txt (100%) rename snippets/rust/practices/expect-over-allow-good.md => src/plugins/rust/snippets/practices/expect-over-allow-good.txt (100%) rename snippets/rust/practices/no-unwrap-in-prod-bad.md => src/plugins/rust/snippets/practices/no-unwrap-in-prod-bad.txt (100%) rename snippets/rust/practices/no-unwrap-in-prod-good.md => src/plugins/rust/snippets/practices/no-unwrap-in-prod-good.txt (100%) rename snippets/rust/practices/one-assertion-per-test-good.md => src/plugins/rust/snippets/practices/one-assertion-per-test-good.txt (100%) rename snippets/rust/practices/prefer-iterators-bad.md => src/plugins/rust/snippets/practices/prefer-iterators-bad.txt (100%) rename snippets/rust/practices/prefer-iterators-good.md => src/plugins/rust/snippets/practices/prefer-iterators-good.txt (100%) rename snippets/rust/practices/result-not-panic-bad.md => src/plugins/rust/snippets/practices/result-not-panic-bad.txt (100%) rename snippets/rust/practices/result-not-panic-good.md => src/plugins/rust/snippets/practices/result-not-panic-good.txt (100%) rename snippets/rust/practices/send-sync-bad.md => src/plugins/rust/snippets/practices/send-sync-bad.txt (100%) rename snippets/rust/practices/send-sync-good.md => src/plugins/rust/snippets/practices/send-sync-good.txt (100%) rename snippets/rust/practices/static-over-dynamic-dispatch-good.md => src/plugins/rust/snippets/practices/static-over-dynamic-dispatch-good.txt (100%) rename snippets/rust/practices/str-over-string-bad.md => src/plugins/rust/snippets/practices/str-over-string-bad.txt (100%) rename snippets/rust/practices/str-over-string-good.md => src/plugins/rust/snippets/practices/str-over-string-good.txt (100%) rename snippets/rust/practices/thiserror-vs-anyhow-good.md => src/plugins/rust/snippets/practices/thiserror-vs-anyhow-good.txt (100%) rename snippets/rust/practices/type-state-pattern-good.md => src/plugins/rust/snippets/practices/type-state-pattern-good.txt (100%) rename snippets/ui-ux/cheatsheet.md => src/plugins/ui-ux/snippets/cheatsheet.txt (100%) rename snippets/ui-ux/components/badge.md => src/plugins/ui-ux/snippets/components/badge.txt (100%) rename snippets/ui-ux/components/button.md => src/plugins/ui-ux/snippets/components/button.txt (100%) rename snippets/ui-ux/components/card.md => src/plugins/ui-ux/snippets/components/card.txt (100%) rename snippets/ui-ux/components/form-input.md => src/plugins/ui-ux/snippets/components/form-input.txt (100%) rename snippets/ui-ux/principles/4px-grid.md => src/plugins/ui-ux/snippets/principles/4px-grid.txt (100%) rename snippets/ui-ux/principles/5-elevation-levels.md => src/plugins/ui-ux/snippets/principles/5-elevation-levels.txt (100%) rename snippets/ui-ux/principles/dark-mode.md => src/plugins/ui-ux/snippets/principles/dark-mode.txt (100%) rename snippets/ui-ux/principles/easing-rules.md => src/plugins/ui-ux/snippets/principles/easing-rules.txt (100%) rename snippets/ui-ux/principles/focus-management.md => src/plugins/ui-ux/snippets/principles/focus-management.txt (100%) rename snippets/ui-ux/principles/mobile-first.md => src/plugins/ui-ux/snippets/principles/mobile-first.txt (100%) rename snippets/ui-ux/principles/oklch-color.md => src/plugins/ui-ux/snippets/principles/oklch-color.txt (100%) rename snippets/ui-ux/principles/prose-width.md => src/plugins/ui-ux/snippets/principles/prose-width.txt (100%) rename snippets/ui-ux/principles/reduced-motion.md => src/plugins/ui-ux/snippets/principles/reduced-motion.txt (100%) rename snippets/ui-ux/principles/semantic-status-colors.md => src/plugins/ui-ux/snippets/principles/semantic-status-colors.txt (100%) rename snippets/ui-ux/principles/touch-targets.md => src/plugins/ui-ux/snippets/principles/touch-targets.txt (100%) rename snippets/ui-ux/principles/type-scale.md => src/plugins/ui-ux/snippets/principles/type-scale.txt (100%) rename snippets/ui-ux/principles/warm-shadows.md => src/plugins/ui-ux/snippets/principles/warm-shadows.txt (100%) rename snippets/ui-ux/principles/warm-vs-cool.md => src/plugins/ui-ux/snippets/principles/warm-vs-cool.txt (100%) rename snippets/ui-ux/principles/wcag-contrast.md => src/plugins/ui-ux/snippets/principles/wcag-contrast.txt (100%) rename snippets/ui-ux/references/elevation-table.md => src/plugins/ui-ux/snippets/references/elevation-table.txt (100%) rename snippets/ui-ux/references/motion-table.md => src/plugins/ui-ux/snippets/references/motion-table.txt (100%) rename snippets/ui-ux/references/spacing-table.md => src/plugins/ui-ux/snippets/references/spacing-table.txt (100%) rename snippets/ui-ux/references/type-scale-table.md => src/plugins/ui-ux/snippets/references/type-scale-table.txt (100%) rename snippets/ui-ux/references/wcag-table.md => src/plugins/ui-ux/snippets/references/wcag-table.txt (100%) create mode 100644 summary.md diff --git a/Dockerfile b/Dockerfile index 7195f5e..bb19074 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,7 @@ FROM node:24-alpine WORKDIR /app COPY package.json package-lock.json ./ -RUN npm ci --omit=dev -COPY dist/ dist/ -COPY snippets/ snippets/ +RUN npm ci +COPY src/ src/ USER node -ENTRYPOINT ["node", "dist/index.js"] +ENTRYPOINT ["npx", "tsx", "src/index.ts"] diff --git a/README.md b/README.md index 81564b4..14a4fe6 100755 --- a/README.md +++ b/README.md @@ -255,7 +255,7 @@ Build once, reuse forever. The wrapper script keeps **one** named container aliv ```bash git clone https://github.com/orkait/hyperstack.git cd hyperstack -npm install && npm run build +npm install docker build -t hyperstack . ``` @@ -356,28 +356,26 @@ src/ โ”œโ”€โ”€ index.ts # Entry - creates McpServer, loads all plugins โ”œโ”€โ”€ registry.ts # Plugin interface + loadPlugins() โ”œโ”€โ”€ shared/ -โ”‚ โ””โ”€โ”€ loader-factory.ts # createSnippetLoader() - reads .md files at runtime +โ”‚ โ””โ”€โ”€ loader-factory.ts # createSnippetLoader() - reads .txt files at runtime โ””โ”€โ”€ plugins/ โ”œโ”€โ”€ reactflow/ # @xyflow/react v12 + โ”‚ โ””โ”€โ”€ snippets/ # 94 .txt files โ”œโ”€โ”€ motion/ # motion/react v12 + โ”‚ โ””โ”€โ”€ snippets/ # 79 .txt files โ”œโ”€โ”€ lenis/ # Lenis smooth scroll + โ”‚ โ””โ”€โ”€ snippets/ # 31 .txt files โ”œโ”€โ”€ react/ # React 19 + Next.js App Router + โ”‚ โ””โ”€โ”€ snippets/ # 13 .txt files โ”œโ”€โ”€ echo/ # Echo Go framework + โ”‚ โ””โ”€โ”€ snippets/ # 33 .txt files โ”œโ”€โ”€ golang/ # Go best practices + design patterns + โ”‚ โ””โ”€โ”€ snippets/ # 43 .txt files โ”œโ”€โ”€ rust/ # Rust best practices + โ”‚ โ””โ”€โ”€ snippets/ # 28 .txt files โ”œโ”€โ”€ design-tokens/ # Tailwind v4 OKLCH token system + โ”‚ โ””โ”€โ”€ snippets/ # 24 .txt files โ””โ”€โ”€ ui-ux/ # UI/UX design principles - -snippets/ -โ”œโ”€โ”€ reactflow/ # 94 .md files -โ”œโ”€โ”€ motion/ # 79 .md files -โ”œโ”€โ”€ lenis/ # 31 .md files -โ”œโ”€โ”€ react/ # 13 .md files -โ”œโ”€โ”€ echo/ # 33 .md files -โ”œโ”€โ”€ golang/ # 43 .md files -โ”œโ”€โ”€ rust/ # 28 .md files -โ”œโ”€โ”€ design-tokens/ # 24 .md files -โ””โ”€โ”€ ui-ux/ # 25 .md files + โ””โ”€โ”€ snippets/ # 25 .txt files scripts/ โ””โ”€โ”€ start-mcp.sh # Single-container Docker wrapper @@ -407,18 +405,17 @@ bad: snippet("practices/error-wrapping-bad.md"), ```bash npm install -npm run build # compile TypeScript to dist/ -npm run dev # watch mode -npm start # run server directly +npm start # run server using tsx +npm run dev # watch mode using tsx ``` ```bash # Verify all plugins load and tools are registered correctly -node --input-type=module <<'EOF' -import { reactflowPlugin } from './dist/plugins/reactflow/index.js'; -import { motionPlugin } from './dist/plugins/motion/index.js'; -import { lenisPlugin } from './dist/plugins/lenis/index.js'; -import { golangPlugin } from './dist/plugins/golang/index.js'; +npx tsx <<'EOF' +import { reactflowPlugin } from './src/plugins/reactflow/index.js'; +import { motionPlugin } from './src/plugins/motion/index.js'; +import { lenisPlugin } from './src/plugins/lenis/index.js'; +import { golangPlugin } from './src/plugins/golang/index.js'; const tools = []; const fake = { tool: (n) => tools.push(n), resource: () => {} }; [reactflowPlugin, motionPlugin, lenisPlugin, golangPlugin].forEach(p => p.register(fake)); @@ -431,3 +428,5 @@ EOF ## ๐Ÿ“„ License MIT ยฉ [Orkait](https://github.com/orkait) +T ยฉ [Orkait](https://github.com/orkait) +ait) diff --git a/package-lock.json b/package-lock.json index a725a08..d2d52c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,12 +17,455 @@ }, "devDependencies": { "@types/node": "^20.0.0", + "tsx": "^4.21.0", "typescript": "^5.5.0" }, "engines": { "node": ">=18" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@hono/node-server": { "version": "1.19.11", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", @@ -349,6 +792,48 @@ "node": ">= 0.4" } }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -507,6 +992,21 @@ "node": ">= 0.8" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -553,6 +1053,19 @@ "node": ">= 0.4" } }, + "node_modules/get-tsconfig": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -901,6 +1414,16 @@ "node": ">=0.10.0" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -1085,6 +1608,26 @@ "node": ">=0.6" } }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", diff --git a/package.json b/package.json index 068c9f4..ec9dc88 100644 --- a/package.json +++ b/package.json @@ -2,25 +2,28 @@ "name": "@orkait-ai/hyperstack", "version": "1.0.0", "description": "Unified MCP server for frontend libraries: React Flow, Motion for React, and more", - "main": "dist/index.js", + "main": "src/index.ts", "bin": { - "hyperstack": "dist/index.js" + "hyperstack": "src/index.ts" }, "type": "module", "scripts": { - "build": "tsc", - "start": "node dist/index.js", - "dev": "tsc --watch" + "build": "tsc --noEmit", + "start": "tsx src/index.ts", + "dev": "tsx watch src/index.ts" }, "author": "Orkait", "license": "MIT", - "engines": { "node": ">=18" }, + "engines": { + "node": ">=18" + }, "dependencies": { "@modelcontextprotocol/sdk": "^1.17.0", "zod": "^3.23.0" }, "devDependencies": { "@types/node": "^20.0.0", + "tsx": "^4.21.0", "typescript": "^5.5.0" } } diff --git a/scripts/start-mcp.sh b/scripts/start-mcp.sh index e8795d7..0aab4c2 100755 --- a/scripts/start-mcp.sh +++ b/scripts/start-mcp.sh @@ -28,4 +28,4 @@ LOCK_FILE="/tmp/${CONTAINER}.lock" ) 200>"$LOCK_FILE" -exec docker exec -i "$CONTAINER" node dist/index.js +exec docker exec -i "$CONTAINER" npx tsx src/index.ts diff --git a/src/plugins/design-tokens/data.ts b/src/plugins/design-tokens/data.ts index da5a4f2..e8edf01 100644 --- a/src/plugins/design-tokens/data.ts +++ b/src/plugins/design-tokens/data.ts @@ -43,7 +43,7 @@ export const TOKEN_CATEGORIES: TokenCategory[] = [ description: "OKLCH-based color system using three-layer architecture: @theme primitives โ†’ :root/:dark semantics โ†’ domain tokens. Role-based naming prevents color-name coupling.", layer: "semantic", - cssExample: snippet("categories/colors.md"), + cssExample: snippet("categories/colors.txt"), tailwindExample: `

Badge`, @@ -188,7 +188,7 @@ export const TOKEN_CATEGORIES: TokenCategory[] = [ description: "5-level elevation shadow scale using oklch-tinted warm shadows. Dark mode uses bg-color elevation instead of box shadows.", layer: "semantic", - cssExample: snippet("categories/shadows-elevation.md"), + cssExample: snippet("categories/shadows-elevation.txt"), tailwindExample: `
Card
@@ -219,7 +219,7 @@ export const TOKEN_CATEGORIES: TokenCategory[] = [ description: "Duration and easing token system for consistent UI animation. Always includes prefers-reduced-motion override.", layer: "primitive", - cssExample: snippet("categories/motion.md"), + cssExample: snippet("categories/motion.txt"), tailwindExample: ` @@ -312,7 +312,7 @@ export const TOKEN_CATEGORIES: TokenCategory[] = [ description: "Density mode system for compact/default/comfortable UI via CSS class overrides on the root element.", layer: "domain", - cssExample: snippet("categories/density.md"), + cssExample: snippet("categories/density.txt"), tailwindExample: `// React: density context provider const DensityProvider = ({ density, children }) => (
@@ -410,7 +410,7 @@ export const TOKEN_PROCEDURES: TokenProcedure[] = [ step: 1, title: "Define color ramp primitives in @theme", description: "Create 11-stop OKLCH ramps for brand, neutral, and pop colors. These are static compile-time values.", - code: snippet("procedures/step-1-colors.md"), + code: snippet("procedures/step-1-colors.txt"), rules: [ "Use @theme (not :root) for primitive ramps โ€” they become Tailwind static utilities", "Hue angle (H) must be consistent across all stops in a ramp", @@ -426,7 +426,7 @@ export const TOKEN_PROCEDURES: TokenProcedure[] = [ step: 2, title: "Map semantic tokens in :root and .dark", description: "Create role-based semantic tokens that reference primitives. These are the tokens your components actually use.", - code: snippet("procedures/step-2-spacing.md"), + code: snippet("procedures/step-2-spacing.txt"), rules: [ "Components must only reference semantic tokens, never primitive ramp values directly", "Dark mode requires bg-color elevation (lighter bg per level) since shadows are invisible", @@ -442,7 +442,7 @@ export const TOKEN_PROCEDURES: TokenProcedure[] = [ step: 3, title: "Bridge to Tailwind v4 utilities with @theme inline", description: "Use @theme inline to expose runtime-swappable CSS custom properties as Tailwind utility classes.", - code: snippet("procedures/step-3-typography.md"), + code: snippet("procedures/step-3-typography.txt"), rules: [ "@theme inline is read at runtime โ€” it reflects CSS custom property values dynamically", "Plain @theme is static โ€” values are baked in at build time", @@ -458,7 +458,7 @@ export const TOKEN_PROCEDURES: TokenProcedure[] = [ step: 4, title: "Define spacing system", description: "Set the 4px base multiplier and create named semantic spacing tokens.", - code: snippet("procedures/step-4-component-sizing.md"), + code: snippet("procedures/step-4-component-sizing.txt"), rules: [ "--spacing is the global multiplier โ€” changing it scales ALL numeric spacing utilities", "Named tokens auto-generate Tailwind utilities: p-card, py-section-y, gap-grid-cards", @@ -472,7 +472,7 @@ export const TOKEN_PROCEDURES: TokenProcedure[] = [ step: 5, title: "Set up typography scale", description: "Define fluid heading sizes with clamp(), fixed body sizes, and line-height/tracking tokens.", - code: snippet("procedures/step-5-remaining.md"), + code: snippet("procedures/step-5-remaining.txt"), rules: [ "Body text (16px) is NEVER fluid โ€” use fixed rem values", "Headings MUST have tight line-height (1.05โ€“1.2), not body line-height (1.6+)", @@ -486,7 +486,7 @@ export const TOKEN_PROCEDURES: TokenProcedure[] = [ step: 6, title: "Define shadows and elevation", description: "Build the 5-level elevation shadow system using warm oklch-tinted shadows.", - code: snippet("procedures/step-6-accessibility.md"), + code: snippet("procedures/step-6-accessibility.txt"), rules: ["Use oklch-tinted shadows, never rgba(0,0,0)", "Dark mode disables all shadows, uses bg-color elevation instead"], gotchas: ["rgba(0,0,0) shadows look cold on warm backgrounds. The warm hue tint (H=60) makes shadows feel natural."], }, @@ -494,7 +494,7 @@ export const TOKEN_PROCEDURES: TokenProcedure[] = [ step: 7, title: "Define motion and z-index tokens", description: "Add duration, easing, and z-index scale with prefers-reduced-motion.", - code: snippet("procedures/step-7-validation.md"), + code: snippet("procedures/step-7-validation.txt"), rules: ["prefers-reduced-motion MUST be in @layer base with !important", "Never use arbitrary z-index values"], gotchas: ["Forgetting prefers-reduced-motion is a WCAG 2.3.3 violation."], }, @@ -502,7 +502,7 @@ export const TOKEN_PROCEDURES: TokenProcedure[] = [ step: 8, title: "Run contrast audit and verify token usage", description: "After building the full token system, run a contrast audit on all color token pairs.", - code: snippet("procedures/step-8-deliverables.md"), + code: snippet("procedures/step-8-deliverables.txt"), rules: [ "Run contrast audit AFTER finalizing the palette โ€” before building components", "Status colors often need L adjusted to ~0.55 for AA compliance with white foreground", diff --git a/snippets/design-tokens/categories/border-radius.md b/src/plugins/design-tokens/snippets/categories/border-radius.txt similarity index 100% rename from snippets/design-tokens/categories/border-radius.md rename to src/plugins/design-tokens/snippets/categories/border-radius.txt diff --git a/snippets/design-tokens/categories/colors.md b/src/plugins/design-tokens/snippets/categories/colors.txt similarity index 100% rename from snippets/design-tokens/categories/colors.md rename to src/plugins/design-tokens/snippets/categories/colors.txt diff --git a/snippets/design-tokens/categories/component-sizing.md b/src/plugins/design-tokens/snippets/categories/component-sizing.txt similarity index 100% rename from snippets/design-tokens/categories/component-sizing.md rename to src/plugins/design-tokens/snippets/categories/component-sizing.txt diff --git a/snippets/design-tokens/categories/density.md b/src/plugins/design-tokens/snippets/categories/density.txt similarity index 100% rename from snippets/design-tokens/categories/density.md rename to src/plugins/design-tokens/snippets/categories/density.txt diff --git a/snippets/design-tokens/categories/motion.md b/src/plugins/design-tokens/snippets/categories/motion.txt similarity index 100% rename from snippets/design-tokens/categories/motion.md rename to src/plugins/design-tokens/snippets/categories/motion.txt diff --git a/snippets/design-tokens/categories/opacity.md b/src/plugins/design-tokens/snippets/categories/opacity.txt similarity index 100% rename from snippets/design-tokens/categories/opacity.md rename to src/plugins/design-tokens/snippets/categories/opacity.txt diff --git a/snippets/design-tokens/categories/shadows-elevation.md b/src/plugins/design-tokens/snippets/categories/shadows-elevation.txt similarity index 100% rename from snippets/design-tokens/categories/shadows-elevation.md rename to src/plugins/design-tokens/snippets/categories/shadows-elevation.txt diff --git a/snippets/design-tokens/categories/spacing.md b/src/plugins/design-tokens/snippets/categories/spacing.txt similarity index 100% rename from snippets/design-tokens/categories/spacing.md rename to src/plugins/design-tokens/snippets/categories/spacing.txt diff --git a/snippets/design-tokens/categories/typography.md b/src/plugins/design-tokens/snippets/categories/typography.txt similarity index 100% rename from snippets/design-tokens/categories/typography.md rename to src/plugins/design-tokens/snippets/categories/typography.txt diff --git a/snippets/design-tokens/categories/z-index.md b/src/plugins/design-tokens/snippets/categories/z-index.txt similarity index 100% rename from snippets/design-tokens/categories/z-index.md rename to src/plugins/design-tokens/snippets/categories/z-index.txt diff --git a/snippets/design-tokens/procedures/step-1-colors.md b/src/plugins/design-tokens/snippets/procedures/step-1-colors.txt similarity index 100% rename from snippets/design-tokens/procedures/step-1-colors.md rename to src/plugins/design-tokens/snippets/procedures/step-1-colors.txt diff --git a/snippets/design-tokens/procedures/step-2-spacing.md b/src/plugins/design-tokens/snippets/procedures/step-2-spacing.txt similarity index 100% rename from snippets/design-tokens/procedures/step-2-spacing.md rename to src/plugins/design-tokens/snippets/procedures/step-2-spacing.txt diff --git a/snippets/design-tokens/procedures/step-3-typography.md b/src/plugins/design-tokens/snippets/procedures/step-3-typography.txt similarity index 100% rename from snippets/design-tokens/procedures/step-3-typography.md rename to src/plugins/design-tokens/snippets/procedures/step-3-typography.txt diff --git a/snippets/design-tokens/procedures/step-4-component-sizing.md b/src/plugins/design-tokens/snippets/procedures/step-4-component-sizing.txt similarity index 100% rename from snippets/design-tokens/procedures/step-4-component-sizing.md rename to src/plugins/design-tokens/snippets/procedures/step-4-component-sizing.txt diff --git a/snippets/design-tokens/procedures/step-5-remaining.md b/src/plugins/design-tokens/snippets/procedures/step-5-remaining.txt similarity index 100% rename from snippets/design-tokens/procedures/step-5-remaining.md rename to src/plugins/design-tokens/snippets/procedures/step-5-remaining.txt diff --git a/snippets/design-tokens/procedures/step-6-accessibility.md b/src/plugins/design-tokens/snippets/procedures/step-6-accessibility.txt similarity index 100% rename from snippets/design-tokens/procedures/step-6-accessibility.md rename to src/plugins/design-tokens/snippets/procedures/step-6-accessibility.txt diff --git a/snippets/design-tokens/procedures/step-7-validation.md b/src/plugins/design-tokens/snippets/procedures/step-7-validation.txt similarity index 100% rename from snippets/design-tokens/procedures/step-7-validation.md rename to src/plugins/design-tokens/snippets/procedures/step-7-validation.txt diff --git a/snippets/design-tokens/procedures/step-8-deliverables.md b/src/plugins/design-tokens/snippets/procedures/step-8-deliverables.txt similarity index 100% rename from snippets/design-tokens/procedures/step-8-deliverables.md rename to src/plugins/design-tokens/snippets/procedures/step-8-deliverables.txt diff --git a/snippets/design-tokens/references/color-roles.md b/src/plugins/design-tokens/snippets/references/color-roles.txt similarity index 100% rename from snippets/design-tokens/references/color-roles.md rename to src/plugins/design-tokens/snippets/references/color-roles.txt diff --git a/snippets/design-tokens/references/token-checklist.md b/src/plugins/design-tokens/snippets/references/token-checklist.txt similarity index 100% rename from snippets/design-tokens/references/token-checklist.md rename to src/plugins/design-tokens/snippets/references/token-checklist.txt diff --git a/snippets/design-tokens/templates/colors-tailwind-v4.md b/src/plugins/design-tokens/snippets/templates/colors-tailwind-v4.txt similarity index 100% rename from snippets/design-tokens/templates/colors-tailwind-v4.md rename to src/plugins/design-tokens/snippets/templates/colors-tailwind-v4.txt diff --git a/snippets/design-tokens/templates/motion.md b/src/plugins/design-tokens/snippets/templates/motion.txt similarity index 100% rename from snippets/design-tokens/templates/motion.md rename to src/plugins/design-tokens/snippets/templates/motion.txt diff --git a/snippets/design-tokens/templates/spacing.md b/src/plugins/design-tokens/snippets/templates/spacing.txt similarity index 100% rename from snippets/design-tokens/templates/spacing.md rename to src/plugins/design-tokens/snippets/templates/spacing.txt diff --git a/snippets/design-tokens/templates/typography.md b/src/plugins/design-tokens/snippets/templates/typography.txt similarity index 100% rename from snippets/design-tokens/templates/typography.md rename to src/plugins/design-tokens/snippets/templates/typography.txt diff --git a/src/plugins/design-tokens/tools/generate.ts b/src/plugins/design-tokens/tools/generate.ts index 4df3936..58a86d9 100644 --- a/src/plugins/design-tokens/tools/generate.ts +++ b/src/plugins/design-tokens/tools/generate.ts @@ -2,10 +2,10 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { snippet } from "../loader.js"; -const TEMPLATE_COLORS = snippet("templates/colors-tailwind-v4.md"); -const TEMPLATE_SPACING = snippet("templates/spacing.md"); -const TEMPLATE_TYPOGRAPHY = snippet("templates/typography.md"); -const TEMPLATE_MOTION = snippet("templates/motion.md"); +const TEMPLATE_COLORS = snippet("templates/colors-tailwind-v4.txt"); +const TEMPLATE_SPACING = snippet("templates/spacing.txt"); +const TEMPLATE_TYPOGRAPHY = snippet("templates/typography.txt"); +const TEMPLATE_MOTION = snippet("templates/motion.txt"); export function register(server: McpServer): void { server.tool( diff --git a/src/plugins/echo/data.ts b/src/plugins/echo/data.ts index fb8ce82..39a0708 100644 --- a/src/plugins/echo/data.ts +++ b/src/plugins/echo/data.ts @@ -46,7 +46,7 @@ export const RECIPES: Recipe[] = [ category: "routing", description: "Minimal Echo server with a single GET route", when: "Starting a new Echo project or verifying your setup", - code: snippet("recipes/hello-world.md"), + code: snippet("recipes/hello-world.txt"), gotchas: [ "e.Logger.Fatal calls os.Exit on error โ€” use it only in main", "echo.New() returns a configured instance; always use this, not raw http.Server", @@ -58,7 +58,7 @@ export const RECIPES: Recipe[] = [ category: "crud", description: "Full CRUD REST API with JSON binding and echo.Context", when: "Building a resource-oriented REST API with JSON payloads", - code: snippet("recipes/crud-api.md"), + code: snippet("recipes/crud-api.txt"), gotchas: [ "c.Bind() reads Body only once โ€” do not call it twice", "Return echo.NewHTTPError for client errors; Echo renders these as JSON automatically", @@ -72,7 +72,7 @@ export const RECIPES: Recipe[] = [ category: "websocket", description: "WebSocket upgrade with read/write loop and proper cleanup", when: "Building real-time bidirectional communication (chat, live updates, collaborative tools)", - code: snippet("recipes/websocket.md"), + code: snippet("recipes/websocket.txt"), gotchas: [ "Always defer ws.Close() immediately after upgrade", "WebSocket handler owns the loop โ€” it blocks until the connection closes", @@ -87,7 +87,7 @@ export const RECIPES: Recipe[] = [ category: "sse", description: "Server-Sent Events with Flush(), content-type header, and client disconnect via context.Done()", when: "One-way server-to-client streaming (live logs, dashboards, notifications) without WebSocket overhead", - code: snippet("recipes/sse.md"), + code: snippet("recipes/sse.txt"), gotchas: [ "Flush() is REQUIRED after every write โ€” without it data buffers and never reaches the client", "SSE format: 'data: \\n\\n' โ€” double newline terminates the event", @@ -103,7 +103,7 @@ export const RECIPES: Recipe[] = [ category: "auth", description: "JWT middleware setup, token generation, and protected routes", when: "Stateless authentication for REST APIs or microservices", - code: snippet("recipes/jwt-auth.md"), + code: snippet("recipes/jwt-auth.txt"), gotchas: [ "Always validate alg, iss, aud, and exp when verifying tokens", "Use echojwt package (not echo's built-in deprecated JWT) for v4+", @@ -118,7 +118,7 @@ export const RECIPES: Recipe[] = [ category: "middleware", description: "CORS middleware with configuration for allowed origins, methods, and headers", when: "Your API is called from a browser on a different domain", - code: snippet("recipes/cors.md"), + code: snippet("recipes/cors.txt"), gotchas: [ "CORS middleware must be registered BEFORE any route handlers", "AllowCredentials: true requires explicit origins โ€” wildcard '*' is rejected by browsers", @@ -132,7 +132,7 @@ export const RECIPES: Recipe[] = [ category: "graceful-shutdown", description: "Signal handling with e.Shutdown(ctx) and configurable timeout", when: "Production deployments where in-flight requests must complete before shutdown", - code: snippet("recipes/graceful-shutdown.md"), + code: snippet("recipes/graceful-shutdown.txt"), gotchas: [ "e.Start() must run in a goroutine; otherwise signal handling never executes", "Check for http.ErrServerClosed โ€” it is the normal shutdown error, not a real failure", @@ -147,7 +147,7 @@ export const RECIPES: Recipe[] = [ category: "file", description: "Multipart file upload with c.FormFile(), validation, and disk save", when: "Accepting user-uploaded files (images, documents, etc.)", - code: snippet("recipes/file-upload.md"), + code: snippet("recipes/file-upload.txt"), gotchas: [ "Always use filepath.Base() to sanitize filenames โ€” path traversal attacks use '../' sequences", "Set BodyLimit middleware AND validate file.Size โ€” both layers of defense", @@ -162,7 +162,7 @@ export const RECIPES: Recipe[] = [ category: "file", description: "File download with c.Attachment() (download prompt) and c.Inline() (browser display)", when: "Serving files to clients, either as downloads or for inline rendering", - code: snippet("recipes/file-download.md"), + code: snippet("recipes/file-download.txt"), gotchas: [ "Never use user-supplied filenames directly in file paths โ€” always validate against an allowlist or database", "c.Attachment() triggers a download dialog; c.Inline() lets the browser display it", @@ -176,7 +176,7 @@ export const RECIPES: Recipe[] = [ category: "tls", description: "Automatic TLS with Let's Encrypt via AutoTLSManager", when: "Production server that needs HTTPS without manual certificate management", - code: snippet("recipes/auto-tls.md"), + code: snippet("recipes/auto-tls.txt"), gotchas: [ "Requires ports 80 and 443 to be open โ€” Let's Encrypt uses HTTP-01 challenge on port 80", "HostWhitelist is REQUIRED to prevent certificate issuance for arbitrary domains", @@ -190,7 +190,7 @@ export const RECIPES: Recipe[] = [ category: "http2", description: "HTTP/2 with manual TLS certificate and server push", when: "Performance-critical serving where HTTP/2 multiplexing reduces latency", - code: snippet("recipes/http2.md"), + code: snippet("recipes/http2.txt"), gotchas: [ "HTTP/2 in browsers requires TLS โ€” plain-text h2c is only for trusted internal networks", "Server Push is deprecated in Chrome 106+ and removed in many browsers; prefer preload hints", @@ -204,7 +204,7 @@ export const RECIPES: Recipe[] = [ category: "middleware", description: "Logger โ†’ Recover โ†’ Auth โ†’ Custom middleware chain and why order matters", when: "Understanding the execution model of Echo middleware and setting up a production chain", - code: snippet("recipes/middleware-chain.md"), + code: snippet("recipes/middleware-chain.txt"), gotchas: [ "Logger BEFORE Recover: if Recover is first, panics are caught before Logger sees the request complete", "e.Use() registers global middleware; group.Use() registers only for that group", @@ -219,7 +219,7 @@ export const RECIPES: Recipe[] = [ category: "proxy", description: "Reverse proxy setup with load balancing to backend targets", when: "Fronting backend services, implementing API gateway patterns, or load balancing", - code: snippet("recipes/reverse-proxy.md"), + code: snippet("recipes/reverse-proxy.txt"), gotchas: [ "ProxyWithConfig with RoundRobinBalancer handles load balancing automatically", "Set appropriate timeouts on the proxy to avoid hanging connections", @@ -233,7 +233,7 @@ export const RECIPES: Recipe[] = [ category: "streaming", description: "Chunked streaming response with explicit Flush() for large or live data", when: "Streaming large files, live log tailing, or progressive data delivery", - code: snippet("recipes/streaming-response.md"), + code: snippet("recipes/streaming-response.txt"), gotchas: [ "Flush() is MANDATORY โ€” without it the entire response buffers until the handler returns", "Do NOT use Gzip middleware on streaming routes โ€” it buffers the entire response", @@ -247,7 +247,7 @@ export const RECIPES: Recipe[] = [ category: "routing", description: "e.Group() for resource grouping, API versioning, and scoped middleware", when: "Organizing routes by version, resource, or auth requirement", - code: snippet("recipes/route-groups.md"), + code: snippet("recipes/route-groups.txt"), gotchas: [ "Group middleware only applies to routes registered on that group โ€” not the parent", "Groups can be nested: v1.Group('/users') creates /v1/users prefix", @@ -261,7 +261,7 @@ export const RECIPES: Recipe[] = [ category: "middleware", description: "Request timeout middleware with context cancellation", when: "Enforcing maximum request duration to protect against slow clients or upstream hangs", - code: snippet("recipes/timeout.md"), + code: snippet("recipes/timeout.txt"), gotchas: [ "Always check context.Done() in handlers โ€” TimeoutMiddleware cancels the context, not the goroutine", "Set timeout > your slowest expected operation but < your load balancer's timeout", @@ -274,7 +274,7 @@ export const RECIPES: Recipe[] = [ category: "file", description: "Embed static assets into the binary with //go:embed and serve via http.FS", when: "Shipping a self-contained binary with bundled frontend assets, templates, or static files", - code: snippet("recipes/embed-resources.md"), + code: snippet("recipes/embed-resources.txt"), gotchas: [ "The //go:embed directive must be in the same package as the variable it annotates", "fs.Sub() strips the top-level directory prefix from the embedded FS", @@ -288,7 +288,7 @@ export const RECIPES: Recipe[] = [ category: "routing", description: "Route requests by subdomain using e.Host() with static or wildcard subdomains", when: "Multi-tenant apps, API/admin separation, or white-label subdomain routing", - code: snippet("recipes/subdomain-routing.md"), + code: snippet("recipes/subdomain-routing.txt"), gotchas: [ "e.Host() requires the Host header to match โ€” test with /etc/hosts entries locally", "Wildcard subdomain capture uses :subdomain in the host pattern", @@ -302,7 +302,7 @@ export const RECIPES: Recipe[] = [ category: "routing", description: "JSONP response for legacy cross-domain requests using c.JSONP()", when: "Supporting legacy clients or environments where CORS is not available", - code: snippet("recipes/jsonp.md"), + code: snippet("recipes/jsonp.txt"), gotchas: [ "JSONP is a legacy technique โ€” prefer CORS for all modern use cases", "Always validate the callback parameter โ€” never reflect arbitrary input to avoid XSS", @@ -321,87 +321,87 @@ export const MIDDLEWARE: MiddlewareRef[] = [ name: "Logger", purpose: "Logs request method, path, status, latency, and bytes", usage: "e.Use(middleware.Logger())", - code: snippet("middleware/logger.md"), + code: snippet("middleware/logger.txt"), order: "FIRST โ€” must be before Recover to log all requests including panics", }, { name: "Recover", purpose: "Catches panics, logs the stack trace, and returns HTTP 500", usage: "e.Use(middleware.Recover())", - code: snippet("middleware/recover.md"), + code: snippet("middleware/recover.txt"), order: "SECOND โ€” after Logger, before all other middleware", }, { name: "CORS", purpose: "Sets Cross-Origin Resource Sharing headers", usage: "e.Use(middleware.CORSWithConfig(...))", - code: snippet("middleware/cors.md"), + code: snippet("middleware/cors.txt"), order: "Before auth middleware so OPTIONS preflight requests pass through", }, { name: "JWT", purpose: "Validates JWT Bearer tokens and sets claims on context", usage: "group.Use(echojwt.WithConfig(...))", - code: snippet("middleware/jwt.md"), + code: snippet("middleware/jwt.txt"), order: "After CORS, on protected route groups only", }, { name: "CSRF", purpose: "Protects against Cross-Site Request Forgery attacks", usage: "e.Use(middleware.CSRF())", - code: snippet("middleware/csrf.md"), + code: snippet("middleware/csrf.txt"), order: "After session middleware", }, { name: "RateLimiter", purpose: "Limits request rate per IP using in-memory store", usage: "e.Use(middleware.RateLimiter(...))", - code: snippet("middleware/rate-limiter.md"), + code: snippet("middleware/rate-limiter.txt"), order: "Early in chain โ€” before auth to prevent brute force", }, { name: "BasicAuth", purpose: "HTTP Basic Authentication", usage: "group.Use(middleware.BasicAuth(...))", - code: snippet("middleware/basic-auth.md"), + code: snippet("middleware/basic-auth.txt"), }, { name: "KeyAuth", purpose: "Validates API keys from header, query, or cookie", usage: "e.Use(middleware.KeyAuth(...))", - code: snippet("middleware/key-auth.md"), + code: snippet("middleware/key-auth.txt"), }, { name: "Gzip", purpose: "Compresses responses with gzip encoding", usage: "e.Use(middleware.Gzip())", - code: snippet("middleware/gzip.md"), + code: snippet("middleware/gzip.txt"), order: "Do NOT use on SSE or streaming routes โ€” it buffers the entire response", }, { name: "Secure", purpose: "Sets security headers (HSTS, XSS protection, content type nosniff, etc.)", usage: "e.Use(middleware.Secure())", - code: snippet("middleware/secure.md"), + code: snippet("middleware/secure.txt"), }, { name: "BodyLimit", purpose: "Limits request body size to prevent abuse", usage: "e.Use(middleware.BodyLimit('2M'))", - code: snippet("middleware/body-limit.md"), + code: snippet("middleware/body-limit.txt"), order: "Early in chain, before body reading middleware", }, { name: "RequestID", purpose: "Attaches a unique X-Request-ID to each request for tracing", usage: "e.Use(middleware.RequestID())", - code: snippet("middleware/request-id.md"), + code: snippet("middleware/request-id.txt"), }, { name: "Timeout", purpose: "Cancels requests that exceed a time limit", usage: "e.Use(middleware.TimeoutWithConfig(...))", - code: snippet("middleware/timeout.md"), + code: snippet("middleware/timeout.txt"), }, ]; diff --git a/snippets/echo/cheatsheet.md b/src/plugins/echo/snippets/cheatsheet.txt similarity index 100% rename from snippets/echo/cheatsheet.md rename to src/plugins/echo/snippets/cheatsheet.txt diff --git a/snippets/echo/middleware/basic-auth.md b/src/plugins/echo/snippets/middleware/basic-auth.txt similarity index 100% rename from snippets/echo/middleware/basic-auth.md rename to src/plugins/echo/snippets/middleware/basic-auth.txt diff --git a/snippets/echo/middleware/body-limit.md b/src/plugins/echo/snippets/middleware/body-limit.txt similarity index 100% rename from snippets/echo/middleware/body-limit.md rename to src/plugins/echo/snippets/middleware/body-limit.txt diff --git a/snippets/echo/middleware/cors.md b/src/plugins/echo/snippets/middleware/cors.txt similarity index 100% rename from snippets/echo/middleware/cors.md rename to src/plugins/echo/snippets/middleware/cors.txt diff --git a/snippets/echo/middleware/csrf.md b/src/plugins/echo/snippets/middleware/csrf.txt similarity index 100% rename from snippets/echo/middleware/csrf.md rename to src/plugins/echo/snippets/middleware/csrf.txt diff --git a/snippets/echo/middleware/gzip.md b/src/plugins/echo/snippets/middleware/gzip.txt similarity index 100% rename from snippets/echo/middleware/gzip.md rename to src/plugins/echo/snippets/middleware/gzip.txt diff --git a/snippets/echo/middleware/jwt.md b/src/plugins/echo/snippets/middleware/jwt.txt similarity index 100% rename from snippets/echo/middleware/jwt.md rename to src/plugins/echo/snippets/middleware/jwt.txt diff --git a/snippets/echo/middleware/key-auth.md b/src/plugins/echo/snippets/middleware/key-auth.txt similarity index 100% rename from snippets/echo/middleware/key-auth.md rename to src/plugins/echo/snippets/middleware/key-auth.txt diff --git a/snippets/echo/middleware/logger.md b/src/plugins/echo/snippets/middleware/logger.txt similarity index 100% rename from snippets/echo/middleware/logger.md rename to src/plugins/echo/snippets/middleware/logger.txt diff --git a/snippets/echo/middleware/rate-limiter.md b/src/plugins/echo/snippets/middleware/rate-limiter.txt similarity index 100% rename from snippets/echo/middleware/rate-limiter.md rename to src/plugins/echo/snippets/middleware/rate-limiter.txt diff --git a/snippets/echo/middleware/recover.md b/src/plugins/echo/snippets/middleware/recover.txt similarity index 100% rename from snippets/echo/middleware/recover.md rename to src/plugins/echo/snippets/middleware/recover.txt diff --git a/snippets/echo/middleware/request-id.md b/src/plugins/echo/snippets/middleware/request-id.txt similarity index 100% rename from snippets/echo/middleware/request-id.md rename to src/plugins/echo/snippets/middleware/request-id.txt diff --git a/snippets/echo/middleware/secure.md b/src/plugins/echo/snippets/middleware/secure.txt similarity index 100% rename from snippets/echo/middleware/secure.md rename to src/plugins/echo/snippets/middleware/secure.txt diff --git a/snippets/echo/middleware/timeout.md b/src/plugins/echo/snippets/middleware/timeout.txt similarity index 100% rename from snippets/echo/middleware/timeout.md rename to src/plugins/echo/snippets/middleware/timeout.txt diff --git a/snippets/echo/recipes/auto-tls.md b/src/plugins/echo/snippets/recipes/auto-tls.txt similarity index 100% rename from snippets/echo/recipes/auto-tls.md rename to src/plugins/echo/snippets/recipes/auto-tls.txt diff --git a/snippets/echo/recipes/cors.md b/src/plugins/echo/snippets/recipes/cors.txt similarity index 100% rename from snippets/echo/recipes/cors.md rename to src/plugins/echo/snippets/recipes/cors.txt diff --git a/snippets/echo/recipes/crud-api.md b/src/plugins/echo/snippets/recipes/crud-api.txt similarity index 100% rename from snippets/echo/recipes/crud-api.md rename to src/plugins/echo/snippets/recipes/crud-api.txt diff --git a/snippets/echo/recipes/embed-resources.md b/src/plugins/echo/snippets/recipes/embed-resources.txt similarity index 100% rename from snippets/echo/recipes/embed-resources.md rename to src/plugins/echo/snippets/recipes/embed-resources.txt diff --git a/snippets/echo/recipes/file-download.md b/src/plugins/echo/snippets/recipes/file-download.txt similarity index 100% rename from snippets/echo/recipes/file-download.md rename to src/plugins/echo/snippets/recipes/file-download.txt diff --git a/snippets/echo/recipes/file-upload.md b/src/plugins/echo/snippets/recipes/file-upload.txt similarity index 100% rename from snippets/echo/recipes/file-upload.md rename to src/plugins/echo/snippets/recipes/file-upload.txt diff --git a/snippets/echo/recipes/graceful-shutdown.md b/src/plugins/echo/snippets/recipes/graceful-shutdown.txt similarity index 100% rename from snippets/echo/recipes/graceful-shutdown.md rename to src/plugins/echo/snippets/recipes/graceful-shutdown.txt diff --git a/snippets/echo/recipes/hello-world.md b/src/plugins/echo/snippets/recipes/hello-world.txt similarity index 100% rename from snippets/echo/recipes/hello-world.md rename to src/plugins/echo/snippets/recipes/hello-world.txt diff --git a/snippets/echo/recipes/http2.md b/src/plugins/echo/snippets/recipes/http2.txt similarity index 100% rename from snippets/echo/recipes/http2.md rename to src/plugins/echo/snippets/recipes/http2.txt diff --git a/snippets/echo/recipes/jsonp.md b/src/plugins/echo/snippets/recipes/jsonp.txt similarity index 100% rename from snippets/echo/recipes/jsonp.md rename to src/plugins/echo/snippets/recipes/jsonp.txt diff --git a/snippets/echo/recipes/jwt-auth.md b/src/plugins/echo/snippets/recipes/jwt-auth.txt similarity index 100% rename from snippets/echo/recipes/jwt-auth.md rename to src/plugins/echo/snippets/recipes/jwt-auth.txt diff --git a/snippets/echo/recipes/middleware-chain.md b/src/plugins/echo/snippets/recipes/middleware-chain.txt similarity index 100% rename from snippets/echo/recipes/middleware-chain.md rename to src/plugins/echo/snippets/recipes/middleware-chain.txt diff --git a/snippets/echo/recipes/reverse-proxy.md b/src/plugins/echo/snippets/recipes/reverse-proxy.txt similarity index 100% rename from snippets/echo/recipes/reverse-proxy.md rename to src/plugins/echo/snippets/recipes/reverse-proxy.txt diff --git a/snippets/echo/recipes/route-groups.md b/src/plugins/echo/snippets/recipes/route-groups.txt similarity index 100% rename from snippets/echo/recipes/route-groups.md rename to src/plugins/echo/snippets/recipes/route-groups.txt diff --git a/snippets/echo/recipes/sse.md b/src/plugins/echo/snippets/recipes/sse.txt similarity index 100% rename from snippets/echo/recipes/sse.md rename to src/plugins/echo/snippets/recipes/sse.txt diff --git a/snippets/echo/recipes/streaming-response.md b/src/plugins/echo/snippets/recipes/streaming-response.txt similarity index 100% rename from snippets/echo/recipes/streaming-response.md rename to src/plugins/echo/snippets/recipes/streaming-response.txt diff --git a/snippets/echo/recipes/subdomain-routing.md b/src/plugins/echo/snippets/recipes/subdomain-routing.txt similarity index 100% rename from snippets/echo/recipes/subdomain-routing.md rename to src/plugins/echo/snippets/recipes/subdomain-routing.txt diff --git a/snippets/echo/recipes/timeout.md b/src/plugins/echo/snippets/recipes/timeout.txt similarity index 100% rename from snippets/echo/recipes/timeout.md rename to src/plugins/echo/snippets/recipes/timeout.txt diff --git a/snippets/echo/recipes/websocket.md b/src/plugins/echo/snippets/recipes/websocket.txt similarity index 100% rename from snippets/echo/recipes/websocket.md rename to src/plugins/echo/snippets/recipes/websocket.txt diff --git a/src/plugins/golang/data.ts b/src/plugins/golang/data.ts index c7b4d43..7892411 100644 --- a/src/plugins/golang/data.ts +++ b/src/plugins/golang/data.ts @@ -38,8 +38,8 @@ export const BEST_PRACTICES: BestPractice[] = [ priority: "P0", rule: "camelCase for unexported, PascalCase for exported. Short, descriptive names.", reason: "Go convention. Exported names are part of the package API.", - good: snippet("practices/naming-conventions-good.md"), - bad: snippet("practices/naming-conventions-bad.md"), + good: snippet("practices/naming-conventions-good.txt"), + bad: snippet("practices/naming-conventions-bad.txt"), }, { name: "small-interfaces", @@ -47,8 +47,8 @@ export const BEST_PRACTICES: BestPractice[] = [ priority: "P0", rule: "Keep interfaces small (1-2 methods). Define at consumer, not producer.", reason: "Go proverb: the bigger the interface, the weaker the abstraction. Consumer-side definition enables decoupling without circular deps.", - good: snippet("practices/small-interfaces-good.md"), - bad: snippet("practices/small-interfaces-bad.md"), + good: snippet("practices/small-interfaces-good.txt"), + bad: snippet("practices/small-interfaces-bad.txt"), }, { name: "constructor-pattern", @@ -56,7 +56,7 @@ export const BEST_PRACTICES: BestPractice[] = [ priority: "P0", rule: "Use NewType() constructor functions for structs requiring validation or defaults.", reason: "Prevents zero-value misuse. Validation at construction, not at every use site.", - good: snippet("practices/constructor-pattern-good.md"), + good: snippet("practices/constructor-pattern-good.txt"), }, // ERROR HANDLING { @@ -65,8 +65,8 @@ export const BEST_PRACTICES: BestPractice[] = [ priority: "P0", rule: "Always wrap errors with context: fmt.Errorf(\"context: %w\", err)", reason: "Provides call stack context without expensive stack traces. %w enables errors.Is/As.", - good: snippet("practices/error-wrapping-good.md"), - bad: snippet("practices/error-wrapping-bad.md"), + good: snippet("practices/error-wrapping-good.txt"), + bad: snippet("practices/error-wrapping-bad.txt"), }, { name: "handle-once", @@ -74,8 +74,8 @@ export const BEST_PRACTICES: BestPractice[] = [ priority: "P0", rule: "Handle errors once: Log OR Return, never both.", reason: "Logging and returning causes duplicate error messages at every stack level.", - bad: snippet("practices/handle-once-bad.md"), - good: snippet("practices/handle-once-good.md"), + bad: snippet("practices/handle-once-bad.txt"), + good: snippet("practices/handle-once-good.txt"), }, { name: "errors-is-as", @@ -83,8 +83,8 @@ export const BEST_PRACTICES: BestPractice[] = [ priority: "P0", rule: "Use errors.Is() for sentinel errors, errors.As() for error types.", reason: "Works correctly with wrapped errors (%w). Direct == comparison breaks wrapping.", - good: snippet("practices/errors-is-as-good.md"), - bad: snippet("practices/errors-is-as-bad.md"), + good: snippet("practices/errors-is-as-good.txt"), + bad: snippet("practices/errors-is-as-bad.txt"), }, // CONCURRENCY { @@ -93,8 +93,8 @@ export const BEST_PRACTICES: BestPractice[] = [ priority: "P0", rule: "Pass context.Context as the first parameter to every function that does I/O.", reason: "Enables cancellation propagation and deadline enforcement across service boundaries.", - good: snippet("practices/context-first-param-good.md"), - bad: snippet("practices/context-first-param-bad.md"), + good: snippet("practices/context-first-param-good.txt"), + bad: snippet("practices/context-first-param-bad.txt"), }, { name: "goroutine-lifecycle", @@ -102,8 +102,8 @@ export const BEST_PRACTICES: BestPractice[] = [ priority: "P0", rule: "Never start a goroutine without knowing how it stops.", reason: "Goroutine leaks exhaust memory. Every goroutine needs a clear exit condition.", - good: snippet("practices/goroutine-lifecycle-good.md"), - bad: snippet("practices/goroutine-lifecycle-bad.md"), + good: snippet("practices/goroutine-lifecycle-good.txt"), + bad: snippet("practices/goroutine-lifecycle-bad.txt"), }, { name: "errgroup", @@ -111,8 +111,8 @@ export const BEST_PRACTICES: BestPractice[] = [ priority: "P0", rule: "Use errgroup over WaitGroup when goroutines can fail.", reason: "errgroup propagates the first error and cancels the context for all siblings.", - good: snippet("practices/errgroup-good.md"), - bad: snippet("practices/errgroup-bad.md"), + good: snippet("practices/errgroup-good.txt"), + bad: snippet("practices/errgroup-bad.txt"), }, // SECURITY { @@ -121,8 +121,8 @@ export const BEST_PRACTICES: BestPractice[] = [ priority: "P0", rule: "ALWAYS use crypto/rand for security-sensitive randomness. Never math/rand.", reason: "math/rand is deterministic and predictable. crypto/rand uses OS entropy.", - good: snippet("practices/crypto-rand-good.md"), - bad: snippet("practices/crypto-rand-bad.md"), + good: snippet("practices/crypto-rand-good.txt"), + bad: snippet("practices/crypto-rand-bad.txt"), }, { name: "parameterized-queries", @@ -130,8 +130,8 @@ export const BEST_PRACTICES: BestPractice[] = [ priority: "P0", rule: "Always use parameterized queries. Never string concatenation for SQL.", reason: "SQL injection is a critical vulnerability. String concatenation is always wrong.", - good: snippet("practices/parameterized-queries-good.md"), - bad: snippet("practices/parameterized-queries-bad.md"), + good: snippet("practices/parameterized-queries-good.txt"), + bad: snippet("practices/parameterized-queries-bad.txt"), }, // API SERVER { @@ -140,7 +140,7 @@ export const BEST_PRACTICES: BestPractice[] = [ priority: "P0", rule: "MUST implement graceful shutdown with signal handling.", reason: "Without graceful shutdown, in-flight requests are aborted on deploy/restart.", - good: snippet("practices/graceful-shutdown-good.md"), + good: snippet("practices/graceful-shutdown-good.txt"), }, { name: "thin-handlers", @@ -148,7 +148,7 @@ export const BEST_PRACTICES: BestPractice[] = [ priority: "P0", rule: "Keep handlers thin: parse โ†’ call service โ†’ respond. No business logic in handlers.", reason: "Business logic in handlers is untestable and non-reusable.", - good: snippet("practices/thin-handlers-good.md"), + good: snippet("practices/thin-handlers-good.txt"), }, // TESTING { @@ -157,7 +157,7 @@ export const BEST_PRACTICES: BestPractice[] = [ priority: "P0", rule: "Use table-driven test pattern for all test cases.", reason: "DRY, readable, easy to add cases, works well with t.Run.", - good: snippet("practices/table-driven-tests-good.md"), + good: snippet("practices/table-driven-tests-good.txt"), }, // DATABASE { @@ -166,8 +166,8 @@ export const BEST_PRACTICES: BestPractice[] = [ priority: "P0", rule: "Use the repository pattern with interfaces. Always use QueryContext/ExecContext with context. Always defer rows.Close().", reason: "Repository pattern decouples business logic from database implementation. Context enables cancellation. Forgetting rows.Close() causes connection leaks.", - good: snippet("practices/database-repository-good.md"), - bad: snippet("practices/database-repository-bad.md"), + good: snippet("practices/database-repository-good.txt"), + bad: snippet("practices/database-repository-bad.txt"), }, // CONFIG { @@ -176,8 +176,8 @@ export const BEST_PRACTICES: BestPractice[] = [ priority: "P1", rule: "Load all config from environment variables into a typed struct at startup. Validate on startup. Never scatter os.Getenv() calls.", reason: "Centralized validation prevents silent misconfiguration. 12-factor compliance. Typed struct makes config injectable and testable.", - good: snippet("practices/config-env-vars-good.md"), - bad: snippet("practices/config-env-vars-bad.md"), + good: snippet("practices/config-env-vars-good.txt"), + bad: snippet("practices/config-env-vars-bad.txt"), }, // LOGGING { @@ -186,8 +186,8 @@ export const BEST_PRACTICES: BestPractice[] = [ priority: "P1", rule: "Use log/slog with structured key-value pairs. JSON in production, text in development. Never log.Fatal() in libraries.", reason: "Structured logs are machine-parseable and alertable. log.Fatal() in libraries kills the process without allowing cleanup.", - good: snippet("practices/structured-logging-good.md"), - bad: snippet("practices/structured-logging-bad.md"), + good: snippet("practices/structured-logging-good.txt"), + bad: snippet("practices/structured-logging-bad.txt"), }, // TOOLING { @@ -196,7 +196,7 @@ export const BEST_PRACTICES: BestPractice[] = [ priority: "P1", rule: "Configure golangci-lint with errcheck, gosec, staticcheck, bodyclose, and noctx. Run in CI.", reason: "Automated linting catches error handling gaps, security issues, and resource leaks before code review.", - good: snippet("practices/golangci-lint-good.md"), + good: snippet("practices/golangci-lint-good.txt"), }, ]; @@ -211,7 +211,7 @@ export const DESIGN_PATTERNS: DesignPattern[] = [ goApproach: "Variadic options functions instead of config structs or builder chains", when: "Constructor with 5+ optional parameters", oopEquivalent: "Builder pattern", - code: snippet("patterns/functional-options.md"), + code: snippet("patterns/functional-options.txt"), }, { name: "adapter", @@ -219,7 +219,7 @@ export const DESIGN_PATTERNS: DesignPattern[] = [ goApproach: "Wrapper struct that implements an interface using a different underlying type", when: "Integrating external libraries or legacy code behind a clean interface", oopEquivalent: "Adapter / Wrapper", - code: snippet("patterns/adapter.md"), + code: snippet("patterns/adapter.txt"), }, { name: "middleware-decorator", @@ -227,29 +227,29 @@ export const DESIGN_PATTERNS: DesignPattern[] = [ goApproach: "Higher-order functions wrapping handlers โ€” the standard HTTP middleware pattern", when: "Adding cross-cutting behavior (logging, auth, rate limiting) without modifying handlers", oopEquivalent: "Decorator pattern", - code: snippet("patterns/middleware-decorator.md"), + code: snippet("patterns/middleware-decorator.txt"), }, { name: "worker-pool", category: "concurrency", goApproach: "Bounded goroutine pool using buffered channel as semaphore", when: "Parallel work with bounded concurrency (CPU/network limits)", - code: snippet("patterns/worker-pool.md"), + code: snippet("patterns/worker-pool.txt"), }, { name: "pipeline", category: "concurrency", goApproach: "Chain of goroutines connected by channels โ€” each stage transforms the stream", when: "Stage-by-stage stream processing (ETL, data transformation)", - code: snippet("patterns/pipeline.md"), + code: snippet("patterns/pipeline.txt"), }, { name: "consumer-side-interface", category: "structural", goApproach: "Define interfaces in the consuming package, not the implementing package", when: "Always โ€” this is the idiomatic Go approach to dependency management", - code: snippet("patterns/consumer-side-interface.md"), - antiPattern: snippet("patterns/consumer-side-interface-anti.md"), + code: snippet("patterns/consumer-side-interface.txt"), + antiPattern: snippet("patterns/consumer-side-interface-anti.txt"), }, { name: "strategy", @@ -257,14 +257,14 @@ export const DESIGN_PATTERNS: DesignPattern[] = [ goApproach: "Interface injection โ€” pass the algorithm as a function or interface", when: "Multiple algorithms that can be swapped at runtime", oopEquivalent: "Strategy pattern", - code: snippet("patterns/strategy.md"), + code: snippet("patterns/strategy.txt"), }, { name: "fan-out-fan-in", category: "concurrency", goApproach: "Distribute work across N goroutines (fan-out), then merge results into one channel (fan-in)", when: "Parallel independent work items with result collection", - code: snippet("patterns/fan-out-fan-in.md"), + code: snippet("patterns/fan-out-fan-in.txt"), }, { name: "observer", @@ -272,7 +272,7 @@ export const DESIGN_PATTERNS: DesignPattern[] = [ goApproach: "Channel-based event bus โ€” subscribers receive from buffered channels", when: "Decoupling event producers from consumers without shared state", oopEquivalent: "Observer / Pub-Sub pattern", - code: snippet("patterns/observer.md"), + code: snippet("patterns/observer.txt"), }, { name: "command", @@ -280,7 +280,7 @@ export const DESIGN_PATTERNS: DesignPattern[] = [ goApproach: "Function closures as commands โ€” submitted to a worker channel for execution", when: "Job queues, undo stacks, deferred execution, task pipelines", oopEquivalent: "Command pattern", - code: snippet("patterns/command.md"), + code: snippet("patterns/command.txt"), }, ]; diff --git a/snippets/golang/cheatsheet.md b/src/plugins/golang/snippets/cheatsheet.txt similarity index 100% rename from snippets/golang/cheatsheet.md rename to src/plugins/golang/snippets/cheatsheet.txt diff --git a/snippets/golang/patterns/adapter.md b/src/plugins/golang/snippets/patterns/adapter.txt similarity index 100% rename from snippets/golang/patterns/adapter.md rename to src/plugins/golang/snippets/patterns/adapter.txt diff --git a/snippets/golang/patterns/command.md b/src/plugins/golang/snippets/patterns/command.txt similarity index 100% rename from snippets/golang/patterns/command.md rename to src/plugins/golang/snippets/patterns/command.txt diff --git a/snippets/golang/patterns/consumer-side-interface-anti.md b/src/plugins/golang/snippets/patterns/consumer-side-interface-anti.txt similarity index 100% rename from snippets/golang/patterns/consumer-side-interface-anti.md rename to src/plugins/golang/snippets/patterns/consumer-side-interface-anti.txt diff --git a/snippets/golang/patterns/consumer-side-interface.md b/src/plugins/golang/snippets/patterns/consumer-side-interface.txt similarity index 100% rename from snippets/golang/patterns/consumer-side-interface.md rename to src/plugins/golang/snippets/patterns/consumer-side-interface.txt diff --git a/snippets/golang/patterns/fan-out-fan-in.md b/src/plugins/golang/snippets/patterns/fan-out-fan-in.txt similarity index 100% rename from snippets/golang/patterns/fan-out-fan-in.md rename to src/plugins/golang/snippets/patterns/fan-out-fan-in.txt diff --git a/snippets/golang/patterns/functional-options.md b/src/plugins/golang/snippets/patterns/functional-options.txt similarity index 100% rename from snippets/golang/patterns/functional-options.md rename to src/plugins/golang/snippets/patterns/functional-options.txt diff --git a/snippets/golang/patterns/middleware-decorator.md b/src/plugins/golang/snippets/patterns/middleware-decorator.txt similarity index 100% rename from snippets/golang/patterns/middleware-decorator.md rename to src/plugins/golang/snippets/patterns/middleware-decorator.txt diff --git a/snippets/golang/patterns/observer.md b/src/plugins/golang/snippets/patterns/observer.txt similarity index 100% rename from snippets/golang/patterns/observer.md rename to src/plugins/golang/snippets/patterns/observer.txt diff --git a/snippets/golang/patterns/pipeline.md b/src/plugins/golang/snippets/patterns/pipeline.txt similarity index 100% rename from snippets/golang/patterns/pipeline.md rename to src/plugins/golang/snippets/patterns/pipeline.txt diff --git a/snippets/golang/patterns/strategy.md b/src/plugins/golang/snippets/patterns/strategy.txt similarity index 100% rename from snippets/golang/patterns/strategy.md rename to src/plugins/golang/snippets/patterns/strategy.txt diff --git a/snippets/golang/patterns/worker-pool.md b/src/plugins/golang/snippets/patterns/worker-pool.txt similarity index 100% rename from snippets/golang/patterns/worker-pool.md rename to src/plugins/golang/snippets/patterns/worker-pool.txt diff --git a/snippets/golang/practices/config-env-vars-bad.md b/src/plugins/golang/snippets/practices/config-env-vars-bad.txt similarity index 100% rename from snippets/golang/practices/config-env-vars-bad.md rename to src/plugins/golang/snippets/practices/config-env-vars-bad.txt diff --git a/snippets/golang/practices/config-env-vars-good.md b/src/plugins/golang/snippets/practices/config-env-vars-good.txt similarity index 100% rename from snippets/golang/practices/config-env-vars-good.md rename to src/plugins/golang/snippets/practices/config-env-vars-good.txt diff --git a/snippets/golang/practices/constructor-pattern-good.md b/src/plugins/golang/snippets/practices/constructor-pattern-good.txt similarity index 100% rename from snippets/golang/practices/constructor-pattern-good.md rename to src/plugins/golang/snippets/practices/constructor-pattern-good.txt diff --git a/snippets/golang/practices/context-first-param-bad.md b/src/plugins/golang/snippets/practices/context-first-param-bad.txt similarity index 100% rename from snippets/golang/practices/context-first-param-bad.md rename to src/plugins/golang/snippets/practices/context-first-param-bad.txt diff --git a/snippets/golang/practices/context-first-param-good.md b/src/plugins/golang/snippets/practices/context-first-param-good.txt similarity index 100% rename from snippets/golang/practices/context-first-param-good.md rename to src/plugins/golang/snippets/practices/context-first-param-good.txt diff --git a/snippets/golang/practices/crypto-rand-bad.md b/src/plugins/golang/snippets/practices/crypto-rand-bad.txt similarity index 100% rename from snippets/golang/practices/crypto-rand-bad.md rename to src/plugins/golang/snippets/practices/crypto-rand-bad.txt diff --git a/snippets/golang/practices/crypto-rand-good.md b/src/plugins/golang/snippets/practices/crypto-rand-good.txt similarity index 100% rename from snippets/golang/practices/crypto-rand-good.md rename to src/plugins/golang/snippets/practices/crypto-rand-good.txt diff --git a/snippets/golang/practices/database-repository-bad.md b/src/plugins/golang/snippets/practices/database-repository-bad.txt similarity index 100% rename from snippets/golang/practices/database-repository-bad.md rename to src/plugins/golang/snippets/practices/database-repository-bad.txt diff --git a/snippets/golang/practices/database-repository-good.md b/src/plugins/golang/snippets/practices/database-repository-good.txt similarity index 100% rename from snippets/golang/practices/database-repository-good.md rename to src/plugins/golang/snippets/practices/database-repository-good.txt diff --git a/snippets/golang/practices/errgroup-bad.md b/src/plugins/golang/snippets/practices/errgroup-bad.txt similarity index 100% rename from snippets/golang/practices/errgroup-bad.md rename to src/plugins/golang/snippets/practices/errgroup-bad.txt diff --git a/snippets/golang/practices/errgroup-good.md b/src/plugins/golang/snippets/practices/errgroup-good.txt similarity index 100% rename from snippets/golang/practices/errgroup-good.md rename to src/plugins/golang/snippets/practices/errgroup-good.txt diff --git a/snippets/golang/practices/error-wrapping-bad.md b/src/plugins/golang/snippets/practices/error-wrapping-bad.txt similarity index 100% rename from snippets/golang/practices/error-wrapping-bad.md rename to src/plugins/golang/snippets/practices/error-wrapping-bad.txt diff --git a/snippets/golang/practices/error-wrapping-good.md b/src/plugins/golang/snippets/practices/error-wrapping-good.txt similarity index 100% rename from snippets/golang/practices/error-wrapping-good.md rename to src/plugins/golang/snippets/practices/error-wrapping-good.txt diff --git a/snippets/golang/practices/errors-is-as-bad.md b/src/plugins/golang/snippets/practices/errors-is-as-bad.txt similarity index 100% rename from snippets/golang/practices/errors-is-as-bad.md rename to src/plugins/golang/snippets/practices/errors-is-as-bad.txt diff --git a/snippets/golang/practices/errors-is-as-good.md b/src/plugins/golang/snippets/practices/errors-is-as-good.txt similarity index 100% rename from snippets/golang/practices/errors-is-as-good.md rename to src/plugins/golang/snippets/practices/errors-is-as-good.txt diff --git a/snippets/golang/practices/golangci-lint-good.md b/src/plugins/golang/snippets/practices/golangci-lint-good.txt similarity index 100% rename from snippets/golang/practices/golangci-lint-good.md rename to src/plugins/golang/snippets/practices/golangci-lint-good.txt diff --git a/snippets/golang/practices/goroutine-lifecycle-bad.md b/src/plugins/golang/snippets/practices/goroutine-lifecycle-bad.txt similarity index 100% rename from snippets/golang/practices/goroutine-lifecycle-bad.md rename to src/plugins/golang/snippets/practices/goroutine-lifecycle-bad.txt diff --git a/snippets/golang/practices/goroutine-lifecycle-good.md b/src/plugins/golang/snippets/practices/goroutine-lifecycle-good.txt similarity index 100% rename from snippets/golang/practices/goroutine-lifecycle-good.md rename to src/plugins/golang/snippets/practices/goroutine-lifecycle-good.txt diff --git a/snippets/golang/practices/graceful-shutdown-good.md b/src/plugins/golang/snippets/practices/graceful-shutdown-good.txt similarity index 100% rename from snippets/golang/practices/graceful-shutdown-good.md rename to src/plugins/golang/snippets/practices/graceful-shutdown-good.txt diff --git a/snippets/golang/practices/handle-once-bad.md b/src/plugins/golang/snippets/practices/handle-once-bad.txt similarity index 100% rename from snippets/golang/practices/handle-once-bad.md rename to src/plugins/golang/snippets/practices/handle-once-bad.txt diff --git a/snippets/golang/practices/handle-once-good.md b/src/plugins/golang/snippets/practices/handle-once-good.txt similarity index 100% rename from snippets/golang/practices/handle-once-good.md rename to src/plugins/golang/snippets/practices/handle-once-good.txt diff --git a/snippets/golang/practices/naming-conventions-bad.md b/src/plugins/golang/snippets/practices/naming-conventions-bad.txt similarity index 100% rename from snippets/golang/practices/naming-conventions-bad.md rename to src/plugins/golang/snippets/practices/naming-conventions-bad.txt diff --git a/snippets/golang/practices/naming-conventions-good.md b/src/plugins/golang/snippets/practices/naming-conventions-good.txt similarity index 100% rename from snippets/golang/practices/naming-conventions-good.md rename to src/plugins/golang/snippets/practices/naming-conventions-good.txt diff --git a/snippets/golang/practices/parameterized-queries-bad.md b/src/plugins/golang/snippets/practices/parameterized-queries-bad.txt similarity index 100% rename from snippets/golang/practices/parameterized-queries-bad.md rename to src/plugins/golang/snippets/practices/parameterized-queries-bad.txt diff --git a/snippets/golang/practices/parameterized-queries-good.md b/src/plugins/golang/snippets/practices/parameterized-queries-good.txt similarity index 100% rename from snippets/golang/practices/parameterized-queries-good.md rename to src/plugins/golang/snippets/practices/parameterized-queries-good.txt diff --git a/snippets/golang/practices/small-interfaces-bad.md b/src/plugins/golang/snippets/practices/small-interfaces-bad.txt similarity index 100% rename from snippets/golang/practices/small-interfaces-bad.md rename to src/plugins/golang/snippets/practices/small-interfaces-bad.txt diff --git a/snippets/golang/practices/small-interfaces-good.md b/src/plugins/golang/snippets/practices/small-interfaces-good.txt similarity index 100% rename from snippets/golang/practices/small-interfaces-good.md rename to src/plugins/golang/snippets/practices/small-interfaces-good.txt diff --git a/snippets/golang/practices/structured-logging-bad.md b/src/plugins/golang/snippets/practices/structured-logging-bad.txt similarity index 100% rename from snippets/golang/practices/structured-logging-bad.md rename to src/plugins/golang/snippets/practices/structured-logging-bad.txt diff --git a/snippets/golang/practices/structured-logging-good.md b/src/plugins/golang/snippets/practices/structured-logging-good.txt similarity index 100% rename from snippets/golang/practices/structured-logging-good.md rename to src/plugins/golang/snippets/practices/structured-logging-good.txt diff --git a/snippets/golang/practices/table-driven-tests-good.md b/src/plugins/golang/snippets/practices/table-driven-tests-good.txt similarity index 100% rename from snippets/golang/practices/table-driven-tests-good.md rename to src/plugins/golang/snippets/practices/table-driven-tests-good.txt diff --git a/snippets/golang/practices/thin-handlers-good.md b/src/plugins/golang/snippets/practices/thin-handlers-good.txt similarity index 100% rename from snippets/golang/practices/thin-handlers-good.md rename to src/plugins/golang/snippets/practices/thin-handlers-good.txt diff --git a/src/plugins/lenis/data.ts b/src/plugins/lenis/data.ts index 255ffa7..6e03eec 100644 --- a/src/plugins/lenis/data.ts +++ b/src/plugins/lenis/data.ts @@ -156,22 +156,22 @@ const reactLenis: ApiEntry = { description: "The content to be scroll-wrapped.", }, ], - usage: snippet("usage/react-lenis.md"), + usage: snippet("usage/react-lenis.txt"), examples: [ { title: "Root layout setup", category: "setup", - code: snippet("examples/react-lenis-root.md"), + code: snippet("examples/react-lenis-root.txt"), }, { title: "Container scroll (non-root)", category: "setup", - code: snippet("examples/react-lenis-container.md"), + code: snippet("examples/react-lenis-container.txt"), }, { title: "Accessing the Lenis instance via ref", category: "setup", - code: snippet("examples/react-lenis-ref.md"), + code: snippet("examples/react-lenis-ref.txt"), }, ], tips: [ @@ -213,27 +213,27 @@ const useLenis: ApiEntry = { }, ], returns: "Lenis | undefined", - usage: snippet("usage/use-lenis.md"), + usage: snippet("usage/use-lenis.txt"), examples: [ { title: "Scroll to element", category: "navigation", - code: snippet("examples/use-lenis-scroll-to.md"), + code: snippet("examples/use-lenis-scroll-to.txt"), }, { title: "Scroll progress tracker", category: "scroll", - code: snippet("examples/use-lenis-progress.md"), + code: snippet("examples/use-lenis-progress.txt"), }, { title: "Scroll-linked parallax", category: "scroll", - code: snippet("examples/use-lenis-parallax.md"), + code: snippet("examples/use-lenis-parallax.txt"), }, { title: "Stop/start scrolling", category: "control", - code: snippet("examples/use-lenis-modal.md"), + code: snippet("examples/use-lenis-modal.txt"), }, ], tips: [ @@ -267,12 +267,12 @@ const lenisRef: ApiEntry = { description: "The outer wrapper DOM element that Lenis is attached to.", }, ], - usage: snippet("usage/lenis-ref.md"), + usage: snippet("usage/lenis-ref.txt"), examples: [ { title: "Imperative scroll from outside context", category: "setup", - code: snippet("examples/lenis-ref-imperative.md"), + code: snippet("examples/lenis-ref-imperative.txt"), }, ], tips: [ @@ -359,17 +359,17 @@ const lenisOptions: ApiEntry = { description: "The inner content element. Used for non-root (container) scroll.", }, ], - usage: snippet("usage/lenis-options.md"), + usage: snippet("usage/lenis-options.txt"), examples: [ { title: "Tuned scroll feel for a marketing site", category: "options", - code: snippet("options/tuned-marketing.md"), + code: snippet("options/tuned-marketing.txt"), }, { title: "Horizontal scroll options", category: "options", - code: snippet("options/horizontal.md"), + code: snippet("options/horizontal.txt"), }, ], tips: [ @@ -395,7 +395,7 @@ export const PATTERNS: Record = { "full-page": { name: "full-page", description: "Standard root layout setup โ€” ReactLenis wraps the entire app for full-page smooth scrolling.", - code: snippet("patterns/full-page.md"), + code: snippet("patterns/full-page.txt"), tips: [ "root={true} is required for full-page scroll โ€” without it, Lenis creates an overflow:hidden container.", "The CSS import is mandatory โ€” skip it and the layout breaks.", @@ -404,7 +404,7 @@ export const PATTERNS: Record = { "next-js": { name: "next-js", description: "Next.js App Router pattern using a dedicated SmoothScrollProvider client component to wrap the layout.", - code: snippet("patterns/next-js.md"), + code: snippet("patterns/next-js.txt"), tips: [ "Keep app/layout.tsx as a Server Component โ€” extract the 'use client' directive into SmoothScrollProvider.", "This preserves RSC boundaries and avoids unnecessarily client-rendering the entire layout.", @@ -413,7 +413,7 @@ export const PATTERNS: Record = { "gsap-integration": { name: "gsap-integration", description: "Integrate Lenis with GSAP ScrollTrigger by disabling autoRaf and driving Lenis from GSAP's ticker.", - code: snippet("patterns/gsap-integration.md"), + code: snippet("patterns/gsap-integration.txt"), tips: [ "autoRaf: false is required โ€” if GSAP and Lenis both run their own RAF loops, scroll updates fire twice per frame causing desync.", "gsap.ticker.lagSmoothing(0) prevents GSAP from adjusting delta time which would cause Lenis to stutter after tab switches.", @@ -423,7 +423,7 @@ export const PATTERNS: Record = { "framer-motion-integration": { name: "framer-motion-integration", description: "Integrate Lenis with Framer Motion by disabling autoRaf and syncing via frame.update.", - code: snippet("patterns/framer-motion-integration.md"), + code: snippet("patterns/framer-motion-integration.txt"), tips: [ "Use frame from 'motion' (not 'framer-motion') โ€” this is the Framer Motion v11+ low-level scheduler.", "frame.update(fn, true) schedules the update to run on every frame. The second argument (true) enables loop mode.", @@ -433,7 +433,7 @@ export const PATTERNS: Record = { "custom-container": { name: "custom-container", description: "Scoped scroll container using wrapper and content refs for non-window smooth scroll.", - code: snippet("patterns/custom-container.md"), + code: snippet("patterns/custom-container.txt"), tips: [ "The wrapper element needs overflow: hidden and a fixed height for container scroll to work.", "The content element is the scrollable inner div โ€” it should grow naturally with its children.", @@ -443,7 +443,7 @@ export const PATTERNS: Record = { "accessibility": { name: "accessibility", description: "Respect prefers-reduced-motion by disabling smooth scrolling for users who prefer it.", - code: snippet("patterns/accessibility.md"), + code: snippet("patterns/accessibility.txt"), tips: [ "Never force smooth scrolling on users who have opted out via prefers-reduced-motion.", "When skipping ReactLenis, native scroll is used โ€” no polyfill needed.", @@ -453,7 +453,7 @@ export const PATTERNS: Record = { "scroll-to-nav": { name: "scroll-to-nav", description: "Navigation link that uses lenis.scrollTo() for smooth in-page anchor navigation.", - code: snippet("patterns/scroll-to-nav.md"), + code: snippet("patterns/scroll-to-nav.txt"), tips: [ "offset compensates for sticky headers โ€” pass a negative value equal to the header height.", "lenis.scrollTo() accepts a CSS selector ('#section'), HTMLElement, or pixel number.", diff --git a/snippets/lenis/cheatsheet.md b/src/plugins/lenis/snippets/cheatsheet.txt similarity index 100% rename from snippets/lenis/cheatsheet.md rename to src/plugins/lenis/snippets/cheatsheet.txt diff --git a/snippets/lenis/css/prevent-scroll.md b/src/plugins/lenis/snippets/css/prevent-scroll.txt similarity index 100% rename from snippets/lenis/css/prevent-scroll.md rename to src/plugins/lenis/snippets/css/prevent-scroll.txt diff --git a/snippets/lenis/css/required.md b/src/plugins/lenis/snippets/css/required.txt similarity index 100% rename from snippets/lenis/css/required.md rename to src/plugins/lenis/snippets/css/required.txt diff --git a/snippets/lenis/examples/lenis-ref-imperative.md b/src/plugins/lenis/snippets/examples/lenis-ref-imperative.txt similarity index 100% rename from snippets/lenis/examples/lenis-ref-imperative.md rename to src/plugins/lenis/snippets/examples/lenis-ref-imperative.txt diff --git a/snippets/lenis/examples/react-lenis-container.md b/src/plugins/lenis/snippets/examples/react-lenis-container.txt similarity index 100% rename from snippets/lenis/examples/react-lenis-container.md rename to src/plugins/lenis/snippets/examples/react-lenis-container.txt diff --git a/snippets/lenis/examples/react-lenis-ref.md b/src/plugins/lenis/snippets/examples/react-lenis-ref.txt similarity index 100% rename from snippets/lenis/examples/react-lenis-ref.md rename to src/plugins/lenis/snippets/examples/react-lenis-ref.txt diff --git a/snippets/lenis/examples/react-lenis-root.md b/src/plugins/lenis/snippets/examples/react-lenis-root.txt similarity index 100% rename from snippets/lenis/examples/react-lenis-root.md rename to src/plugins/lenis/snippets/examples/react-lenis-root.txt diff --git a/snippets/lenis/examples/use-lenis-modal.md b/src/plugins/lenis/snippets/examples/use-lenis-modal.txt similarity index 100% rename from snippets/lenis/examples/use-lenis-modal.md rename to src/plugins/lenis/snippets/examples/use-lenis-modal.txt diff --git a/snippets/lenis/examples/use-lenis-parallax.md b/src/plugins/lenis/snippets/examples/use-lenis-parallax.txt similarity index 100% rename from snippets/lenis/examples/use-lenis-parallax.md rename to src/plugins/lenis/snippets/examples/use-lenis-parallax.txt diff --git a/snippets/lenis/examples/use-lenis-progress.md b/src/plugins/lenis/snippets/examples/use-lenis-progress.txt similarity index 100% rename from snippets/lenis/examples/use-lenis-progress.md rename to src/plugins/lenis/snippets/examples/use-lenis-progress.txt diff --git a/snippets/lenis/examples/use-lenis-scroll-to.md b/src/plugins/lenis/snippets/examples/use-lenis-scroll-to.txt similarity index 100% rename from snippets/lenis/examples/use-lenis-scroll-to.md rename to src/plugins/lenis/snippets/examples/use-lenis-scroll-to.txt diff --git a/snippets/lenis/options/horizontal.md b/src/plugins/lenis/snippets/options/horizontal.txt similarity index 100% rename from snippets/lenis/options/horizontal.md rename to src/plugins/lenis/snippets/options/horizontal.txt diff --git a/snippets/lenis/options/tuned-marketing.md b/src/plugins/lenis/snippets/options/tuned-marketing.txt similarity index 100% rename from snippets/lenis/options/tuned-marketing.md rename to src/plugins/lenis/snippets/options/tuned-marketing.txt diff --git a/snippets/lenis/patterns/accessibility.md b/src/plugins/lenis/snippets/patterns/accessibility.txt similarity index 100% rename from snippets/lenis/patterns/accessibility.md rename to src/plugins/lenis/snippets/patterns/accessibility.txt diff --git a/snippets/lenis/patterns/custom-container.md b/src/plugins/lenis/snippets/patterns/custom-container.txt similarity index 100% rename from snippets/lenis/patterns/custom-container.md rename to src/plugins/lenis/snippets/patterns/custom-container.txt diff --git a/snippets/lenis/patterns/framer-motion-integration.md b/src/plugins/lenis/snippets/patterns/framer-motion-integration.txt similarity index 100% rename from snippets/lenis/patterns/framer-motion-integration.md rename to src/plugins/lenis/snippets/patterns/framer-motion-integration.txt diff --git a/snippets/lenis/patterns/full-page.md b/src/plugins/lenis/snippets/patterns/full-page.txt similarity index 100% rename from snippets/lenis/patterns/full-page.md rename to src/plugins/lenis/snippets/patterns/full-page.txt diff --git a/snippets/lenis/patterns/gsap-integration.md b/src/plugins/lenis/snippets/patterns/gsap-integration.txt similarity index 100% rename from snippets/lenis/patterns/gsap-integration.md rename to src/plugins/lenis/snippets/patterns/gsap-integration.txt diff --git a/snippets/lenis/patterns/next-js.md b/src/plugins/lenis/snippets/patterns/next-js.txt similarity index 100% rename from snippets/lenis/patterns/next-js.md rename to src/plugins/lenis/snippets/patterns/next-js.txt diff --git a/snippets/lenis/patterns/scroll-to-nav.md b/src/plugins/lenis/snippets/patterns/scroll-to-nav.txt similarity index 100% rename from snippets/lenis/patterns/scroll-to-nav.md rename to src/plugins/lenis/snippets/patterns/scroll-to-nav.txt diff --git a/snippets/lenis/recipes/back-to-top.md b/src/plugins/lenis/snippets/recipes/back-to-top.txt similarity index 100% rename from snippets/lenis/recipes/back-to-top.md rename to src/plugins/lenis/snippets/recipes/back-to-top.txt diff --git a/snippets/lenis/recipes/direction-indicator.md b/src/plugins/lenis/snippets/recipes/direction-indicator.txt similarity index 100% rename from snippets/lenis/recipes/direction-indicator.md rename to src/plugins/lenis/snippets/recipes/direction-indicator.txt diff --git a/snippets/lenis/recipes/gsap-complete.md b/src/plugins/lenis/snippets/recipes/gsap-complete.txt similarity index 100% rename from snippets/lenis/recipes/gsap-complete.md rename to src/plugins/lenis/snippets/recipes/gsap-complete.txt diff --git a/snippets/lenis/recipes/horizontal-scroll-section.md b/src/plugins/lenis/snippets/recipes/horizontal-scroll-section.txt similarity index 100% rename from snippets/lenis/recipes/horizontal-scroll-section.md rename to src/plugins/lenis/snippets/recipes/horizontal-scroll-section.txt diff --git a/snippets/lenis/recipes/parallax-layer.md b/src/plugins/lenis/snippets/recipes/parallax-layer.txt similarity index 100% rename from snippets/lenis/recipes/parallax-layer.md rename to src/plugins/lenis/snippets/recipes/parallax-layer.txt diff --git a/snippets/lenis/recipes/scroll-locked-modal.md b/src/plugins/lenis/snippets/recipes/scroll-locked-modal.txt similarity index 100% rename from snippets/lenis/recipes/scroll-locked-modal.md rename to src/plugins/lenis/snippets/recipes/scroll-locked-modal.txt diff --git a/snippets/lenis/recipes/scroll-progress-bar.md b/src/plugins/lenis/snippets/recipes/scroll-progress-bar.txt similarity index 100% rename from snippets/lenis/recipes/scroll-progress-bar.md rename to src/plugins/lenis/snippets/recipes/scroll-progress-bar.txt diff --git a/snippets/lenis/usage/lenis-options.md b/src/plugins/lenis/snippets/usage/lenis-options.txt similarity index 100% rename from snippets/lenis/usage/lenis-options.md rename to src/plugins/lenis/snippets/usage/lenis-options.txt diff --git a/snippets/lenis/usage/lenis-ref.md b/src/plugins/lenis/snippets/usage/lenis-ref.txt similarity index 100% rename from snippets/lenis/usage/lenis-ref.md rename to src/plugins/lenis/snippets/usage/lenis-ref.txt diff --git a/snippets/lenis/usage/react-lenis.md b/src/plugins/lenis/snippets/usage/react-lenis.txt similarity index 100% rename from snippets/lenis/usage/react-lenis.md rename to src/plugins/lenis/snippets/usage/react-lenis.txt diff --git a/snippets/lenis/usage/use-lenis.md b/src/plugins/lenis/snippets/usage/use-lenis.txt similarity index 100% rename from snippets/lenis/usage/use-lenis.md rename to src/plugins/lenis/snippets/usage/use-lenis.txt diff --git a/src/plugins/motion/data.ts b/src/plugins/motion/data.ts index e399614..7f09417 100755 --- a/src/plugins/motion/data.ts +++ b/src/plugins/motion/data.ts @@ -124,89 +124,89 @@ const motionComponent: ApiEntry = { { name: "onLayoutAnimationComplete", type: "() => void", description: "Layout animation complete callback." }, { name: "propagate", type: "{ tap?: boolean }", description: "Control gesture propagation." }, ], - usage: snippet("usage/motion.md"), + usage: snippet("usage/motion.txt"), examples: [ { title: "Basic fade in", category: "animation", - code: snippet("examples/motion/basic-fade-in.md"), + code: snippet("examples/motion/basic-fade-in.txt"), }, { title: "Hover and tap", category: "gestures", - code: snippet("examples/motion/hover-and-tap.md"), + code: snippet("examples/motion/hover-and-tap.txt"), }, { title: "Keyframes", category: "keyframes", - code: snippet("examples/motion/keyframes.md"), + code: snippet("examples/motion/keyframes.txt"), }, { title: "Wildcard keyframe (start from current value)", category: "keyframes", - code: snippet("examples/motion/wildcard-keyframe-start-from-current-value.md"), + code: snippet("examples/motion/wildcard-keyframe-start-from-current-value.txt"), }, { title: "Variants with orchestration", category: "variants", - code: snippet("examples/motion/variants-with-orchestration.md"), + code: snippet("examples/motion/variants-with-orchestration.txt"), }, { title: "Drag with constraints", category: "drag", - code: snippet("examples/motion/drag-with-constraints.md"), + code: snippet("examples/motion/drag-with-constraints.txt"), }, { title: "Scroll-triggered entrance", category: "scroll", - code: snippet("examples/motion/scroll-triggered-entrance.md"), + code: snippet("examples/motion/scroll-triggered-entrance.txt"), }, { title: "Layout animation", category: "layout", - code: snippet("examples/motion/layout-animation.md"), + code: snippet("examples/motion/layout-animation.txt"), }, { title: "Shared layout with layoutId", category: "layout", - code: snippet("examples/motion/shared-layout-with-layoutid.md"), + code: snippet("examples/motion/shared-layout-with-layoutid.txt"), }, { title: "SVG line drawing", category: "svg", - code: snippet("examples/motion/svg-line-drawing.md"), + code: snippet("examples/motion/svg-line-drawing.txt"), }, { title: "SVG path morphing", category: "svg", - code: snippet("examples/motion/svg-path-morphing.md"), + code: snippet("examples/motion/svg-path-morphing.txt"), description: "Paths must have same number and type of instructions.", }, { title: "Scroll image reveal with clipPath", category: "scroll", - code: snippet("examples/motion/scroll-image-reveal-with-clippath.md"), + code: snippet("examples/motion/scroll-image-reveal-with-clippath.txt"), }, { title: "Snap-to-grid drag", category: "drag", - code: snippet("examples/motion/snap-to-grid-drag.md"), + code: snippet("examples/motion/snap-to-grid-drag.txt"), }, { title: "Animate counter without re-renders", category: "animation", - code: snippet("examples/motion/animate-counter-without-re-renders.md"), + code: snippet("examples/motion/animate-counter-without-re-renders.txt"), description: "Pass a MotionValue as child to render its latest value.", }, { title: "Animate CSS variables", category: "animation", - code: snippet("examples/motion/animate-css-variables.md"), + code: snippet("examples/motion/animate-css-variables.txt"), }, { title: "Dynamic variants with custom", category: "variants", - code: snippet("examples/motion/dynamic-variants-with-custom.md"), + code: snippet("examples/motion/dynamic-variants-with-custom.txt"), }, ], tips: [ @@ -236,17 +236,17 @@ const animatePresence: ApiEntry = { { name: "propagate", type: "boolean", description: "If true, nested AnimatePresence children fire exit animations when parent exits." }, { name: "root", type: "ShadowRoot | HTMLElement", description: "Root element for popLayout styles. Defaults to document.head. Set to a ShadowRoot for shadow DOM usage." }, ], - usage: snippet("usage/AnimatePresence.md"), + usage: snippet("usage/AnimatePresence.txt"), examples: [ { title: "Modal with exit animation", category: "exit", - code: snippet("examples/AnimatePresence/modal-with-exit-animation.md"), + code: snippet("examples/AnimatePresence/modal-with-exit-animation.txt"), }, { title: "Page transitions with wait mode", category: "exit", - code: snippet("examples/AnimatePresence/page-transitions-with-wait-mode.md"), + code: snippet("examples/AnimatePresence/page-transitions-with-wait-mode.txt"), }, ], tips: [ @@ -267,12 +267,12 @@ const layoutGroup: ApiEntry = { props: [ { name: "id", type: "string", description: "Namespace layoutId values to prevent conflicts between multiple instances." }, ], - usage: snippet("usage/LayoutGroup.md"), + usage: snippet("usage/LayoutGroup.txt"), examples: [ { title: "Synchronized accordion items", category: "layout", - code: snippet("examples/LayoutGroup/synchronized-accordion-items.md"), + code: snippet("examples/LayoutGroup/synchronized-accordion-items.txt"), }, ], relatedApis: ["motion", "AnimatePresence"], @@ -288,12 +288,12 @@ const lazyMotion: ApiEntry = { { name: "features", type: "FeatureBundle | () => Promise", description: "domAnimation (~15kb: animations, variants, exit, tap, hover, focus) or domMax (~25kb: adds pan, drag, layout)." }, { name: "strict", type: "boolean", description: "If true, throws error if motion component is used instead of m." }, ], - usage: snippet("usage/LazyMotion.md"), + usage: snippet("usage/LazyMotion.txt"), examples: [ { title: "Async feature loading", category: "performance", - code: snippet("examples/LazyMotion/async-feature-loading.md"), + code: snippet("examples/LazyMotion/async-feature-loading.txt"), }, ], tips: [ @@ -314,17 +314,17 @@ const motionConfig: ApiEntry = { { name: "reducedMotion", type: "'never' | 'user' | 'always'", description: "never: ignore device setting. user: respect prefers-reduced-motion. always: force reduced motion.", default: "'never'" }, { name: "nonce", type: "string", description: "CSP nonce attribute for generated style blocks." }, ], - usage: snippet("usage/MotionConfig.md"), + usage: snippet("usage/MotionConfig.txt"), examples: [ { title: "Global spring transition", category: "transitions", - code: snippet("examples/MotionConfig/global-spring-transition.md"), + code: snippet("examples/MotionConfig/global-spring-transition.txt"), }, { title: "Respect reduced motion", category: "performance", - code: snippet("examples/MotionConfig/respect-reduced-motion.md"), + code: snippet("examples/MotionConfig/respect-reduced-motion.txt"), }, ], relatedApis: ["motion", "LazyMotion"], @@ -341,12 +341,12 @@ const reorderGroup: ApiEntry = { { name: "values", type: "T[]", description: "Array representing the current list order." }, { name: "onReorder", type: "(newOrder: T[]) => void", description: "Callback with reordered array." }, ], - usage: snippet("usage/Reorder.Group.md"), + usage: snippet("usage/Reorder.Group.txt"), examples: [ { title: "Reorderable list with exit animations", category: "reorder", - code: snippet("examples/Reorder.Group/reorderable-list-with-exit-animations.md"), + code: snippet("examples/Reorder.Group/reorderable-list-with-exit-animations.txt"), }, ], relatedApis: ["Reorder.Item", "AnimatePresence"], @@ -361,7 +361,7 @@ const reorderItem: ApiEntry = { { name: "as", type: "string", description: "Rendered element.", default: "'li'" }, { name: "value", type: "T", description: "The value this item represents in the values array." }, ], - usage: snippet("usage/Reorder.Item.md"), + usage: snippet("usage/Reorder.Item.txt"), examples: [], relatedApis: ["Reorder.Group"], }; @@ -377,22 +377,22 @@ const useAnimate: ApiEntry = { "Imperative animation control scoped to a component subtree. Returns [scope, animate]. The scope ref is attached to a container, and animate() can target elements within it by CSS selector.", importPath: 'import { useAnimate } from "motion/react"', returns: "[scope: RefObject, animate: AnimateFunction]", - usage: snippet("usage/useAnimate.md"), + usage: snippet("usage/useAnimate.txt"), examples: [ { title: "Staggered list entrance", category: "animation", - code: snippet("examples/useAnimate/staggered-list-entrance.md"), + code: snippet("examples/useAnimate/staggered-list-entrance.txt"), }, { title: "Animation sequence", category: "animation", - code: snippet("examples/useAnimate/animation-sequence.md"), + code: snippet("examples/useAnimate/animation-sequence.txt"), }, { title: "Exit animation with usePresence", category: "exit", - code: snippet("examples/useAnimate/exit-animation-with-usepresence.md"), + code: snippet("examples/useAnimate/exit-animation-with-usepresence.txt"), }, ], tips: [ @@ -410,12 +410,12 @@ const useMotionValue: ApiEntry = { "Creates a motion value that tracks state and velocity without triggering React re-renders. Pass to motion component style or SVG attribute props.", importPath: 'import { useMotionValue } from "motion/react"', returns: "MotionValue", - usage: snippet("usage/useMotionValue.md"), + usage: snippet("usage/useMotionValue.txt"), examples: [ { title: "Track drag position", category: "drag", - code: snippet("examples/useMotionValue/track-drag-position.md"), + code: snippet("examples/useMotionValue/track-drag-position.txt"), }, ], tips: [ @@ -433,17 +433,17 @@ const useTransform: ApiEntry = { "Creates a derived motion value from one or more source motion values via mapping or a transform function.", importPath: 'import { useTransform } from "motion/react"', returns: "MotionValue", - usage: snippet("usage/useTransform.md"), + usage: snippet("usage/useTransform.txt"), examples: [ { title: "Parallax scroll effect", category: "scroll", - code: snippet("examples/useTransform/parallax-scroll-effect.md"), + code: snippet("examples/useTransform/parallax-scroll-effect.txt"), }, { title: "Scroll-linked color change", category: "scroll", - code: snippet("examples/useTransform/scroll-linked-color-change.md"), + code: snippet("examples/useTransform/scroll-linked-color-change.txt"), }, ], props: [ @@ -461,17 +461,17 @@ const useSpring: ApiEntry = { "Creates a motion value that animates to targets with spring physics. Can track another motion value.", importPath: 'import { useSpring } from "motion/react"', returns: "MotionValue", - usage: snippet("usage/useSpring.md"), + usage: snippet("usage/useSpring.txt"), examples: [ { title: "Smooth scroll tracking", category: "scroll", - code: snippet("examples/useSpring/smooth-scroll-tracking.md"), + code: snippet("examples/useSpring/smooth-scroll-tracking.txt"), }, { title: "Mouse follower", category: "animation", - code: snippet("examples/useSpring/mouse-follower.md"), + code: snippet("examples/useSpring/mouse-follower.txt"), }, ], props: [ @@ -493,22 +493,22 @@ const useScroll: ApiEntry = { "Creates scroll-linked motion values. Tracks window or element scroll position. Supports hardware-accelerated ScrollTimeline.", importPath: 'import { useScroll } from "motion/react"', returns: "{ scrollX, scrollY, scrollXProgress, scrollYProgress }", - usage: snippet("usage/useScroll.md"), + usage: snippet("usage/useScroll.txt"), examples: [ { title: "Scroll progress bar", category: "scroll", - code: snippet("examples/useScroll/scroll-progress-bar.md"), + code: snippet("examples/useScroll/scroll-progress-bar.txt"), }, { title: "Element reveal on scroll", category: "scroll", - code: snippet("examples/useScroll/element-reveal-on-scroll.md"), + code: snippet("examples/useScroll/element-reveal-on-scroll.txt"), }, { title: "Horizontal scroll section", category: "scroll", - code: snippet("examples/useScroll/horizontal-scroll-section.md"), + code: snippet("examples/useScroll/horizontal-scroll-section.txt"), }, ], props: [ @@ -532,12 +532,12 @@ const useInView: ApiEntry = { description: "Detects when an element is in the viewport. Returns a boolean that triggers re-renders.", importPath: 'import { useInView } from "motion/react"', returns: "boolean", - usage: snippet("usage/useInView.md"), + usage: snippet("usage/useInView.txt"), examples: [ { title: "Trigger animation when in view", category: "scroll", - code: snippet("examples/useInView/trigger-animation-when-in-view.md"), + code: snippet("examples/useInView/trigger-animation-when-in-view.txt"), }, ], props: [ @@ -556,12 +556,12 @@ const useMotionValueEvent: ApiEntry = { description: "Lifecycle-managed event listener for motion values. Automatically cleans up on unmount.", importPath: 'import { useMotionValueEvent } from "motion/react"', - usage: snippet("usage/useMotionValueEvent.md"), + usage: snippet("usage/useMotionValueEvent.txt"), examples: [ { title: "Detect scroll direction", category: "scroll", - code: snippet("examples/useMotionValueEvent/detect-scroll-direction.md"), + code: snippet("examples/useMotionValueEvent/detect-scroll-direction.txt"), }, ], relatedApis: ["useMotionValue", "useScroll"], @@ -574,12 +574,12 @@ const useVelocity: ApiEntry = { "Returns a motion value tracking the velocity (per second) of another numerical motion value.", importPath: 'import { useVelocity } from "motion/react"', returns: "MotionValue", - usage: snippet("usage/useVelocity.md"), + usage: snippet("usage/useVelocity.txt"), examples: [ { title: "Velocity-based skew on drag", category: "drag", - code: snippet("examples/useVelocity/velocity-based-skew-on-drag.md"), + code: snippet("examples/useVelocity/velocity-based-skew-on-drag.txt"), }, ], relatedApis: ["useMotionValue", "useTransform"], @@ -592,12 +592,12 @@ const useTime: ApiEntry = { "Returns a motion value that updates every frame with milliseconds since creation. Useful for perpetual animations.", importPath: 'import { useTime } from "motion/react"', returns: "MotionValue", - usage: snippet("usage/useTime.md"), + usage: snippet("usage/useTime.txt"), examples: [ { title: "Perpetual rotation", category: "animation", - code: snippet("examples/useTime/perpetual-rotation.md"), + code: snippet("examples/useTime/perpetual-rotation.txt"), }, ], relatedApis: ["useTransform", "useMotionValue"], @@ -610,12 +610,12 @@ const useMotionTemplate: ApiEntry = { "Tagged template literal that creates a motion value from a string containing other motion values.", importPath: 'import { useMotionTemplate } from "motion/react"', returns: "MotionValue", - usage: snippet("usage/useMotionTemplate.md"), + usage: snippet("usage/useMotionTemplate.txt"), examples: [ { title: "Dynamic gradient", category: "animation", - code: snippet("examples/useMotionTemplate/dynamic-gradient.md"), + code: snippet("examples/useMotionTemplate/dynamic-gradient.txt"), }, ], relatedApis: ["useMotionValue", "useTransform"], @@ -627,12 +627,12 @@ const useDragControls: ApiEntry = { description: "Manual drag initiation from any element, not just the dragged element itself.", importPath: 'import { useDragControls } from "motion/react"', returns: "DragControls", - usage: snippet("usage/useDragControls.md"), + usage: snippet("usage/useDragControls.txt"), examples: [ { title: "Custom drag handle", category: "drag", - code: snippet("examples/useDragControls/custom-drag-handle.md"), + code: snippet("examples/useDragControls/custom-drag-handle.txt"), }, ], relatedApis: ["motion"], @@ -643,12 +643,12 @@ const useAnimationFrame: ApiEntry = { kind: "hook", description: "Runs a callback every animation frame. Callback receives (time, delta).", importPath: 'import { useAnimationFrame } from "motion/react"', - usage: snippet("usage/useAnimationFrame.md"), + usage: snippet("usage/useAnimationFrame.txt"), examples: [ { title: "Continuous rotation", category: "animation", - code: snippet("examples/useAnimationFrame/continuous-rotation.md"), + code: snippet("examples/useAnimationFrame/continuous-rotation.txt"), }, ], relatedApis: ["useTime"], @@ -661,7 +661,7 @@ const useReducedMotion: ApiEntry = { "Returns true if the device has Reduced Motion enabled (prefers-reduced-motion: reduce). Reactively updates.", importPath: 'import { useReducedMotion } from "motion/react"', returns: "boolean", - usage: snippet("usage/useReducedMotion.md"), + usage: snippet("usage/useReducedMotion.txt"), examples: [], tips: ["Prefer MotionConfig reducedMotion='user' for app-wide reduced motion handling."], relatedApis: ["MotionConfig"], @@ -673,7 +673,7 @@ const useIsPresent: ApiEntry = { description: "Returns boolean indicating whether the component is still present in the tree (for AnimatePresence exit flow).", importPath: 'import { useIsPresent } from "motion/react"', returns: "boolean", - usage: snippet("usage/useIsPresent.md"), + usage: snippet("usage/useIsPresent.txt"), examples: [], relatedApis: ["AnimatePresence", "usePresence"], }; @@ -685,7 +685,7 @@ const usePresence: ApiEntry = { "Returns [isPresent, safeToRemove] for manual exit animation control with AnimatePresence. Call safeToRemove() when your custom exit animation is done.", importPath: 'import { usePresence } from "motion/react"', returns: "[isPresent: boolean, safeToRemove: () => void]", - usage: snippet("usage/usePresence.md"), + usage: snippet("usage/usePresence.txt"), examples: [], relatedApis: ["AnimatePresence", "useIsPresent", "useAnimate"], }; @@ -696,7 +696,7 @@ const usePresenceData: ApiEntry = { description: "Access the custom prop data from the parent AnimatePresence.", importPath: 'import { usePresenceData } from "motion/react"', returns: "any", - usage: snippet("usage/usePresenceData.md"), + usage: snippet("usage/usePresenceData.txt"), examples: [], relatedApis: ["AnimatePresence"], }; @@ -711,12 +711,12 @@ const staggerFn: ApiEntry = { description: "Creates staggered delays for animation sequences. Use with useAnimate's animate() or with delayChildren in variant transitions.", importPath: 'import { stagger } from "motion/react"', - usage: snippet("usage/stagger.md"), + usage: snippet("usage/stagger.txt"), examples: [ { title: "Staggered list with easing", category: "animation", - code: snippet("examples/stagger/staggered-list-with-easing.md"), + code: snippet("examples/stagger/staggered-list-with-easing.txt"), }, ], props: [ @@ -734,12 +734,12 @@ const animateFn: ApiEntry = { "Imperative animate function. Targets CSS selectors, DOM elements, motion values, or objects. Supports timeline sequences.", importPath: 'import { animate } from "motion/react"', returns: "AnimationControls (time, speed, duration, play, pause, stop, cancel, complete, then)", - usage: snippet("usage/animate.md"), + usage: snippet("usage/animate.txt"), examples: [ { title: "Timeline with sequencing", category: "animation", - code: snippet("examples/animate/timeline-with-sequencing.md"), + code: snippet("examples/animate/timeline-with-sequencing.txt"), }, ], tips: [ @@ -760,7 +760,7 @@ const useWillChange: ApiEntry = { description: "Returns an optimized will-change MotionValue. Pass to style.willChange to automatically manage will-change CSS property during animations.", importPath: 'import { useWillChange } from "motion/react"', returns: "WillChange", - usage: snippet("usage/useWillChange.md"), + usage: snippet("usage/useWillChange.txt"), examples: [], relatedApis: ["useMotionValue"], }; @@ -771,12 +771,12 @@ const useCycle: ApiEntry = { description: "Cycles through a list of values. Returns [currentValue, cycleFunction]. Call cycle() to advance, or cycle(index) to jump.", importPath: 'import { useCycle } from "motion/react"', returns: "[T, (index?: number) => void]", - usage: snippet("usage/useCycle.md"), + usage: snippet("usage/useCycle.txt"), examples: [ { title: "Toggle animation state", category: "animation", - code: snippet("examples/useCycle/toggle-animation-state.md"), + code: snippet("examples/useCycle/toggle-animation-state.txt"), }, ], relatedApis: ["motion"], @@ -788,12 +788,12 @@ const usePageInView: ApiEntry = { description: "Returns true when the current page/tab is the user's active tab. Uses document.visibilitychange. Useful for pausing animations or video when tab is hidden.", importPath: 'import { usePageInView } from "motion/react"', returns: "boolean", - usage: snippet("usage/usePageInView.md"), + usage: snippet("usage/usePageInView.txt"), examples: [ { title: "Pause video when tab hidden", category: "performance", - code: snippet("examples/usePageInView/pause-video-when-tab-hidden.md"), + code: snippet("examples/usePageInView/pause-video-when-tab-hidden.txt"), }, ], relatedApis: ["useInView"], @@ -809,12 +809,12 @@ const hoverFn: ApiEntry = { description: "Standalone hover gesture function. Under 1kb. Returns a cleanup function. The callback can return a cleanup that fires on hover end.", importPath: 'import { hover } from "motion"', returns: "() => void (cleanup)", - usage: snippet("usage/hover.md"), + usage: snippet("usage/hover.txt"), examples: [ { title: "Standalone hover with React ref", category: "hover", - code: snippet("examples/hover/standalone-hover-with-react-ref.md"), + code: snippet("examples/hover/standalone-hover-with-react-ref.txt"), }, ], tips: ["Under 1kb โ€” smallest hover animation possible.", "Import from 'motion' (not 'motion/react')."], @@ -827,7 +827,7 @@ const pressFn: ApiEntry = { description: "Standalone press gesture function with keyboard accessibility (Enter/Space). Returns a cleanup function.", importPath: 'import { press } from "motion"', returns: "() => void (cleanup)", - usage: snippet("usage/press.md"), + usage: snippet("usage/press.txt"), examples: [], tips: ["Keyboard accessible: responds to Enter and Space keys.", "Import from 'motion' (not 'motion/react')."], relatedApis: ["motion", "hover"], @@ -839,7 +839,7 @@ const scrollFn: ApiEntry = { description: "Standalone scroll-linked animation function. Framework-agnostic. Can accept a callback or an animation to link to scroll progress.", importPath: 'import { scroll } from "motion"', returns: "() => void (cleanup)", - usage: snippet("usage/scroll.md"), + usage: snippet("usage/scroll.txt"), examples: [], tips: ["Framework-agnostic โ€” works without React.", "Import from 'motion' (not 'motion/react')."], relatedApis: ["useScroll", "animate"], @@ -851,7 +851,7 @@ const inViewFn: ApiEntry = { description: "Standalone IntersectionObserver wrapper. Framework-agnostic. Callback can return a cleanup function that fires when element leaves viewport.", importPath: 'import { inView } from "motion"', returns: "() => void (cleanup)", - usage: snippet("usage/inView.md"), + usage: snippet("usage/inView.txt"), examples: [], tips: ["Framework-agnostic โ€” works without React.", "Import from 'motion' (not 'motion/react')."], relatedApis: ["useInView"], @@ -861,7 +861,7 @@ const inViewFn: ApiEntry = { // TRANSITIONS REFERENCE // --------------------------------------------------------------------------- -export const TRANSITIONS_REFERENCE = snippet("transitions.md"); +export const TRANSITIONS_REFERENCE = snippet("transitions.txt"); // --------------------------------------------------------------------------- // ALL APIS REGISTRY diff --git a/snippets/motion/examples/AnimatePresence/modal-with-exit-animation.md b/src/plugins/motion/snippets/examples/AnimatePresence/modal-with-exit-animation.txt similarity index 100% rename from snippets/motion/examples/AnimatePresence/modal-with-exit-animation.md rename to src/plugins/motion/snippets/examples/AnimatePresence/modal-with-exit-animation.txt diff --git a/snippets/motion/examples/AnimatePresence/page-transitions-with-wait-mode.md b/src/plugins/motion/snippets/examples/AnimatePresence/page-transitions-with-wait-mode.txt similarity index 100% rename from snippets/motion/examples/AnimatePresence/page-transitions-with-wait-mode.md rename to src/plugins/motion/snippets/examples/AnimatePresence/page-transitions-with-wait-mode.txt diff --git a/snippets/motion/examples/LayoutGroup/synchronized-accordion-items.md b/src/plugins/motion/snippets/examples/LayoutGroup/synchronized-accordion-items.txt similarity index 100% rename from snippets/motion/examples/LayoutGroup/synchronized-accordion-items.md rename to src/plugins/motion/snippets/examples/LayoutGroup/synchronized-accordion-items.txt diff --git a/snippets/motion/examples/LazyMotion/async-feature-loading.md b/src/plugins/motion/snippets/examples/LazyMotion/async-feature-loading.txt similarity index 100% rename from snippets/motion/examples/LazyMotion/async-feature-loading.md rename to src/plugins/motion/snippets/examples/LazyMotion/async-feature-loading.txt diff --git a/snippets/motion/examples/MotionConfig/global-spring-transition.md b/src/plugins/motion/snippets/examples/MotionConfig/global-spring-transition.txt similarity index 100% rename from snippets/motion/examples/MotionConfig/global-spring-transition.md rename to src/plugins/motion/snippets/examples/MotionConfig/global-spring-transition.txt diff --git a/snippets/motion/examples/MotionConfig/respect-reduced-motion.md b/src/plugins/motion/snippets/examples/MotionConfig/respect-reduced-motion.txt similarity index 100% rename from snippets/motion/examples/MotionConfig/respect-reduced-motion.md rename to src/plugins/motion/snippets/examples/MotionConfig/respect-reduced-motion.txt diff --git a/snippets/motion/examples/Reorder.Group/reorderable-list-with-exit-animations.md b/src/plugins/motion/snippets/examples/Reorder.Group/reorderable-list-with-exit-animations.txt similarity index 100% rename from snippets/motion/examples/Reorder.Group/reorderable-list-with-exit-animations.md rename to src/plugins/motion/snippets/examples/Reorder.Group/reorderable-list-with-exit-animations.txt diff --git a/snippets/motion/examples/animate/timeline-with-sequencing.md b/src/plugins/motion/snippets/examples/animate/timeline-with-sequencing.txt similarity index 100% rename from snippets/motion/examples/animate/timeline-with-sequencing.md rename to src/plugins/motion/snippets/examples/animate/timeline-with-sequencing.txt diff --git a/snippets/motion/examples/hover/standalone-hover-with-react-ref.md b/src/plugins/motion/snippets/examples/hover/standalone-hover-with-react-ref.txt similarity index 100% rename from snippets/motion/examples/hover/standalone-hover-with-react-ref.md rename to src/plugins/motion/snippets/examples/hover/standalone-hover-with-react-ref.txt diff --git a/snippets/motion/examples/motion/animate-counter-without-re-renders.md b/src/plugins/motion/snippets/examples/motion/animate-counter-without-re-renders.txt similarity index 100% rename from snippets/motion/examples/motion/animate-counter-without-re-renders.md rename to src/plugins/motion/snippets/examples/motion/animate-counter-without-re-renders.txt diff --git a/snippets/motion/examples/motion/animate-css-variables.md b/src/plugins/motion/snippets/examples/motion/animate-css-variables.txt similarity index 100% rename from snippets/motion/examples/motion/animate-css-variables.md rename to src/plugins/motion/snippets/examples/motion/animate-css-variables.txt diff --git a/snippets/motion/examples/motion/basic-fade-in.md b/src/plugins/motion/snippets/examples/motion/basic-fade-in.txt similarity index 100% rename from snippets/motion/examples/motion/basic-fade-in.md rename to src/plugins/motion/snippets/examples/motion/basic-fade-in.txt diff --git a/snippets/motion/examples/motion/drag-with-constraints.md b/src/plugins/motion/snippets/examples/motion/drag-with-constraints.txt similarity index 100% rename from snippets/motion/examples/motion/drag-with-constraints.md rename to src/plugins/motion/snippets/examples/motion/drag-with-constraints.txt diff --git a/snippets/motion/examples/motion/dynamic-variants-with-custom.md b/src/plugins/motion/snippets/examples/motion/dynamic-variants-with-custom.txt similarity index 100% rename from snippets/motion/examples/motion/dynamic-variants-with-custom.md rename to src/plugins/motion/snippets/examples/motion/dynamic-variants-with-custom.txt diff --git a/snippets/motion/examples/motion/hover-and-tap.md b/src/plugins/motion/snippets/examples/motion/hover-and-tap.txt similarity index 100% rename from snippets/motion/examples/motion/hover-and-tap.md rename to src/plugins/motion/snippets/examples/motion/hover-and-tap.txt diff --git a/snippets/motion/examples/motion/keyframes.md b/src/plugins/motion/snippets/examples/motion/keyframes.txt similarity index 100% rename from snippets/motion/examples/motion/keyframes.md rename to src/plugins/motion/snippets/examples/motion/keyframes.txt diff --git a/snippets/motion/examples/motion/layout-animation.md b/src/plugins/motion/snippets/examples/motion/layout-animation.txt similarity index 100% rename from snippets/motion/examples/motion/layout-animation.md rename to src/plugins/motion/snippets/examples/motion/layout-animation.txt diff --git a/snippets/motion/examples/motion/scroll-image-reveal-with-clippath.md b/src/plugins/motion/snippets/examples/motion/scroll-image-reveal-with-clippath.txt similarity index 100% rename from snippets/motion/examples/motion/scroll-image-reveal-with-clippath.md rename to src/plugins/motion/snippets/examples/motion/scroll-image-reveal-with-clippath.txt diff --git a/snippets/motion/examples/motion/scroll-triggered-entrance.md b/src/plugins/motion/snippets/examples/motion/scroll-triggered-entrance.txt similarity index 100% rename from snippets/motion/examples/motion/scroll-triggered-entrance.md rename to src/plugins/motion/snippets/examples/motion/scroll-triggered-entrance.txt diff --git a/snippets/motion/examples/motion/shared-layout-with-layoutid.md b/src/plugins/motion/snippets/examples/motion/shared-layout-with-layoutid.txt similarity index 100% rename from snippets/motion/examples/motion/shared-layout-with-layoutid.md rename to src/plugins/motion/snippets/examples/motion/shared-layout-with-layoutid.txt diff --git a/snippets/motion/examples/motion/snap-to-grid-drag.md b/src/plugins/motion/snippets/examples/motion/snap-to-grid-drag.txt similarity index 100% rename from snippets/motion/examples/motion/snap-to-grid-drag.md rename to src/plugins/motion/snippets/examples/motion/snap-to-grid-drag.txt diff --git a/snippets/motion/examples/motion/svg-line-drawing.md b/src/plugins/motion/snippets/examples/motion/svg-line-drawing.txt similarity index 100% rename from snippets/motion/examples/motion/svg-line-drawing.md rename to src/plugins/motion/snippets/examples/motion/svg-line-drawing.txt diff --git a/snippets/motion/examples/motion/svg-path-morphing.md b/src/plugins/motion/snippets/examples/motion/svg-path-morphing.txt similarity index 100% rename from snippets/motion/examples/motion/svg-path-morphing.md rename to src/plugins/motion/snippets/examples/motion/svg-path-morphing.txt diff --git a/snippets/motion/examples/motion/variants-with-orchestration.md b/src/plugins/motion/snippets/examples/motion/variants-with-orchestration.txt similarity index 100% rename from snippets/motion/examples/motion/variants-with-orchestration.md rename to src/plugins/motion/snippets/examples/motion/variants-with-orchestration.txt diff --git a/snippets/motion/examples/motion/wildcard-keyframe-start-from-current-value.md b/src/plugins/motion/snippets/examples/motion/wildcard-keyframe-start-from-current-value.txt similarity index 100% rename from snippets/motion/examples/motion/wildcard-keyframe-start-from-current-value.md rename to src/plugins/motion/snippets/examples/motion/wildcard-keyframe-start-from-current-value.txt diff --git a/snippets/motion/examples/stagger/staggered-list-with-easing.md b/src/plugins/motion/snippets/examples/stagger/staggered-list-with-easing.txt similarity index 100% rename from snippets/motion/examples/stagger/staggered-list-with-easing.md rename to src/plugins/motion/snippets/examples/stagger/staggered-list-with-easing.txt diff --git a/snippets/motion/examples/useAnimate/animation-sequence.md b/src/plugins/motion/snippets/examples/useAnimate/animation-sequence.txt similarity index 100% rename from snippets/motion/examples/useAnimate/animation-sequence.md rename to src/plugins/motion/snippets/examples/useAnimate/animation-sequence.txt diff --git a/snippets/motion/examples/useAnimate/exit-animation-with-usepresence.md b/src/plugins/motion/snippets/examples/useAnimate/exit-animation-with-usepresence.txt similarity index 100% rename from snippets/motion/examples/useAnimate/exit-animation-with-usepresence.md rename to src/plugins/motion/snippets/examples/useAnimate/exit-animation-with-usepresence.txt diff --git a/snippets/motion/examples/useAnimate/staggered-list-entrance.md b/src/plugins/motion/snippets/examples/useAnimate/staggered-list-entrance.txt similarity index 100% rename from snippets/motion/examples/useAnimate/staggered-list-entrance.md rename to src/plugins/motion/snippets/examples/useAnimate/staggered-list-entrance.txt diff --git a/snippets/motion/examples/useAnimationFrame/continuous-rotation.md b/src/plugins/motion/snippets/examples/useAnimationFrame/continuous-rotation.txt similarity index 100% rename from snippets/motion/examples/useAnimationFrame/continuous-rotation.md rename to src/plugins/motion/snippets/examples/useAnimationFrame/continuous-rotation.txt diff --git a/snippets/motion/examples/useCycle/toggle-animation-state.md b/src/plugins/motion/snippets/examples/useCycle/toggle-animation-state.txt similarity index 100% rename from snippets/motion/examples/useCycle/toggle-animation-state.md rename to src/plugins/motion/snippets/examples/useCycle/toggle-animation-state.txt diff --git a/snippets/motion/examples/useDragControls/custom-drag-handle.md b/src/plugins/motion/snippets/examples/useDragControls/custom-drag-handle.txt similarity index 100% rename from snippets/motion/examples/useDragControls/custom-drag-handle.md rename to src/plugins/motion/snippets/examples/useDragControls/custom-drag-handle.txt diff --git a/snippets/motion/examples/useInView/trigger-animation-when-in-view.md b/src/plugins/motion/snippets/examples/useInView/trigger-animation-when-in-view.txt similarity index 100% rename from snippets/motion/examples/useInView/trigger-animation-when-in-view.md rename to src/plugins/motion/snippets/examples/useInView/trigger-animation-when-in-view.txt diff --git a/snippets/motion/examples/useMotionTemplate/dynamic-gradient.md b/src/plugins/motion/snippets/examples/useMotionTemplate/dynamic-gradient.txt similarity index 100% rename from snippets/motion/examples/useMotionTemplate/dynamic-gradient.md rename to src/plugins/motion/snippets/examples/useMotionTemplate/dynamic-gradient.txt diff --git a/snippets/motion/examples/useMotionValue/track-drag-position.md b/src/plugins/motion/snippets/examples/useMotionValue/track-drag-position.txt similarity index 100% rename from snippets/motion/examples/useMotionValue/track-drag-position.md rename to src/plugins/motion/snippets/examples/useMotionValue/track-drag-position.txt diff --git a/snippets/motion/examples/useMotionValueEvent/detect-scroll-direction.md b/src/plugins/motion/snippets/examples/useMotionValueEvent/detect-scroll-direction.txt similarity index 100% rename from snippets/motion/examples/useMotionValueEvent/detect-scroll-direction.md rename to src/plugins/motion/snippets/examples/useMotionValueEvent/detect-scroll-direction.txt diff --git a/snippets/motion/examples/usePageInView/pause-video-when-tab-hidden.md b/src/plugins/motion/snippets/examples/usePageInView/pause-video-when-tab-hidden.txt similarity index 100% rename from snippets/motion/examples/usePageInView/pause-video-when-tab-hidden.md rename to src/plugins/motion/snippets/examples/usePageInView/pause-video-when-tab-hidden.txt diff --git a/snippets/motion/examples/useScroll/element-reveal-on-scroll.md b/src/plugins/motion/snippets/examples/useScroll/element-reveal-on-scroll.txt similarity index 100% rename from snippets/motion/examples/useScroll/element-reveal-on-scroll.md rename to src/plugins/motion/snippets/examples/useScroll/element-reveal-on-scroll.txt diff --git a/snippets/motion/examples/useScroll/horizontal-scroll-section.md b/src/plugins/motion/snippets/examples/useScroll/horizontal-scroll-section.txt similarity index 100% rename from snippets/motion/examples/useScroll/horizontal-scroll-section.md rename to src/plugins/motion/snippets/examples/useScroll/horizontal-scroll-section.txt diff --git a/snippets/motion/examples/useScroll/scroll-progress-bar.md b/src/plugins/motion/snippets/examples/useScroll/scroll-progress-bar.txt similarity index 100% rename from snippets/motion/examples/useScroll/scroll-progress-bar.md rename to src/plugins/motion/snippets/examples/useScroll/scroll-progress-bar.txt diff --git a/snippets/motion/examples/useSpring/mouse-follower.md b/src/plugins/motion/snippets/examples/useSpring/mouse-follower.txt similarity index 100% rename from snippets/motion/examples/useSpring/mouse-follower.md rename to src/plugins/motion/snippets/examples/useSpring/mouse-follower.txt diff --git a/snippets/motion/examples/useSpring/smooth-scroll-tracking.md b/src/plugins/motion/snippets/examples/useSpring/smooth-scroll-tracking.txt similarity index 100% rename from snippets/motion/examples/useSpring/smooth-scroll-tracking.md rename to src/plugins/motion/snippets/examples/useSpring/smooth-scroll-tracking.txt diff --git a/snippets/motion/examples/useTime/perpetual-rotation.md b/src/plugins/motion/snippets/examples/useTime/perpetual-rotation.txt similarity index 100% rename from snippets/motion/examples/useTime/perpetual-rotation.md rename to src/plugins/motion/snippets/examples/useTime/perpetual-rotation.txt diff --git a/snippets/motion/examples/useTransform/parallax-scroll-effect.md b/src/plugins/motion/snippets/examples/useTransform/parallax-scroll-effect.txt similarity index 100% rename from snippets/motion/examples/useTransform/parallax-scroll-effect.md rename to src/plugins/motion/snippets/examples/useTransform/parallax-scroll-effect.txt diff --git a/snippets/motion/examples/useTransform/scroll-linked-color-change.md b/src/plugins/motion/snippets/examples/useTransform/scroll-linked-color-change.txt similarity index 100% rename from snippets/motion/examples/useTransform/scroll-linked-color-change.md rename to src/plugins/motion/snippets/examples/useTransform/scroll-linked-color-change.txt diff --git a/snippets/motion/examples/useVelocity/velocity-based-skew-on-drag.md b/src/plugins/motion/snippets/examples/useVelocity/velocity-based-skew-on-drag.txt similarity index 100% rename from snippets/motion/examples/useVelocity/velocity-based-skew-on-drag.md rename to src/plugins/motion/snippets/examples/useVelocity/velocity-based-skew-on-drag.txt diff --git a/snippets/motion/transitions.md b/src/plugins/motion/snippets/transitions.txt similarity index 100% rename from snippets/motion/transitions.md rename to src/plugins/motion/snippets/transitions.txt diff --git a/snippets/motion/usage/AnimatePresence.md b/src/plugins/motion/snippets/usage/AnimatePresence.txt similarity index 100% rename from snippets/motion/usage/AnimatePresence.md rename to src/plugins/motion/snippets/usage/AnimatePresence.txt diff --git a/snippets/motion/usage/LayoutGroup.md b/src/plugins/motion/snippets/usage/LayoutGroup.txt similarity index 100% rename from snippets/motion/usage/LayoutGroup.md rename to src/plugins/motion/snippets/usage/LayoutGroup.txt diff --git a/snippets/motion/usage/LazyMotion.md b/src/plugins/motion/snippets/usage/LazyMotion.txt similarity index 100% rename from snippets/motion/usage/LazyMotion.md rename to src/plugins/motion/snippets/usage/LazyMotion.txt diff --git a/snippets/motion/usage/MotionConfig.md b/src/plugins/motion/snippets/usage/MotionConfig.txt similarity index 100% rename from snippets/motion/usage/MotionConfig.md rename to src/plugins/motion/snippets/usage/MotionConfig.txt diff --git a/snippets/motion/usage/Reorder.Group.md b/src/plugins/motion/snippets/usage/Reorder.Group.txt similarity index 100% rename from snippets/motion/usage/Reorder.Group.md rename to src/plugins/motion/snippets/usage/Reorder.Group.txt diff --git a/snippets/motion/usage/Reorder.Item.md b/src/plugins/motion/snippets/usage/Reorder.Item.txt similarity index 100% rename from snippets/motion/usage/Reorder.Item.md rename to src/plugins/motion/snippets/usage/Reorder.Item.txt diff --git a/snippets/motion/usage/animate.md b/src/plugins/motion/snippets/usage/animate.txt similarity index 100% rename from snippets/motion/usage/animate.md rename to src/plugins/motion/snippets/usage/animate.txt diff --git a/snippets/motion/usage/hover.md b/src/plugins/motion/snippets/usage/hover.txt similarity index 100% rename from snippets/motion/usage/hover.md rename to src/plugins/motion/snippets/usage/hover.txt diff --git a/snippets/motion/usage/inView.md b/src/plugins/motion/snippets/usage/inView.txt similarity index 100% rename from snippets/motion/usage/inView.md rename to src/plugins/motion/snippets/usage/inView.txt diff --git a/snippets/motion/usage/motion.md b/src/plugins/motion/snippets/usage/motion.txt similarity index 100% rename from snippets/motion/usage/motion.md rename to src/plugins/motion/snippets/usage/motion.txt diff --git a/snippets/motion/usage/press.md b/src/plugins/motion/snippets/usage/press.txt similarity index 100% rename from snippets/motion/usage/press.md rename to src/plugins/motion/snippets/usage/press.txt diff --git a/snippets/motion/usage/scroll.md b/src/plugins/motion/snippets/usage/scroll.txt similarity index 100% rename from snippets/motion/usage/scroll.md rename to src/plugins/motion/snippets/usage/scroll.txt diff --git a/snippets/motion/usage/stagger.md b/src/plugins/motion/snippets/usage/stagger.txt similarity index 100% rename from snippets/motion/usage/stagger.md rename to src/plugins/motion/snippets/usage/stagger.txt diff --git a/snippets/motion/usage/useAnimate.md b/src/plugins/motion/snippets/usage/useAnimate.txt similarity index 100% rename from snippets/motion/usage/useAnimate.md rename to src/plugins/motion/snippets/usage/useAnimate.txt diff --git a/snippets/motion/usage/useAnimationFrame.md b/src/plugins/motion/snippets/usage/useAnimationFrame.txt similarity index 100% rename from snippets/motion/usage/useAnimationFrame.md rename to src/plugins/motion/snippets/usage/useAnimationFrame.txt diff --git a/snippets/motion/usage/useCycle.md b/src/plugins/motion/snippets/usage/useCycle.txt similarity index 100% rename from snippets/motion/usage/useCycle.md rename to src/plugins/motion/snippets/usage/useCycle.txt diff --git a/snippets/motion/usage/useDragControls.md b/src/plugins/motion/snippets/usage/useDragControls.txt similarity index 100% rename from snippets/motion/usage/useDragControls.md rename to src/plugins/motion/snippets/usage/useDragControls.txt diff --git a/snippets/motion/usage/useInView.md b/src/plugins/motion/snippets/usage/useInView.txt similarity index 100% rename from snippets/motion/usage/useInView.md rename to src/plugins/motion/snippets/usage/useInView.txt diff --git a/snippets/motion/usage/useIsPresent.md b/src/plugins/motion/snippets/usage/useIsPresent.txt similarity index 100% rename from snippets/motion/usage/useIsPresent.md rename to src/plugins/motion/snippets/usage/useIsPresent.txt diff --git a/snippets/motion/usage/useMotionTemplate.md b/src/plugins/motion/snippets/usage/useMotionTemplate.txt similarity index 100% rename from snippets/motion/usage/useMotionTemplate.md rename to src/plugins/motion/snippets/usage/useMotionTemplate.txt diff --git a/snippets/motion/usage/useMotionValue.md b/src/plugins/motion/snippets/usage/useMotionValue.txt similarity index 100% rename from snippets/motion/usage/useMotionValue.md rename to src/plugins/motion/snippets/usage/useMotionValue.txt diff --git a/snippets/motion/usage/useMotionValueEvent.md b/src/plugins/motion/snippets/usage/useMotionValueEvent.txt similarity index 100% rename from snippets/motion/usage/useMotionValueEvent.md rename to src/plugins/motion/snippets/usage/useMotionValueEvent.txt diff --git a/snippets/motion/usage/usePageInView.md b/src/plugins/motion/snippets/usage/usePageInView.txt similarity index 100% rename from snippets/motion/usage/usePageInView.md rename to src/plugins/motion/snippets/usage/usePageInView.txt diff --git a/snippets/motion/usage/usePresence.md b/src/plugins/motion/snippets/usage/usePresence.txt similarity index 100% rename from snippets/motion/usage/usePresence.md rename to src/plugins/motion/snippets/usage/usePresence.txt diff --git a/snippets/motion/usage/usePresenceData.md b/src/plugins/motion/snippets/usage/usePresenceData.txt similarity index 100% rename from snippets/motion/usage/usePresenceData.md rename to src/plugins/motion/snippets/usage/usePresenceData.txt diff --git a/snippets/motion/usage/useReducedMotion.md b/src/plugins/motion/snippets/usage/useReducedMotion.txt similarity index 100% rename from snippets/motion/usage/useReducedMotion.md rename to src/plugins/motion/snippets/usage/useReducedMotion.txt diff --git a/snippets/motion/usage/useScroll.md b/src/plugins/motion/snippets/usage/useScroll.txt similarity index 100% rename from snippets/motion/usage/useScroll.md rename to src/plugins/motion/snippets/usage/useScroll.txt diff --git a/snippets/motion/usage/useSpring.md b/src/plugins/motion/snippets/usage/useSpring.txt similarity index 100% rename from snippets/motion/usage/useSpring.md rename to src/plugins/motion/snippets/usage/useSpring.txt diff --git a/snippets/motion/usage/useTime.md b/src/plugins/motion/snippets/usage/useTime.txt similarity index 100% rename from snippets/motion/usage/useTime.md rename to src/plugins/motion/snippets/usage/useTime.txt diff --git a/snippets/motion/usage/useTransform.md b/src/plugins/motion/snippets/usage/useTransform.txt similarity index 100% rename from snippets/motion/usage/useTransform.md rename to src/plugins/motion/snippets/usage/useTransform.txt diff --git a/snippets/motion/usage/useVelocity.md b/src/plugins/motion/snippets/usage/useVelocity.txt similarity index 100% rename from snippets/motion/usage/useVelocity.md rename to src/plugins/motion/snippets/usage/useVelocity.txt diff --git a/snippets/motion/usage/useWillChange.md b/src/plugins/motion/snippets/usage/useWillChange.txt similarity index 100% rename from snippets/motion/usage/useWillChange.md rename to src/plugins/motion/snippets/usage/useWillChange.txt diff --git a/src/plugins/react/data.ts b/src/plugins/react/data.ts index dcfc36c..11897eb 100644 --- a/src/plugins/react/data.ts +++ b/src/plugins/react/data.ts @@ -33,8 +33,8 @@ export const PATTERNS: Pattern[] = [ category: "rendering", description: "Server Components are the default. Use 'use client' only when interactivity is required.", when: "Every new component. Decide server vs client first.", - code: snippet("patterns/rsc-default.md"), - antiPattern: snippet("patterns/rsc-anti.md"), + code: snippet("patterns/rsc-default.txt"), + antiPattern: snippet("patterns/rsc-anti.txt"), tips: [ "SEO-critical content โ†’ RSC/SSR/SSG/ISR", "Non-SEO + interactive โ†’ Client Component", @@ -46,8 +46,8 @@ export const PATTERNS: Pattern[] = [ category: "state", description: "Strict state placement order: URL โ†’ server โ†’ local โ†’ Zustand โ†’ Context (injection only).", when: "Deciding where to put any new piece of state.", - code: snippet("patterns/state-hierarchy.md"), - antiPattern: snippet("patterns/state-hierarchy-anti.md"), + code: snippet("patterns/state-hierarchy.txt"), + antiPattern: snippet("patterns/state-hierarchy-anti.txt"), tips: [ "Ask: can this live in the URL? If yes โ†’ URL state", "Context is for injection (stable values), not reactive state", @@ -59,15 +59,15 @@ export const PATTERNS: Pattern[] = [ category: "data-fetching", description: "Fetch in Server Components. Never useEffect for data fetching.", when: "Any data that can be fetched at request time.", - code: snippet("patterns/data-fetching-rsc.md"), - antiPattern: snippet("patterns/data-fetching-anti.md"), + code: snippet("patterns/data-fetching-rsc.txt"), + antiPattern: snippet("patterns/data-fetching-anti.txt"), }, { name: "zustand-store", category: "state", description: "Zustand for shared client state. Slice pattern for large stores.", when: "State shared across multiple client components that isn't URL or server state.", - code: snippet("patterns/zustand-store.md"), + code: snippet("patterns/zustand-store.txt"), tips: ["Never use Redux", "Slice selectors prevent unnecessary re-renders", "persist middleware for auth/settings"], }, { @@ -75,7 +75,7 @@ export const PATTERNS: Pattern[] = [ category: "rendering", description: "Suspense for async RSC children. Error boundaries for error states.", when: "Any page with async data in RSC children.", - code: snippet("patterns/suspense-boundary.md"), + code: snippet("patterns/suspense-boundary.txt"), tips: ["Skeleton over spinner for layout-stable loading", "ErrorBoundary wraps Suspense"], }, { @@ -83,22 +83,22 @@ export const PATTERNS: Pattern[] = [ category: "routing", description: "generateMetadata for dynamic SEO in Next.js App Router.", when: "Every page that needs SEO (title, description, OG).", - code: snippet("patterns/nextjs-metadata.md"), + code: snippet("patterns/nextjs-metadata.txt"), }, { name: "composition-pattern", category: "architecture", description: "Avoid prop drilling >2 levels. Use composition or context injection.", when: "Props being passed through >2 intermediate components.", - code: snippet("patterns/composition-pattern.md"), - antiPattern: snippet("patterns/composition-anti.md"), + code: snippet("patterns/composition-pattern.txt"), + antiPattern: snippet("patterns/composition-anti.txt"), }, { name: "component-template", category: "architecture", description: "Standard component scaffold with TypeScript + Tailwind + shadcn/ui + lucide-react.", when: "Creating any new React component.", - code: snippet("patterns/component-template.md"), + code: snippet("patterns/component-template.txt"), tips: [ "Always use cn() for conditional classNames", "Prefer named exports over default exports for components", diff --git a/snippets/react/cheatsheet.md b/src/plugins/react/snippets/cheatsheet.txt similarity index 100% rename from snippets/react/cheatsheet.md rename to src/plugins/react/snippets/cheatsheet.txt diff --git a/snippets/react/patterns/component-template.md b/src/plugins/react/snippets/patterns/component-template.txt similarity index 100% rename from snippets/react/patterns/component-template.md rename to src/plugins/react/snippets/patterns/component-template.txt diff --git a/snippets/react/patterns/composition-anti.md b/src/plugins/react/snippets/patterns/composition-anti.txt similarity index 100% rename from snippets/react/patterns/composition-anti.md rename to src/plugins/react/snippets/patterns/composition-anti.txt diff --git a/snippets/react/patterns/composition-pattern.md b/src/plugins/react/snippets/patterns/composition-pattern.txt similarity index 100% rename from snippets/react/patterns/composition-pattern.md rename to src/plugins/react/snippets/patterns/composition-pattern.txt diff --git a/snippets/react/patterns/data-fetching-anti.md b/src/plugins/react/snippets/patterns/data-fetching-anti.txt similarity index 100% rename from snippets/react/patterns/data-fetching-anti.md rename to src/plugins/react/snippets/patterns/data-fetching-anti.txt diff --git a/snippets/react/patterns/data-fetching-rsc.md b/src/plugins/react/snippets/patterns/data-fetching-rsc.txt similarity index 100% rename from snippets/react/patterns/data-fetching-rsc.md rename to src/plugins/react/snippets/patterns/data-fetching-rsc.txt diff --git a/snippets/react/patterns/nextjs-metadata.md b/src/plugins/react/snippets/patterns/nextjs-metadata.txt similarity index 100% rename from snippets/react/patterns/nextjs-metadata.md rename to src/plugins/react/snippets/patterns/nextjs-metadata.txt diff --git a/snippets/react/patterns/rsc-anti.md b/src/plugins/react/snippets/patterns/rsc-anti.txt similarity index 100% rename from snippets/react/patterns/rsc-anti.md rename to src/plugins/react/snippets/patterns/rsc-anti.txt diff --git a/snippets/react/patterns/rsc-default.md b/src/plugins/react/snippets/patterns/rsc-default.txt similarity index 100% rename from snippets/react/patterns/rsc-default.md rename to src/plugins/react/snippets/patterns/rsc-default.txt diff --git a/snippets/react/patterns/state-hierarchy-anti.md b/src/plugins/react/snippets/patterns/state-hierarchy-anti.txt similarity index 100% rename from snippets/react/patterns/state-hierarchy-anti.md rename to src/plugins/react/snippets/patterns/state-hierarchy-anti.txt diff --git a/snippets/react/patterns/state-hierarchy.md b/src/plugins/react/snippets/patterns/state-hierarchy.txt similarity index 100% rename from snippets/react/patterns/state-hierarchy.md rename to src/plugins/react/snippets/patterns/state-hierarchy.txt diff --git a/snippets/react/patterns/suspense-boundary.md b/src/plugins/react/snippets/patterns/suspense-boundary.txt similarity index 100% rename from snippets/react/patterns/suspense-boundary.md rename to src/plugins/react/snippets/patterns/suspense-boundary.txt diff --git a/snippets/react/patterns/zustand-store.md b/src/plugins/react/snippets/patterns/zustand-store.txt similarity index 100% rename from snippets/react/patterns/zustand-store.md rename to src/plugins/react/snippets/patterns/zustand-store.txt diff --git a/src/plugins/reactflow/data/api-types.ts b/src/plugins/reactflow/data/api-types.ts index 40d7367..c9ac0be 100644 --- a/src/plugins/reactflow/data/api-types.ts +++ b/src/plugins/reactflow/data/api-types.ts @@ -27,12 +27,12 @@ const nodeType: ApiEntry = { { name: "origin", type: "NodeOrigin", description: "Origin point [0-1, 0-1] for positioning.", default: "[0, 0]" }, { name: "measured", type: "{ width?: number; height?: number }", description: "Read-only measured dimensions." }, ], - usage: snippet("usage/Node.md"), + usage: snippet("usage/Node.txt"), examples: [ { title: "Typed custom node data", category: "custom-nodes", - code: snippet("examples/Node/typed-custom-node-data.md"), + code: snippet("examples/Node/typed-custom-node-data.txt"), }, ], tips: [ @@ -70,7 +70,7 @@ const edgeType: ApiEntry = { { name: "zIndex", type: "number", description: "Z-index." }, { name: "interactionWidth", type: "number", description: "Width of invisible click target.", default: "20" }, ], - usage: snippet("usage/Edge.md"), + usage: snippet("usage/Edge.txt"), examples: [], tips: [ "Default edge types: 'default' (bezier), 'straight', 'step', 'smoothstep', 'simplebezier'.", @@ -105,7 +105,7 @@ const nodePropsType: ApiEntry = { { name: "sourcePosition", type: "Position", description: "Source handle position (default nodes only)." }, { name: "targetPosition", type: "Position", description: "Target handle position (default nodes only)." }, ], - usage: snippet("usage/NodeProps.md"), + usage: snippet("usage/NodeProps.txt"), examples: [], tips: [ "The generic parameter is a Node type (not raw data). Use Node to define it.", @@ -150,7 +150,7 @@ const edgePropsType: ApiEntry = { { name: "style", type: "CSSProperties", description: "Edge SVG path styles." }, { name: "interactionWidth", type: "number", description: "Width of invisible click target." }, ], - usage: snippet("usage/EdgeProps.md"), + usage: snippet("usage/EdgeProps.txt"), examples: [], tips: [ "The generic parameter is an Edge type (not raw data). Use Edge to define it.", @@ -171,7 +171,7 @@ const connectionType: ApiEntry = { { name: "sourceHandle", type: "string | null", description: "Source handle ID." }, { name: "targetHandle", type: "string | null", description: "Target handle ID." }, ], - usage: snippet("usage/Connection.md"), + usage: snippet("usage/Connection.txt"), examples: [], relatedApis: ["Edge", "addEdge", "useConnection"], }; @@ -186,7 +186,7 @@ const viewportType: ApiEntry = { { name: "y", type: "number", description: "Y offset." }, { name: "zoom", type: "number", description: "Zoom level." }, ], - usage: snippet("usage/Viewport.md"), + usage: snippet("usage/Viewport.txt"), examples: [], relatedApis: ["useViewport", "useReactFlow"], }; @@ -224,7 +224,7 @@ const reactFlowInstanceType: ApiEntry = { { name: "isNodeIntersecting()", type: "(node, area, partially?) => boolean", description: "Check if node intersects area." }, { name: "getNodesBounds()", type: "(nodes) => Rect", description: "Get bounding box of nodes." }, ], - usage: snippet("usage/ReactFlowInstance.md"), + usage: snippet("usage/ReactFlowInstance.txt"), examples: [], relatedApis: ["useReactFlow", "ReactFlowProvider"], }; diff --git a/src/plugins/reactflow/data/components.ts b/src/plugins/reactflow/data/components.ts index c33b159..2db6250 100644 --- a/src/plugins/reactflow/data/components.ts +++ b/src/plugins/reactflow/data/components.ts @@ -65,17 +65,17 @@ const reactFlowComponent: ApiEntry = { { name: "autoPanOnConnect", type: "boolean", description: "Pan viewport when creating connections near edge.", default: "true" }, { name: "autoPanOnNodeDrag", type: "boolean", description: "Pan viewport when dragging nodes near edge.", default: "true" }, ], - usage: snippet("usage/ReactFlow.md"), + usage: snippet("usage/ReactFlow.txt"), examples: [ { title: "Controlled flow with Zustand", category: "state-management", - code: snippet("examples/ReactFlow/controlled-flow-zustand.md"), + code: snippet("examples/ReactFlow/controlled-flow-zustand.txt"), }, { title: "Uncontrolled flow", category: "quickstart", - code: snippet("examples/ReactFlow/uncontrolled-flow.md"), + code: snippet("examples/ReactFlow/uncontrolled-flow.txt"), }, ], tips: [ @@ -101,12 +101,12 @@ const backgroundComponent: ApiEntry = { { name: "color", type: "string", description: "Pattern color." }, { name: "lineWidth", type: "number", description: "Stroke width for lines/cross variant.", default: "1" }, ], - usage: snippet("usage/Background.md"), + usage: snippet("usage/Background.txt"), examples: [ { title: "Cross pattern background", category: "styling", - code: snippet("examples/Background/cross-pattern-background.md"), + code: snippet("examples/Background/cross-pattern-background.txt"), }, ], relatedApis: ["ReactFlow", "MiniMap", "Controls"], @@ -125,12 +125,12 @@ const controlsComponent: ApiEntry = { { name: "position", type: "PanelPosition", description: "Corner position.", default: "'bottom-left'" }, { name: "orientation", type: "'horizontal' | 'vertical'", description: "Layout direction.", default: "'vertical'" }, ], - usage: snippet("usage/Controls.md"), + usage: snippet("usage/Controls.txt"), examples: [ { title: "Custom control button", category: "interaction", - code: snippet("examples/Controls/custom-control-button.md"), + code: snippet("examples/Controls/custom-control-button.txt"), }, ], relatedApis: ["ReactFlow", "ControlButton", "Panel"], @@ -151,7 +151,7 @@ const miniMapComponent: ApiEntry = { { name: "pannable", type: "boolean", description: "Allow panning via minimap.", default: "false" }, { name: "zoomable", type: "boolean", description: "Allow zooming via minimap.", default: "false" }, ], - usage: snippet("usage/MiniMap.md"), + usage: snippet("usage/MiniMap.txt"), examples: [], relatedApis: ["ReactFlow", "Background", "Controls"], }; @@ -164,7 +164,7 @@ const panelComponent: ApiEntry = { props: [ { name: "position", type: "PanelPosition", description: "Corner or side position. E.g. 'top-left', 'top-right', 'bottom-left', 'bottom-right'." }, ], - usage: snippet("usage/Panel.md"), + usage: snippet("usage/Panel.txt"), examples: [], relatedApis: ["ReactFlow", "Controls", "MiniMap"], }; @@ -185,12 +185,12 @@ const handleComponent: ApiEntry = { { name: "isValidConnection", type: "IsValidConnection", description: "Custom validation logic for connections to this handle." }, { name: "onConnect", type: "OnConnect", description: "Callback when connection is made to this handle." }, ], - usage: snippet("usage/Handle.md"), + usage: snippet("usage/Handle.txt"), examples: [ { title: "Multiple handles", category: "custom-nodes", - code: snippet("examples/Handle/multiple-handles.md"), + code: snippet("examples/Handle/multiple-handles.txt"), }, ], tips: [ @@ -217,12 +217,12 @@ const nodeResizerComponent: ApiEntry = { { name: "lineStyle", type: "CSSProperties", description: "Style the resize border lines." }, { name: "keepAspectRatio", type: "boolean", description: "Maintain aspect ratio when resizing.", default: "false" }, ], - usage: snippet("usage/NodeResizer.md"), + usage: snippet("usage/NodeResizer.txt"), examples: [ { title: "Resizable node with handles", category: "custom-nodes", - code: snippet("examples/NodeResizer/resizable-node-with-handles.md"), + code: snippet("examples/NodeResizer/resizable-node-with-handles.txt"), }, ], relatedApis: ["NodeResizeControl", "Handle"], @@ -241,7 +241,7 @@ const nodeToolbarComponent: ApiEntry = { { name: "offset", type: "number", description: "Distance from node.", default: "10" }, { name: "nodeId", type: "string | string[]", description: "Attach to specific node(s)." }, ], - usage: snippet("usage/NodeToolbar.md"), + usage: snippet("usage/NodeToolbar.txt"), examples: [], relatedApis: ["EdgeToolbar", "Handle"], }; @@ -252,12 +252,12 @@ const edgeLabelRendererComponent: ApiEntry = { description: "Portal for rendering complex HTML labels on edges. Since edges are SVG, this provides a div-based renderer positioned on top of edges.", importPath: "import { EdgeLabelRenderer } from '@xyflow/react'", - usage: snippet("usage/EdgeLabelRenderer.md"), + usage: snippet("usage/EdgeLabelRenderer.txt"), examples: [ { title: "Edge with delete button", category: "custom-edges", - code: snippet("examples/EdgeLabelRenderer/edge-with-delete-button.md"), + code: snippet("examples/EdgeLabelRenderer/edge-with-delete-button.txt"), }, ], relatedApis: ["BaseEdge", "getBezierPath", "EdgeToolbar"], @@ -276,7 +276,7 @@ const baseEdgeComponent: ApiEntry = { { name: "label", type: "ReactNode", description: "Edge label content." }, { name: "interactionWidth", type: "number", description: "Width of invisible click target.", default: "20" }, ], - usage: snippet("usage/BaseEdge.md"), + usage: snippet("usage/BaseEdge.txt"), examples: [], relatedApis: ["EdgeLabelRenderer", "getBezierPath", "getSmoothStepPath"], }; @@ -297,7 +297,7 @@ const edgeTextComponent: ApiEntry = { { name: "labelBgPadding", type: "[number, number]", description: "Padding around label background.", default: "[2, 4]" }, { name: "labelBgBorderRadius", type: "number", description: "Border radius of label background.", default: "2" }, ], - usage: snippet("usage/EdgeText.md"), + usage: snippet("usage/EdgeText.txt"), examples: [], relatedApis: ["BaseEdge", "EdgeLabelRenderer", "getBezierPath"], }; @@ -308,7 +308,7 @@ const viewportPortalComponent: ApiEntry = { description: "Renders components in the same viewport coordinate system as nodes and edges. Content zooms and pans with the flow.", importPath: "import { ViewportPortal } from '@xyflow/react'", - usage: snippet("usage/ViewportPortal.md"), + usage: snippet("usage/ViewportPortal.txt"), examples: [], relatedApis: ["ReactFlow", "Panel"], }; @@ -322,7 +322,7 @@ const edgeToolbarComponent: ApiEntry = { props: [ { name: "position", type: "Position", description: "Side of edge.", default: "Position.Top" }, ], - usage: snippet("usage/EdgeToolbar.md"), + usage: snippet("usage/EdgeToolbar.txt"), examples: [], relatedApis: ["NodeToolbar", "EdgeLabelRenderer"], }; @@ -350,7 +350,7 @@ const nodeResizeControlComponent: ApiEntry = { { name: "autoScale", type: "boolean", description: "Scale controls with zoom level.", default: "true" }, { name: "resizeDirection", type: "'horizontal' | 'vertical'", description: "Constrain resize direction." }, ], - usage: snippet("usage/NodeResizeControl.md"), + usage: snippet("usage/NodeResizeControl.txt"), examples: [], tips: [ "Use NodeResizeControl when you need a custom resize UI (icon, button, etc).", @@ -368,12 +368,12 @@ const controlButtonComponent: ApiEntry = { props: [ { name: "...props", type: "ButtonHTMLAttributes", description: "Any valid HTML button props." }, ], - usage: snippet("usage/ControlButton.md"), + usage: snippet("usage/ControlButton.txt"), examples: [ { title: "Custom control with layout button", category: "interaction", - code: snippet("examples/ControlButton/custom-control-with-layout-button.md"), + code: snippet("examples/ControlButton/custom-control-with-layout-button.txt"), }, ], relatedApis: ["Controls", "Panel"], @@ -385,7 +385,7 @@ const reactFlowProviderComponent: ApiEntry = { description: "Provides the React Flow context to child components. Required when using hooks like useReactFlow outside of the ReactFlow component.", importPath: "import { ReactFlowProvider } from '@xyflow/react'", - usage: snippet("usage/ReactFlowProvider.md"), + usage: snippet("usage/ReactFlowProvider.txt"), examples: [], tips: [ "If you render and need to use hooks like useReactFlow in sibling or parent components, wrap everything in .", diff --git a/src/plugins/reactflow/data/hooks.ts b/src/plugins/reactflow/data/hooks.ts index 3cd8bfd..6ee3aa8 100644 --- a/src/plugins/reactflow/data/hooks.ts +++ b/src/plugins/reactflow/data/hooks.ts @@ -8,17 +8,17 @@ const useReactFlowHook: ApiEntry = { "Returns a ReactFlowInstance to update nodes/edges, manipulate the viewport, or query flow state. Does NOT cause re-renders on state changes.", importPath: "import { useReactFlow } from '@xyflow/react'", returns: "ReactFlowInstance", - usage: snippet("usage/useReactFlow.md"), + usage: snippet("usage/useReactFlow.txt"), examples: [ { title: "Add node on button click", category: "interaction", - code: snippet("examples/useReactFlow/add-node-on-button-click.md"), + code: snippet("examples/useReactFlow/add-node-on-button-click.txt"), }, { title: "Delete selected elements", category: "interaction", - code: snippet("examples/useReactFlow/delete-selected-elements.md"), + code: snippet("examples/useReactFlow/delete-selected-elements.txt"), }, ], tips: [ @@ -36,12 +36,12 @@ const useNodesStateHook: ApiEntry = { "Like React's useState but with a built-in change handler for nodes. Quick prototyping of controlled flows without Zustand.", importPath: "import { useNodesState } from '@xyflow/react'", returns: "[Node[], setNodes, onNodesChange]", - usage: snippet("usage/useNodesState.md"), + usage: snippet("usage/useNodesState.txt"), examples: [ { title: "Minimal controlled flow", category: "quickstart", - code: snippet("examples/useNodesState/minimal-controlled-flow.md"), + code: snippet("examples/useNodesState/minimal-controlled-flow.txt"), }, ], tips: ["For production apps with complex state, prefer Zustand with applyNodeChanges/applyEdgeChanges."], @@ -55,7 +55,7 @@ const useEdgesStateHook: ApiEntry = { "Like React's useState but with a built-in change handler for edges. Quick prototyping of controlled flows without Zustand.", importPath: "import { useEdgesState } from '@xyflow/react'", returns: "[Edge[], setEdges, onEdgesChange]", - usage: snippet("usage/useEdgesState.md"), + usage: snippet("usage/useEdgesState.txt"), examples: [], relatedApis: ["useNodesState", "applyEdgeChanges", "addEdge"], }; @@ -67,7 +67,7 @@ const useNodesHook: ApiEntry = { "Returns the current nodes array. Components using this hook re-render whenever ANY node changes (position, selection, etc).", importPath: "import { useNodes } from '@xyflow/react'", returns: "Node[]", - usage: snippet("usage/useNodes.md"), + usage: snippet("usage/useNodes.txt"), examples: [], tips: ["Can cause excessive re-renders. Prefer useReactFlow().getNodes() for on-demand access, or useNodesData for specific node data."], relatedApis: ["useEdges", "useReactFlow", "useNodesData"], @@ -80,7 +80,7 @@ const useEdgesHook: ApiEntry = { "Returns the current edges array. Components using this hook re-render whenever any edge changes.", importPath: "import { useEdges } from '@xyflow/react'", returns: "Edge[]", - usage: snippet("usage/useEdges.md"), + usage: snippet("usage/useEdges.txt"), examples: [], tips: ["Can cause excessive re-renders. Prefer useReactFlow().getEdges() for on-demand access."], relatedApis: ["useNodes", "useReactFlow"], @@ -93,12 +93,12 @@ const useNodesDataHook: ApiEntry = { "Subscribe to data changes of specific nodes by ID. More efficient than useNodes when you only need certain nodes' data.", importPath: "import { useNodesData } from '@xyflow/react'", returns: "Pick[]", - usage: snippet("usage/useNodesData.md"), + usage: snippet("usage/useNodesData.txt"), examples: [ { title: "Display connected node data", category: "custom-nodes", - code: snippet("examples/useNodesData/display-connected-node-data.md"), + code: snippet("examples/useNodesData/display-connected-node-data.txt"), }, ], relatedApis: ["useNodes", "useHandleConnections", "useNodeConnections"], @@ -111,7 +111,7 @@ const useNodeIdHook: ApiEntry = { "Returns the ID of the node it is used inside. Useful deep in the render tree without prop drilling.", importPath: "import { useNodeId } from '@xyflow/react'", returns: "string | null", - usage: snippet("usage/useNodeId.md"), + usage: snippet("usage/useNodeId.txt"), examples: [], relatedApis: ["useInternalNode", "useNodesData"], }; @@ -123,12 +123,12 @@ const useConnectionHook: ApiEntry = { "Returns the current connection state during an active connection interaction. Returns null properties when no connection is active. Useful for colorizing handles based on validity.", importPath: "import { useConnection } from '@xyflow/react'", returns: "ConnectionState", - usage: snippet("usage/useConnection.md"), + usage: snippet("usage/useConnection.txt"), examples: [ { title: "Colorize handle during connection", category: "connections", - code: snippet("examples/useConnection/colorize-handle-during-connection.md"), + code: snippet("examples/useConnection/colorize-handle-during-connection.txt"), }, ], relatedApis: ["useHandleConnections", "Handle"], @@ -141,7 +141,7 @@ const useHandleConnectionsHook: ApiEntry = { "Returns an array of connections for a specific handle. Re-renders when edge changes affect the handle.", importPath: "import { useHandleConnections } from '@xyflow/react'", returns: "HandleConnection[]", - usage: snippet("usage/useHandleConnections.md"), + usage: snippet("usage/useHandleConnections.txt"), examples: [], relatedApis: ["useNodeConnections", "useConnection", "Handle"], }; @@ -152,7 +152,7 @@ const useNodeConnectionsHook: ApiEntry = { description: "Returns an array of connections for a node. Can filter by handle type and ID.", importPath: "import { useNodeConnections } from '@xyflow/react'", returns: "NodeConnection[]", - usage: snippet("usage/useNodeConnections.md"), + usage: snippet("usage/useNodeConnections.txt"), examples: [], relatedApis: ["useHandleConnections", "useConnection"], }; @@ -162,7 +162,7 @@ const useOnSelectionChangeHook: ApiEntry = { kind: "hook", description: "Listen for changes to both node and edge selection.", importPath: "import { useOnSelectionChange } from '@xyflow/react'", - usage: snippet("usage/useOnSelectionChange.md"), + usage: snippet("usage/useOnSelectionChange.txt"), examples: [], relatedApis: ["useReactFlow", "ReactFlow"], }; @@ -173,7 +173,7 @@ const useOnViewportChangeHook: ApiEntry = { description: "Listen for viewport changes (pan, zoom). Provides callbacks for start, change, and end phases.", importPath: "import { useOnViewportChange } from '@xyflow/react'", - usage: snippet("usage/useOnViewportChange.md"), + usage: snippet("usage/useOnViewportChange.txt"), examples: [], relatedApis: ["useViewport", "useReactFlow"], }; @@ -184,7 +184,7 @@ const useViewportHook: ApiEntry = { description: "Returns the current viewport { x, y, zoom }. Re-renders on every viewport change.", importPath: "import { useViewport } from '@xyflow/react'", returns: "Viewport", - usage: snippet("usage/useViewport.md"), + usage: snippet("usage/useViewport.txt"), examples: [], tips: ["Causes re-render on every pan/zoom. Use useOnViewportChange for event-based approach, or useReactFlow().getViewport() for on-demand."], relatedApis: ["useOnViewportChange", "useReactFlow"], @@ -196,7 +196,7 @@ const useStoreHook: ApiEntry = { description: "Subscribe to internal React Flow Zustand store. Re-exported from Zustand. Use selectors to minimize re-renders.", importPath: "import { useStore } from '@xyflow/react'", - usage: snippet("usage/useStore.md"), + usage: snippet("usage/useStore.txt"), examples: [], tips: ["Always use a selector function to avoid re-rendering on every state change.", "For most use cases, prefer useReactFlow, useNodes, or useEdges instead."], relatedApis: ["useStoreApi", "useReactFlow"], @@ -209,7 +209,7 @@ const useStoreApiHook: ApiEntry = { "Returns the Zustand store object directly for on-demand state access without causing re-renders.", importPath: "import { useStoreApi } from '@xyflow/react'", returns: "StoreApi", - usage: snippet("usage/useStoreApi.md"), + usage: snippet("usage/useStoreApi.txt"), examples: [], relatedApis: ["useStore", "useReactFlow"], }; @@ -221,12 +221,12 @@ const useNodesInitializedHook: ApiEntry = { "Returns whether all nodes have been measured and given width/height. Returns false when new nodes are added, then true once measured.", importPath: "import { useNodesInitialized } from '@xyflow/react'", returns: "boolean", - usage: snippet("usage/useNodesInitialized.md"), + usage: snippet("usage/useNodesInitialized.txt"), examples: [ { title: "Auto-layout on mount", category: "layout", - code: snippet("examples/useNodesInitialized/auto-layout-on-mount.md"), + code: snippet("examples/useNodesInitialized/auto-layout-on-mount.txt"), }, ], relatedApis: ["useReactFlow"], @@ -239,7 +239,7 @@ const useUpdateNodeInternalsHook: ApiEntry = { "Notify React Flow when you programmatically add/remove handles or change handle positions on a node.", importPath: "import { useUpdateNodeInternals } from '@xyflow/react'", returns: "(nodeId: string | string[]) => void", - usage: snippet("usage/useUpdateNodeInternals.md"), + usage: snippet("usage/useUpdateNodeInternals.txt"), examples: [], tips: ["Call this after dynamically adding/removing Handle components inside a custom node."], relatedApis: ["Handle"], @@ -251,7 +251,7 @@ const useKeyPressHook: ApiEntry = { description: "Listen for specific key codes and returns whether they are currently pressed.", importPath: "import { useKeyPress } from '@xyflow/react'", returns: "boolean", - usage: snippet("usage/useKeyPress.md"), + usage: snippet("usage/useKeyPress.txt"), examples: [], relatedApis: ["ReactFlow"], }; @@ -262,7 +262,7 @@ const useInternalNodeHook: ApiEntry = { description: "Returns an InternalNode object with additional computed properties like positionAbsolute and measured dimensions.", importPath: "import { useInternalNode } from '@xyflow/react'", returns: "InternalNode | undefined", - usage: snippet("usage/useInternalNode.md"), + usage: snippet("usage/useInternalNode.txt"), examples: [], relatedApis: ["useReactFlow", "useNodeId"], }; diff --git a/src/plugins/reactflow/data/migration.ts b/src/plugins/reactflow/data/migration.ts index 1af18c7..7d64a62 100644 --- a/src/plugins/reactflow/data/migration.ts +++ b/src/plugins/reactflow/data/migration.ts @@ -1,3 +1,3 @@ import { snippet } from "./loader.js"; -export const V12_MIGRATION = snippet("migration.md"); +export const V12_MIGRATION = snippet("migration.txt"); diff --git a/src/plugins/reactflow/data/patterns.ts b/src/plugins/reactflow/data/patterns.ts index ea0945f..40b6b5c 100644 --- a/src/plugins/reactflow/data/patterns.ts +++ b/src/plugins/reactflow/data/patterns.ts @@ -2,21 +2,21 @@ import type { PatternSection } from "./types.js"; import { snippet } from "./loader.js"; export const PATTERNS: Record = { - "zustand-store": snippet("patterns/zustand-store.md"), - "undo-redo": snippet("patterns/undo-redo.md"), - "drag-and-drop": snippet("patterns/drag-and-drop.md"), - "auto-layout-dagre": snippet("patterns/auto-layout-dagre.md"), - "auto-layout-elk": snippet("patterns/auto-layout-elk.md"), - "context-menu": snippet("patterns/context-menu.md"), - "copy-paste": snippet("patterns/copy-paste.md"), - "save-restore": snippet("patterns/save-restore.md"), - "prevent-cycles": snippet("patterns/prevent-cycles.md"), - "keyboard-shortcuts": snippet("patterns/keyboard-shortcuts.md"), - "performance": snippet("patterns/performance.md"), - "dark-mode": snippet("patterns/dark-mode.md"), - "ssr": snippet("patterns/ssr.md"), - "subflows": snippet("patterns/subflows.md"), - "edge-reconnection": snippet("patterns/edge-reconnection.md"), - "custom-connection-line": snippet("patterns/custom-connection-line.md"), - "auto-layout-on-mount": snippet("patterns/auto-layout-on-mount.md"), + "zustand-store": snippet("patterns/zustand-store.txt"), + "undo-redo": snippet("patterns/undo-redo.txt"), + "drag-and-drop": snippet("patterns/drag-and-drop.txt"), + "auto-layout-dagre": snippet("patterns/auto-layout-dagre.txt"), + "auto-layout-elk": snippet("patterns/auto-layout-elk.txt"), + "context-menu": snippet("patterns/context-menu.txt"), + "copy-paste": snippet("patterns/copy-paste.txt"), + "save-restore": snippet("patterns/save-restore.txt"), + "prevent-cycles": snippet("patterns/prevent-cycles.txt"), + "keyboard-shortcuts": snippet("patterns/keyboard-shortcuts.txt"), + "performance": snippet("patterns/performance.txt"), + "dark-mode": snippet("patterns/dark-mode.txt"), + "ssr": snippet("patterns/ssr.txt"), + "subflows": snippet("patterns/subflows.txt"), + "edge-reconnection": snippet("patterns/edge-reconnection.txt"), + "custom-connection-line": snippet("patterns/custom-connection-line.txt"), + "auto-layout-on-mount": snippet("patterns/auto-layout-on-mount.txt"), }; diff --git a/src/plugins/reactflow/data/templates.ts b/src/plugins/reactflow/data/templates.ts index f986b03..86fe3dd 100644 --- a/src/plugins/reactflow/data/templates.ts +++ b/src/plugins/reactflow/data/templates.ts @@ -1,7 +1,7 @@ import { snippet } from "./loader.js"; export const TEMPLATES = { - "custom-node": snippet("templates/custom-node.md"), - "custom-edge": snippet("templates/custom-edge.md"), - "zustand-store": snippet("templates/zustand-store.md"), + "custom-node": snippet("templates/custom-node.txt"), + "custom-edge": snippet("templates/custom-edge.txt"), + "zustand-store": snippet("templates/zustand-store.txt"), }; diff --git a/src/plugins/reactflow/data/utilities.ts b/src/plugins/reactflow/data/utilities.ts index e3069fb..33b4ef2 100644 --- a/src/plugins/reactflow/data/utilities.ts +++ b/src/plugins/reactflow/data/utilities.ts @@ -8,7 +8,7 @@ const addEdgeUtil: ApiEntry = { "Convenience function to add a new edge to an array. Validates and prevents duplicates.", importPath: "import { addEdge } from '@xyflow/react'", returns: "Edge[]", - usage: snippet("usage/addEdge.md"), + usage: snippet("usage/addEdge.txt"), examples: [], relatedApis: ["ReactFlow", "useEdgesState"], }; @@ -20,7 +20,7 @@ const applyNodeChangesUtil: ApiEntry = { "Apply an array of NodeChange objects to your nodes array. Used in Zustand stores for controlled flows.", importPath: "import { applyNodeChanges } from '@xyflow/react'", returns: "Node[]", - usage: snippet("usage/applyNodeChanges.md"), + usage: snippet("usage/applyNodeChanges.txt"), examples: [], relatedApis: ["applyEdgeChanges", "useNodesState"], }; @@ -32,7 +32,7 @@ const applyEdgeChangesUtil: ApiEntry = { "Apply an array of EdgeChange objects to your edges array. Used in Zustand stores for controlled flows.", importPath: "import { applyEdgeChanges } from '@xyflow/react'", returns: "Edge[]", - usage: snippet("usage/applyEdgeChanges.md"), + usage: snippet("usage/applyEdgeChanges.txt"), examples: [], relatedApis: ["applyNodeChanges", "useEdgesState"], }; @@ -43,7 +43,7 @@ const getBezierPathUtil: ApiEntry = { description: "Returns SVG path string and label position for a bezier edge between two points.", importPath: "import { getBezierPath } from '@xyflow/react'", returns: "[path: string, labelX: number, labelY: number, offsetX: number, offsetY: number]", - usage: snippet("usage/getBezierPath.md"), + usage: snippet("usage/getBezierPath.txt"), examples: [], relatedApis: ["getSmoothStepPath", "getStraightPath", "getSimpleBezierPath", "BaseEdge"], }; @@ -54,7 +54,7 @@ const getSmoothStepPathUtil: ApiEntry = { description: "Returns SVG path string for a stepped/rounded edge with configurable border radius.", importPath: "import { getSmoothStepPath } from '@xyflow/react'", returns: "[path, labelX, labelY, offsetX, offsetY]", - usage: snippet("usage/getSmoothStepPath.md"), + usage: snippet("usage/getSmoothStepPath.txt"), examples: [], relatedApis: ["getBezierPath", "getStraightPath"], }; @@ -65,7 +65,7 @@ const getStraightPathUtil: ApiEntry = { description: "Calculates a straight line path between two points.", importPath: "import { getStraightPath } from '@xyflow/react'", returns: "[path, labelX, labelY]", - usage: snippet("usage/getStraightPath.md"), + usage: snippet("usage/getStraightPath.txt"), examples: [], relatedApis: ["getBezierPath", "getSmoothStepPath"], }; @@ -76,7 +76,7 @@ const getSimpleBezierPathUtil: ApiEntry = { description: "Returns SVG path for a simple bezier curve (less pronounced curve than getBezierPath).", importPath: "import { getSimpleBezierPath } from '@xyflow/react'", returns: "[path, labelX, labelY, offsetX, offsetY]", - usage: snippet("usage/getSimpleBezierPath.md"), + usage: snippet("usage/getSimpleBezierPath.txt"), examples: [], relatedApis: ["getBezierPath"], }; @@ -87,7 +87,7 @@ const getConnectedEdgesUtil: ApiEntry = { description: "Given nodes and all edges, returns edges that connect any of the given nodes together.", importPath: "import { getConnectedEdges } from '@xyflow/react'", returns: "Edge[]", - usage: snippet("usage/getConnectedEdges.md"), + usage: snippet("usage/getConnectedEdges.txt"), examples: [], relatedApis: ["getIncomers", "getOutgoers"], }; @@ -98,7 +98,7 @@ const getIncomersUtil: ApiEntry = { description: "Returns nodes connected to the given node as the source of an edge (upstream nodes).", importPath: "import { getIncomers } from '@xyflow/react'", returns: "Node[]", - usage: snippet("usage/getIncomers.md"), + usage: snippet("usage/getIncomers.txt"), examples: [], relatedApis: ["getOutgoers", "getConnectedEdges"], }; @@ -109,7 +109,7 @@ const getOutgoersUtil: ApiEntry = { description: "Returns nodes connected to the given node as the target of an edge (downstream nodes).", importPath: "import { getOutgoers } from '@xyflow/react'", returns: "Node[]", - usage: snippet("usage/getOutgoers.md"), + usage: snippet("usage/getOutgoers.txt"), examples: [], relatedApis: ["getIncomers", "getConnectedEdges"], }; @@ -120,7 +120,7 @@ const getNodesBoundsUtil: ApiEntry = { description: "Returns the bounding box containing all given nodes. Useful with getViewportForBounds.", importPath: "import { getNodesBounds } from '@xyflow/react'", returns: "Rect", - usage: snippet("usage/getNodesBounds.md"), + usage: snippet("usage/getNodesBounds.txt"), examples: [], relatedApis: ["getViewportForBounds"], }; @@ -131,7 +131,7 @@ const getViewportForBoundsUtil: ApiEntry = { description: "Returns the viewport to fit given bounds. Useful for server-side viewport calculation or custom fit-view logic.", importPath: "import { getViewportForBounds } from '@xyflow/react'", returns: "Viewport", - usage: snippet("usage/getViewportForBounds.md"), + usage: snippet("usage/getViewportForBounds.txt"), examples: [], relatedApis: ["getNodesBounds", "useReactFlow"], }; @@ -142,12 +142,12 @@ const reconnectEdgeUtil: ApiEntry = { description: "Reconnect an existing edge with new source/target. Used in onReconnect handlers.", importPath: "import { reconnectEdge } from '@xyflow/react'", returns: "Edge[]", - usage: snippet("usage/reconnectEdge.md"), + usage: snippet("usage/reconnectEdge.txt"), examples: [ { title: "Edge reconnection", category: "connections", - code: snippet("examples/reconnectEdge/edge-reconnection.md"), + code: snippet("examples/reconnectEdge/edge-reconnection.txt"), }, ], relatedApis: ["addEdge", "ReactFlow"], @@ -159,7 +159,7 @@ const isNodeUtil: ApiEntry = { description: "Type guard to check if an object is a valid Node.", importPath: "import { isNode } from '@xyflow/react'", returns: "boolean", - usage: snippet("usage/isNode.md"), + usage: snippet("usage/isNode.txt"), examples: [], relatedApis: ["isEdge"], }; @@ -170,7 +170,7 @@ const isEdgeUtil: ApiEntry = { description: "Type guard to check if an object is a valid Edge.", importPath: "import { isEdge } from '@xyflow/react'", returns: "boolean", - usage: snippet("usage/isEdge.md"), + usage: snippet("usage/isEdge.txt"), examples: [], relatedApis: ["isNode"], }; diff --git a/snippets/reactflow/examples/Background/cross-pattern-background.md b/src/plugins/reactflow/snippets/examples/Background/cross-pattern-background.txt similarity index 100% rename from snippets/reactflow/examples/Background/cross-pattern-background.md rename to src/plugins/reactflow/snippets/examples/Background/cross-pattern-background.txt diff --git a/snippets/reactflow/examples/ControlButton/custom-control-with-layout-button.md b/src/plugins/reactflow/snippets/examples/ControlButton/custom-control-with-layout-button.txt similarity index 100% rename from snippets/reactflow/examples/ControlButton/custom-control-with-layout-button.md rename to src/plugins/reactflow/snippets/examples/ControlButton/custom-control-with-layout-button.txt diff --git a/snippets/reactflow/examples/Controls/custom-control-button.md b/src/plugins/reactflow/snippets/examples/Controls/custom-control-button.txt similarity index 100% rename from snippets/reactflow/examples/Controls/custom-control-button.md rename to src/plugins/reactflow/snippets/examples/Controls/custom-control-button.txt diff --git a/snippets/reactflow/examples/EdgeLabelRenderer/edge-with-delete-button.md b/src/plugins/reactflow/snippets/examples/EdgeLabelRenderer/edge-with-delete-button.txt similarity index 100% rename from snippets/reactflow/examples/EdgeLabelRenderer/edge-with-delete-button.md rename to src/plugins/reactflow/snippets/examples/EdgeLabelRenderer/edge-with-delete-button.txt diff --git a/snippets/reactflow/examples/Handle/multiple-handles.md b/src/plugins/reactflow/snippets/examples/Handle/multiple-handles.txt similarity index 100% rename from snippets/reactflow/examples/Handle/multiple-handles.md rename to src/plugins/reactflow/snippets/examples/Handle/multiple-handles.txt diff --git a/snippets/reactflow/examples/Node/typed-custom-node-data.md b/src/plugins/reactflow/snippets/examples/Node/typed-custom-node-data.txt similarity index 100% rename from snippets/reactflow/examples/Node/typed-custom-node-data.md rename to src/plugins/reactflow/snippets/examples/Node/typed-custom-node-data.txt diff --git a/snippets/reactflow/examples/NodeResizer/resizable-node-with-handles.md b/src/plugins/reactflow/snippets/examples/NodeResizer/resizable-node-with-handles.txt similarity index 100% rename from snippets/reactflow/examples/NodeResizer/resizable-node-with-handles.md rename to src/plugins/reactflow/snippets/examples/NodeResizer/resizable-node-with-handles.txt diff --git a/snippets/reactflow/examples/ReactFlow/controlled-flow-zustand.md b/src/plugins/reactflow/snippets/examples/ReactFlow/controlled-flow-zustand.txt similarity index 100% rename from snippets/reactflow/examples/ReactFlow/controlled-flow-zustand.md rename to src/plugins/reactflow/snippets/examples/ReactFlow/controlled-flow-zustand.txt diff --git a/snippets/reactflow/examples/ReactFlow/uncontrolled-flow.md b/src/plugins/reactflow/snippets/examples/ReactFlow/uncontrolled-flow.txt similarity index 100% rename from snippets/reactflow/examples/ReactFlow/uncontrolled-flow.md rename to src/plugins/reactflow/snippets/examples/ReactFlow/uncontrolled-flow.txt diff --git a/snippets/reactflow/examples/reconnectEdge/edge-reconnection.md b/src/plugins/reactflow/snippets/examples/reconnectEdge/edge-reconnection.txt similarity index 100% rename from snippets/reactflow/examples/reconnectEdge/edge-reconnection.md rename to src/plugins/reactflow/snippets/examples/reconnectEdge/edge-reconnection.txt diff --git a/snippets/reactflow/examples/useConnection/colorize-handle-during-connection.md b/src/plugins/reactflow/snippets/examples/useConnection/colorize-handle-during-connection.txt similarity index 100% rename from snippets/reactflow/examples/useConnection/colorize-handle-during-connection.md rename to src/plugins/reactflow/snippets/examples/useConnection/colorize-handle-during-connection.txt diff --git a/snippets/reactflow/examples/useNodesData/display-connected-node-data.md b/src/plugins/reactflow/snippets/examples/useNodesData/display-connected-node-data.txt similarity index 100% rename from snippets/reactflow/examples/useNodesData/display-connected-node-data.md rename to src/plugins/reactflow/snippets/examples/useNodesData/display-connected-node-data.txt diff --git a/snippets/reactflow/examples/useNodesInitialized/auto-layout-on-mount.md b/src/plugins/reactflow/snippets/examples/useNodesInitialized/auto-layout-on-mount.txt similarity index 100% rename from snippets/reactflow/examples/useNodesInitialized/auto-layout-on-mount.md rename to src/plugins/reactflow/snippets/examples/useNodesInitialized/auto-layout-on-mount.txt diff --git a/snippets/reactflow/examples/useNodesState/minimal-controlled-flow.md b/src/plugins/reactflow/snippets/examples/useNodesState/minimal-controlled-flow.txt similarity index 100% rename from snippets/reactflow/examples/useNodesState/minimal-controlled-flow.md rename to src/plugins/reactflow/snippets/examples/useNodesState/minimal-controlled-flow.txt diff --git a/snippets/reactflow/examples/useReactFlow/add-node-on-button-click.md b/src/plugins/reactflow/snippets/examples/useReactFlow/add-node-on-button-click.txt similarity index 100% rename from snippets/reactflow/examples/useReactFlow/add-node-on-button-click.md rename to src/plugins/reactflow/snippets/examples/useReactFlow/add-node-on-button-click.txt diff --git a/snippets/reactflow/examples/useReactFlow/delete-selected-elements.md b/src/plugins/reactflow/snippets/examples/useReactFlow/delete-selected-elements.txt similarity index 100% rename from snippets/reactflow/examples/useReactFlow/delete-selected-elements.md rename to src/plugins/reactflow/snippets/examples/useReactFlow/delete-selected-elements.txt diff --git a/snippets/reactflow/migration.md b/src/plugins/reactflow/snippets/migration.txt similarity index 100% rename from snippets/reactflow/migration.md rename to src/plugins/reactflow/snippets/migration.txt diff --git a/snippets/reactflow/patterns/auto-layout-dagre.md b/src/plugins/reactflow/snippets/patterns/auto-layout-dagre.txt similarity index 100% rename from snippets/reactflow/patterns/auto-layout-dagre.md rename to src/plugins/reactflow/snippets/patterns/auto-layout-dagre.txt diff --git a/snippets/reactflow/patterns/auto-layout-elk.md b/src/plugins/reactflow/snippets/patterns/auto-layout-elk.txt similarity index 100% rename from snippets/reactflow/patterns/auto-layout-elk.md rename to src/plugins/reactflow/snippets/patterns/auto-layout-elk.txt diff --git a/snippets/reactflow/patterns/auto-layout-on-mount.md b/src/plugins/reactflow/snippets/patterns/auto-layout-on-mount.txt similarity index 100% rename from snippets/reactflow/patterns/auto-layout-on-mount.md rename to src/plugins/reactflow/snippets/patterns/auto-layout-on-mount.txt diff --git a/snippets/reactflow/patterns/context-menu.md b/src/plugins/reactflow/snippets/patterns/context-menu.txt similarity index 100% rename from snippets/reactflow/patterns/context-menu.md rename to src/plugins/reactflow/snippets/patterns/context-menu.txt diff --git a/snippets/reactflow/patterns/copy-paste.md b/src/plugins/reactflow/snippets/patterns/copy-paste.txt similarity index 100% rename from snippets/reactflow/patterns/copy-paste.md rename to src/plugins/reactflow/snippets/patterns/copy-paste.txt diff --git a/snippets/reactflow/patterns/custom-connection-line.md b/src/plugins/reactflow/snippets/patterns/custom-connection-line.txt similarity index 100% rename from snippets/reactflow/patterns/custom-connection-line.md rename to src/plugins/reactflow/snippets/patterns/custom-connection-line.txt diff --git a/snippets/reactflow/patterns/dark-mode.md b/src/plugins/reactflow/snippets/patterns/dark-mode.txt similarity index 100% rename from snippets/reactflow/patterns/dark-mode.md rename to src/plugins/reactflow/snippets/patterns/dark-mode.txt diff --git a/snippets/reactflow/patterns/drag-and-drop.md b/src/plugins/reactflow/snippets/patterns/drag-and-drop.txt similarity index 100% rename from snippets/reactflow/patterns/drag-and-drop.md rename to src/plugins/reactflow/snippets/patterns/drag-and-drop.txt diff --git a/snippets/reactflow/patterns/edge-reconnection.md b/src/plugins/reactflow/snippets/patterns/edge-reconnection.txt similarity index 100% rename from snippets/reactflow/patterns/edge-reconnection.md rename to src/plugins/reactflow/snippets/patterns/edge-reconnection.txt diff --git a/snippets/reactflow/patterns/keyboard-shortcuts.md b/src/plugins/reactflow/snippets/patterns/keyboard-shortcuts.txt similarity index 100% rename from snippets/reactflow/patterns/keyboard-shortcuts.md rename to src/plugins/reactflow/snippets/patterns/keyboard-shortcuts.txt diff --git a/snippets/reactflow/patterns/performance.md b/src/plugins/reactflow/snippets/patterns/performance.txt similarity index 100% rename from snippets/reactflow/patterns/performance.md rename to src/plugins/reactflow/snippets/patterns/performance.txt diff --git a/snippets/reactflow/patterns/prevent-cycles.md b/src/plugins/reactflow/snippets/patterns/prevent-cycles.txt similarity index 100% rename from snippets/reactflow/patterns/prevent-cycles.md rename to src/plugins/reactflow/snippets/patterns/prevent-cycles.txt diff --git a/snippets/reactflow/patterns/save-restore.md b/src/plugins/reactflow/snippets/patterns/save-restore.txt similarity index 100% rename from snippets/reactflow/patterns/save-restore.md rename to src/plugins/reactflow/snippets/patterns/save-restore.txt diff --git a/snippets/reactflow/patterns/ssr.md b/src/plugins/reactflow/snippets/patterns/ssr.txt similarity index 100% rename from snippets/reactflow/patterns/ssr.md rename to src/plugins/reactflow/snippets/patterns/ssr.txt diff --git a/snippets/reactflow/patterns/subflows.md b/src/plugins/reactflow/snippets/patterns/subflows.txt similarity index 100% rename from snippets/reactflow/patterns/subflows.md rename to src/plugins/reactflow/snippets/patterns/subflows.txt diff --git a/snippets/reactflow/patterns/undo-redo.md b/src/plugins/reactflow/snippets/patterns/undo-redo.txt similarity index 100% rename from snippets/reactflow/patterns/undo-redo.md rename to src/plugins/reactflow/snippets/patterns/undo-redo.txt diff --git a/snippets/reactflow/patterns/zustand-store.md b/src/plugins/reactflow/snippets/patterns/zustand-store.txt similarity index 100% rename from snippets/reactflow/patterns/zustand-store.md rename to src/plugins/reactflow/snippets/patterns/zustand-store.txt diff --git a/snippets/reactflow/templates/custom-edge.md b/src/plugins/reactflow/snippets/templates/custom-edge.txt similarity index 100% rename from snippets/reactflow/templates/custom-edge.md rename to src/plugins/reactflow/snippets/templates/custom-edge.txt diff --git a/snippets/reactflow/templates/custom-node.md b/src/plugins/reactflow/snippets/templates/custom-node.txt similarity index 100% rename from snippets/reactflow/templates/custom-node.md rename to src/plugins/reactflow/snippets/templates/custom-node.txt diff --git a/snippets/reactflow/templates/zustand-store.md b/src/plugins/reactflow/snippets/templates/zustand-store.txt similarity index 100% rename from snippets/reactflow/templates/zustand-store.md rename to src/plugins/reactflow/snippets/templates/zustand-store.txt diff --git a/snippets/reactflow/usage/Background.md b/src/plugins/reactflow/snippets/usage/Background.txt similarity index 100% rename from snippets/reactflow/usage/Background.md rename to src/plugins/reactflow/snippets/usage/Background.txt diff --git a/snippets/reactflow/usage/BaseEdge.md b/src/plugins/reactflow/snippets/usage/BaseEdge.txt similarity index 100% rename from snippets/reactflow/usage/BaseEdge.md rename to src/plugins/reactflow/snippets/usage/BaseEdge.txt diff --git a/snippets/reactflow/usage/Connection.md b/src/plugins/reactflow/snippets/usage/Connection.txt similarity index 100% rename from snippets/reactflow/usage/Connection.md rename to src/plugins/reactflow/snippets/usage/Connection.txt diff --git a/snippets/reactflow/usage/ControlButton.md b/src/plugins/reactflow/snippets/usage/ControlButton.txt similarity index 100% rename from snippets/reactflow/usage/ControlButton.md rename to src/plugins/reactflow/snippets/usage/ControlButton.txt diff --git a/snippets/reactflow/usage/Controls.md b/src/plugins/reactflow/snippets/usage/Controls.txt similarity index 100% rename from snippets/reactflow/usage/Controls.md rename to src/plugins/reactflow/snippets/usage/Controls.txt diff --git a/snippets/reactflow/usage/Edge.md b/src/plugins/reactflow/snippets/usage/Edge.txt similarity index 100% rename from snippets/reactflow/usage/Edge.md rename to src/plugins/reactflow/snippets/usage/Edge.txt diff --git a/snippets/reactflow/usage/EdgeLabelRenderer.md b/src/plugins/reactflow/snippets/usage/EdgeLabelRenderer.txt similarity index 100% rename from snippets/reactflow/usage/EdgeLabelRenderer.md rename to src/plugins/reactflow/snippets/usage/EdgeLabelRenderer.txt diff --git a/snippets/reactflow/usage/EdgeProps.md b/src/plugins/reactflow/snippets/usage/EdgeProps.txt similarity index 100% rename from snippets/reactflow/usage/EdgeProps.md rename to src/plugins/reactflow/snippets/usage/EdgeProps.txt diff --git a/snippets/reactflow/usage/EdgeText.md b/src/plugins/reactflow/snippets/usage/EdgeText.txt similarity index 100% rename from snippets/reactflow/usage/EdgeText.md rename to src/plugins/reactflow/snippets/usage/EdgeText.txt diff --git a/snippets/reactflow/usage/EdgeToolbar.md b/src/plugins/reactflow/snippets/usage/EdgeToolbar.txt similarity index 100% rename from snippets/reactflow/usage/EdgeToolbar.md rename to src/plugins/reactflow/snippets/usage/EdgeToolbar.txt diff --git a/snippets/reactflow/usage/Handle.md b/src/plugins/reactflow/snippets/usage/Handle.txt similarity index 100% rename from snippets/reactflow/usage/Handle.md rename to src/plugins/reactflow/snippets/usage/Handle.txt diff --git a/snippets/reactflow/usage/MiniMap.md b/src/plugins/reactflow/snippets/usage/MiniMap.txt similarity index 100% rename from snippets/reactflow/usage/MiniMap.md rename to src/plugins/reactflow/snippets/usage/MiniMap.txt diff --git a/snippets/reactflow/usage/Node.md b/src/plugins/reactflow/snippets/usage/Node.txt similarity index 100% rename from snippets/reactflow/usage/Node.md rename to src/plugins/reactflow/snippets/usage/Node.txt diff --git a/snippets/reactflow/usage/NodeProps.md b/src/plugins/reactflow/snippets/usage/NodeProps.txt similarity index 100% rename from snippets/reactflow/usage/NodeProps.md rename to src/plugins/reactflow/snippets/usage/NodeProps.txt diff --git a/snippets/reactflow/usage/NodeResizeControl.md b/src/plugins/reactflow/snippets/usage/NodeResizeControl.txt similarity index 100% rename from snippets/reactflow/usage/NodeResizeControl.md rename to src/plugins/reactflow/snippets/usage/NodeResizeControl.txt diff --git a/snippets/reactflow/usage/NodeResizer.md b/src/plugins/reactflow/snippets/usage/NodeResizer.txt similarity index 100% rename from snippets/reactflow/usage/NodeResizer.md rename to src/plugins/reactflow/snippets/usage/NodeResizer.txt diff --git a/snippets/reactflow/usage/NodeToolbar.md b/src/plugins/reactflow/snippets/usage/NodeToolbar.txt similarity index 100% rename from snippets/reactflow/usage/NodeToolbar.md rename to src/plugins/reactflow/snippets/usage/NodeToolbar.txt diff --git a/snippets/reactflow/usage/Panel.md b/src/plugins/reactflow/snippets/usage/Panel.txt similarity index 100% rename from snippets/reactflow/usage/Panel.md rename to src/plugins/reactflow/snippets/usage/Panel.txt diff --git a/snippets/reactflow/usage/ReactFlow.md b/src/plugins/reactflow/snippets/usage/ReactFlow.txt similarity index 100% rename from snippets/reactflow/usage/ReactFlow.md rename to src/plugins/reactflow/snippets/usage/ReactFlow.txt diff --git a/snippets/reactflow/usage/ReactFlowInstance.md b/src/plugins/reactflow/snippets/usage/ReactFlowInstance.txt similarity index 100% rename from snippets/reactflow/usage/ReactFlowInstance.md rename to src/plugins/reactflow/snippets/usage/ReactFlowInstance.txt diff --git a/snippets/reactflow/usage/ReactFlowProvider.md b/src/plugins/reactflow/snippets/usage/ReactFlowProvider.txt similarity index 100% rename from snippets/reactflow/usage/ReactFlowProvider.md rename to src/plugins/reactflow/snippets/usage/ReactFlowProvider.txt diff --git a/snippets/reactflow/usage/Viewport.md b/src/plugins/reactflow/snippets/usage/Viewport.txt similarity index 100% rename from snippets/reactflow/usage/Viewport.md rename to src/plugins/reactflow/snippets/usage/Viewport.txt diff --git a/snippets/reactflow/usage/ViewportPortal.md b/src/plugins/reactflow/snippets/usage/ViewportPortal.txt similarity index 100% rename from snippets/reactflow/usage/ViewportPortal.md rename to src/plugins/reactflow/snippets/usage/ViewportPortal.txt diff --git a/snippets/reactflow/usage/addEdge.md b/src/plugins/reactflow/snippets/usage/addEdge.txt similarity index 100% rename from snippets/reactflow/usage/addEdge.md rename to src/plugins/reactflow/snippets/usage/addEdge.txt diff --git a/snippets/reactflow/usage/applyEdgeChanges.md b/src/plugins/reactflow/snippets/usage/applyEdgeChanges.txt similarity index 100% rename from snippets/reactflow/usage/applyEdgeChanges.md rename to src/plugins/reactflow/snippets/usage/applyEdgeChanges.txt diff --git a/snippets/reactflow/usage/applyNodeChanges.md b/src/plugins/reactflow/snippets/usage/applyNodeChanges.txt similarity index 100% rename from snippets/reactflow/usage/applyNodeChanges.md rename to src/plugins/reactflow/snippets/usage/applyNodeChanges.txt diff --git a/snippets/reactflow/usage/getBezierPath.md b/src/plugins/reactflow/snippets/usage/getBezierPath.txt similarity index 100% rename from snippets/reactflow/usage/getBezierPath.md rename to src/plugins/reactflow/snippets/usage/getBezierPath.txt diff --git a/snippets/reactflow/usage/getConnectedEdges.md b/src/plugins/reactflow/snippets/usage/getConnectedEdges.txt similarity index 100% rename from snippets/reactflow/usage/getConnectedEdges.md rename to src/plugins/reactflow/snippets/usage/getConnectedEdges.txt diff --git a/snippets/reactflow/usage/getIncomers.md b/src/plugins/reactflow/snippets/usage/getIncomers.txt similarity index 100% rename from snippets/reactflow/usage/getIncomers.md rename to src/plugins/reactflow/snippets/usage/getIncomers.txt diff --git a/snippets/reactflow/usage/getNodesBounds.md b/src/plugins/reactflow/snippets/usage/getNodesBounds.txt similarity index 100% rename from snippets/reactflow/usage/getNodesBounds.md rename to src/plugins/reactflow/snippets/usage/getNodesBounds.txt diff --git a/snippets/reactflow/usage/getOutgoers.md b/src/plugins/reactflow/snippets/usage/getOutgoers.txt similarity index 100% rename from snippets/reactflow/usage/getOutgoers.md rename to src/plugins/reactflow/snippets/usage/getOutgoers.txt diff --git a/snippets/reactflow/usage/getSimpleBezierPath.md b/src/plugins/reactflow/snippets/usage/getSimpleBezierPath.txt similarity index 100% rename from snippets/reactflow/usage/getSimpleBezierPath.md rename to src/plugins/reactflow/snippets/usage/getSimpleBezierPath.txt diff --git a/snippets/reactflow/usage/getSmoothStepPath.md b/src/plugins/reactflow/snippets/usage/getSmoothStepPath.txt similarity index 100% rename from snippets/reactflow/usage/getSmoothStepPath.md rename to src/plugins/reactflow/snippets/usage/getSmoothStepPath.txt diff --git a/snippets/reactflow/usage/getStraightPath.md b/src/plugins/reactflow/snippets/usage/getStraightPath.txt similarity index 100% rename from snippets/reactflow/usage/getStraightPath.md rename to src/plugins/reactflow/snippets/usage/getStraightPath.txt diff --git a/snippets/reactflow/usage/getViewportForBounds.md b/src/plugins/reactflow/snippets/usage/getViewportForBounds.txt similarity index 100% rename from snippets/reactflow/usage/getViewportForBounds.md rename to src/plugins/reactflow/snippets/usage/getViewportForBounds.txt diff --git a/snippets/reactflow/usage/isEdge.md b/src/plugins/reactflow/snippets/usage/isEdge.txt similarity index 100% rename from snippets/reactflow/usage/isEdge.md rename to src/plugins/reactflow/snippets/usage/isEdge.txt diff --git a/snippets/reactflow/usage/isNode.md b/src/plugins/reactflow/snippets/usage/isNode.txt similarity index 100% rename from snippets/reactflow/usage/isNode.md rename to src/plugins/reactflow/snippets/usage/isNode.txt diff --git a/snippets/reactflow/usage/reconnectEdge.md b/src/plugins/reactflow/snippets/usage/reconnectEdge.txt similarity index 100% rename from snippets/reactflow/usage/reconnectEdge.md rename to src/plugins/reactflow/snippets/usage/reconnectEdge.txt diff --git a/snippets/reactflow/usage/useConnection.md b/src/plugins/reactflow/snippets/usage/useConnection.txt similarity index 100% rename from snippets/reactflow/usage/useConnection.md rename to src/plugins/reactflow/snippets/usage/useConnection.txt diff --git a/snippets/reactflow/usage/useEdges.md b/src/plugins/reactflow/snippets/usage/useEdges.txt similarity index 100% rename from snippets/reactflow/usage/useEdges.md rename to src/plugins/reactflow/snippets/usage/useEdges.txt diff --git a/snippets/reactflow/usage/useEdgesState.md b/src/plugins/reactflow/snippets/usage/useEdgesState.txt similarity index 100% rename from snippets/reactflow/usage/useEdgesState.md rename to src/plugins/reactflow/snippets/usage/useEdgesState.txt diff --git a/snippets/reactflow/usage/useHandleConnections.md b/src/plugins/reactflow/snippets/usage/useHandleConnections.txt similarity index 100% rename from snippets/reactflow/usage/useHandleConnections.md rename to src/plugins/reactflow/snippets/usage/useHandleConnections.txt diff --git a/snippets/reactflow/usage/useInternalNode.md b/src/plugins/reactflow/snippets/usage/useInternalNode.txt similarity index 100% rename from snippets/reactflow/usage/useInternalNode.md rename to src/plugins/reactflow/snippets/usage/useInternalNode.txt diff --git a/snippets/reactflow/usage/useKeyPress.md b/src/plugins/reactflow/snippets/usage/useKeyPress.txt similarity index 100% rename from snippets/reactflow/usage/useKeyPress.md rename to src/plugins/reactflow/snippets/usage/useKeyPress.txt diff --git a/snippets/reactflow/usage/useNodeConnections.md b/src/plugins/reactflow/snippets/usage/useNodeConnections.txt similarity index 100% rename from snippets/reactflow/usage/useNodeConnections.md rename to src/plugins/reactflow/snippets/usage/useNodeConnections.txt diff --git a/snippets/reactflow/usage/useNodeId.md b/src/plugins/reactflow/snippets/usage/useNodeId.txt similarity index 100% rename from snippets/reactflow/usage/useNodeId.md rename to src/plugins/reactflow/snippets/usage/useNodeId.txt diff --git a/snippets/reactflow/usage/useNodes.md b/src/plugins/reactflow/snippets/usage/useNodes.txt similarity index 100% rename from snippets/reactflow/usage/useNodes.md rename to src/plugins/reactflow/snippets/usage/useNodes.txt diff --git a/snippets/reactflow/usage/useNodesData.md b/src/plugins/reactflow/snippets/usage/useNodesData.txt similarity index 100% rename from snippets/reactflow/usage/useNodesData.md rename to src/plugins/reactflow/snippets/usage/useNodesData.txt diff --git a/snippets/reactflow/usage/useNodesInitialized.md b/src/plugins/reactflow/snippets/usage/useNodesInitialized.txt similarity index 100% rename from snippets/reactflow/usage/useNodesInitialized.md rename to src/plugins/reactflow/snippets/usage/useNodesInitialized.txt diff --git a/snippets/reactflow/usage/useNodesState.md b/src/plugins/reactflow/snippets/usage/useNodesState.txt similarity index 100% rename from snippets/reactflow/usage/useNodesState.md rename to src/plugins/reactflow/snippets/usage/useNodesState.txt diff --git a/snippets/reactflow/usage/useOnSelectionChange.md b/src/plugins/reactflow/snippets/usage/useOnSelectionChange.txt similarity index 100% rename from snippets/reactflow/usage/useOnSelectionChange.md rename to src/plugins/reactflow/snippets/usage/useOnSelectionChange.txt diff --git a/snippets/reactflow/usage/useOnViewportChange.md b/src/plugins/reactflow/snippets/usage/useOnViewportChange.txt similarity index 100% rename from snippets/reactflow/usage/useOnViewportChange.md rename to src/plugins/reactflow/snippets/usage/useOnViewportChange.txt diff --git a/snippets/reactflow/usage/useReactFlow.md b/src/plugins/reactflow/snippets/usage/useReactFlow.txt similarity index 100% rename from snippets/reactflow/usage/useReactFlow.md rename to src/plugins/reactflow/snippets/usage/useReactFlow.txt diff --git a/snippets/reactflow/usage/useStore.md b/src/plugins/reactflow/snippets/usage/useStore.txt similarity index 100% rename from snippets/reactflow/usage/useStore.md rename to src/plugins/reactflow/snippets/usage/useStore.txt diff --git a/snippets/reactflow/usage/useStoreApi.md b/src/plugins/reactflow/snippets/usage/useStoreApi.txt similarity index 100% rename from snippets/reactflow/usage/useStoreApi.md rename to src/plugins/reactflow/snippets/usage/useStoreApi.txt diff --git a/snippets/reactflow/usage/useUpdateNodeInternals.md b/src/plugins/reactflow/snippets/usage/useUpdateNodeInternals.txt similarity index 100% rename from snippets/reactflow/usage/useUpdateNodeInternals.md rename to src/plugins/reactflow/snippets/usage/useUpdateNodeInternals.txt diff --git a/snippets/reactflow/usage/useViewport.md b/src/plugins/reactflow/snippets/usage/useViewport.txt similarity index 100% rename from snippets/reactflow/usage/useViewport.md rename to src/plugins/reactflow/snippets/usage/useViewport.txt diff --git a/src/plugins/rust/data.ts b/src/plugins/rust/data.ts index 764d46c..9ab8e13 100644 --- a/src/plugins/rust/data.ts +++ b/src/plugins/rust/data.ts @@ -27,30 +27,30 @@ export const BEST_PRACTICES: BestPractice[] = [ chapter: "coding-styles", rule: "Prefer &T over .clone() unless ownership transfer is required.", reason: "Cloning allocates heap memory unnecessarily. References are zero-cost.", - good: snippet("practices/borrow-over-clone-good.md"), - bad: snippet("practices/borrow-over-clone-bad.md"), + good: snippet("practices/borrow-over-clone-good.txt"), + bad: snippet("practices/borrow-over-clone-bad.txt"), }, { name: "str-over-string", chapter: "coding-styles", rule: "Use &str over String, &[T] over Vec in function parameters.", reason: "&str accepts both &String and string literals. More flexible and zero-copy.", - good: snippet("practices/str-over-string-good.md"), - bad: snippet("practices/str-over-string-bad.md"), + good: snippet("practices/str-over-string-good.txt"), + bad: snippet("practices/str-over-string-bad.txt"), }, { name: "copy-by-value", chapter: "coding-styles", rule: "Small Copy types (โ‰ค24 bytes) can be passed by value โ€” no need for &T.", reason: "Copying small types (u32, bool, small structs) is as fast or faster than a reference.", - good: snippet("practices/copy-by-value-good.md"), + good: snippet("practices/copy-by-value-good.txt"), }, { name: "cow-ambiguous-ownership", chapter: "coding-styles", rule: "Use Cow<'_, T> when ownership is sometimes required and sometimes not.", reason: "Avoids always cloning (wasteful) or always borrowing (restrictive).", - good: snippet("practices/cow-ambiguous-ownership-good.md"), + good: snippet("practices/cow-ambiguous-ownership-good.txt"), }, // ERROR HANDLING @@ -59,16 +59,16 @@ export const BEST_PRACTICES: BestPractice[] = [ chapter: "error-handling", rule: "Return Result for fallible operations. Never panic! in production code.", reason: "panic! unwinds the stack and kills the thread. Use it only for programmer errors.", - good: snippet("practices/result-not-panic-good.md"), - bad: snippet("practices/result-not-panic-bad.md"), + good: snippet("practices/result-not-panic-good.txt"), + bad: snippet("practices/result-not-panic-bad.txt"), }, { name: "no-unwrap-in-prod", chapter: "error-handling", rule: "Never use unwrap() or expect() outside of tests.", reason: "Both panic on None/Err. Use ? operator or proper error handling.", - good: snippet("practices/no-unwrap-in-prod-good.md"), - bad: snippet("practices/no-unwrap-in-prod-bad.md"), + good: snippet("practices/no-unwrap-in-prod-good.txt"), + bad: snippet("practices/no-unwrap-in-prod-bad.txt"), tips: ["expect() is slightly better than unwrap() (message on panic), but still banned in prod"], }, { @@ -76,7 +76,7 @@ export const BEST_PRACTICES: BestPractice[] = [ chapter: "error-handling", rule: "thiserror for library errors, anyhow for binary/application errors.", reason: "Libraries need typed errors (callers match on them). Binaries just need context strings.", - good: snippet("practices/thiserror-vs-anyhow-good.md"), + good: snippet("practices/thiserror-vs-anyhow-good.txt"), }, // PERFORMANCE @@ -85,24 +85,24 @@ export const BEST_PRACTICES: BestPractice[] = [ chapter: "performance", rule: "Always benchmark with --release flag. Debug builds are 10-100x slower.", reason: "Debug builds disable optimizations. Benchmarks without --release are meaningless.", - good: snippet("practices/benchmark-release-good.md"), - bad: snippet("practices/benchmark-release-bad.md"), + good: snippet("practices/benchmark-release-good.txt"), + bad: snippet("practices/benchmark-release-bad.txt"), }, { name: "avoid-clone-in-loops", chapter: "performance", rule: "Avoid cloning in loops. Use .iter() instead of .into_iter() for Copy types.", reason: "Cloning in a loop = N allocations. References or Copy types are zero-cost.", - good: snippet("practices/avoid-clone-in-loops-good.md"), - bad: snippet("practices/avoid-clone-in-loops-bad.md"), + good: snippet("practices/avoid-clone-in-loops-good.txt"), + bad: snippet("practices/avoid-clone-in-loops-bad.txt"), }, { name: "prefer-iterators", chapter: "performance", rule: "Prefer iterator chains over manual loops. Avoid premature .collect().", reason: "Iterators are lazy โ€” they don't allocate until consumed. collect() is the allocation point.", - good: snippet("practices/prefer-iterators-good.md"), - bad: snippet("practices/prefer-iterators-bad.md"), + good: snippet("practices/prefer-iterators-good.txt"), + bad: snippet("practices/prefer-iterators-bad.txt"), }, // CLIPPY @@ -122,8 +122,8 @@ export const BEST_PRACTICES: BestPractice[] = [ chapter: "clippy", rule: "Use #[expect(clippy::lint_name)] over #[allow(...)]. Add a justification comment.", reason: "#[expect] fails if the warning no longer fires (lint was fixed). #[allow] silently rots.", - good: snippet("practices/expect-over-allow-good.md"), - bad: snippet("practices/expect-over-allow-bad.md"), + good: snippet("practices/expect-over-allow-good.txt"), + bad: snippet("practices/expect-over-allow-bad.txt"), }, // TESTING @@ -132,22 +132,22 @@ export const BEST_PRACTICES: BestPractice[] = [ chapter: "testing", rule: "Name tests descriptively: process_should_return_error_when_input_empty()", reason: "Test names are documentation. Vague names like test_process() don't explain what's tested.", - good: snippet("practices/descriptive-test-names-good.md"), - bad: snippet("practices/descriptive-test-names-bad.md"), + good: snippet("practices/descriptive-test-names-good.txt"), + bad: snippet("practices/descriptive-test-names-bad.txt"), }, { name: "one-assertion-per-test", chapter: "testing", rule: "One assertion per test when possible.", reason: "Multiple assertions in one test: first failure hides the rest. Separate tests give clearer failures.", - good: snippet("practices/one-assertion-per-test-good.md"), + good: snippet("practices/one-assertion-per-test-good.txt"), }, { name: "doc-tests", chapter: "documentation", rule: "Use doc tests (///) for public API usage examples. They run with cargo test.", reason: "Doc tests are the only examples guaranteed to stay correct โ€” they compile and run.", - good: snippet("practices/doc-tests-good.md"), + good: snippet("practices/doc-tests-good.txt"), }, // GENERICS @@ -156,7 +156,7 @@ export const BEST_PRACTICES: BestPractice[] = [ chapter: "generics", rule: "Prefer generics (static dispatch) for performance-critical code. Use dyn Trait for heterogeneous collections.", reason: "Generics monomorphize at compile time โ€” zero runtime cost. dyn Trait has vtable overhead.", - good: snippet("practices/static-over-dynamic-dispatch-good.md"), + good: snippet("practices/static-over-dynamic-dispatch-good.txt"), }, // TYPE STATE @@ -165,7 +165,7 @@ export const BEST_PRACTICES: BestPractice[] = [ chapter: "type-state", rule: "Encode valid state transitions in the type system using PhantomData.", reason: "Catches invalid operations at compile time, not runtime. Zero cost abstraction.", - good: snippet("practices/type-state-pattern-good.md"), + good: snippet("practices/type-state-pattern-good.txt"), tips: ["Use when invalid state transitions are a real risk", "Don't over-engineer โ€” simple enums often suffice"], }, @@ -180,8 +180,8 @@ export const BEST_PRACTICES: BestPractice[] = [ "Mutex: interior mutability for T: Send", "Rc and RefCell are NOT thread-safe โ€” local only", ], - good: snippet("practices/send-sync-good.md"), - bad: snippet("practices/send-sync-bad.md"), + good: snippet("practices/send-sync-good.txt"), + bad: snippet("practices/send-sync-bad.txt"), }, ]; diff --git a/snippets/rust/cheatsheet.md b/src/plugins/rust/snippets/cheatsheet.txt similarity index 100% rename from snippets/rust/cheatsheet.md rename to src/plugins/rust/snippets/cheatsheet.txt diff --git a/snippets/rust/practices/avoid-clone-in-loops-bad.md b/src/plugins/rust/snippets/practices/avoid-clone-in-loops-bad.txt similarity index 100% rename from snippets/rust/practices/avoid-clone-in-loops-bad.md rename to src/plugins/rust/snippets/practices/avoid-clone-in-loops-bad.txt diff --git a/snippets/rust/practices/avoid-clone-in-loops-good.md b/src/plugins/rust/snippets/practices/avoid-clone-in-loops-good.txt similarity index 100% rename from snippets/rust/practices/avoid-clone-in-loops-good.md rename to src/plugins/rust/snippets/practices/avoid-clone-in-loops-good.txt diff --git a/snippets/rust/practices/benchmark-release-bad.md b/src/plugins/rust/snippets/practices/benchmark-release-bad.txt similarity index 100% rename from snippets/rust/practices/benchmark-release-bad.md rename to src/plugins/rust/snippets/practices/benchmark-release-bad.txt diff --git a/snippets/rust/practices/benchmark-release-good.md b/src/plugins/rust/snippets/practices/benchmark-release-good.txt similarity index 100% rename from snippets/rust/practices/benchmark-release-good.md rename to src/plugins/rust/snippets/practices/benchmark-release-good.txt diff --git a/snippets/rust/practices/borrow-over-clone-bad.md b/src/plugins/rust/snippets/practices/borrow-over-clone-bad.txt similarity index 100% rename from snippets/rust/practices/borrow-over-clone-bad.md rename to src/plugins/rust/snippets/practices/borrow-over-clone-bad.txt diff --git a/snippets/rust/practices/borrow-over-clone-good.md b/src/plugins/rust/snippets/practices/borrow-over-clone-good.txt similarity index 100% rename from snippets/rust/practices/borrow-over-clone-good.md rename to src/plugins/rust/snippets/practices/borrow-over-clone-good.txt diff --git a/snippets/rust/practices/copy-by-value-good.md b/src/plugins/rust/snippets/practices/copy-by-value-good.txt similarity index 100% rename from snippets/rust/practices/copy-by-value-good.md rename to src/plugins/rust/snippets/practices/copy-by-value-good.txt diff --git a/snippets/rust/practices/cow-ambiguous-ownership-good.md b/src/plugins/rust/snippets/practices/cow-ambiguous-ownership-good.txt similarity index 100% rename from snippets/rust/practices/cow-ambiguous-ownership-good.md rename to src/plugins/rust/snippets/practices/cow-ambiguous-ownership-good.txt diff --git a/snippets/rust/practices/descriptive-test-names-bad.md b/src/plugins/rust/snippets/practices/descriptive-test-names-bad.txt similarity index 100% rename from snippets/rust/practices/descriptive-test-names-bad.md rename to src/plugins/rust/snippets/practices/descriptive-test-names-bad.txt diff --git a/snippets/rust/practices/descriptive-test-names-good.md b/src/plugins/rust/snippets/practices/descriptive-test-names-good.txt similarity index 100% rename from snippets/rust/practices/descriptive-test-names-good.md rename to src/plugins/rust/snippets/practices/descriptive-test-names-good.txt diff --git a/snippets/rust/practices/doc-tests-good.md b/src/plugins/rust/snippets/practices/doc-tests-good.txt similarity index 100% rename from snippets/rust/practices/doc-tests-good.md rename to src/plugins/rust/snippets/practices/doc-tests-good.txt diff --git a/snippets/rust/practices/expect-over-allow-bad.md b/src/plugins/rust/snippets/practices/expect-over-allow-bad.txt similarity index 100% rename from snippets/rust/practices/expect-over-allow-bad.md rename to src/plugins/rust/snippets/practices/expect-over-allow-bad.txt diff --git a/snippets/rust/practices/expect-over-allow-good.md b/src/plugins/rust/snippets/practices/expect-over-allow-good.txt similarity index 100% rename from snippets/rust/practices/expect-over-allow-good.md rename to src/plugins/rust/snippets/practices/expect-over-allow-good.txt diff --git a/snippets/rust/practices/no-unwrap-in-prod-bad.md b/src/plugins/rust/snippets/practices/no-unwrap-in-prod-bad.txt similarity index 100% rename from snippets/rust/practices/no-unwrap-in-prod-bad.md rename to src/plugins/rust/snippets/practices/no-unwrap-in-prod-bad.txt diff --git a/snippets/rust/practices/no-unwrap-in-prod-good.md b/src/plugins/rust/snippets/practices/no-unwrap-in-prod-good.txt similarity index 100% rename from snippets/rust/practices/no-unwrap-in-prod-good.md rename to src/plugins/rust/snippets/practices/no-unwrap-in-prod-good.txt diff --git a/snippets/rust/practices/one-assertion-per-test-good.md b/src/plugins/rust/snippets/practices/one-assertion-per-test-good.txt similarity index 100% rename from snippets/rust/practices/one-assertion-per-test-good.md rename to src/plugins/rust/snippets/practices/one-assertion-per-test-good.txt diff --git a/snippets/rust/practices/prefer-iterators-bad.md b/src/plugins/rust/snippets/practices/prefer-iterators-bad.txt similarity index 100% rename from snippets/rust/practices/prefer-iterators-bad.md rename to src/plugins/rust/snippets/practices/prefer-iterators-bad.txt diff --git a/snippets/rust/practices/prefer-iterators-good.md b/src/plugins/rust/snippets/practices/prefer-iterators-good.txt similarity index 100% rename from snippets/rust/practices/prefer-iterators-good.md rename to src/plugins/rust/snippets/practices/prefer-iterators-good.txt diff --git a/snippets/rust/practices/result-not-panic-bad.md b/src/plugins/rust/snippets/practices/result-not-panic-bad.txt similarity index 100% rename from snippets/rust/practices/result-not-panic-bad.md rename to src/plugins/rust/snippets/practices/result-not-panic-bad.txt diff --git a/snippets/rust/practices/result-not-panic-good.md b/src/plugins/rust/snippets/practices/result-not-panic-good.txt similarity index 100% rename from snippets/rust/practices/result-not-panic-good.md rename to src/plugins/rust/snippets/practices/result-not-panic-good.txt diff --git a/snippets/rust/practices/send-sync-bad.md b/src/plugins/rust/snippets/practices/send-sync-bad.txt similarity index 100% rename from snippets/rust/practices/send-sync-bad.md rename to src/plugins/rust/snippets/practices/send-sync-bad.txt diff --git a/snippets/rust/practices/send-sync-good.md b/src/plugins/rust/snippets/practices/send-sync-good.txt similarity index 100% rename from snippets/rust/practices/send-sync-good.md rename to src/plugins/rust/snippets/practices/send-sync-good.txt diff --git a/snippets/rust/practices/static-over-dynamic-dispatch-good.md b/src/plugins/rust/snippets/practices/static-over-dynamic-dispatch-good.txt similarity index 100% rename from snippets/rust/practices/static-over-dynamic-dispatch-good.md rename to src/plugins/rust/snippets/practices/static-over-dynamic-dispatch-good.txt diff --git a/snippets/rust/practices/str-over-string-bad.md b/src/plugins/rust/snippets/practices/str-over-string-bad.txt similarity index 100% rename from snippets/rust/practices/str-over-string-bad.md rename to src/plugins/rust/snippets/practices/str-over-string-bad.txt diff --git a/snippets/rust/practices/str-over-string-good.md b/src/plugins/rust/snippets/practices/str-over-string-good.txt similarity index 100% rename from snippets/rust/practices/str-over-string-good.md rename to src/plugins/rust/snippets/practices/str-over-string-good.txt diff --git a/snippets/rust/practices/thiserror-vs-anyhow-good.md b/src/plugins/rust/snippets/practices/thiserror-vs-anyhow-good.txt similarity index 100% rename from snippets/rust/practices/thiserror-vs-anyhow-good.md rename to src/plugins/rust/snippets/practices/thiserror-vs-anyhow-good.txt diff --git a/snippets/rust/practices/type-state-pattern-good.md b/src/plugins/rust/snippets/practices/type-state-pattern-good.txt similarity index 100% rename from snippets/rust/practices/type-state-pattern-good.md rename to src/plugins/rust/snippets/practices/type-state-pattern-good.txt diff --git a/src/plugins/rust/tools/cheatsheet.ts b/src/plugins/rust/tools/cheatsheet.ts index 5857c76..2cf0a0a 100644 --- a/src/plugins/rust/tools/cheatsheet.ts +++ b/src/plugins/rust/tools/cheatsheet.ts @@ -1,7 +1,7 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { snippet } from "../loader.js"; -const CHEATSHEET = snippet("cheatsheet.md"); +const CHEATSHEET = snippet("cheatsheet.txt"); export function register(server: McpServer): void { server.tool( diff --git a/src/plugins/ui-ux/data.ts b/src/plugins/ui-ux/data.ts index 0479da5..8c6b823 100644 --- a/src/plugins/ui-ux/data.ts +++ b/src/plugins/ui-ux/data.ts @@ -46,7 +46,7 @@ export const PRINCIPLES: Principle[] = [ domain: "typography", rule: "Use mathematical ratios for type scale, not arbitrary sizes.", detail: "Common ratios: 1.25 (Major Third), 1.333 (Perfect Fourth), 1.414 (Augmented Fourth). Recommended web scale: Display 48-72px, H1 40-56px, H2 28-40px, H3 20-24px, Subtitle 16-20px, Body 16px, Body-sm 14px, Caption 13px, Overline 12px.", - cssExample: snippet("principles/type-scale.md"), + cssExample: snippet("principles/type-scale.txt"), antiPatterns: ["Random px values with no ratio", "Fluid body text (causes reflow)", "More than 2 type families"], }, { @@ -75,7 +75,7 @@ export const PRINCIPLES: Principle[] = [ domain: "typography", rule: "Max prose width: 65ch (~600px). Beyond this, eye tracking degrades.", detail: "Apply `max-width: 65ch` to all body text containers. This is not the page width โ€” cards and components can be wider.", - cssExample: snippet("principles/prose-width.md"), + cssExample: snippet("principles/prose-width.txt"), antiPatterns: ["Full-width paragraphs", "Applying 65ch to the whole layout"], }, @@ -85,7 +85,7 @@ export const PRINCIPLES: Principle[] = [ domain: "color", rule: "Use OKLCH for new projects โ€” perceptually uniform, P3 gamut, Tailwind v4 native.", detail: "oklch(L C H): L=Lightness 0โ€“1, C=Chroma 0โ€“0.4, H=Hue 0โ€“360ยฐ. Each axis is independent. To darken: reduce L. To desaturate: reduce C. To shift hue: adjust H only.", - cssExample: snippet("principles/oklch-color.md"), + cssExample: snippet("principles/oklch-color.txt"), antiPatterns: ["Mixing HSL and OKLCH", "Using rgb() for brand colors in new projects"], }, { @@ -108,7 +108,7 @@ export const PRINCIPLES: Principle[] = [ domain: "color", rule: "Dark mode: redesign, don't invert. Warm charcoal, not black.", detail: "Background: oklch(0.13 0.008 265) โ€” warm charcoal, not #000. Text: oklch(0.94 0.008 265) โ€” off-white, not #fff. Higher elevation = lighter bg (shadows invisible on dark). Reduce saturation slightly (vivid on dark = neon). Primary accents brighten: brand-600 โ†’ brand-400.", - cssExample: snippet("principles/dark-mode.md"), + cssExample: snippet("principles/dark-mode.txt"), antiPatterns: ["Pure black background (#000)", "Pure white text (#fff) on dark", "Inverting light mode colors directly"], }, { @@ -116,7 +116,7 @@ export const PRINCIPLES: Principle[] = [ domain: "color", rule: "Always provide solid + soft variant for status colors (success/error/warning/info).", detail: "Solid: passes 4.5:1 with white text (L ~0.55). Soft: light tinted bg + dark text for non-critical contexts. Warning uses dark text (not white) because amber is too light.", - cssExample: snippet("principles/semantic-status-colors.md"), + cssExample: snippet("principles/semantic-status-colors.txt"), antiPatterns: ["White text on warning (fails contrast)", "Same color for solid and soft"], }, @@ -142,7 +142,7 @@ export const PRINCIPLES: Principle[] = [ domain: "elevation", rule: "5 surface levels. Each must be visually distinguishable via bg color, not just borders.", detail: "Level 0: flat (page bg). Level 1: subtle (inline cards). Level 2: raised (standard cards). Level 3: elevated (dropdowns, popovers). Level 4: floating (modals, dialogs). In dark mode: no shadows โ€” use progressively lighter bg colors.", - cssExample: snippet("principles/5-elevation-levels.md"), + cssExample: snippet("principles/5-elevation-levels.txt"), antiPatterns: ["Borders as the only elevation signal", "Shadows on dark backgrounds"], }, { @@ -150,7 +150,7 @@ export const PRINCIPLES: Principle[] = [ domain: "elevation", rule: "Shadows must be warm-tinted (oklch), never rgba(0,0,0,...).", detail: "Pure black shadows look harsh and disconnected from warm UIs. Tint shadows with a warm hue at very low opacity.", - cssExample: snippet("principles/warm-shadows.md"), + cssExample: snippet("principles/warm-shadows.txt"), antiPatterns: ["rgba(0,0,0,...) shadows in warm UI", "High-opacity shadows (>0.2)"], }, @@ -167,7 +167,7 @@ export const PRINCIPLES: Principle[] = [ domain: "motion", rule: "ease-out for entering. ease-in for exiting. ease-in-out for repositioning. NEVER linear.", detail: "ease-out: elements appearing โ€” starts fast, settles naturally. ease-in: elements leaving โ€” starts gently, ends decisively. ease-in-out: elements moving to new position. Spring/bounce: playful feedback only.", - cssExample: snippet("principles/easing-rules.md"), + cssExample: snippet("principles/easing-rules.txt"), antiPatterns: ["Linear easing for any UI motion", "Bounce/spring for serious/professional contexts"], }, { @@ -175,7 +175,7 @@ export const PRINCIPLES: Principle[] = [ domain: "motion", rule: "ALWAYS implement prefers-reduced-motion. Not optional.", detail: "Place in @layer base with !important. Applies to all elements and pseudos. Some users have vestibular disorders โ€” motion can cause nausea.", - cssExample: snippet("principles/reduced-motion.md"), + cssExample: snippet("principles/reduced-motion.txt"), antiPatterns: ["Missing prefers-reduced-motion", "Only disabling some animations"], }, @@ -185,7 +185,7 @@ export const PRINCIPLES: Principle[] = [ domain: "accessibility", rule: "Touch targets: min 44ร—44px (WCAG), recommended 48ร—48px. Gap โ‰ฅ 8px between targets.", detail: "Visual size โ‰  touch target. A 34px button can have a 44px hit area via padding or ::after pseudo-element. Gap prevents accidental taps on adjacent targets.", - cssExample: snippet("principles/touch-targets.md"), + cssExample: snippet("principles/touch-targets.txt"), antiPatterns: ["< 44px touch targets on mobile", "Adjacent buttons with no gap"], }, { @@ -193,7 +193,7 @@ export const PRINCIPLES: Principle[] = [ domain: "accessibility", rule: "Every interactive element MUST have visible focus indicator: 2px ring, 2px offset, primary color.", detail: "Trap focus in modals. Return focus to trigger on close. Never `outline: none` without a custom focus style.", - cssExample: snippet("principles/focus-management.md"), + cssExample: snippet("principles/focus-management.txt"), antiPatterns: ["outline: none without custom focus", "Focus styles with < 3:1 contrast", "No focus trap in modals"], }, { @@ -217,7 +217,7 @@ export const PRINCIPLES: Principle[] = [ domain: "responsive", rule: "Write mobile styles first, override with min-width queries.", detail: "Mobile-first produces smaller CSS. Most traffic is mobile. Progressive enhancement > graceful degradation.", - cssExample: snippet("principles/mobile-first.md"), + cssExample: snippet("principles/mobile-first.txt"), antiPatterns: ["Desktop-first with max-width overrides", "Separate mobile stylesheet"], }, ]; @@ -238,7 +238,7 @@ export const COMPONENT_PATTERNS: ComponentPattern[] = [ "Touch target min 44px โ€” use padding to expand if needed", "Primary: solid brand bg. Secondary: outlined. Ghost: transparent. Destructive: error color.", ], - code: snippet("components/button.md"), + code: snippet("components/button.txt"), }, { name: "card", diff --git a/snippets/ui-ux/cheatsheet.md b/src/plugins/ui-ux/snippets/cheatsheet.txt similarity index 100% rename from snippets/ui-ux/cheatsheet.md rename to src/plugins/ui-ux/snippets/cheatsheet.txt diff --git a/snippets/ui-ux/components/badge.md b/src/plugins/ui-ux/snippets/components/badge.txt similarity index 100% rename from snippets/ui-ux/components/badge.md rename to src/plugins/ui-ux/snippets/components/badge.txt diff --git a/snippets/ui-ux/components/button.md b/src/plugins/ui-ux/snippets/components/button.txt similarity index 100% rename from snippets/ui-ux/components/button.md rename to src/plugins/ui-ux/snippets/components/button.txt diff --git a/snippets/ui-ux/components/card.md b/src/plugins/ui-ux/snippets/components/card.txt similarity index 100% rename from snippets/ui-ux/components/card.md rename to src/plugins/ui-ux/snippets/components/card.txt diff --git a/snippets/ui-ux/components/form-input.md b/src/plugins/ui-ux/snippets/components/form-input.txt similarity index 100% rename from snippets/ui-ux/components/form-input.md rename to src/plugins/ui-ux/snippets/components/form-input.txt diff --git a/snippets/ui-ux/principles/4px-grid.md b/src/plugins/ui-ux/snippets/principles/4px-grid.txt similarity index 100% rename from snippets/ui-ux/principles/4px-grid.md rename to src/plugins/ui-ux/snippets/principles/4px-grid.txt diff --git a/snippets/ui-ux/principles/5-elevation-levels.md b/src/plugins/ui-ux/snippets/principles/5-elevation-levels.txt similarity index 100% rename from snippets/ui-ux/principles/5-elevation-levels.md rename to src/plugins/ui-ux/snippets/principles/5-elevation-levels.txt diff --git a/snippets/ui-ux/principles/dark-mode.md b/src/plugins/ui-ux/snippets/principles/dark-mode.txt similarity index 100% rename from snippets/ui-ux/principles/dark-mode.md rename to src/plugins/ui-ux/snippets/principles/dark-mode.txt diff --git a/snippets/ui-ux/principles/easing-rules.md b/src/plugins/ui-ux/snippets/principles/easing-rules.txt similarity index 100% rename from snippets/ui-ux/principles/easing-rules.md rename to src/plugins/ui-ux/snippets/principles/easing-rules.txt diff --git a/snippets/ui-ux/principles/focus-management.md b/src/plugins/ui-ux/snippets/principles/focus-management.txt similarity index 100% rename from snippets/ui-ux/principles/focus-management.md rename to src/plugins/ui-ux/snippets/principles/focus-management.txt diff --git a/snippets/ui-ux/principles/mobile-first.md b/src/plugins/ui-ux/snippets/principles/mobile-first.txt similarity index 100% rename from snippets/ui-ux/principles/mobile-first.md rename to src/plugins/ui-ux/snippets/principles/mobile-first.txt diff --git a/snippets/ui-ux/principles/oklch-color.md b/src/plugins/ui-ux/snippets/principles/oklch-color.txt similarity index 100% rename from snippets/ui-ux/principles/oklch-color.md rename to src/plugins/ui-ux/snippets/principles/oklch-color.txt diff --git a/snippets/ui-ux/principles/prose-width.md b/src/plugins/ui-ux/snippets/principles/prose-width.txt similarity index 100% rename from snippets/ui-ux/principles/prose-width.md rename to src/plugins/ui-ux/snippets/principles/prose-width.txt diff --git a/snippets/ui-ux/principles/reduced-motion.md b/src/plugins/ui-ux/snippets/principles/reduced-motion.txt similarity index 100% rename from snippets/ui-ux/principles/reduced-motion.md rename to src/plugins/ui-ux/snippets/principles/reduced-motion.txt diff --git a/snippets/ui-ux/principles/semantic-status-colors.md b/src/plugins/ui-ux/snippets/principles/semantic-status-colors.txt similarity index 100% rename from snippets/ui-ux/principles/semantic-status-colors.md rename to src/plugins/ui-ux/snippets/principles/semantic-status-colors.txt diff --git a/snippets/ui-ux/principles/touch-targets.md b/src/plugins/ui-ux/snippets/principles/touch-targets.txt similarity index 100% rename from snippets/ui-ux/principles/touch-targets.md rename to src/plugins/ui-ux/snippets/principles/touch-targets.txt diff --git a/snippets/ui-ux/principles/type-scale.md b/src/plugins/ui-ux/snippets/principles/type-scale.txt similarity index 100% rename from snippets/ui-ux/principles/type-scale.md rename to src/plugins/ui-ux/snippets/principles/type-scale.txt diff --git a/snippets/ui-ux/principles/warm-shadows.md b/src/plugins/ui-ux/snippets/principles/warm-shadows.txt similarity index 100% rename from snippets/ui-ux/principles/warm-shadows.md rename to src/plugins/ui-ux/snippets/principles/warm-shadows.txt diff --git a/snippets/ui-ux/principles/warm-vs-cool.md b/src/plugins/ui-ux/snippets/principles/warm-vs-cool.txt similarity index 100% rename from snippets/ui-ux/principles/warm-vs-cool.md rename to src/plugins/ui-ux/snippets/principles/warm-vs-cool.txt diff --git a/snippets/ui-ux/principles/wcag-contrast.md b/src/plugins/ui-ux/snippets/principles/wcag-contrast.txt similarity index 100% rename from snippets/ui-ux/principles/wcag-contrast.md rename to src/plugins/ui-ux/snippets/principles/wcag-contrast.txt diff --git a/snippets/ui-ux/references/elevation-table.md b/src/plugins/ui-ux/snippets/references/elevation-table.txt similarity index 100% rename from snippets/ui-ux/references/elevation-table.md rename to src/plugins/ui-ux/snippets/references/elevation-table.txt diff --git a/snippets/ui-ux/references/motion-table.md b/src/plugins/ui-ux/snippets/references/motion-table.txt similarity index 100% rename from snippets/ui-ux/references/motion-table.md rename to src/plugins/ui-ux/snippets/references/motion-table.txt diff --git a/snippets/ui-ux/references/spacing-table.md b/src/plugins/ui-ux/snippets/references/spacing-table.txt similarity index 100% rename from snippets/ui-ux/references/spacing-table.md rename to src/plugins/ui-ux/snippets/references/spacing-table.txt diff --git a/snippets/ui-ux/references/type-scale-table.md b/src/plugins/ui-ux/snippets/references/type-scale-table.txt similarity index 100% rename from snippets/ui-ux/references/type-scale-table.md rename to src/plugins/ui-ux/snippets/references/type-scale-table.txt diff --git a/snippets/ui-ux/references/wcag-table.md b/src/plugins/ui-ux/snippets/references/wcag-table.txt similarity index 100% rename from snippets/ui-ux/references/wcag-table.md rename to src/plugins/ui-ux/snippets/references/wcag-table.txt diff --git a/src/shared/loader-factory.ts b/src/shared/loader-factory.ts index a22c2eb..a44b0de 100755 --- a/src/shared/loader-factory.ts +++ b/src/shared/loader-factory.ts @@ -5,7 +5,8 @@ import { fileURLToPath } from "url"; const sharedDir = dirname(fileURLToPath(import.meta.url)); export function createSnippetLoader(pluginName: string): (rel: string) => string { - const snippetsDir = join(sharedDir, "../../snippets", pluginName); + // Traverse up from src/shared to src/plugins//snippets + const snippetsDir = join(sharedDir, "../plugins", pluginName, "snippets"); return function snippet(rel: string): string { try { @@ -13,7 +14,7 @@ export function createSnippetLoader(pluginName: string): (rel: string) => string } catch (err) { const code = (err as NodeJS.ErrnoException).code; if (code === "ENOENT") { - throw new Error(`Snippet not found: snippets/${pluginName}/${rel}`); + throw new Error(`Snippet not found: src/plugins/${pluginName}/snippets/${rel}`); } throw err; } diff --git a/summary.md b/summary.md new file mode 100644 index 0000000..744f646 --- /dev/null +++ b/summary.md @@ -0,0 +1,123 @@ +# Migration: unified-mcp + unified-skill โ†’ unified + +## Goal + +Merge two tightly-coupled repos into one monorepo called `unified`. + +Current repos: +- `github.com/orkait/unified-mcp` - MCP server (TypeScript, Docker) +- `github.com/orkait/unified-skill` - Claude Code skill (SKILL.md only) + +Target repo: `github.com/orkait/unified` + +--- + +## Target Structure + +``` +unified/ +โ”œโ”€โ”€ SKILL.md โ† moved from unified-skill root (must stay at root for ~/.claude/skills/) +โ”œโ”€โ”€ README.md โ† new combined README +โ”œโ”€โ”€ mcp/ โ† everything from unified-mcp root +โ”‚ โ”œโ”€โ”€ src/ +โ”‚ โ”œโ”€โ”€ snippets/ +โ”‚ โ”œโ”€โ”€ scripts/ +โ”‚ โ”œโ”€โ”€ dist/ โ† gitignored +โ”‚ โ”œโ”€โ”€ Dockerfile +โ”‚ โ”œโ”€โ”€ package.json +โ”‚ โ”œโ”€โ”€ package-lock.json +โ”‚ โ””โ”€โ”€ tsconfig.json +โ””โ”€โ”€ .gitignore +``` + +**Critical constraint:** `SKILL.md` must stay at the repo root. Claude Code skill loading resolves `SKILL.md` relative to the cloned directory. If the user clones to `~/.claude/skills/unified`, it reads `~/.claude/skills/unified/SKILL.md`. + +--- + +## Migration Steps + +### 1. Create new GitHub repo +- Name: `unified` under `orkait` org +- Description: "MCP server + Claude Code skill for AI-assisted development" +- Public, MIT license, no auto-init + +### 2. Set up local repo +```bash +mkdir unified && cd unified +git init +git remote add origin git@github.com:orkait/unified.git +``` + +### 3. Copy files from unified-mcp +```bash +# from unified-mcp root, copy everything into mcp/ subdirectory +cp -r src snippets scripts Dockerfile package.json package-lock.json tsconfig.json .dockerignore mcp/ +``` + +### 4. Copy SKILL.md from unified-skill +```bash +# SKILL.md goes at root, not inside mcp/ +cp ../unified-skill/SKILL.md ./SKILL.md +``` + +### 5. .gitignore +Create `.gitignore` at root: +``` +mcp/node_modules/ +mcp/dist/ +``` + +### 6. Update scripts/start-mcp.sh +The script references paths relative to itself. Update any absolute or relative path assumptions to account for being inside `mcp/` subdirectory. The script should `cd` to its own directory (`mcp/`) before running node. + +### 7. Update Dockerfile +The Dockerfile WORKDIR and COPY paths are relative to build context. Build context will now be `mcp/` so no changes needed IF docker build is run from inside `mcp/`. Document this in README. + +### 8. Update MCP config path +Users must update their `~/.claude.json` or equivalent: +```json +{ + "mcpServers": { + "unified": { + "command": "/path/to/unified/mcp/scripts/start-mcp.sh" + } + } +} +``` + +### 9. Write combined README.md +- Hero block: `unified` as the name, tagline covers both skill + MCP +- Single install section: clone once, configure MCP path to `mcp/scripts/start-mcp.sh` +- Skill section: `~/.claude/skills/unified` path +- Plugin table from unified-mcp README +- Tools section from unified-mcp README (collapsible) +- Badge style: flat-square, matching current unified-mcp badges + +### 10. Commit and push +```bash +git add . +git commit -m "feat: initial unified monorepo - merge unified-mcp and unified-skill" +git push -u origin main +``` + +### 11. Archive old repos +On GitHub, go to Settings โ†’ Archive on both `unified-mcp` and `unified-skill`. Add a notice to their READMEs pointing to the new repo. + +--- + +## Key Constraints + +- `SKILL.md` at root is non-negotiable - do not nest it +- `mcp/` contains everything that was previously at unified-mcp root +- Do not rename any tool names in TypeScript - MCP tool names are user-facing +- `npm run build` and `npm start` run from inside `mcp/` not repo root +- Docker build context is `mcp/` directory + +--- + +## Out of Scope + +- No changes to plugin code, snippet files, or tool implementations +- No changes to SKILL.md content +- No new plugins +- No TypeScript refactors From cfe82f675860a8164a23d06a75cfdffaceae0a54 Mon Sep 17 00:00:00 2001 From: Kailas Mahavarkar <66670953+KailasMahavarkar@users.noreply.github.com> Date: Thu, 9 Apr 2026 00:13:22 +0530 Subject: [PATCH 07/65] ci: add github action to publish ghcr docker image and update readme --- .github/workflows/publish.yml | 43 ++++++++++++++++++++++++++++ README.md | 53 ++++++++++++----------------------- 2 files changed, 61 insertions(+), 35 deletions(-) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..6a587fd --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,43 @@ +name: Publish Docker image + +on: + push: + branches: ['main'] + release: + types: [published] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push-image: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file diff --git a/README.md b/README.md index 14a4fe6..309103b 100755 --- a/README.md +++ b/README.md @@ -248,65 +248,48 @@ git clone https://github.com/orkait/hyperstack.git ~/.claude/skills/hyperstack ## ๐Ÿš€ Install -### ๐Ÿณ Docker (recommended) +The easiest way to use Hyperstack is via our pre-built Docker image. Docker will automatically download and run the server without any cloning or building required. -Build once, reuse forever. The wrapper script keeps **one** named container alive and runs each MCP session inside it via `docker exec` - no duplicate containers, no matter how many AI sessions are open. - -```bash -git clone https://github.com/orkait/hyperstack.git -cd hyperstack -npm install -docker build -t hyperstack . -``` - -Add to your MCP config: - -
-Claude Code - ~/.claude.json +Add the following to your MCP config (`~/.claude.json` or Cursor config): ```json { "mcpServers": { "hyperstack": { - "command": "/absolute/path/to/hyperstack/scripts/start-mcp.sh" + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "--memory=256m", + "--cpus=0.5", + "ghcr.io/orkait/hyperstack:main" + ] } } } ``` -
- -
-Claude Desktop / Cursor / Windsurf - their respective config files - -```json -{ - "mcpServers": { - "hyperstack": { - "command": "/absolute/path/to/hyperstack/scripts/start-mcp.sh" - } - } -} -``` - -
+*Note: The `--memory=256m` and `--cpus=0.5` flags ensure the server runs with strict resource limits, preventing it from consuming too much RAM or compute.* --- -### ๐Ÿ“ฆ Without Docker (Node directly) +### ๐Ÿ“ฆ Local Development (Node directly) + +If you prefer to run it locally without Docker: ```bash git clone https://github.com/orkait/hyperstack.git cd hyperstack -npm install && npm run build +npm install ``` ```json { "mcpServers": { "hyperstack": { - "command": "node", - "args": ["/absolute/path/to/hyperstack/dist/index.js"] + "command": "npx", + "args": ["tsx", "/absolute/path/to/hyperstack/src/index.ts"] } } } From 98f7271a8b3848716c2eebd22c2c8ae4bf996a84 Mon Sep 17 00:00:00 2001 From: Kailas Mahavarkar <66670953+KailasMahavarkar@users.noreply.github.com> Date: Thu, 9 Apr 2026 00:20:49 +0530 Subject: [PATCH 08/65] ci: switch to Docker Hub (superorkait) instead of ghcr.io --- .github/workflows/publish.yml | 16 ++++------------ README.md | 2 +- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 6a587fd..9ba19dc 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -6,33 +6,25 @@ on: release: types: [published] -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - jobs: build-and-push-image: runs-on: ubuntu-latest - permissions: - contents: read - packages: write steps: - name: Checkout repository uses: actions/checkout@v4 - - name: Log in to the Container registry + - name: Log in to Docker Hub uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Extract metadata (tags, labels) for Docker id: meta uses: docker/metadata-action@v5 with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + images: superorkait/hyperstack - name: Build and push Docker image uses: docker/build-push-action@v5 diff --git a/README.md b/README.md index 309103b..275d7e2 100755 --- a/README.md +++ b/README.md @@ -263,7 +263,7 @@ Add the following to your MCP config (`~/.claude.json` or Cursor config): "--rm", "--memory=256m", "--cpus=0.5", - "ghcr.io/orkait/hyperstack:main" + "superorkait/hyperstack:main" ] } } From 10570409adf22d8b63f755ad5aa4f98d27baf5fc Mon Sep 17 00:00:00 2001 From: Kailas Mahavarkar <66670953+KailasMahavarkar@users.noreply.github.com> Date: Thu, 9 Apr 2026 00:46:21 +0530 Subject: [PATCH 09/65] ci: switch back to ghcr.io for more secure and reliable builds --- .github/workflows/publish.yml | 16 ++++++++++++---- README.md | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9ba19dc..6a587fd 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -6,25 +6,33 @@ on: release: types: [published] +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + jobs: build-and-push-image: runs-on: ubuntu-latest + permissions: + contents: read + packages: write steps: - name: Checkout repository uses: actions/checkout@v4 - - name: Log in to Docker Hub + - name: Log in to the Container registry uses: docker/login-action@v3 with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata (tags, labels) for Docker id: meta uses: docker/metadata-action@v5 with: - images: superorkait/hyperstack + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - name: Build and push Docker image uses: docker/build-push-action@v5 diff --git a/README.md b/README.md index 275d7e2..309103b 100755 --- a/README.md +++ b/README.md @@ -263,7 +263,7 @@ Add the following to your MCP config (`~/.claude.json` or Cursor config): "--rm", "--memory=256m", "--cpus=0.5", - "superorkait/hyperstack:main" + "ghcr.io/orkait/hyperstack:main" ] } } From 54690fafa41666c0c261d15eaa6a03523a046c38 Mon Sep 17 00:00:00 2001 From: Kailas Mahavarkar <66670953+KailasMahavarkar@users.noreply.github.com> Date: Thu, 9 Apr 2026 00:47:11 +0530 Subject: [PATCH 10/65] ci: fix build by removing src/ from .dockerignore --- .dockerignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.dockerignore b/.dockerignore index fa4d46d..8a9ea42 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,4 @@ node_modules/ -src/ tests/ docs/ .git/ From 23c8121a99146b83d35e5b1ab769712b6ba28637 Mon Sep 17 00:00:00 2001 From: Kailas Mahavarkar <66670953+KailasMahavarkar@users.noreply.github.com> Date: Thu, 9 Apr 2026 01:24:12 +0530 Subject: [PATCH 11/65] docs: remove stale npm install instructions and focus on docker zero-install --- README.md | 305 ++++-------------------------------------------------- 1 file changed, 23 insertions(+), 282 deletions(-) diff --git a/README.md b/README.md index 309103b..bf5372e 100755 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # hyperstack -**One MCP server. Every library your AI needs. Zero conflicts.** +**One MCP server + AI Skill. Every library your AI needs. Zero conflicts.**

MIT @@ -25,7 +25,7 @@
-> Plugin-based MCP server that gives your AI assistant deep knowledge of frontend and backend libraries - +> Plugin-based MCP server and AI Skill that gives your AI assistant deep knowledge of frontend and backend libraries - > API refs, patterns, code generation, design systems - all through a single process with namespaced tools.

@@ -44,211 +44,9 @@ git clone https://github.com/orkait/hyperstack.git ~/.claude/skills/hyperstack --- -## ๐Ÿงฉ Plugins - -| Plugin | Library / Domain | Tools | What's included | -|--------|-----------------|:-----:|-----------------| -| **reactflow** | [@xyflow/react](https://reactflow.dev) v12 | 8 | 56 APIs, 17 patterns, 3 templates, migration guide | -| **motion** | [Motion for React](https://motion.dev) v12 | 6 | 33 APIs, 14 example categories, transition reference | -| **lenis** | [Lenis](https://lenis.darkroom.engineering) smooth scroll | 6 | API reference, 7 patterns, 7 recipes, CSS rules, GSAP integration | -| **react** | React 19 + Next.js App Router | 4 | RSC patterns, state hierarchy, data fetching, Zustand, composition | -| **echo** | [Echo](https://echo.labstack.com) Go web framework | 6 | 19 recipes, 13 middleware, decision matrix, cheatsheet | -| **golang** | Go best practices + design patterns | 6 | 18 best practices, 10 design patterns, anti-patterns, cheatsheet | -| **rust** | Rust best practices | 4 | 18 practices (good/bad pairs), ownership guide, cheatsheet | -| **design-tokens** | Tailwind v4 + OKLCH token system | 7 | 10 token categories, 8 build procedures, color ramp templates | -| **ui-ux** | UI/UX design principles | 6 | Typography, color, spacing, elevation, motion, a11y, component patterns | - ---- - -## ๐Ÿ› ๏ธ Tools - -
-โš›๏ธ React Flow - reactflow_* - -| Tool | What it does | -|------|-------------| -| `reactflow_list_apis` | Browse all 56 APIs grouped by kind - components, hooks, utilities, types | -| `reactflow_get_api` | Full reference for any API: props table, usage snippet, examples, tips | -| `reactflow_search_docs` | Full-text search across all docs and code examples | -| `reactflow_get_examples` | Curated code examples by category | -| `reactflow_get_pattern` | Complete enterprise patterns with full implementation code | -| `reactflow_get_template` | Production-ready starters: `custom-node`, `custom-edge`, `zustand-store` | -| `reactflow_get_migration_guide` | v11 โ†’ v12 breaking changes with before/after diffs | -| `reactflow_generate_flow` | Generate a complete flow component from a plain English description | - -
-17 available patterns - -`zustand-store` ยท `undo-redo` ยท `drag-and-drop` ยท `auto-layout-dagre` ยท `auto-layout-elk` ยท `context-menu` ยท `copy-paste` ยท `save-restore` ยท `prevent-cycles` ยท `keyboard-shortcuts` ยท `performance` ยท `dark-mode` ยท `ssr` ยท `subflows` ยท `edge-reconnection` ยท `custom-connection-line` ยท `auto-layout-on-mount` - -
-
- -
-๐ŸŽฌ Motion for React - motion_* - -| Tool | What it does | -|------|-------------| -| `motion_list_apis` | Browse all 33 APIs grouped by kind - components, hooks, functions | -| `motion_get_api` | Full reference for any API: props table, usage snippet, examples, tips | -| `motion_search_docs` | Full-text search across all docs and code examples | -| `motion_get_examples` | Curated animation examples by category | -| `motion_get_transitions` | Complete transition reference: tween, spring, inertia, orchestration | -| `motion_generate_animation` | Generate a Motion animation snippet from a plain English description | - -
-14 example categories - -`animation` ยท `gestures` ยท `scroll` ยท `layout` ยท `exit` ยท `drag` ยท `hover` ยท `svg` ยท `transitions` ยท `variants` ยท `keyframes` ยท `spring` ยท `reorder` ยท `performance` - -
-
- -
-๐ŸŒŠ Lenis - lenis_* - -| Tool | What it does | -|------|-------------| -| `lenis_list_apis` | Browse all Lenis APIs - options, methods, events | -| `lenis_get_api` | Full reference for any API with usage snippet | -| `lenis_get_pattern` | Integration patterns: Next.js, GSAP, Framer Motion, custom container | -| `lenis_generate_setup` | Generate a complete Lenis setup from a description | -| `lenis_cheatsheet` | Required CSS, `data-lenis-prevent` usage, pitfalls table | -| `lenis_search_docs` | Full-text search across all Lenis docs | - -
-7 patterns and 7 recipes - -**Patterns:** `full-page` ยท `next-js` ยท `gsap-integration` ยท `framer-motion-integration` ยท `custom-container` ยท `accessibility` ยท `scroll-to-nav` - -**Recipes:** `scroll-progress-bar` ยท `back-to-top` ยท `horizontal-scroll-section` ยท `scroll-locked-modal` ยท `parallax-layer` ยท `direction-indicator` ยท `gsap-complete` - -
-
- -
-โš›๏ธ React + Next.js - react_* - -| Tool | What it does | -|------|-------------| -| `react_list_patterns` | List all React/Next.js patterns with categories | -| `react_get_pattern` | Full pattern: code, anti-pattern, tips | -| `react_get_constraints` | Hard rules and banned patterns (no `useEffect` for fetching, no Redux, etc.) | -| `react_search_docs` | Search across patterns and rules | - -
- -
-๐Ÿน Echo (Go) - echo_* - -| Tool | What it does | -|------|-------------| -| `echo_list_recipes` | Browse all 19 recipes by category | -| `echo_get_recipe` | Full recipe with complete runnable code | -| `echo_list_middleware` | Browse all 13 middleware with purpose and order guidance | -| `echo_get_middleware` | Full middleware reference with usage and gotchas | -| `echo_decision_matrix` | When to use what - Echo vs stdlib vs alternatives | -| `echo_search_docs` | Full-text search across all recipes and middleware | - -
-19 recipes - -`hello-world` ยท `crud-api` ยท `jwt-auth` ยท `websocket` ยท `sse` ยท `file-upload` ยท `file-download` ยท `graceful-shutdown` ยท `middleware-chain` ยท `cors` ยท `route-groups` ยท `http2` ยท `auto-tls` ยท `reverse-proxy` ยท `streaming-response` ยท `embed-resources` ยท `timeout` ยท `subdomain-routing` ยท `jsonp` - -
-
- -
-๐Ÿน Golang - golang_* - -| Tool | What it does | -|------|-------------| -| `golang_list_practices` | Browse all 18 best practices by topic | -| `golang_get_practice` | Full practice: rule, reason, good/bad code examples | -| `golang_list_patterns` | Browse all 10 design patterns by category | -| `golang_get_pattern` | Full pattern with Go-idiomatic implementation | -| `golang_get_antipatterns` | Common Go mistakes and their fixes | -| `golang_search_docs` | Search across practices and patterns | - -
-Topics and patterns - -**Practice topics:** `fundamentals` ยท `error-handling` ยท `concurrency` ยท `api-server` ยท `database` ยท `config` ยท `logging` ยท `security` ยท `testing` - -**Pattern categories:** `creational` (functional-options) ยท `structural` (adapter, middleware-decorator, consumer-side-interface) ยท `behavioral` (strategy, observer, command) ยท `concurrency` (worker-pool, pipeline, fan-out-fan-in) - -
-
- -
-๐Ÿฆ€ Rust - rust_* - -| Tool | What it does | -|------|-------------| -| `rust_list_practices` | Browse all 18 best practices by topic | -| `rust_get_practice` | Full practice: rule, reason, good/bad examples | -| `rust_search_docs` | Search across all practices | -| `rust_cheatsheet` | Ownership rules, pointer type table, performance tips | - -
- -
-๐ŸŽจ Design Tokens - design_tokens_* - -| Tool | What it does | -|------|-------------| -| `design_tokens_list_categories` | Browse all 10 token categories with descriptions | -| `design_tokens_get_category` | Full CSS + rules + gotchas for a token category | -| `design_tokens_get_color_ramp` | Color ramp reference: stops, oklch values, semantic roles | -| `design_tokens_get_procedure` | Step-by-step token build procedures (8 steps) | -| `design_tokens_get_gotchas` | All gotchas across every category and procedure | -| `design_tokens_generate` | Generate a complete Tailwind v4 token file from a palette | -| `design_tokens_search` | Search across all categories, ramps, and procedures | - -
-10 token categories - -`colors` ยท `spacing` ยท `typography` ยท `component-sizing` ยท `border-radius` ยท `shadows-elevation` ยท `motion` ยท `z-index` ยท `opacity` ยท `grid-layout` - -
-
- -
-๐Ÿ’… UI/UX Principles - ui_ux_* - -| Tool | What it does | -|------|-------------| -| `ui_ux_list_principles` | Browse all principles by domain | -| `ui_ux_get_principle` | Full principle: rule, detail, CSS example, anti-patterns | -| `ui_ux_get_component_pattern` | Component spec: variants, states, sizing rules, CSS | -| `ui_ux_get_checklist` | Pre-ship checklist per domain (typography, color, a11y, motion) | -| `ui_ux_get_gotchas` | All common UI mistakes and their fixes | -| `ui_ux_search` | Search across principles, patterns, and gotchas | - -
-Domains and components - -**Domains:** `typography` ยท `color` ยท `spacing` ยท `elevation` ยท `motion` ยท `accessibility` ยท `responsive` ยท `components` - -**Component patterns:** `button` ยท `card` ยท `badge` ยท `form-input` - -
-
- ---- - -## ๐Ÿ“„ Resources - -| Resource | URI | Description | -|----------|-----|-------------| -| React Flow cheatsheet | `reactflow://cheatsheet` | Quick reference for @xyflow/react v12 | -| Motion cheatsheet | `motion://react/cheatsheet` | Quick reference for motion/react v12 | - ---- - ## ๐Ÿš€ Install -The easiest way to use Hyperstack is via our pre-built Docker image. Docker will automatically download and run the server without any cloning or building required. +The easiest way to use Hyperstack is via our pre-built Docker image. Docker will automatically download and run the server without any manual cloning or installation required. Add the following to your MCP config (`~/.claude.json` or Cursor config): @@ -263,7 +61,7 @@ Add the following to your MCP config (`~/.claude.json` or Cursor config): "--rm", "--memory=256m", "--cpus=0.5", - "ghcr.io/orkait/hyperstack:main" + "superorkait/hyperstack:main" ] } } @@ -274,61 +72,19 @@ Add the following to your MCP config (`~/.claude.json` or Cursor config): --- -### ๐Ÿ“ฆ Local Development (Node directly) - -If you prefer to run it locally without Docker: - -```bash -git clone https://github.com/orkait/hyperstack.git -cd hyperstack -npm install -``` - -```json -{ - "mcpServers": { - "hyperstack": { - "command": "npx", - "args": ["tsx", "/absolute/path/to/hyperstack/src/index.ts"] - } - } -} -``` - ---- - -## ๐Ÿ’ก Why unified? - -Running a separate MCP server per library means one Docker container per server at startup. Two libraries = two containers. Ten libraries = ten containers - every session. - -`hyperstack` runs everything in **one process**. All plugins share the same server, same connection, same container. - -Tool names are namespaced per plugin (`reactflow_list_apis` vs `motion_list_apis`) so there are zero naming conflicts - the LLM always knows which library a tool belongs to. - ---- - -## ๐Ÿ”Œ Adding a Plugin - -1. Create `src/plugins//` with: - - `data.ts` - reference data (keep all code examples in `snippets//` as `.md` files) - - `loader.ts` - `export const snippet = createSnippetLoader("")` - - `tools/.ts` - one file per tool, each exporting `register(server)`; prefix all tool names with `_` - - `index.ts` - export `const Plugin: Plugin = { name: "", register }` - -2. Register in `src/index.ts`: - ```typescript - import { shadcnPlugin } from "./plugins/shadcn/index.js"; - loadPlugins(server, [...existingPlugins, shadcnPlugin]); - ``` - -3. Rebuild and redeploy: - ```bash - npm run build - docker build -t hyperstack . - docker rm -f hyperstack-daemon # next session recreates it - ``` +## ๐Ÿงฉ Plugins -No changes to your MCP config required. +| Plugin | Library / Domain | Tools | What's included | +|--------|-----------------|:-----:|-----------------| +| **reactflow** | [@xyflow/react](https://reactflow.dev) v12 | 8 | 56 APIs, 17 patterns, 3 templates, migration guide | +| **motion** | [Motion for React](https://motion.dev) v12 | 6 | 33 APIs, 14 example categories, transition reference | +| **lenis** | [Lenis](https://lenis.darkroom.engineering) smooth scroll | 6 | API reference, 7 patterns, 7 recipes, CSS rules, GSAP integration | +| **react** | React 19 + Next.js App Router | 4 | RSC patterns, state hierarchy, data fetching, Zustand, composition | +| **echo** | [Echo](https://echo.labstack.com) Go web framework | 6 | 19 recipes, 13 middleware, decision matrix, cheatsheet | +| **golang** | Go best practices + design patterns | 6 | 18 best practices, 10 design patterns, anti-patterns, cheatsheet | +| **rust** | Rust best practices | 4 | 18 practices (good/bad pairs), ownership guide, cheatsheet | +| **design-tokens** | Tailwind v4 + OKLCH token system | 7 | 10 token categories, 8 build procedures, color ramp templates | +| **ui-ux** | UI/UX design principles | 6 | Typography, color, spacing, elevation, motion, a11y, component patterns | --- @@ -359,9 +115,6 @@ src/ โ”‚ โ””โ”€โ”€ snippets/ # 24 .txt files โ””โ”€โ”€ ui-ux/ # UI/UX design principles โ””โ”€โ”€ snippets/ # 25 .txt files - -scripts/ -โ””โ”€โ”€ start-mcp.sh # Single-container Docker wrapper ``` **Plugin interface:** @@ -372,44 +125,32 @@ export interface Plugin { } ``` -Every plugin stores all code examples as `.md` files loaded at runtime: +Every plugin stores all code examples as `.txt` files loaded at runtime: ```typescript // loader.ts export const snippet = createSnippetLoader("golang"); // data.ts -good: snippet("practices/error-wrapping-good.md"), -bad: snippet("practices/error-wrapping-bad.md"), +good: snippet("practices/error-wrapping-good.txt"), +bad: snippet("practices/error-wrapping-bad.txt"), ``` --- ## ๐Ÿ›  Development +To contribute or run locally from source: + ```bash +git clone https://github.com/orkait/hyperstack.git +cd hyperstack npm install npm start # run server using tsx npm run dev # watch mode using tsx ``` -```bash -# Verify all plugins load and tools are registered correctly -npx tsx <<'EOF' -import { reactflowPlugin } from './src/plugins/reactflow/index.js'; -import { motionPlugin } from './src/plugins/motion/index.js'; -import { lenisPlugin } from './src/plugins/lenis/index.js'; -import { golangPlugin } from './src/plugins/golang/index.js'; -const tools = []; -const fake = { tool: (n) => tools.push(n), resource: () => {} }; -[reactflowPlugin, motionPlugin, lenisPlugin, golangPlugin].forEach(p => p.register(fake)); -console.log('Tools registered:', tools.length); -EOF -``` - --- ## ๐Ÿ“„ License MIT ยฉ [Orkait](https://github.com/orkait) -T ยฉ [Orkait](https://github.com/orkait) -ait) From 5bb9b6c29946d89ce2610293bc85a94c90057ca6 Mon Sep 17 00:00:00 2001 From: Kailas Mahavarkar <66670953+KailasMahavarkar@users.noreply.github.com> Date: Thu, 9 Apr 2026 01:27:33 +0530 Subject: [PATCH 12/65] docs: restore Plugins and Tools sections to README --- README.md | 221 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 190 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index bf5372e..2a59920 100755 --- a/README.md +++ b/README.md @@ -32,18 +32,6 @@ --- -## ๐Ÿค AI Skill Included - -This repository includes `SKILL.md` - a Claude Code skill that teaches your AI assistant *when and how* to use these tools. The skill handles judgment and gotchas; the MCP server handles the data. - -To use the skill, clone this repository into your Claude Code skills directory: - -```bash -git clone https://github.com/orkait/hyperstack.git ~/.claude/skills/hyperstack -``` - ---- - ## ๐Ÿš€ Install The easiest way to use Hyperstack is via our pre-built Docker image. Docker will automatically download and run the server without any manual cloning or installation required. @@ -68,7 +56,19 @@ Add the following to your MCP config (`~/.claude.json` or Cursor config): } ``` -*Note: The `--memory=256m` and `--cpus=0.5` flags ensure the server runs with strict resource limits, preventing it from consuming too much RAM or compute.* +*Note: The `--memory=256m` and `--cpus=0.5` flags ensure the server runs with strict resource limits.* + +--- + +## ๐Ÿค AI Skill Included + +This repository includes `SKILL.md` - a Claude Code skill that teaches your AI assistant *when and how* to use these tools. The skill handles judgment and gotchas; the MCP server handles the data. + +To use the skill, clone this repository into your Claude Code skills directory: + +```bash +git clone https://github.com/orkait/hyperstack.git ~/.claude/skills/hyperstack +``` --- @@ -88,6 +88,183 @@ Add the following to your MCP config (`~/.claude.json` or Cursor config): --- +## ๐Ÿ› ๏ธ Tools + +
+โš›๏ธ React Flow - reactflow_* + +| Tool | What it does | +|------|-------------| +| `reactflow_list_apis` | Browse all 56 APIs grouped by kind - components, hooks, utilities, types | +| `reactflow_get_api` | Full reference for any API: props table, usage snippet, examples, tips | +| `reactflow_search_docs` | Full-text search across all docs and code examples | +| `reactflow_get_examples` | Curated code examples by category | +| `reactflow_get_pattern` | Complete enterprise patterns with full implementation code | +| `reactflow_get_template` | Production-ready starters: `custom-node`, `custom-edge`, `zustand-store` | +| `reactflow_get_migration_guide` | v11 โ†’ v12 breaking changes with before/after diffs | +| `reactflow_generate_flow` | Generate a complete flow component from a plain English description | + +
+17 available patterns + +`zustand-store` ยท `undo-redo` ยท `drag-and-drop` ยท `auto-layout-dagre` ยท `auto-layout-elk` ยท `context-menu` ยท `copy-paste` ยท `save-restore` ยท `prevent-cycles` ยท `keyboard-shortcuts` ยท `performance` ยท `dark-mode` ยท `ssr` ยท `subflows` ยท `edge-reconnection` ยท `custom-connection-line` ยท `auto-layout-on-mount` + +
+
+ +
+๐ŸŽฌ Motion for React - motion_* + +| Tool | What it does | +|------|-------------| +| `motion_list_apis` | Browse all 33 APIs grouped by kind - components, hooks, functions | +| `motion_get_api` | Full reference for any API: props table, usage snippet, examples, tips | +| `motion_search_docs` | Full-text search across all docs and code examples | +| `motion_get_examples` | Curated animation examples by category | +| `motion_get_transitions` | Complete transition reference: tween, spring, inertia, orchestration | +| `motion_generate_animation` | Generate a Motion animation snippet from a plain English description | + +
+14 example categories + +`animation` ยท `gestures` ยท `scroll` ยท `layout` ยท `exit` ยท `drag` ยท `hover` ยท `svg` ยท `transitions` ยท `variants` ยท `keyframes` ยท `spring` ยท `reorder` ยท `performance` + +
+
+ +
+๐ŸŒŠ Lenis - lenis_* + +| Tool | What it does | +|------|-------------| +| `lenis_list_apis` | Browse all Lenis APIs - options, methods, events | +| `lenis_get_api` | Full reference for any API with usage snippet | +| `lenis_get_pattern` | Integration patterns: Next.js, GSAP, Framer Motion, custom container | +| `lenis_generate_setup` | Generate a complete Lenis setup from a description | +| `lenis_cheatsheet` | Required CSS, `data-lenis-prevent` usage, pitfalls table | +| `lenis_search_docs` | Full-text search across all Lenis docs | + +
+7 patterns and 7 recipes + +**Patterns:** `full-page` ยท `next-js` ยท `gsap-integration` ยท `framer-motion-integration` ยท `custom-container` ยท `accessibility` ยท `scroll-to-nav` + +**Recipes:** `scroll-progress-bar` ยท `back-to-top` ยท `horizontal-scroll-section` ยท `scroll-locked-modal` ยท `parallax-layer` ยท `direction-indicator` ยท `gsap-complete` + +
+
+ +
+โš›๏ธ React + Next.js - react_* + +| Tool | What it does | +|------|-------------| +| `react_list_patterns` | List all React/Next.js patterns with categories | +| `react_get_pattern` | Full pattern: code, anti-pattern, tips | +| `react_get_constraints` | Hard rules and banned patterns (no `useEffect` for fetching, no Redux, etc.) | +| `react_search_docs` | Search across patterns and rules | + +
+ +
+๐Ÿน Echo (Go) - echo_* + +| Tool | What it does | +|------|-------------| +| `echo_list_recipes` | Browse all 19 recipes by category | +| `echo_get_recipe` | Full recipe with complete runnable code | +| `echo_list_middleware` | Browse all 13 middleware with purpose and order guidance | +| `echo_get_middleware` | Full middleware reference with usage and gotchas | +| `echo_decision_matrix` | When to use what - Echo vs stdlib vs alternatives | +| `echo_search_docs` | Full-text search across all recipes and middleware | + +
+19 recipes + +`hello-world` ยท `crud-api` ยท `jwt-auth` ยท `websocket` ยท `sse` ยท `file-upload` ยท `file-download` ยท `graceful-shutdown` ยท `middleware-chain` ยท `cors` ยท `route-groups` ยท `http2` ยท `auto-tls` ยท `reverse-proxy` ยท `streaming-response` ยท `embed-resources` ยท `timeout` ยท `subdomain-routing` ยท `jsonp` + +
+
+ +
+๐Ÿน Golang - golang_* + +| Tool | What it does | +|------|-------------| +| `golang_list_practices` | Browse all 18 best practices by topic | +| `golang_get_practice` | Full practice: rule, reason, good/bad code examples | +| `golang_list_patterns` | Browse all 10 design patterns by category | +| `golang_get_pattern` | Full pattern with Go-idiomatic implementation | +| `golang_get_antipatterns` | Common Go mistakes and their fixes | +| `golang_search_docs` | Search across practices and patterns | + +
+Topics and patterns + +**Practice topics:** `fundamentals` ยท `error-handling` ยท `concurrency` ยท `api-server` ยท `database` ยท `config` ยท `logging` ยท `security` ยท `testing` + +**Pattern categories:** `creational` (functional-options) ยท `structural` (adapter, middleware-decorator, consumer-side-interface) ยท `behavioral` (strategy, observer, command) ยท `concurrency` (worker-pool, pipeline, fan-out-fan-in) + +
+
+ +
+Cr Rust - rust_* + +| Tool | What it does | +|------|-------------| +| `rust_list_practices` | Browse all 18 best practices by topic | +| `rust_get_practice` | Full practice: rule, reason, good/bad examples | +| `rust_search_docs` | Search across all practices | +| `rust_cheatsheet` | Ownership rules, pointer type table, performance tips | + +
+ +
+๐ŸŽจ Design Tokens - design_tokens_* + +| Tool | What it does | +|------|-------------| +| `design_tokens_list_categories` | Browse all 10 token categories with descriptions | +| `design_tokens_get_category` | Full CSS + rules + gotchas for a token category | +| `design_tokens_get_color_ramp` | Color ramp reference: stops, oklch values, semantic roles | +| `design_tokens_get_procedure` | Step-by-step token build procedures (8 steps) | +| `design_tokens_get_gotchas` | All gotchas across every category and procedure | +| `design_tokens_generate` | Generate a complete Tailwind v4 token file from a palette | +| `design_tokens_search` | Search across all categories, ramps, and procedures | + +
+10 token categories + +`colors` ยท `spacing` ยท `typography` ยท `component-sizing` ยท `border-radius` ยท `shadows-elevation` ยท `motion` ยท `z-index` ยท `opacity` ยท `grid-layout` + +
+
+ +
+๐Ÿ’… UI/UX Principles - ui_ux_* + +| Tool | What it does | +|------|-------------| +| `ui_ux_list_principles` | Browse all principles by domain | +| `ui_ux_get_principle` | Full principle: rule, detail, CSS example, anti-patterns | +| `ui_ux_get_component_pattern` | Component spec: variants, states, sizing rules, CSS | +| `ui_ux_get_checklist` | Pre-ship checklist per domain (typography, color, a11y, motion) | +| `ui_ux_get_gotchas` | All common UI mistakes and their fixes | +| `ui_ux_search` | Search across principles, patterns, and gotchas | + +
+Domains and components + +**Domains:** `typography` ยท `color` ยท `spacing` ยท `elevation` ยท `motion` ยท `accessibility` ยท `responsive` ยท `components` + +**Component patterns:** `button` ยท `card` ยท `badge` ยท `form-input` + +
+
+ +--- + ## ๐Ÿ—๏ธ Architecture ``` @@ -117,24 +294,6 @@ src/ โ””โ”€โ”€ snippets/ # 25 .txt files ``` -**Plugin interface:** -```typescript -export interface Plugin { - name: string; - register: (server: McpServer) => void; -} -``` - -Every plugin stores all code examples as `.txt` files loaded at runtime: -```typescript -// loader.ts -export const snippet = createSnippetLoader("golang"); - -// data.ts -good: snippet("practices/error-wrapping-good.txt"), -bad: snippet("practices/error-wrapping-bad.txt"), -``` - --- ## ๐Ÿ›  Development From f28e2a8eadbf5e288b90c893bce4d1d3b904bbe8 Mon Sep 17 00:00:00 2001 From: Kailas Mahavarkar <66670953+KailasMahavarkar@users.noreply.github.com> Date: Thu, 9 Apr 2026 01:29:25 +0530 Subject: [PATCH 13/65] docs: add Agent-First Install method to README --- README.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2a59920..3ce5c4e 100755 --- a/README.md +++ b/README.md @@ -34,9 +34,16 @@ ## ๐Ÿš€ Install -The easiest way to use Hyperstack is via our pre-built Docker image. Docker will automatically download and run the server without any manual cloning or installation required. +### โšก Agent-First Install (Easiest) +If you are using an AI agent (like Claude Code), simply ask it: +> "Install https://github.com/orkait/hyperstack as an MCP server in my system" -Add the following to your MCP config (`~/.claude.json` or Cursor config): +The agent will read this README, pull the pre-built Docker image, and configure your system automatically. + +--- + +### ๐Ÿณ Docker (Manual) +If you prefer to configure it yourself, add the following to your MCP config (`~/.claude.json` or Cursor config): ```json { From 1276ea9ca26208a31a1544c686cc24463b97a05e Mon Sep 17 00:00:00 2001 From: Kailas Mahavarkar <66670953+KailasMahavarkar@users.noreply.github.com> Date: Thu, 9 Apr 2026 02:09:36 +0530 Subject: [PATCH 14/65] feat: integrate 10 new static engineering skills --- README.md | 10 + SKILL.md | 45 + src/index.ts | 20 + src/plugins/behaviour-analysis/index.ts | 3 + .../behaviour-analysis/snippets/SKILL.txt | 162 ++++ .../snippets/references/heuristics.txt | 114 +++ src/plugins/design-patterns-skill/index.ts | 3 + .../design-patterns-skill/snippets/SKILL.txt | 124 +++ .../snippets/references/misc/overview.txt | 170 ++++ .../patterns/design-architecture.txt | 477 +++++++++++ .../references/patterns/error-handling.txt | 364 ++++++++ .../references/patterns/maintainability.txt | 548 ++++++++++++ .../references/patterns/readability.txt | 195 +++++ .../references/patterns/simplicity.txt | 279 +++++++ .../snippets/references/patterns/testing.txt | 309 +++++++ src/plugins/engineering-discipline/index.ts | 3 + .../engineering-discipline/snippets/SKILL.txt | 157 ++++ .../architecture/architecture-reasoning.txt | 374 +++++++++ .../architecture/negative-doubt.txt | 427 ++++++++++ .../references/architecture/output-format.txt | 49 ++ .../architecture/task-classification.txt | 129 +++ .../architecture/verification-gates.txt | 484 +++++++++++ .../snippets/references/misc/overview.txt | 450 ++++++++++ .../patterns/design-architecture.txt | 477 +++++++++++ .../references/patterns/error-handling.txt | 364 ++++++++ .../references/patterns/maintainability.txt | 548 ++++++++++++ .../references/patterns/readability.txt | 195 +++++ .../references/patterns/simplicity.txt | 279 +++++++ .../snippets/references/patterns/testing.txt | 309 +++++++ src/plugins/excalidraw/index.ts | 3 + src/plugins/excalidraw/snippets/README.txt | 76 ++ src/plugins/excalidraw/snippets/SKILL.txt | 279 +++++++ .../excalidraw/snippets/references/arrows.txt | 288 +++++++ .../excalidraw/snippets/references/colors.txt | 91 ++ .../snippets/references/examples.txt | 381 +++++++++ .../excalidraw/snippets/references/export.txt | 124 +++ .../snippets/references/json-format.txt | 210 +++++ .../snippets/references/validation.txt | 182 ++++ src/plugins/frame-animator/index.ts | 3 + src/plugins/frame-animator/snippets/SKILL.txt | 163 ++++ .../snippets/references/principles.txt | 189 +++++ src/plugins/golang-design-pattern/index.ts | 3 + .../golang-design-pattern/snippets/SKILL.txt | 141 ++++ .../references/examples/anti-patterns.txt | 77 ++ .../examples/concurrency-error-testing.txt | 148 ++++ .../examples/creational-patterns.txt | 60 ++ .../structural-behavioral-patterns.txt | 105 +++ .../snippets/references/misc/overview.txt | 122 +++ .../references/patterns/anti-patterns.txt | 775 +++++++++++++++++ .../patterns/full-patterns-guide.txt | 779 ++++++++++++++++++ .../snippets/scripts/detect-antipatterns.sh | 101 +++ src/plugins/pinchtab/index.ts | 3 + src/plugins/pinchtab/snippets/SKILL.txt | 494 +++++++++++ src/plugins/pinchtab/snippets/TRUST.txt | 83 ++ .../references/agent-optimization.txt | 174 ++++ .../pinchtab/snippets/references/api.txt | 426 ++++++++++ .../pinchtab/snippets/references/commands.txt | 206 +++++ .../pinchtab/snippets/references/env.txt | 36 + .../pinchtab/snippets/references/profiles.txt | 111 +++ src/plugins/react-pro-coder/index.ts | 3 + .../react-pro-coder/snippets/SKILL.txt | 169 ++++ .../snippets/examples/audit.example.txt | 7 + .../snippets/examples/bugfix.example.txt | 7 + .../snippets/examples/new-feature.example.txt | 12 + .../snippets/examples/refactor.example.txt | 7 + .../detect-variant-mapping-pattern.txt | 49 ++ .../templates/audit-report.template.txt | 34 + .../templates/component.test.template.tsx | 10 + .../snippets/templates/component.tsx.template | 22 + .../templates/component.types.template.ts | 7 + .../snippets/templates/lib.cn.template.ts | 4 + .../snippets/templates/page.tsx.template | 21 + .../templates/seo-checklist.template.txt | 12 + .../snippets/templates/test-suite.template.ts | 7 + .../variant-mapping-detection.template.txt | 19 + .../snippets/utils/intent-map.txt | 8 + src/plugins/readme-writer/index.ts | 3 + src/plugins/readme-writer/snippets/SKILL.txt | 335 ++++++++ .../snippets/assets/README.template.txt | 79 ++ .../references/readme-anti-patterns.txt | 117 +++ .../snippets/references/readme-rubric.txt | 59 ++ src/plugins/security-review/index.ts | 3 + src/plugins/security-review/snippets/LICENSE | 22 + .../security-review/snippets/SKILL.txt | 312 +++++++ .../snippets/infrastructure/docker.txt | 432 ++++++++++ .../snippets/languages/javascript.txt | 388 +++++++++ .../snippets/languages/python.txt | 363 ++++++++ .../snippets/references/api-security.txt | 519 ++++++++++++ .../snippets/references/authentication.txt | 353 ++++++++ .../snippets/references/authorization.txt | 372 +++++++++ .../snippets/references/business-logic.txt | 443 ++++++++++ .../snippets/references/cryptography.txt | 329 ++++++++ .../snippets/references/csrf.txt | 398 +++++++++ .../snippets/references/data-protection.txt | 378 +++++++++ .../snippets/references/deserialization.txt | 410 +++++++++ .../snippets/references/error-handling.txt | 436 ++++++++++ .../snippets/references/file-security.txt | 457 ++++++++++ .../snippets/references/injection.txt | 259 ++++++ .../snippets/references/logging.txt | 433 ++++++++++ .../snippets/references/misconfiguration.txt | 435 ++++++++++ .../snippets/references/modern-threats.txt | 475 +++++++++++ .../snippets/references/ssrf.txt | 415 ++++++++++ .../snippets/references/supply-chain.txt | 405 +++++++++ .../snippets/references/xss.txt | 336 ++++++++ src/shared/static-skill.ts | 63 ++ 105 files changed, 22328 insertions(+) create mode 100644 src/plugins/behaviour-analysis/index.ts create mode 100755 src/plugins/behaviour-analysis/snippets/SKILL.txt create mode 100755 src/plugins/behaviour-analysis/snippets/references/heuristics.txt create mode 100644 src/plugins/design-patterns-skill/index.ts create mode 100755 src/plugins/design-patterns-skill/snippets/SKILL.txt create mode 100755 src/plugins/design-patterns-skill/snippets/references/misc/overview.txt create mode 100755 src/plugins/design-patterns-skill/snippets/references/patterns/design-architecture.txt create mode 100755 src/plugins/design-patterns-skill/snippets/references/patterns/error-handling.txt create mode 100755 src/plugins/design-patterns-skill/snippets/references/patterns/maintainability.txt create mode 100755 src/plugins/design-patterns-skill/snippets/references/patterns/readability.txt create mode 100755 src/plugins/design-patterns-skill/snippets/references/patterns/simplicity.txt create mode 100755 src/plugins/design-patterns-skill/snippets/references/patterns/testing.txt create mode 100644 src/plugins/engineering-discipline/index.ts create mode 100755 src/plugins/engineering-discipline/snippets/SKILL.txt create mode 100755 src/plugins/engineering-discipline/snippets/references/architecture/architecture-reasoning.txt create mode 100755 src/plugins/engineering-discipline/snippets/references/architecture/negative-doubt.txt create mode 100755 src/plugins/engineering-discipline/snippets/references/architecture/output-format.txt create mode 100755 src/plugins/engineering-discipline/snippets/references/architecture/task-classification.txt create mode 100755 src/plugins/engineering-discipline/snippets/references/architecture/verification-gates.txt create mode 100755 src/plugins/engineering-discipline/snippets/references/misc/overview.txt create mode 100755 src/plugins/engineering-discipline/snippets/references/patterns/design-architecture.txt create mode 100755 src/plugins/engineering-discipline/snippets/references/patterns/error-handling.txt create mode 100755 src/plugins/engineering-discipline/snippets/references/patterns/maintainability.txt create mode 100755 src/plugins/engineering-discipline/snippets/references/patterns/readability.txt create mode 100755 src/plugins/engineering-discipline/snippets/references/patterns/simplicity.txt create mode 100755 src/plugins/engineering-discipline/snippets/references/patterns/testing.txt create mode 100644 src/plugins/excalidraw/index.ts create mode 100755 src/plugins/excalidraw/snippets/README.txt create mode 100755 src/plugins/excalidraw/snippets/SKILL.txt create mode 100755 src/plugins/excalidraw/snippets/references/arrows.txt create mode 100755 src/plugins/excalidraw/snippets/references/colors.txt create mode 100755 src/plugins/excalidraw/snippets/references/examples.txt create mode 100755 src/plugins/excalidraw/snippets/references/export.txt create mode 100755 src/plugins/excalidraw/snippets/references/json-format.txt create mode 100755 src/plugins/excalidraw/snippets/references/validation.txt create mode 100644 src/plugins/frame-animator/index.ts create mode 100755 src/plugins/frame-animator/snippets/SKILL.txt create mode 100755 src/plugins/frame-animator/snippets/references/principles.txt create mode 100644 src/plugins/golang-design-pattern/index.ts create mode 100755 src/plugins/golang-design-pattern/snippets/SKILL.txt create mode 100755 src/plugins/golang-design-pattern/snippets/references/examples/anti-patterns.txt create mode 100755 src/plugins/golang-design-pattern/snippets/references/examples/concurrency-error-testing.txt create mode 100755 src/plugins/golang-design-pattern/snippets/references/examples/creational-patterns.txt create mode 100755 src/plugins/golang-design-pattern/snippets/references/examples/structural-behavioral-patterns.txt create mode 100755 src/plugins/golang-design-pattern/snippets/references/misc/overview.txt create mode 100755 src/plugins/golang-design-pattern/snippets/references/patterns/anti-patterns.txt create mode 100755 src/plugins/golang-design-pattern/snippets/references/patterns/full-patterns-guide.txt create mode 100755 src/plugins/golang-design-pattern/snippets/scripts/detect-antipatterns.sh create mode 100644 src/plugins/pinchtab/index.ts create mode 100644 src/plugins/pinchtab/snippets/SKILL.txt create mode 100644 src/plugins/pinchtab/snippets/TRUST.txt create mode 100644 src/plugins/pinchtab/snippets/references/agent-optimization.txt create mode 100644 src/plugins/pinchtab/snippets/references/api.txt create mode 100644 src/plugins/pinchtab/snippets/references/commands.txt create mode 100644 src/plugins/pinchtab/snippets/references/env.txt create mode 100644 src/plugins/pinchtab/snippets/references/profiles.txt create mode 100644 src/plugins/react-pro-coder/index.ts create mode 100755 src/plugins/react-pro-coder/snippets/SKILL.txt create mode 100755 src/plugins/react-pro-coder/snippets/examples/audit.example.txt create mode 100755 src/plugins/react-pro-coder/snippets/examples/bugfix.example.txt create mode 100755 src/plugins/react-pro-coder/snippets/examples/new-feature.example.txt create mode 100755 src/plugins/react-pro-coder/snippets/examples/refactor.example.txt create mode 100755 src/plugins/react-pro-coder/snippets/patterns/detect-variant-mapping-pattern.txt create mode 100755 src/plugins/react-pro-coder/snippets/templates/audit-report.template.txt create mode 100755 src/plugins/react-pro-coder/snippets/templates/component.test.template.tsx create mode 100755 src/plugins/react-pro-coder/snippets/templates/component.tsx.template create mode 100755 src/plugins/react-pro-coder/snippets/templates/component.types.template.ts create mode 100755 src/plugins/react-pro-coder/snippets/templates/lib.cn.template.ts create mode 100755 src/plugins/react-pro-coder/snippets/templates/page.tsx.template create mode 100755 src/plugins/react-pro-coder/snippets/templates/seo-checklist.template.txt create mode 100755 src/plugins/react-pro-coder/snippets/templates/test-suite.template.ts create mode 100755 src/plugins/react-pro-coder/snippets/templates/variant-mapping-detection.template.txt create mode 100755 src/plugins/react-pro-coder/snippets/utils/intent-map.txt create mode 100644 src/plugins/readme-writer/index.ts create mode 100755 src/plugins/readme-writer/snippets/SKILL.txt create mode 100755 src/plugins/readme-writer/snippets/assets/README.template.txt create mode 100755 src/plugins/readme-writer/snippets/references/readme-anti-patterns.txt create mode 100755 src/plugins/readme-writer/snippets/references/readme-rubric.txt create mode 100644 src/plugins/security-review/index.ts create mode 100755 src/plugins/security-review/snippets/LICENSE create mode 100755 src/plugins/security-review/snippets/SKILL.txt create mode 100755 src/plugins/security-review/snippets/infrastructure/docker.txt create mode 100755 src/plugins/security-review/snippets/languages/javascript.txt create mode 100755 src/plugins/security-review/snippets/languages/python.txt create mode 100755 src/plugins/security-review/snippets/references/api-security.txt create mode 100755 src/plugins/security-review/snippets/references/authentication.txt create mode 100755 src/plugins/security-review/snippets/references/authorization.txt create mode 100755 src/plugins/security-review/snippets/references/business-logic.txt create mode 100755 src/plugins/security-review/snippets/references/cryptography.txt create mode 100755 src/plugins/security-review/snippets/references/csrf.txt create mode 100755 src/plugins/security-review/snippets/references/data-protection.txt create mode 100755 src/plugins/security-review/snippets/references/deserialization.txt create mode 100755 src/plugins/security-review/snippets/references/error-handling.txt create mode 100755 src/plugins/security-review/snippets/references/file-security.txt create mode 100755 src/plugins/security-review/snippets/references/injection.txt create mode 100755 src/plugins/security-review/snippets/references/logging.txt create mode 100755 src/plugins/security-review/snippets/references/misconfiguration.txt create mode 100755 src/plugins/security-review/snippets/references/modern-threats.txt create mode 100755 src/plugins/security-review/snippets/references/ssrf.txt create mode 100755 src/plugins/security-review/snippets/references/supply-chain.txt create mode 100755 src/plugins/security-review/snippets/references/xss.txt create mode 100644 src/shared/static-skill.ts diff --git a/README.md b/README.md index 3ce5c4e..abf9dbd 100755 --- a/README.md +++ b/README.md @@ -92,6 +92,16 @@ git clone https://github.com/orkait/hyperstack.git ~/.claude/skills/hyperstack | **rust** | Rust best practices | 4 | 18 practices (good/bad pairs), ownership guide, cheatsheet | | **design-tokens** | Tailwind v4 + OKLCH token system | 7 | 10 token categories, 8 build procedures, color ramp templates | | **ui-ux** | UI/UX design principles | 6 | Typography, color, spacing, elevation, motion, a11y, component patterns | +| **behaviour-analysis** | Interaction & State Audits | 2 | Heuristics, state inventory, edge case sweeps | +| **design-patterns-skill** | Core Programming Principles | 2 | Clean Code, Pragmatic Programmer patterns | +| **engineering-discipline** | Senior SDE-3 Framework | 2 | Architecture reasoning, verification gates | +| **excalidraw** | Architecture Diagrams | 2 | Automated diagram generation guidelines | +| **frame-animator** | Character Animation | 2 | Frame-based tick animation and expressions | +| **golang-design-pattern** | Go Design Patterns | 2 | Patterns adapted for Go's philosophy | +| **pinchtab** | Browser Automation | 2 | Web scraping and browser testing guidelines | +| **react-pro-coder** | Senior React/Next.js SDE | 2 | RSC-first constraints, core web vitals | +| **readme-writer** | Project Documentation | 2 | Evidence-based README generation | +| **security-review** | Security Audits | 2 | OWASP review, vulnerability checklists | --- diff --git a/SKILL.md b/SKILL.md index 133e646..8be2e91 100755 --- a/SKILL.md +++ b/SKILL.md @@ -62,6 +62,28 @@ triggers: - typography scale - color contrast - wcag + - behaviour analysis + - state audit + - nielsen heuristics + - design patterns + - clean code + - engineering discipline + - code review + - architecture diagram + - excalidraw + - frame animation + - golang patterns + - pinchtab automation + - web scraping + - browser testing + - react pro + - rsc constraints + - core web vitals + - readme generation + - project documentation + - security review + - owasp + - vulnerability check activation: mode: fuzzy priority: high @@ -447,3 +469,26 @@ Elevation: 5 levels distinguished by bg-color not just borders; dark mode uses l Motion: exits faster than entrances (subtract 50-100ms); ease-out entering, ease-in exiting, ease-in-out repositioning; never linear; details via ui_ux_get_principle duration-rules Pre-ship: run ui_ux_get_checklist for each domain before shipping a new component or page + +--- + +## Additional Engineering Skills + +Hyperstack also bundles 10 specialized engineering skills that do not rely on standard API references but instead provide comprehensive guidelines, checklists, and principles. + +For each of these skills, you can: +1. List all available documents: `[skill_name]_list_docs()` +2. Get the content of a specific document: `[skill_name]_get_doc({ path: "..." })` (Always start by reading `SKILL.txt`) + +| Skill Prefix | Focus Area | +|--------------|------------| +| `behaviour_analysis` | UI/UX state audits, Nielsen heuristics | +| `design_patterns_skill` | Clean Code, Pragmatic Programmer concepts | +| `engineering_discipline`| Architecture reasoning, verification gates | +| `excalidraw` | Automated architecture diagram generation | +| `frame_animator` | Frame-based tick animation & expressions | +| `golang_design_pattern`| Go-specific implementations of design patterns | +| `pinchtab` | Browser automation, scraping, web testing | +| `react_pro_coder` | Senior Next.js/React constraints, RSC rules | +| `readme_writer` | Evidence-based README generation | +| `security_review` | OWASP audits, vulnerability checklists | diff --git a/src/index.ts b/src/index.ts index e9e7a4e..bfb7606 100755 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,16 @@ import { golangPlugin } from "./plugins/golang/index.js"; import { rustPlugin } from "./plugins/rust/index.js"; import { designTokensPlugin } from "./plugins/design-tokens/index.js"; import { uiUxPlugin } from "./plugins/ui-ux/index.js"; +import { behaviourAnalysisPlugin } from "./plugins/behaviour-analysis/index.js"; +import { designPatternsSkillPlugin } from "./plugins/design-patterns-skill/index.js"; +import { engineeringDisciplinePlugin } from "./plugins/engineering-discipline/index.js"; +import { excalidrawPlugin } from "./plugins/excalidraw/index.js"; +import { frameAnimatorPlugin } from "./plugins/frame-animator/index.js"; +import { golangDesignPatternPlugin } from "./plugins/golang-design-pattern/index.js"; +import { pinchtabPlugin } from "./plugins/pinchtab/index.js"; +import { reactProCoderPlugin } from "./plugins/react-pro-coder/index.js"; +import { readmeWriterPlugin } from "./plugins/readme-writer/index.js"; +import { securityReviewPlugin } from "./plugins/security-review/index.js"; const server = new McpServer({ name: "hyperstack", @@ -28,6 +38,16 @@ loadPlugins(server, [ rustPlugin, designTokensPlugin, uiUxPlugin, + behaviourAnalysisPlugin, + designPatternsSkillPlugin, + engineeringDisciplinePlugin, + excalidrawPlugin, + frameAnimatorPlugin, + golangDesignPatternPlugin, + pinchtabPlugin, + reactProCoderPlugin, + readmeWriterPlugin, + securityReviewPlugin, ]); async function main() { diff --git a/src/plugins/behaviour-analysis/index.ts b/src/plugins/behaviour-analysis/index.ts new file mode 100644 index 0000000..4af17cd --- /dev/null +++ b/src/plugins/behaviour-analysis/index.ts @@ -0,0 +1,3 @@ +import { createStaticSkillPlugin } from "../../shared/static-skill.js"; + +export const behaviourAnalysisPlugin = createStaticSkillPlugin("behaviour-analysis", "The behaviour-analysis skill."); diff --git a/src/plugins/behaviour-analysis/snippets/SKILL.txt b/src/plugins/behaviour-analysis/snippets/SKILL.txt new file mode 100755 index 0000000..e376a30 --- /dev/null +++ b/src/plugins/behaviour-analysis/snippets/SKILL.txt @@ -0,0 +1,162 @@ +--- +name: behaviour-analysis +description: Systematic UI/UX behaviour analysis for interactive applications. Audits every user action, state transition, view mode, and edge case like an experienced QA + UX engineer. Produces a complete interaction matrix with expected vs actual behaviour, finds inconsistencies, dead states, and missing feedback. Use when reviewing UI behaviour, before shipping features, or when something "feels off" but you can't pinpoint why. +compatibility: Requires Read, Grep, Glob, WebSearch tools. Works with any frontend codebase. +metadata: + author: kai + version: "1.0" +--- + +# Behaviour Analysis + +Systematic interaction audit combining UX heuristics, QA state-machine thinking, and developer code-reading. + +## When to Use + +- After implementing a feature with multiple interaction modes +- When the user reports something "doesn't feel right" or "is inconsistent" +- Before shipping โ€” final behavioural review +- When adding a new view mode, action, or state to an existing system + +## Process + +### Phase 1: Inventory (read code, build the map) + +Before judging anything, build a complete picture: + +1. **Identify all state variables** that affect UI behaviour + - Read the store/state management files + - List every piece of state: data, config, transient UI state + - Note which are persisted vs ephemeral + +2. **Identify all user actions** that modify state + - Buttons, clicks, drags, keyboard shortcuts, sliders, toggles + - API calls triggered by actions + - Implicit actions (hover, scroll, resize, mode switch) + +3. **Identify all view modes / display states** + - Tabs, toggles, conditional rendering branches + - How different modes compose (layout mode x view mode x highlight state) + +4. **Identify all feedback mechanisms** + - Visual feedback (highlighting, dimming, borders, badges, glow) + - Textual feedback (labels, counts, status text) + - Animated feedback (transitions, physics, spring effects) + - Absence of feedback (silent failures, no-ops) + +Output: A **state inventory table** and an **action inventory table**. + +### Phase 2: Interaction Matrix (the core analysis) + +Build a matrix: **every action x every relevant state combination**. + +For each cell ask: +- **What should happen?** (expected behaviour โ€” think like a UX designer) +- **What does happen?** (actual behaviour โ€” read the code path) +- **Match?** OK / BUG / UX-ISSUE / MISSING-FEEDBACK + +Structure the matrix by category: + +```markdown +| # | Action | Context/State | Expected | Actual | Status | +|---|--------|---------------|----------|--------|--------| +``` + +Categories to cover: +- **CRUD actions** (create, read, update, delete of primary data) +- **Selection & highlighting** (what gets selected, how, clear) +- **View mode transitions** (switching between modes) +- **Layout mode transitions** (switching layout engines) +- **Configuration changes** (sliders, toggles, settings) +- **Drag & interaction** (drag, hover, click targets) +- **Reset & cleanup** (what gets cleared, what persists) +- **Edge cases** (empty state, max state, conflicting states) + +### Phase 3: Heuristic Audit + +Apply Nielsen's 10 heuristics (adapted for interactive visualizations): + +1. **Visibility of system status** โ€” Does the UI show what's active, selected, loading? +2. **Match between system and real world** โ€” Do labels make sense? Are actions named clearly? +3. **User control and freedom** โ€” Can the user undo/escape from any state? Is there always a way back? +4. **Consistency and standards** โ€” Do similar actions behave the same way everywhere? +5. **Error prevention** โ€” Can the user reach a broken/dead state? +6. **Recognition rather than recall** โ€” Is the current mode/state visible without memorizing? +7. **Flexibility and efficiency** โ€” Are there shortcuts for power users? +8. **Aesthetic and minimalist design** โ€” Is information presented at the right density? +9. **Help users recover from errors** โ€” What happens on API failure, empty results, bad input? +10. **Accessibility** โ€” Keyboard navigation, screen reader, reduced motion? + +Refer to [references/heuristics.md](references/heuristics.md) for detailed questions per heuristic. + +### Phase 4: Edge Case Sweep + +Systematically check: + +**Empty states:** +- No data loaded +- No results +- No highlights active +- Empty search filter results + +**Boundary states:** +- Maximum data (100+ nodes) +- Single node, no edges +- All nodes highlighted +- All sliders at min/max + +**Transition states:** +- Mode switch with active highlights +- Mode switch mid-drag +- Query execution while loading +- Rapid repeated actions (double-click, spam slider) + +**Composition states:** +- Every view mode x every layout mode +- Highlight + search filter active simultaneously +- Collapsed groups + highlighting + path results + +### Phase 5: Report + +Output a structured report: + +```markdown +## State Inventory +[table of all state variables] + +## Action ร— State Matrix +[full interaction matrix with status] + +## Heuristic Findings +[issues grouped by heuristic, with severity] + +## Edge Cases +[bugs and UX issues found] + +## Verdict +[summary: how many behaviours tested, how many correct, critical issues] +``` + +Severity levels: +- **CRITICAL** โ€” broken functionality, data loss, unreachable state +- **HIGH** โ€” major UX inconsistency, confusing behaviour +- **MEDIUM** โ€” minor inconsistency, missing feedback +- **LOW** โ€” cosmetic, nice-to-have + +## Research Enhancement + +Before starting the analysis, search for: +- Current best practices for the specific UI pattern being analyzed (graph viz, form, dashboard, etc.) +- Known UX patterns for the interaction model (drag-and-drop, force-directed graphs, etc.) +- Accessibility guidelines for the specific component type + +Use findings to set expectations in the matrix โ€” "expected behaviour" should be informed by industry standards, not just gut feeling. + +## Key Principles + +- **Think like a user first** โ€” what would someone expect when they click this? +- **Think like QA second** โ€” what's the worst thing that could happen? +- **Think like a developer third** โ€” read the code to verify, don't assume +- **Every action must have visible feedback** โ€” if clicking something does nothing visibly, that's a bug +- **Every state must be escapable** โ€” the user should never be "stuck" +- **Composition must be tested** โ€” features that work alone often break in combination diff --git a/src/plugins/behaviour-analysis/snippets/references/heuristics.txt b/src/plugins/behaviour-analysis/snippets/references/heuristics.txt new file mode 100755 index 0000000..9d846e0 --- /dev/null +++ b/src/plugins/behaviour-analysis/snippets/references/heuristics.txt @@ -0,0 +1,114 @@ +# Heuristic Evaluation Questions + +Detailed questions per Nielsen's heuristic, adapted for interactive data visualizations and modern web apps. + +## 1. Visibility of System Status + +- Is the current view mode clearly indicated (active tab, highlight, selected state)? +- Is there a loading indicator when async operations run? +- Does the active/selected result show a distinct visual treatment? +- When a filter is active, is it obvious that results are filtered? +- Do sliders/toggles show their current value? +- After dragging a node, is the settled state visually clear? +- Is the current layout mode (dagre/cluster) clearly indicated? + +## 2. Match Between System and Real World + +- Do button labels describe what they DO, not what they ARE? ("Clear" not "X") +- Are view mode names intuitive? ("Live" vs "Results" vs "Highlight" โ€” does a new user understand these?) +- Do edge/node labels match the domain vocabulary? +- Are slider labels clear about what they control? + +## 3. User Control and Freedom + +- Can every highlight be cleared? +- Can every mode switch be reversed? +- Can collapsed groups be re-expanded? +- Can the user undo the last action? +- Is there a "reset to defaults" for settings? +- Can the user escape from every state back to a clean view? + +## 4. Consistency and Standards + +- Do all "clear" actions clear the same scope of state? +- Do all click targets have hover states? +- Are all buttons the same size/style for the same level of importance? +- Does clicking behave the same way on result cards, nodes, edges? +- Do both layout modes support the same view modes identically? +- Are keyboard shortcuts consistent with platform conventions? + +## 5. Error Prevention + +- Can slider values be set to break the layout? +- Can the user reach a state where no nodes are visible and there's no indication why? +- Can rapid clicking cause race conditions? +- Does the UI prevent invalid state combinations? +- Are destructive actions (reset, clear all) confirmed or easily undoable? + +## 6. Recognition Rather Than Recall + +- Is the current state visible at all times (not hidden in a menu)? +- Can the user see which result is highlighted without scrolling the results panel? +- Are collapsed group contents summarized (count, kind)? +- Is the search filter text always visible when active? + +## 7. Flexibility and Efficiency + +- Can power users access functions via keyboard? +- Are there shortcuts for common workflows (run + highlight)? +- Can the user adjust layout parameters without opening a dialog? +- Is the most common action the easiest to perform? + +## 8. Aesthetic and Minimalist Design + +- Are controls only shown when relevant? (dagre sliders hidden in cluster mode) +- Is information density appropriate โ€” not too sparse, not overwhelming? +- Are animations purposeful (communicate state change) or decorative (just pretty)? +- Do hover/highlight effects add information or just noise? + +## 9. Help Users Recover from Errors + +- What happens when the API is unreachable? +- What happens when a query returns an error? +- What happens when the graph data is malformed? +- Are error messages actionable ("server unreachable โ€” is it running?")? +- Can the user retry failed operations? + +## 10. Accessibility + +- Can all interactive elements be reached via keyboard (Tab)? +- Do interactive elements have focus indicators? +- Is there sufficient color contrast for all states? +- Do animations respect `prefers-reduced-motion`? +- Are drag interactions achievable without a mouse? +- Do screen readers announce state changes (highlights, mode switches)? +- Are ARIA labels present on non-text interactive elements? + +## Visualization-Specific Heuristics + +Beyond Nielsen's 10, for data visualizations check: + +### Data-Ink Ratio +- Is every visual element carrying information? +- Can any decoration be removed without losing meaning? + +### Gestalt Principles +- Are related nodes visually grouped (proximity, color, enclosure)? +- Do edges clearly connect their endpoints? +- Is the visual hierarchy clear (important nodes larger/brighter)? + +### Interaction Affordance +- Do draggable things look draggable (cursor change)? +- Do clickable things look clickable (hover effect)? +- Are non-interactive elements clearly non-interactive? + +### State Feedback Latency +- Is feedback immediate (<100ms) for direct manipulation (drag)? +- Is feedback fast (<300ms) for triggered actions (click to highlight)? +- Are long operations (layout compute) shown with progress indication? + +## Sources + +- [Nielsen Norman Group: 10 Usability Heuristics](https://www.nngroup.com/articles/ten-usability-heuristics/) +- [Maze: How to Conduct a Heuristic Evaluation](https://maze.co/guides/usability-testing/heuristic-evaluation/) +- [Adam Fard: Heuristic Evaluation Guide](https://adamfard.com/blog/heuristic-evaluation) diff --git a/src/plugins/design-patterns-skill/index.ts b/src/plugins/design-patterns-skill/index.ts new file mode 100644 index 0000000..def9c39 --- /dev/null +++ b/src/plugins/design-patterns-skill/index.ts @@ -0,0 +1,3 @@ +import { createStaticSkillPlugin } from "../../shared/static-skill.js"; + +export const designPatternsSkillPlugin = createStaticSkillPlugin("design-patterns-skill", "The design-patterns-skill skill."); diff --git a/src/plugins/design-patterns-skill/snippets/SKILL.txt b/src/plugins/design-patterns-skill/snippets/SKILL.txt new file mode 100755 index 0000000..586e78f --- /dev/null +++ b/src/plugins/design-patterns-skill/snippets/SKILL.txt @@ -0,0 +1,124 @@ +--- +name: design-patterns-skill +description: Apply core programming principles and design patterns from Clean Code, The Pragmatic Programmer, Code Complete, Refactoring, and Design Patterns. Use when writing code, reviewing PRs, refactoring, or designing system architecture. +triggers: + - "code review" + - "design pattern" + - "refactor" + - "clean code" + - "SOLID" + - "code quality" + - "architecture design" + - "code generation" +activation: + mode: fuzzy + priority: normal + triggers: + - "code review" + - "design pattern" + - "refactor" + - "clean code" + - "SOLID" + - "code quality" + - "architecture design" + - "code generation" +compatibility: ">=1.0.0" +metadata: + version: "1.0.0" +references: + - references/patterns/readability.md + - references/patterns/simplicity.md + - references/patterns/design-architecture.md + - references/patterns/testing.md + - references/patterns/error-handling.md + - references/patterns/maintainability.md +--- + +# Design Patterns & Programming Principles + +## Overview + +Structured guidance on programming principles and design patterns from foundational software engineering books. Ensures code follows industry-standard practices for readability, maintainability, simplicity, and architectural soundness. + +## When to Apply + +- **Code Generation:** Writing new functions, classes, or modules +- **Code Review:** Evaluating pull requests or existing codebases +- **Refactoring:** Improving code structure and clarity +- **Architecture Design:** Choosing appropriate patterns and abstractions + +--- + +## Core Philosophy + +1. **Readability over cleverness** โ€” Code is read more than written +2. **Simplicity over complexity** โ€” Use the simplest solution that works +3. **Testability by design** โ€” Write code that's easy to test +4. **Incremental improvement** โ€” Leave code better than you found it +5. **Patterns as tools** โ€” Apply patterns when they clarify, not by default + +--- + +## Principle Categories + +### 1. Readability & Clarity +- Descriptive naming, consistent formatting, self-documenting code, small focused functions +- **Reference:** `references/patterns/readability.md` + +### 2. Simplicity & Efficiency +- KISS, DRY, YAGNI +- **Reference:** `references/patterns/simplicity.md` + +### 3. Design & Architecture +- SRP, composition over inheritance, program to interfaces +- Patterns: Factory, Strategy, Observer, Decorator, Adapter, Command, Singleton +- **Reference:** `references/patterns/design-architecture.md` + +### 4. Testing & Quality +- Automated testing, focused assertions, edge case coverage +- **Reference:** `references/patterns/testing.md` + +### 5. Error Handling +- Clear error messages, early validation, proper exception usage +- **Reference:** `references/patterns/error-handling.md` + +### 6. Maintainability +- Boy Scout Rule, continuous refactoring, atomic commits, automation +- **Reference:** `references/patterns/maintainability.md` + +--- + +## AI-Specific Guidance + +When generating or reviewing code, always: +1. Check for AI pitfalls listed in each principle +2. Avoid pattern prediction bias โ€” don't use patterns just because they're common +3. Question generic naming โ€” resist `data`, `temp`, `result` without context +4. Validate edge cases โ€” don't skip error handling +5. Keep functions focused โ€” resist combining unrelated operations +6. Match project conventions โ€” maintain consistency with existing codebase + +--- + +## Quick Reference + +| Situation | Apply | +|-----------|-------| +| Function > 20 lines | Split into smaller functions (SRP) | +| Repeated code blocks | Extract to function/constant (DRY) | +| Complex conditionals | Strategy or State pattern | +| Object creation logic | Factory pattern | +| Cross-cutting concerns | Decorator or Observer pattern | +| Incompatible interfaces | Adapter pattern | +| Need undo/logging | Command pattern | +| Global access point | Singleton (use sparingly) | + +--- + +## Sources + +- *Clean Code* โ€” Robert C. Martin +- *The Pragmatic Programmer* โ€” Andrew Hunt & David Thomas +- *Code Complete* โ€” Steve McConnell +- *Refactoring* โ€” Martin Fowler +- *Design Patterns* โ€” Gang of Four diff --git a/src/plugins/design-patterns-skill/snippets/references/misc/overview.txt b/src/plugins/design-patterns-skill/snippets/references/misc/overview.txt new file mode 100755 index 0000000..4bf7af1 --- /dev/null +++ b/src/plugins/design-patterns-skill/snippets/references/misc/overview.txt @@ -0,0 +1,170 @@ +# Design Patterns & Programming Principles Skill + +A comprehensive Kiro skill that provides structured guidance on programming principles and design patterns from foundational software engineering books. + +## Overview + +This skill encapsulates best practices from: +- *Clean Code* by Robert C. Martin +- *The Pragmatic Programmer* by Andrew Hunt & David Thomas +- *Code Complete* by Steve McConnell +- *Refactoring* by Martin Fowler +- *Design Patterns* by Gang of Four + +## Installation + +### For Workspace (Project-Specific) +```bash +mkdir -p .kiro/skills +cp -r design-patterns .kiro/skills/ +``` + +### For Global (All Projects) +```bash +mkdir -p ~/.kiro/skills +cp -r design-patterns ~/.kiro/skills/ +``` + +## Structure + +``` +design-patterns/ +โ”œโ”€โ”€ SKILL.md # Main skill definition +โ”œโ”€โ”€ README.md # This file +โ””โ”€โ”€ references/ + โ”œโ”€โ”€ readability.md # Naming, formatting, documentation + โ”œโ”€โ”€ simplicity.md # KISS, DRY, YAGNI principles + โ”œโ”€โ”€ design-architecture.md # SRP, patterns, composition + โ”œโ”€โ”€ testing.md # Testing strategies and best practices + โ”œโ”€โ”€ error-handling.md # Validation, exceptions, recovery + โ””โ”€โ”€ maintainability.md # Refactoring, commits, automation +``` + +## Usage + +The skill activates automatically when: +- Writing new code +- Reviewing pull requests +- Refactoring existing code +- Designing system architecture +- Assisting with AI code generation + +## Principle Categories + +### 1. Readability & Clarity +- Descriptive naming conventions +- Consistent code formatting +- Self-documenting code principles +- Small, focused functions + +### 2. Simplicity & Efficiency +- KISS (Keep It Simple, Stupid) +- DRY (Don't Repeat Yourself) +- YAGNI (You Aren't Gonna Need It) +- Avoiding premature optimization + +### 3. Design & Architecture +- Single Responsibility Principle (SRP) +- Composition over Inheritance +- Program to Interfaces +- Essential Design Patterns: + - Factory Pattern + - Strategy Pattern + - Observer Pattern + - Decorator Pattern + - Adapter Pattern + - Command Pattern + - Singleton Pattern + +### 4. Testing & Quality +- Test-driven development approach +- Focused test assertions +- Test pyramid (unit/integration/e2e) +- Mocking and test doubles + +### 5. Error Handling +- Clear error messages +- Early input validation +- Exception hierarchies +- Recovery strategies + +### 6. Maintainability +- Boy Scout Rule +- Continuous refactoring +- Incremental commits +- Automation and tooling + +## AI-Specific Guidance + +This skill includes specific guidance for AI code generation, helping avoid common pitfalls such as: +- Generic naming (`data`, `temp`, `result`) +- Over-commenting obvious code +- Skipping edge case validation +- Applying patterns unnecessarily +- Creating monolithic functions +- Duplicating code structures + +## Quick Reference Examples + +### Before & After + +**Poor Code:** +```python +def proc(u): + if u['age'] < 13: return False + db.save(u) + email.send(u['email'], 'Welcome') + return True +``` + +**Improved Code:** +```python +def is_eligible_user(user): + return user['age'] >= 13 + +def save_user(user): + db.save(user) + +def send_welcome_email(user): + email.send(user['email'], 'Welcome to the platform') + +def register_user(user): + if not is_eligible_user(user): + raise ValueError('User must be 13 or older') + save_user(user) + send_welcome_email(user) +``` + +## When to Apply + +| Situation | Recommended Principle/Pattern | +|-----------|------------------------------| +| Function > 20 lines | Split using SRP | +| Repeated code blocks | Extract with DRY | +| Complex conditionals | Strategy or State pattern | +| Object creation complexity | Factory pattern | +| Cross-cutting concerns | Decorator or Observer | +| Incompatible interfaces | Adapter pattern | +| Need undo/logging | Command pattern | + +## Contributing + +This skill is structured to be easily extended. To add new principles or patterns: + +1. Update the relevant reference file in `references/` +2. Add a cross-reference in `SKILL.md` +3. Include examples with "Do", "Don't", and "AI Pitfalls" sections + +## License + +This skill is based on principles from publicly available software engineering literature and industry best practices. + +## Additional Resources + +- [The 7 Most Important Software Design Patterns](https://learningdaily.dev/the-7-most-important-software-design-patterns-d60e546afb0e) +- [Refactoring Guru - Design Patterns](https://refactoring.guru/design-patterns) +- [SOLID Principles](https://en.wikipedia.org/wiki/SOLID) + +## Version + +1.0.0 - Initial release diff --git a/src/plugins/design-patterns-skill/snippets/references/patterns/design-architecture.txt b/src/plugins/design-patterns-skill/snippets/references/patterns/design-architecture.txt new file mode 100755 index 0000000..fc1cd6c --- /dev/null +++ b/src/plugins/design-patterns-skill/snippets/references/patterns/design-architecture.txt @@ -0,0 +1,477 @@ +# Design & Architecture Principles + +## Single Responsibility Principle (SRP) + +**Definition:** Each class or module should have only one reason to change. It should encapsulate one cohesive responsibility. + +**Supported by:** *Clean Code*, *The Pragmatic Programmer*, SOLID Principles + +### Examples + +```python +# Bad - Multiple responsibilities +class User: + def __init__(self, name, email): + self.name = name + self.email = email + + def save_to_database(self): + # Database logic + pass + + def send_welcome_email(self): + # Email logic + pass + + def generate_report(self): + # Reporting logic + pass + +# Good - Separated concerns +class User: + def __init__(self, name, email): + self.name = name + self.email = email + +class UserRepository: + def save(self, user): + # Database logic + pass + +class EmailService: + def send_welcome(self, user): + # Email logic + pass + +class UserReportGenerator: + def generate(self, user): + # Reporting logic + pass +``` + +### Do +- Encapsulate related data and behavior +- Separate concerns (data access, business logic, presentation) +- Create cohesive modules +- Make reasons for change explicit + +### Don't +- Mix data access, logic, and UI in one class +- Create "god objects" that do everything +- Couple unrelated functionality + +### AI Pitfalls +- Cramming multiple operations into one class +- Creating utility classes with unrelated methods +- Mixing infrastructure and domain logic + +--- + +## Composition Over Inheritance + +**Definition:** Prefer combining objects to form behavior over creating deep class hierarchies. Favor "has-a" relationships over "is-a". + +**Supported by:** *The Pragmatic Programmer*, *Design Patterns* + +### Examples + +```python +# Bad - Rigid inheritance hierarchy +class Bird: + def fly(self): + return "Flying" + +class Penguin(Bird): + def fly(self): + raise Exception("Penguins cannot fly") + +# Good - Composition with behavior injection +class FlyBehavior: + def fly(self): + pass + +class CanFly(FlyBehavior): + def fly(self): + return "Flying" + +class CannotFly(FlyBehavior): + def fly(self): + return "Cannot fly" + +class Bird: + def __init__(self, fly_behavior): + self.fly_behavior = fly_behavior + + def perform_fly(self): + return self.fly_behavior.fly() + +# Usage +sparrow = Bird(CanFly()) +penguin = Bird(CannotFly()) +``` + +### Do +- Use interfaces or protocols to define contracts +- Inject dependencies and behaviors +- Compose small, focused objects +- Favor delegation over inheritance + +### Don't +- Create deep inheritance hierarchies (>3 levels) +- Inherit just to override behavior +- Use inheritance for code reuse alone +- Force unnatural "is-a" relationships + +### AI Pitfalls +- Defaulting to inheritance for code reuse +- Creating rigid class hierarchies +- Not recognizing when composition is clearer + +--- + +## Program to an Interface, Not an Implementation + +**Definition:** Depend on abstractions (interfaces, protocols) rather than concrete implementations. This enables flexibility and testability. + +**Supported by:** *Design Patterns*, *Code Complete*, Dependency Inversion Principle + +### Examples + +```python +# Bad - Depends on concrete implementation +class OrderProcessor: + def __init__(self): + self.payment = StripePayment() # Hard-coded dependency + + def process(self, order): + self.payment.charge(order.total) + +# Good - Depends on abstraction +class PaymentProcessor: + def charge(self, amount): + raise NotImplementedError + +class StripePayment(PaymentProcessor): + def charge(self, amount): + # Stripe-specific logic + pass + +class PayPalPayment(PaymentProcessor): + def charge(self, amount): + # PayPal-specific logic + pass + +class OrderProcessor: + def __init__(self, payment_processor: PaymentProcessor): + self.payment = payment_processor + + def process(self, order): + self.payment.charge(order.total) + +# Usage - Easy to swap implementations +processor = OrderProcessor(StripePayment()) +# or +processor = OrderProcessor(PayPalPayment()) +``` + +### Do +- Define interfaces for key abstractions +- Inject dependencies via constructors +- Use dependency injection frameworks when appropriate +- Code against contracts, not implementations + +### Don't +- Hard-code concrete class names +- Use `isinstance()` checks to switch behavior +- Create tight coupling to specific implementations + +### AI Pitfalls +- Using fixed class names instead of interfaces +- Not recognizing opportunities for abstraction +- Creating concrete dependencies in constructors + +--- + +## Essential Design Patterns + +### Factory Pattern + +**Purpose:** Delegate object creation to factory methods or classes. Decouples client code from concrete instantiation. + +**Use when:** Object creation is complex or varies based on conditions. + +```python +# Example +class LoggerFactory: + @staticmethod + def get_logger(log_type): + if log_type == "file": + return FileLogger() + elif log_type == "console": + return ConsoleLogger() + elif log_type == "cloud": + return CloudLogger() + else: + raise ValueError(f"Unknown logger type: {log_type}") + +# Usage +logger = LoggerFactory.get_logger("file") +logger.log("Application started") +``` + +### Strategy Pattern + +**Purpose:** Define a family of interchangeable algorithms and make them swappable at runtime. + +**Use when:** You need different behaviors for the same operation. + +```python +# Example +class SortStrategy: + def sort(self, data): + raise NotImplementedError + +class QuickSort(SortStrategy): + def sort(self, data): + # Quick sort implementation + pass + +class MergeSort(SortStrategy): + def sort(self, data): + # Merge sort implementation + pass + +class DataProcessor: + def __init__(self, sort_strategy: SortStrategy): + self.sorter = sort_strategy + + def process(self, data): + sorted_data = self.sorter.sort(data) + return sorted_data + +# Usage +processor = DataProcessor(MergeSort()) +result = processor.process([3, 1, 4, 1, 5]) +``` + +### Observer Pattern + +**Purpose:** Define a one-to-many dependency where changes in one object notify all dependents automatically. + +**Use when:** Multiple objects need to react to state changes. + +```python +# Example +class Subject: + def __init__(self): + self._observers = [] + + def attach(self, observer): + self._observers.append(observer) + + def notify(self, event): + for observer in self._observers: + observer.update(event) + +class Observer: + def update(self, event): + raise NotImplementedError + +class EmailNotifier(Observer): + def update(self, event): + print(f"Sending email for: {event}") + +class SlackNotifier(Observer): + def update(self, event): + print(f"Posting to Slack: {event}") + +# Usage +order_system = Subject() +order_system.attach(EmailNotifier()) +order_system.attach(SlackNotifier()) +order_system.notify("Order #123 shipped") +``` + +### Decorator Pattern + +**Purpose:** Dynamically add responsibilities to objects without modifying their class. + +**Use when:** You need flexible, composable enhancements. + +```python +# Example +class Notifier: + def send(self, message): + raise NotImplementedError + +class BasicNotifier(Notifier): + def send(self, message): + print(f"Basic notification: {message}") + +class NotifierDecorator(Notifier): + def __init__(self, notifier: Notifier): + self._notifier = notifier + + def send(self, message): + self._notifier.send(message) + +class SlackDecorator(NotifierDecorator): + def send(self, message): + super().send(message) + print(f"Also sent to Slack: {message}") + +class EmailDecorator(NotifierDecorator): + def send(self, message): + super().send(message) + print(f"Also sent via email: {message}") + +# Usage - Compose behaviors +notifier = EmailDecorator(SlackDecorator(BasicNotifier())) +notifier.send("System alert") +``` + +### Adapter Pattern + +**Purpose:** Convert one interface into another that clients expect. Enables incompatible interfaces to work together. + +**Use when:** Integrating legacy systems or third-party libraries. + +```python +# Example +class LegacyPrinter: + def print_text(self, text): + print(f"[LEGACY] {text}") + +class ModernPrinter: + def print(self, content): + raise NotImplementedError + +class PrinterAdapter(ModernPrinter): + def __init__(self, legacy_printer: LegacyPrinter): + self.legacy = legacy_printer + + def print(self, content): + self.legacy.print_text(content) + +# Usage +old_printer = LegacyPrinter() +adapter = PrinterAdapter(old_printer) +adapter.print("Hello World") # Uses modern interface, delegates to legacy +``` + +### Command Pattern + +**Purpose:** Encapsulate a request as an object, enabling queuing, logging, or undoable operations. + +**Use when:** You need to queue operations, support undo/redo, or log actions. + +```python +# Example +class Command: + def execute(self): + raise NotImplementedError + + def undo(self): + raise NotImplementedError + +class Light: + def on(self): + print("Light is ON") + + def off(self): + print("Light is OFF") + +class LightOnCommand(Command): + def __init__(self, light: Light): + self.light = light + + def execute(self): + self.light.on() + + def undo(self): + self.light.off() + +class LightOffCommand(Command): + def __init__(self, light: Light): + self.light = light + + def execute(self): + self.light.off() + + def undo(self): + self.light.on() + +# Usage +living_room_light = Light() +light_on = LightOnCommand(living_room_light) +light_on.execute() # Light is ON +light_on.undo() # Light is OFF +``` + +### Singleton Pattern + +**Purpose:** Ensure only one instance of a class exists globally. + +**Use when:** You need a single point of access (e.g., config, logger, connection pool). + +**Caution:** Often overused. Consider dependency injection instead. + +```python +# Example +class Singleton: + _instance = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + +class ConfigManager(Singleton): + def __init__(self): + if not hasattr(self, 'initialized'): + self.config = {} + self.initialized = True + +# Usage +config1 = ConfigManager() +config2 = ConfigManager() +assert config1 is config2 # Same instance +``` + +--- + +## Pattern Usage Guidelines + +### Do +- Apply patterns when they improve clarity and flexibility +- Choose patterns based on structural fit +- Use patterns to communicate design intent +- Combine patterns when appropriate + +### Don't +- Force patterns into simple code +- Use patterns for the sake of patterns +- Apply patterns without understanding the problem +- Over-abstract with unnecessary pattern layers + +### AI Pitfalls +- Predicting patterns where none are needed +- Misnaming pattern roles (e.g., calling a simple factory a "Factory Pattern") +- Misapplying pattern intent (e.g., Singleton for everything) +- Creating pattern boilerplate without actual benefit + +--- + +## Summary + +Good architecture is: +- **Modular** - clear boundaries and responsibilities +- **Flexible** - uses composition and interfaces +- **Abstract** - depends on contracts, not implementations +- **Pattern-aware** - applies proven solutions appropriately + +When designing systems, ask: +- Does each module have one clear responsibility? +- Can I swap implementations easily? +- Am I using inheritance or composition? +- Does this pattern solve a real structural problem? diff --git a/src/plugins/design-patterns-skill/snippets/references/patterns/error-handling.txt b/src/plugins/design-patterns-skill/snippets/references/patterns/error-handling.txt new file mode 100755 index 0000000..1b1c25f --- /dev/null +++ b/src/plugins/design-patterns-skill/snippets/references/patterns/error-handling.txt @@ -0,0 +1,364 @@ +# Error Handling & Input Validation + +## Handle Errors Clearly + +**Definition:** Use exceptions for unexpected states and provide clear error messages. Fail early and explicitly rather than allowing silent failures. + +**Supported by:** *Code Complete*, *Clean Code*, *The Pragmatic Programmer* + +### Examples + +```python +# Bad - Silent failure +def divide(a, b): + if b == 0: + return None # Caller has to check for None + return a / b + +# Good - Explicit error +def divide(a, b): + if b == 0: + raise ValueError("Cannot divide by zero") + return a / b +``` + +```python +# Bad - Vague error message +def process_user(user): + if not user: + raise Exception("Error") + +# Good - Descriptive error message +def process_user(user): + if user is None: + raise ValueError("User object cannot be None") + if not user.email: + raise ValueError(f"User {user.id} must have a valid email address") +``` + +### Do +- Use exceptions for exceptional conditions +- Provide descriptive error messages +- Include context (what failed, why, what was expected) +- Fail fast - validate inputs early +- Use specific exception types +- Document what exceptions can be raised + +### Don't +- Return None or -1 as error codes +- Swallow exceptions silently +- Use exceptions for control flow +- Provide generic error messages ("Error occurred") +- Catch exceptions you can't handle + +### AI Pitfalls +- Skipping edge-case validation +- Empty except blocks: `except: pass` +- Returning default values instead of raising errors +- Generic exception types instead of specific ones + +--- + +## Validate Inputs Early + +**Definition:** Check preconditions at the entry point of functions. Reject invalid input before processing. + +**Supported by:** *Code Complete*, *Clean Code* + +### Examples + +```python +# Bad - Late validation, partial processing +def register_user(username, email, age): + user = User(username, email, age) + save_to_database(user) + if age < 18: # Too late - already saved! + raise ValueError("User must be 18 or older") + +# Good - Early validation +def register_user(username, email, age): + if not username or len(username) < 3: + raise ValueError("Username must be at least 3 characters") + if not email or '@' not in email: + raise ValueError("Invalid email address") + if age < 18: + raise ValueError("User must be 18 or older") + + user = User(username, email, age) + save_to_database(user) +``` + +### Guard Clauses + +Use guard clauses to validate and exit early: + +```python +# Bad - Nested conditions +def process_order(order): + if order is not None: + if order.items: + if order.total > 0: + # Main logic here + charge_payment(order) + ship_order(order) + +# Good - Guard clauses +def process_order(order): + if order is None: + raise ValueError("Order cannot be None") + if not order.items: + raise ValueError("Order must contain at least one item") + if order.total <= 0: + raise ValueError("Order total must be positive") + + # Main logic - no nesting + charge_payment(order) + ship_order(order) +``` + +### Do +- Validate at function entry +- Use guard clauses to reduce nesting +- Check preconditions explicitly +- Validate types and ranges +- Use type hints and runtime validation + +### Don't +- Defer validation until deep in the logic +- Assume inputs are valid +- Mix validation with business logic + +--- + +## Exception Hierarchy + +**Definition:** Use specific exception types to allow targeted error handling. + +### Examples + +```python +# Bad - Generic exceptions +def fetch_user(user_id): + if user_id < 0: + raise Exception("Invalid ID") + user = db.get(user_id) + if not user: + raise Exception("Not found") + return user + +# Good - Specific exceptions +class InvalidUserIdError(ValueError): + pass + +class UserNotFoundError(LookupError): + pass + +def fetch_user(user_id): + if user_id < 0: + raise InvalidUserIdError(f"User ID must be positive, got {user_id}") + user = db.get(user_id) + if not user: + raise UserNotFoundError(f"User with ID {user_id} not found") + return user + +# Caller can handle specifically +try: + user = fetch_user(user_id) +except InvalidUserIdError as e: + return {"error": "bad_request", "message": str(e)} +except UserNotFoundError as e: + return {"error": "not_found", "message": str(e)} +``` + +### Do +- Create custom exception classes for domain errors +- Inherit from appropriate built-in exceptions +- Use exception hierarchies for related errors +- Document exception types in docstrings + +### Don't +- Raise generic `Exception` or `RuntimeError` +- Create exceptions for every possible error +- Use exceptions for non-exceptional cases + +--- + +## Error Recovery Strategies + +### Retry with Backoff + +```python +import time + +def fetch_with_retry(url, max_attempts=3): + for attempt in range(max_attempts): + try: + return http.get(url) + except TransientError as e: + if attempt == max_attempts - 1: + raise + wait_time = 2 ** attempt # Exponential backoff + time.sleep(wait_time) +``` + +### Fallback Mechanisms + +```python +def get_user_avatar(user_id): + try: + return cdn.fetch_avatar(user_id) + except CDNError: + # Fallback to default avatar + return DEFAULT_AVATAR_URL +``` + +### Circuit Breaker + +```python +class CircuitBreaker: + def __init__(self, failure_threshold=5): + self.failure_count = 0 + self.threshold = failure_threshold + self.state = "closed" # closed, open, half-open + + def call(self, func, *args): + if self.state == "open": + raise CircuitOpenError("Service is temporarily unavailable") + + try: + result = func(*args) + self.on_success() + return result + except Exception as e: + self.on_failure() + raise + + def on_success(self): + self.failure_count = 0 + self.state = "closed" + + def on_failure(self): + self.failure_count += 1 + if self.failure_count >= self.threshold: + self.state = "open" +``` + +--- + +## Logging vs. Exceptions + +**Definition:** Log for diagnostics, use exceptions for control flow. + +### When to Log + +```python +# Log operational info +logger.info(f"Processing order {order_id}") + +# Log warnings for recoverable issues +logger.warning(f"Slow query detected: {duration}ms") + +# Log errors with context +try: + process_payment(order) +except PaymentError as e: + logger.error(f"Payment failed for order {order.id}", exc_info=True) + raise # Re-raise after logging +``` + +### When to Raise Exceptions + +```python +# Invalid input - exception +def set_age(age): + if age < 0 or age > 150: + raise ValueError(f"Invalid age: {age}") + +# Business rule violation - exception +def withdraw(account, amount): + if account.balance < amount: + raise InsufficientFundsError(f"Balance: {account.balance}, requested: {amount}") + +# Operational issue - log + exception +def connect_to_database(): + try: + return db.connect() + except ConnectionError as e: + logger.error("Database connection failed", exc_info=True) + raise DatabaseUnavailableError("Cannot connect to database") from e +``` + +### Do +- Log context before re-raising +- Include exception traceback in logs +- Use structured logging for searchability +- Set appropriate log levels + +### Don't +- Log and swallow exceptions +- Log sensitive data (passwords, tokens) +- Over-log routine operations + +--- + +## Error Messages Best Practices + +### Good Error Messages + +**What went wrong:** +``` +"Invalid email address: 'user@domain' - missing top-level domain" +``` + +**What was expected:** +``` +"Order total must be positive, got -50.00" +``` + +**How to fix it:** +``` +"File not found: '/data/input.csv'. Check that the file exists and path is correct." +``` + +**Actionable context:** +``` +"User authentication failed: Invalid API key. Please check your credentials in the dashboard." +``` + +### Bad Error Messages + +``` +"Error" # Too vague +"Something went wrong" # Unhelpful +"Invalid input" # Missing details +"Error code: 42" # No explanation +``` + +### Do +- Explain what failed and why +- Include actual vs. expected values +- Suggest corrective actions +- Avoid technical jargon for user-facing errors +- Use clear, plain language + +### Don't +- Expose internal implementation details to end users +- Include stack traces in user-facing messages +- Use codes without explanations +- Be condescending ("You entered invalid data") + +--- + +## Summary + +Effective error handling: +- **Fails fast** - Validates early and explicitly +- **Provides clarity** - Error messages explain what and why +- **Uses exceptions correctly** - For exceptional conditions only +- **Enables recovery** - Appropriate retry and fallback strategies + +When handling errors, ask: +- Have I validated all inputs? +- Will the error message help someone fix the issue? +- Am I using the right exception type? +- Should this be logged, raised, or both? diff --git a/src/plugins/design-patterns-skill/snippets/references/patterns/maintainability.txt b/src/plugins/design-patterns-skill/snippets/references/patterns/maintainability.txt new file mode 100755 index 0000000..a085889 --- /dev/null +++ b/src/plugins/design-patterns-skill/snippets/references/patterns/maintainability.txt @@ -0,0 +1,548 @@ +# Maintainability & Best Practices + +## Boy Scout Rule + +**Definition:** "Leave the code better than you found it." Make small improvements whenever you touch existing code. + +**Supported by:** *Clean Code*, *The Pragmatic Programmer* + +### Examples + +```python +# Before - Existing code you're modifying +def calc(a, b): + return a + b + +# After - Improved while making your change +def calculate_sum(a, b): + """Return the sum of two numbers.""" + return a + b +``` + +```python +# Before - Adding a feature to messy code +def processUser(u): + # Check age + if u.age<18:return False + db.save(u) + return True + +# After - Clean up while you're here +def process_user(user): + """Register an eligible user.""" + if not is_eligible_user(user): + return False + save_user(user) + return True + +def is_eligible_user(user): + return user.age >= 18 +``` + +### Do +- Improve variable names +- Extract magic numbers to constants +- Add missing docstrings +- Fix formatting inconsistencies +- Remove dead code +- Simplify complex conditions + +### Don't +- Make unrelated large refactors +- Change behavior without tests +- Add hacks or workarounds +- Ignore obvious issues ("not my code") + +### AI Pitfalls +- Regenerating dirty code without improvements +- Not suggesting cleanup opportunities +- Adding to technical debt instead of reducing it + +--- + +## Continuous Refactoring + +**Definition:** Improve code structure regularly through small, safe changes backed by tests. Refactoring should be ongoing, not a separate phase. + +**Supported by:** *Refactoring*, *Code Complete*, *Clean Code* + +### Common Refactorings + +**Extract Method** +```python +# Before +def process_order(order): + # Validate + if not order.items: + raise ValueError("Empty order") + + # Calculate total + total = 0 + for item in order.items: + total += item.price * item.quantity + + # Apply discount + if order.customer.is_premium: + total *= 0.9 + + return total + +# After +def process_order(order): + validate_order(order) + total = calculate_total(order) + return apply_discount(total, order.customer) + +def validate_order(order): + if not order.items: + raise ValueError("Empty order") + +def calculate_total(order): + return sum(item.price * item.quantity for item in order.items) + +def apply_discount(total, customer): + if customer.is_premium: + return total * 0.9 + return total +``` + +**Extract Variable** +```python +# Before +if (user.age >= 18 and user.has_verified_email and user.account_status == 'active'): + grant_access() + +# After +is_adult = user.age >= 18 +has_verified_email = user.has_verified_email +is_active = user.account_status == 'active' + +if is_adult and has_verified_email and is_active: + grant_access() +``` + +**Rename** +```python +# Before +def fn(x, y): + return x * y + +# After +def calculate_area(width, height): + return width * height +``` + +**Replace Magic Numbers** +```python +# Before +def calculate_price(quantity): + if quantity > 100: + return quantity * 9.99 * 0.85 + return quantity * 9.99 + +# After +UNIT_PRICE = 9.99 +BULK_DISCOUNT = 0.85 +BULK_THRESHOLD = 100 + +def calculate_price(quantity): + price = quantity * UNIT_PRICE + if quantity > BULK_THRESHOLD: + price *= BULK_DISCOUNT + return price +``` + +### Refactoring Workflow + +1. **Ensure tests pass** - Start with green tests +2. **Make one change** - Small, focused refactor +3. **Run tests** - Verify behavior unchanged +4. **Commit** - Save working state +5. **Repeat** - Iterate on improvements + +### Do +- Refactor in small steps +- Run tests after each change +- Commit frequently +- Use IDE refactoring tools +- Keep behavior identical + +### Don't +- Refactor without tests +- Mix refactoring with feature work +- Make multiple changes at once +- Skip running tests +- Delay commits + +### AI Pitfalls +- Suggesting large refactors without incremental steps +- Omitting test runs between changes +- Changing behavior during refactoring + +--- + +## Version Control & Incremental Work + +**Definition:** Commit code in logical, testable chunks. Each commit should represent a complete, working unit of change. + +**Supported by:** *Refactoring*, *The Pragmatic Programmer*, Agile practices + +### Good Commit Practices + +**Atomic Commits** +``` +โœ“ "Add user email validation" +โœ“ "Extract payment processing to service" +โœ“ "Fix off-by-one error in pagination" + +โœ— "Fixed stuff" +โœ— "WIP" +โœ— "Updated files" +``` + +**Commit Messages** +``` +# Good - Imperative mood, clear intent +Add password strength validation + +Implement validation rules: +- Minimum 8 characters +- At least one uppercase letter +- At least one number +- At least one special character + +Closes #123 + +# Bad +fixed login +``` + +### Commit Workflow + +```bash +# 1. Make a focused change +# 2. Run tests +pytest + +# 3. Review changes +git diff + +# 4. Stage related files +git add user_validator.py tests/test_validator.py + +# 5. Commit with clear message +git commit -m "Add email format validation" + +# 6. Repeat for next logical change +``` + +### Do +- Commit working, tested code +- Write descriptive commit messages +- Keep commits focused and atomic +- Use branches for features +- Commit frequently + +### Don't +- Commit broken code +- Mix unrelated changes in one commit +- Skip commit messages +- Commit sensitive data (API keys, passwords) +- Leave uncommitted changes overnight + +### AI Pitfalls +- Generating large changes without guiding commit boundaries +- Not suggesting logical commit points +- Creating code that can't be committed incrementally + +--- + +## Code Reviews + +**Definition:** Systematic examination of code changes by peers to catch issues, share knowledge, and maintain quality. + +### Review Checklist + +**Correctness** +- Does it solve the stated problem? +- Are edge cases handled? +- Is error handling appropriate? +- Are there off-by-one errors or race conditions? + +**Design** +- Is it in the right place? +- Does it follow existing patterns? +- Is complexity warranted? +- Could it be simpler? + +**Readability** +- Are names clear? +- Is logic easy to follow? +- Are comments helpful (not redundant)? +- Is formatting consistent? + +**Testing** +- Are tests included? +- Do tests cover edge cases? +- Are tests readable and maintainable? + +**Security** +- Is input validated? +- Are secrets hardcoded? +- Are SQL queries parameterized? +- Is authentication/authorization correct? + +### Review Etiquette + +**As Reviewer** +``` +โœ“ "Consider extracting this to a helper function for reusability" +โœ“ "Could we add a test for the empty list case?" +โœ“ "This is clever! Can we add a comment explaining the algorithm?" + +โœ— "This is terrible" +โœ— "Why didn't you just..." +โœ— "Obviously this is wrong" +``` + +**As Author** +- Respond to all feedback +- Ask for clarification +- Explain non-obvious decisions +- Be open to suggestions +- Thank reviewers + +### Do +- Review promptly +- Focus on substance over style +- Suggest improvements, don't demand +- Automate style checks +- Learn from reviews you receive + +### Don't +- Approve without reading +- Nitpick trivial issues +- Review your own PRs +- Take criticism personally +- Skip review for "small" changes + +--- + +## Automation and Tooling + +**Definition:** Automate repetitive tasks and use tools to maintain consistency and quality. + +**Supported by:** *The Pragmatic Programmer*, *Clean Code* + +### Essential Tools + +**Linters** - Catch common mistakes +```bash +# Python +pylint myapp/ +flake8 myapp/ + +# JavaScript +eslint src/ + +# Go +golangci-lint run +``` + +**Formatters** - Maintain consistent style +```bash +# Python +black myapp/ + +# JavaScript +prettier --write src/ + +# Rust +rustfmt src/ +``` + +**Type Checkers** - Catch type errors +```bash +# Python +mypy myapp/ + +# TypeScript +tsc --noEmit + +# Flow +flow check +``` + +**Test Runners** - Verify behavior +```bash +# Python +pytest + +# JavaScript +jest + +# Go +go test ./... +``` + +### Continuous Integration + +```yaml +# .github/workflows/ci.yml +name: CI +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install dependencies + run: pip install -r requirements.txt + - name: Lint + run: flake8 . + - name: Type check + run: mypy . + - name: Test + run: pytest --cov + - name: Security scan + run: bandit -r . +``` + +### Pre-commit Hooks + +```bash +# .pre-commit-config.yaml +repos: + - repo: https://github.com/psf/black + hooks: + - id: black + - repo: https://github.com/pycqa/flake8 + hooks: + - id: flake8 + - repo: https://github.com/pre-commit/pre-commit-hooks + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml +``` + +### Do +- Integrate tools into workflow +- Run checks locally before pushing +- Fail builds on violations +- Configure tools consistently +- Update tools regularly + +### Don't +- Rely on manual checks +- Ignore tool warnings +- Skip tools for "quick fixes" +- Disable checks without good reason + +### AI Pitfalls +- Producing code that doesn't pass linting +- Ignoring type annotations +- Generating code incompatible with project tools + +--- + +## Documentation + +**Definition:** Provide context and explanations where code alone isn't sufficient. + +### What to Document + +**APIs and Public Interfaces** +```python +def calculate_shipping_cost(weight_kg: float, destination: str) -> float: + """Calculate shipping cost based on weight and destination. + + Args: + weight_kg: Package weight in kilograms (must be positive) + destination: ISO 3166-1 alpha-2 country code + + Returns: + Shipping cost in USD + + Raises: + ValueError: If weight is negative or destination is invalid + + Example: + >>> calculate_shipping_cost(2.5, 'US') + 12.50 + """ +``` + +**Complex Algorithms** +```python +def dijkstra(graph, start): + """Find shortest paths using Dijkstra's algorithm. + + Time complexity: O((V + E) log V) where V is vertices, E is edges + Space complexity: O(V) + + See: https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm + """ +``` + +**Non-Obvious Decisions** +```python +# Using MD5 for cache keys only - NOT for security +# MD5 is fast and collision-resistant enough for this use case +cache_key = hashlib.md5(url.encode()).hexdigest() +``` + +**Setup and Configuration** +```markdown +# README.md + +## Installation + +pip install -r requirements.txt + +## Configuration + +Set environment variables: +- `DATABASE_URL`: PostgreSQL connection string +- `API_KEY`: Third-party service API key + +## Running + +python app.py +``` + +### Don't Document + +- Obvious code (let code be self-documenting) +- Implementation details that change frequently +- Duplicated information available elsewhere + +### Do +- Keep docs close to code +- Update docs with code changes +- Use examples liberally +- Link to external references + +### Don't +- Let docs become stale +- Over-document simple code +- Duplicate info across files + +--- + +## Summary + +Maintainable code: +- **Improves incrementally** - Boy Scout Rule +- **Refactors continuously** - Small, safe improvements +- **Commits logically** - Atomic, tested changes +- **Automates quality** - Linters, formatters, CI/CD +- **Documents appropriately** - Context where needed + +When maintaining code, ask: +- Can I improve this while I'm here? +- Is this change small and safe? +- Should I commit now? +- Are my tools catching issues? +- Does this need documentation? diff --git a/src/plugins/design-patterns-skill/snippets/references/patterns/readability.txt b/src/plugins/design-patterns-skill/snippets/references/patterns/readability.txt new file mode 100755 index 0000000..edf85e0 --- /dev/null +++ b/src/plugins/design-patterns-skill/snippets/references/patterns/readability.txt @@ -0,0 +1,195 @@ +# Readability & Clarity Principles + +## Descriptive Naming + +**Definition:** Use clear, meaningful names for variables, functions, classes, etc., so code reads like natural language. Avoid vague, abbreviated, or encoded names. Good names explain intent without requiring comments. + +**Supported by:** *Clean Code*, *Code Complete* + +### Examples + +```python +# Bad +def calc(a, b): + return a * b + 3 + +# Good +def calculate_rectangle_area(width, height): + margin = 3 + return width * height + margin +``` + +### Do +- Use nouns for data structures and variables +- Use verbs for functions and methods +- Use consistent domain terminology +- Make names pronounceable and searchable +- Use solution/problem domain names + +### Don't +- Use single-letter names (except loop counters in small scopes) +- Create misleading names +- Use encodings or prefixes (Hungarian notation) +- Use abbreviations unless universally known +- Mix naming conventions in the same scope + +### AI Pitfalls +- Repeating generic names like `data`, `temp`, `foo`, `result` +- Inconsistent naming across similar concepts +- Using placeholder names and forgetting to rename +- Over-shortening meaningful names for brevity + +--- + +## Consistent Style & Formatting + +**Definition:** Follow a uniform coding style and project conventions. Consistency aids readability and reduces cognitive load. + +**Supported by:** *Clean Code*, *Code Complete* + +### Examples + +```javascript +// Bad - Inconsistent spacing, braces, indentation +if(x>0){ +y= x+10; + console.log(y);} + +// Good - Consistent formatting +if (x > 0) { + let result = x + 10; + console.log(result); +} +``` + +### Do +- Stick to one brace style (K&R, Allman, etc.) +- Use consistent indentation (2 or 4 spaces, never mix tabs/spaces) +- Follow language conventions (PEP 8 for Python, Airbnb for JS) +- Maintain consistent line length (80-120 characters) +- Use automated formatters (Prettier, Black, rustfmt) + +### Don't +- Mix different formatting styles in one file +- Ignore project linting rules +- Use inconsistent whitespace +- Create overly long lines + +### AI Pitfalls +- Producing inconsistent formatting across code blocks +- Mixing indentation styles +- Ignoring existing project formatting conventions + +--- + +## Self-Documenting Code (Minimize Comments) + +**Definition:** Write code so its intent is clear from the code itself. Comments should explain *why*, not *what*. + +**Supported by:** *Clean Code*, *Code Complete* + +### Examples + +```python +# Bad - Redundant comment +# Increment i by 1 +i = i + 1 + +# Good - No comment needed +i = i + 1 + +# Acceptable - Explains business rule +# Block access for users under minimum age requirement +if user.age < 13: + block_access() + +# Good - Explains non-obvious why +# Using exponential backoff to avoid API rate limits +retry_delay = base_delay * (2 ** attempt_count) +``` + +### Do +- Use clear naming and logic structure +- Comment complex algorithms or business rules +- Explain performance optimizations +- Document API contracts and side effects +- Add TODO comments for future work (with ticket IDs) + +### Don't +- Write comments that restate the code +- Leave commented-out code +- Write misleading or outdated comments +- Use comments to fix bad naming + +### AI Pitfalls +- Over-commenting obvious operations +- Leaving stale or contradictory comments +- Using comments instead of refactoring unclear code + +--- + +## Small Functions & Single Responsibility + +**Definition:** Functions and methods should do one thing and do it well. Small, cohesive units are easier to understand, test, and maintain. + +**Supported by:** *Clean Code*, *Code Complete* + +### Examples + +```python +# Bad - Function does too many things +def update_user(data): + validate(data) + update_database(data) + send_email(data) + log_activity(data) + invalidate_cache(data) + +# Good - Separated concerns +def update_user(data): + validated_data = validate(data) + save_user(validated_data) + notify_user(validated_data) + +def save_user(data): + update_database(data) + invalidate_cache(data) + +def notify_user(data): + send_email(data) + log_activity(data) +``` + +### Do +- Keep functions under 20-30 lines when possible +- Extract helper functions for complex logic +- Use descriptive function names that indicate purpose +- Limit function parameters (ideally โ‰ค 3) +- Make one level of abstraction per function + +### Don't +- Combine unrelated operations +- Create deeply nested logic +- Use flag arguments to control behavior +- Write functions that both query and modify state + +### AI Pitfalls +- Creating monolithic functions with multiple responsibilities +- Over-fragmenting into excessive tiny functions +- Mixing abstraction levels within one function +- Generating functions that modify global state unexpectedly + +--- + +## Summary + +Readable code is: +- **Self-explanatory** through naming +- **Consistent** in style and structure +- **Minimal in comments** - code speaks for itself +- **Small and focused** - easy to understand at a glance + +When writing or reviewing code, ask: +- Can I understand this without the author present? +- Would I want to debug this at 2 AM? +- Does this follow the team's conventions? diff --git a/src/plugins/design-patterns-skill/snippets/references/patterns/simplicity.txt b/src/plugins/design-patterns-skill/snippets/references/patterns/simplicity.txt new file mode 100755 index 0000000..f0533d2 --- /dev/null +++ b/src/plugins/design-patterns-skill/snippets/references/patterns/simplicity.txt @@ -0,0 +1,279 @@ +# Simplicity & Efficiency Principles + +## KISS (Keep It Simple, Stupid) + +**Definition:** Use the simplest solution that solves the problem. Avoid unnecessary complexity, over-engineering, or premature optimization. + +**Supported by:** *Clean Code*, *The Pragmatic Programmer* + +### Examples + +```javascript +// Bad - Unnecessary abstraction +class SingleValueContainer { + constructor(value) { + this.values = [value]; + } + add(value) { + this.values.push(value); + } + getValue() { + return this.values[0]; + } +} + +// Good - Use built-in features +let numbers = [5]; +numbers.push(7); +let firstNumber = numbers[0]; +``` + +```python +# Bad - Over-complicated +def is_even(n): + return True if n % 2 == 0 else False + +# Good - Direct and clear +def is_even(n): + return n % 2 == 0 +``` + +### Do +- Use language built-ins and standard libraries +- Choose clear, direct solutions +- Optimize only when profiling shows need +- Prefer composition of simple parts +- Write code for the current requirement + +### Don't +- Create abstractions without clear benefit +- Add complexity for hypothetical future needs +- Use clever tricks that obscure intent +- Build custom solutions when standard ones exist + +### AI Pitfalls +- Using classes or design patterns unnecessarily +- Creating abstractions for single-use code +- Over-complicating simple conditional logic +- Generating enterprise patterns for simple scripts + +--- + +## DRY (Don't Repeat Yourself) + +**Definition:** Eliminate duplicated code and logic. Every piece of knowledge should have a single, authoritative representation. + +**Supported by:** *The Pragmatic Programmer*, *Clean Code* + +### Examples + +```python +# Bad - Duplicated logic +def circle_area(radius): + return 3.14159 * radius * radius + +def quarter_circle_area(radius): + return 3.14159 * radius * radius / 4 + +def sphere_volume(radius): + return (4/3) * 3.14159 * radius * radius * radius + +# Good - Extracted constant and reused logic +PI = 3.14159 + +def circle_area(radius): + return PI * radius ** 2 + +def quarter_circle_area(radius): + return circle_area(radius) / 4 + +def sphere_volume(radius): + return (4/3) * PI * radius ** 3 +``` + +```javascript +// Bad - Repeated validation +function createUser(name, email) { + if (!email.includes('@')) throw Error('Invalid email'); + // ... +} + +function updateEmail(userId, email) { + if (!email.includes('@')) throw Error('Invalid email'); + // ... +} + +// Good - Extracted validation +function validateEmail(email) { + if (!email.includes('@')) { + throw Error('Invalid email'); + } +} + +function createUser(name, email) { + validateEmail(email); + // ... +} + +function updateEmail(userId, email) { + validateEmail(email); + // ... +} +``` + +### Do +- Extract common logic into functions +- Use constants for repeated values +- Abstract similar patterns +- Share code across modules appropriately +- Keep abstractions at the right level + +### Don't +- Copy-paste code blocks +- Duplicate business rules +- Repeat validation logic +- Hard-code the same values multiple times +- Create premature abstractions (see Rule of Three) + +### Rule of Three +Wait until you see duplication **three times** before abstracting. Two instances might be coincidental; three suggests a pattern. + +### AI Pitfalls +- Producing repeated code structures from pattern prediction +- Duplicating similar functions instead of parameterizing +- Repeating validation or error handling logic +- Not recognizing when to extract shared utilities + +--- + +## YAGNI (You Aren't Gonna Need It) + +**Definition:** Don't implement features or infrastructure until you actually need them. Avoid speculative development. + +**Supported by:** *The Pragmatic Programmer*, Extreme Programming (XP) + +### Examples + +```python +# Bad - Building for hypothetical futures +def process_order(order): + prepare_invoice(order) + apply_future_discount_system(order) # Not used yet + schedule_loyalty_rewards(order) # Not needed now + prepare_for_blockchain_audit(order) # Speculative + +# Good - Only what's needed now +def process_order(order): + prepare_invoice(order) + charge_payment(order) + ship_order(order) +``` + +```javascript +// Bad - Over-engineered configuration +class DatabaseConfig { + constructor() { + this.primaryHost = 'localhost'; + this.replicaHosts = []; // Not using replication + this.shardingStrategy = null; // Not sharding + this.cacheLayer = null; // No cache yet + } +} + +// Good - Current requirements only +class DatabaseConfig { + constructor(host) { + this.host = host; + } +} +``` + +### Do +- Write code for current, known requirements +- Add features when they're actually requested +- Keep infrastructure minimal +- Refactor when new needs emerge +- Trust that future changes will be manageable + +### Don't +- Build "just in case" features +- Create extensibility points without use cases +- Add configuration for hypothetical scenarios +- Implement features before they're specified + +### AI Pitfalls +- Generating code for unspecified future features +- Adding unnecessary configuration options +- Creating extensibility hooks without current need +- Building infrastructure beyond MVP scope + +--- + +## Premature Optimization + +**Definition:** Don't optimize until you have evidence of a performance problem. Clarity and correctness come first. + +**Supported by:** *The Pragmatic Programmer*, Donald Knuth's famous quote + +> "Premature optimization is the root of all evil" โ€” Donald Knuth + +### Examples + +```python +# Bad - Premature optimization +def find_user(user_id): + # Using complex caching before knowing if it's needed + cache_key = f"user:{user_id}:v2" + if cache_key in cache: + return deserialize(decompress(cache[cache_key])) + user = db.query(user_id) + cache[cache_key] = compress(serialize(user)) + return user + +# Good - Start simple, optimize if needed +def find_user(user_id): + return db.query(user_id) + +# Later, if profiling shows this is slow: +def find_user(user_id): + cached = cache.get(f"user:{user_id}") + if cached: + return cached + user = db.query(user_id) + cache.set(f"user:{user_id}", user) + return user +``` + +### Do +- Write clear, correct code first +- Profile before optimizing +- Optimize only proven bottlenecks +- Measure impact of optimizations +- Document why optimizations were made + +### Don't +- Sacrifice readability for unmeasured performance +- Optimize without profiling data +- Use complex algorithms for small datasets +- Cache everything "just in case" + +### AI Pitfalls +- Adding caching layers without justification +- Using complex data structures for simple cases +- Micro-optimizing at the expense of clarity + +--- + +## Summary + +Simple code is: +- **Direct** - solves the problem at hand +- **DRY** - has no unnecessary duplication +- **Minimal** - contains only what's needed now +- **Clear** - prioritizes readability over premature optimization + +When writing code, ask: +- Is this the simplest approach that works? +- Am I repeating myself? +- Do I actually need this now? +- Am I optimizing based on evidence? diff --git a/src/plugins/design-patterns-skill/snippets/references/patterns/testing.txt b/src/plugins/design-patterns-skill/snippets/references/patterns/testing.txt new file mode 100755 index 0000000..6ddc615 --- /dev/null +++ b/src/plugins/design-patterns-skill/snippets/references/patterns/testing.txt @@ -0,0 +1,309 @@ +# Testing & Quality Principles + +## Write Automated Tests Early + +**Definition:** Use tests to guide design, prevent regressions, and validate behavior. Testing should be part of the development process, not an afterthought. + +**Supported by:** *Refactoring*, *The Pragmatic Programmer*, Test-Driven Development (TDD) + +### Examples + +```python +# Test-first approach +def test_calculate_discount(): + # Arrange + price = 100 + discount_percent = 10 + + # Act + result = calculate_discount(price, discount_percent) + + # Assert + assert result == 90 + +def calculate_discount(price, discount_percent): + return price * (1 - discount_percent / 100) +``` + +```python +# Test edge cases +def test_user_age_validation(): + assert is_adult(18) == True + assert is_adult(17) == False + assert is_adult(0) == False + assert is_adult(150) == True # No upper bound check yet + +def is_adult(age): + return age >= 18 +``` + +### Do +- Write tests before or alongside code +- Test edge cases and boundary conditions +- Test business logic thoroughly +- Use descriptive test names +- Keep tests fast and independent +- Use test fixtures and setup/teardown appropriately + +### Don't +- Skip tests for "simple" code +- Test implementation details instead of behavior +- Write brittle tests that break on refactoring +- Ignore failing tests +- Write tests that depend on external state + +### AI Pitfalls +- Missing tests entirely +- Writing overly broad test functions +- Not testing edge cases or error paths +- Creating tests with vague assertions + +--- + +## One Assert Per Test (Focus) + +**Definition:** Keep tests focused on a single behavior or scenario. This makes failures easy to diagnose. + +**Supported by:** *Clean Code*, TDD best practices + +### Examples + +```python +# Bad - Multiple unrelated assertions +def test_user(): + user = User("Alice", 25) + assert user.name == "Alice" + assert user.age == 25 + assert user.is_adult() == True + assert user.can_vote() == True + assert user.get_greeting() == "Hello, Alice" + +# Good - Focused tests +def test_user_name_is_set_correctly(): + user = User("Alice", 25) + assert user.name == "Alice" + +def test_user_age_is_set_correctly(): + user = User("Alice", 25) + assert user.age == 25 + +def test_user_is_adult_when_age_18_or_above(): + user = User("Alice", 25) + assert user.is_adult() == True + +def test_user_is_not_adult_when_age_below_18(): + user = User("Bob", 17) + assert user.is_adult() == False +``` + +### Guideline Exceptions + +Multiple assertions are acceptable when: +- Testing object state after a single operation +- Verifying related properties of one concept +- Testing list/collection contents + +```python +# Acceptable - Related assertions on same concept +def test_order_creation(): + order = Order(items=[item1, item2]) + assert len(order.items) == 2 + assert order.total == 50.00 + assert order.status == OrderStatus.PENDING +``` + +### Do +- Use test names to describe expected behavior +- Group related tests in test classes +- Use parametrized tests for similar scenarios +- Make test intent crystal clear + +### Don't +- Group many checks together +- Test multiple behaviors in one test +- Create generic test names like `test_user()` + +### AI Pitfalls +- Combining multiple assertions in one test function +- Creating catch-all test functions +- Not using descriptive test names + +--- + +## Test Coverage Guidelines + +**Definition:** Aim for meaningful coverage of critical paths, not just high percentages. Focus on business logic, edge cases, and failure modes. + +### What to Test + +**High Priority:** +- Business logic and algorithms +- Input validation and error handling +- State transitions +- Integration points +- Security-critical code + +**Medium Priority:** +- Data transformations +- Configuration handling +- User-facing features + +**Low Priority:** +- Trivial getters/setters +- Framework-generated code +- External library wrappers + +### Coverage Anti-Patterns + +```python +# Bad - Testing for coverage, not correctness +def test_add(): + add(2, 3) # No assertion! + +# Good - Test actual behavior +def test_add_returns_sum(): + result = add(2, 3) + assert result == 5 +``` + +### Do +- Focus on critical code paths +- Test public interfaces, not private methods +- Use code coverage as a guide, not a goal +- Write tests that catch real bugs + +### Don't +- Aim for 100% coverage blindly +- Test trivial code just for metrics +- Ignore untested critical paths + +--- + +## Test Pyramid + +**Definition:** Balance different types of tests - many unit tests, fewer integration tests, even fewer end-to-end tests. + +``` + /\ + / \ Few E2E tests (slow, brittle) + /____\ + / \ More integration tests (moderate speed) + /________\ + / \ Many unit tests (fast, isolated) +``` + +### Unit Tests +- Test individual functions/classes in isolation +- Fast execution (milliseconds) +- Mock external dependencies +- High count (hundreds to thousands) + +### Integration Tests +- Test interactions between components +- Moderate speed (seconds) +- Use real dependencies where practical +- Medium count (dozens to hundreds) + +### End-to-End Tests +- Test complete user workflows +- Slow execution (minutes) +- Test through actual UI/API +- Low count (handful to dozens) + +### Do +- Rely primarily on unit tests +- Use integration tests for critical paths +- Reserve E2E tests for key user journeys + +### Don't +- Over-rely on E2E tests +- Skip unit tests in favor of integration tests +- Test everything through the UI + +--- + +## Test Quality Checklist + +Good tests are: + +- **Fast** - Run in milliseconds +- **Isolated** - No shared state or order dependency +- **Repeatable** - Same result every time +- **Self-validating** - Pass/fail is clear +- **Timely** - Written close to code + +### Do +- Use test fixtures for setup +- Clean up resources in teardown +- Use meaningful test data +- Avoid test interdependence + +### Don't +- Rely on external services without mocks +- Use production data +- Write flaky tests +- Commit commented-out tests + +--- + +## Mocking & Test Doubles + +**Definition:** Use test doubles (mocks, stubs, fakes) to isolate the code under test. + +### Types of Test Doubles + +**Stub** - Returns canned responses +```python +class StubPaymentGateway: + def charge(self, amount): + return {"status": "success", "transaction_id": "123"} +``` + +**Mock** - Verifies interactions +```python +def test_order_charges_payment(): + mock_gateway = Mock() + processor = OrderProcessor(mock_gateway) + processor.process(order) + mock_gateway.charge.assert_called_once_with(100.00) +``` + +**Fake** - Simplified working implementation +```python +class FakeDatabase: + def __init__(self): + self.data = {} + + def save(self, key, value): + self.data[key] = value + + def get(self, key): + return self.data.get(key) +``` + +### Do +- Mock external dependencies (APIs, databases, file systems) +- Use dependency injection to enable mocking +- Verify behavior, not implementation +- Keep mocks simple + +### Don't +- Mock everything (test real code when possible) +- Create complex mock hierarchies +- Over-specify mock expectations + +--- + +## Summary + +Effective testing: +- **Guides design** - Tests drive better architecture +- **Prevents regressions** - Catches bugs early +- **Documents behavior** - Tests are living specifications +- **Enables refactoring** - Confidence to improve code + +When writing tests, ask: +- Does this test verify actual behavior? +- Will this test catch real bugs? +- Is this test easy to understand and maintain? +- Can this test run quickly and reliably? diff --git a/src/plugins/engineering-discipline/index.ts b/src/plugins/engineering-discipline/index.ts new file mode 100644 index 0000000..8c9e80a --- /dev/null +++ b/src/plugins/engineering-discipline/index.ts @@ -0,0 +1,3 @@ +import { createStaticSkillPlugin } from "../../shared/static-skill.js"; + +export const engineeringDisciplinePlugin = createStaticSkillPlugin("engineering-discipline", "The engineering-discipline skill."); diff --git a/src/plugins/engineering-discipline/snippets/SKILL.txt b/src/plugins/engineering-discipline/snippets/SKILL.txt new file mode 100755 index 0000000..eb0465a --- /dev/null +++ b/src/plugins/engineering-discipline/snippets/SKILL.txt @@ -0,0 +1,157 @@ +--- +name: engineering-discipline +description: Apply senior-level software engineering discipline including design patterns, SOLID principles, architectural reasoning, systematic verification, and safety gates. Use when writing production code, complex features, reviewing code, refactoring systems, or when engineering rigor and correctness are required. Supports both quick reference lookup and full step-by-step process mode. +triggers: + - "build production code" + - "design architecture" + - "code review" + - "refactor" + - "engineering rigor" + - "production feature" + - "complex system" + - "safety gates" + - "design pattern" + - "SOLID principles" +activation: + mode: fuzzy + priority: normal + triggers: + - "build production code" + - "design architecture" + - "code review" + - "refactor" + - "engineering rigor" + - "production feature" + - "complex system" + - "safety gates" + - "design pattern" + - "SOLID principles" +compatibility: ">=2.0.0" +metadata: + version: "2.0.0" +references: + - references/patterns/readability.md + - references/patterns/simplicity.md + - references/patterns/design-architecture.md + - references/patterns/testing.md + - references/patterns/error-handling.md + - references/patterns/maintainability.md + - references/architecture/task-classification.md + - references/architecture/architecture-reasoning.md + - references/architecture/verification-gates.md + - references/architecture/negative-doubt.md + - references/architecture/output-format.md +--- + +# Engineering Discipline - Senior SDE-3 Framework + +## Overview + +Two operating modes: +- **Quick Reference**: Direct lookup of patterns, principles, naming conventions +- **Process Mode**: Full 8-step engineering workflow for complex/production features + +--- + +## Core Philosophy + +**You are not an autocomplete engine. You are an engineering constraint solver.** + +- **Correctness over speed** โ€” Preserve invariants, prevent bugs +- **Architecture over syntax** โ€” Think in layers before coding +- **Long-term maintainability** โ€” Optimize for change velocity +- **Explicit over implicit** โ€” No hidden assumptions +- **Tests lock behavior** โ€” No refactor without tests +- **Patterns require justification** โ€” No pattern without named force + +--- + +## Quick Reference Index + +### Patterns & Principles +- **Readability & Clarity** โ†’ `references/patterns/readability.md` +- **Simplicity & Efficiency** โ†’ `references/patterns/simplicity.md` +- **Design & Architecture** โ†’ `references/patterns/design-architecture.md` +- **Testing & Quality** โ†’ `references/patterns/testing.md` +- **Error Handling** โ†’ `references/patterns/error-handling.md` +- **Maintainability** โ†’ `references/patterns/maintainability.md` + +### Architecture & Process +- **Task Classification** โ†’ `references/architecture/task-classification.md` +- **Architecture Reasoning** โ†’ `references/architecture/architecture-reasoning.md` +- **Verification Gates** โ†’ `references/architecture/verification-gates.md` +- **Negative Doubt Bias** โ†’ `references/architecture/negative-doubt.md` +- **Standard Output Format** โ†’ `references/architecture/output-format.md` + +--- + +## Process Mode: 8-Step Framework + +### Step 0: Environment Gate โ›” +Verify runtime, package manager, dependencies. **Do NOT proceed without valid environment.** + +### Step 1: Task Classification ๐Ÿท๏ธ +Classify as exactly one: New feature | Refactor (behavior preserved) | Bug fix | Review/audit | Documentation only. +**If unclear โ†’ STOP and request clarification.** + +### Step 2: Load Engineering Constraints ๐Ÿ“‹ +Hard rules: clear naming, single responsibility, explicit module boundaries, no circular dependencies, folder structure reflects architecture, tests before refactor, YAGNI, patterns only when forces are named. + +### Step 3: Architecture-First Reasoning ๐Ÿ—๏ธ +Reason in strict order: +1. Responsibilities โ†’ 2. Invariants โ†’ 3. Dependency Direction โ†’ 4. Module Boundaries โ†’ 5. Public APIs โ†’ 6. Folder Structure โ†’ 7. Files โ†’ 8. Functions โ†’ 9. Syntax + +**Never skip layers. If you start at syntax, you'll build wrong.** + +### Step 4: Behavior & Invariants ๐Ÿ”’ +State observable behavior, invariants (input, state, ordering), and public vs private APIs. +**If refactoring and behavior not test-locked โ†’ STOP.** + +### Step 5: Pattern Gate ๐Ÿšง +Use design pattern **only if**: force is stated, invariant it protects is stated, simpler alternatives rejected. +**No force โ†’ no pattern.** + +### Step 6: Code Generation Rules โš™๏ธ +- Prefer deletion over abstraction +- No `utils/`, `common/`, `shared/` without ownership +- One reason to change per file +- Explicit public API per module +- Flat over deep structures +- No global state without justification + +### Step 7: Tests Are Part of Output ๐Ÿงช +If behavior exists: tests must exist, tests define invariants, refactors require tests first. +**No tests โ†’ no refactor.** + +### Step 8: Negative Doubt Routine ๐Ÿ” +Self-verification: (1) list 5 failure modes, (2) falsify assumptions, (3) verify invariants enforced, (4) audit dependencies, (5) try simpler alternative, (6) add failure-mode tests, (7) revise if issues found, (8) log findings. +**If critical issue unaddressed โ†’ HARD STOP.** + +--- + +## When to Use Each Section + +| Situation | Reference | +|-----------|-----------| +| Need pattern advice | `references/patterns/` | +| Building complex feature | Full Process Mode (Steps 0โ€“8) | +| Quick naming question | `references/patterns/readability.md` | +| Refactoring code | Process Mode + `references/patterns/maintainability.md` | +| Code review | Process Mode Step 4 + `references/patterns/testing.md` | +| Error handling unclear | `references/patterns/error-handling.md` | +| Architecture decisions | `references/architecture/architecture-reasoning.md` | +| Standard response format | `references/architecture/output-format.md` | + +--- + +## Critical Reminders + +**Non-Negotiable Rules:** +1. โ›” No refactor without tests +2. โ›” No pattern without named force +3. โ›” No circular dependencies +4. โ›” No assumptions without disclosure +5. โ›” No global state without justification +6. โ›” No proceeding with ambiguous requirements + +**If something cannot be done safely โ†’ Say so and explain why.** diff --git a/src/plugins/engineering-discipline/snippets/references/architecture/architecture-reasoning.txt b/src/plugins/engineering-discipline/snippets/references/architecture/architecture-reasoning.txt new file mode 100755 index 0000000..f23d14d --- /dev/null +++ b/src/plugins/engineering-discipline/snippets/references/architecture/architecture-reasoning.txt @@ -0,0 +1,374 @@ +# Architecture-First Reasoning + +## Core Principle + +**Think in layers of abstraction before writing syntax.** + +Code is the final output of architectural decisions, not the starting point. + +## The Reasoning Ladder (Never Skip Steps) + +### 1. Responsibilities + +**Question:** What does this component *do*? + +Define in terms of: +- Domain concepts (User, Order, Payment) +- Actions (authenticate, validate, process) +- Boundaries (what it does NOT do) + +**Example:** +``` +UserService responsibilities: +โœ“ Create user accounts +โœ“ Authenticate users +โœ“ Update user profiles +โœ— Send emails (NotificationService) +โœ— Store data (UserRepository) +``` + +**Anti-Pattern:** Defining by implementation ("uses database", "calls API") + +### 2. Invariants + +**Question:** What must *always* be true? + +**Types of Invariants:** + +**State Invariants:** +```python +# User age must be 0-150 +assert 0 <= user.age <= 150 + +# Order total must equal sum of items +assert order.total == sum(item.price * item.qty for item in order.items) +``` + +**Ordering Invariants:** +```python +# Payment must occur before shipping +assert order.payment_status == 'paid' before order.ship() +``` + +**Input Invariants:** +```python +# Email must contain @ symbol +assert '@' in email +``` + +**Document Early:** Write invariants before code. + +### 3. Dependency Direction + +**Question:** What depends on what? + +**Rules:** +- High-level modules depend on abstractions, not implementations +- Dependencies point inward (toward domain core) +- No circular dependencies + +**Dependency Graph Example:** +``` +Controller โ†’ Service โ†’ Repository + โ†“ + DTO/Model + +NOT: +Repository โ†’ Service (wrong direction) +Service โ†โ†’ Controller (circular) +``` + +**Test:** Can you replace a dependency without changing dependents? + +### 4. Module Boundaries + +**Question:** What is public vs private? + +**Public API:** +- Exported functions/classes +- Documented contracts +- Stable interfaces + +**Private Implementation:** +- Internal helpers +- Implementation details +- Subject to change + +**Example:** +```python +# user_service.py (public API) +def create_user(name: str, email: str) -> User: + """Public: Create a new user""" + _validate_email(email) # private + return _persist_user(name, email) # private + +# Private helpers (not exported) +def _validate_email(email: str): + ... + +def _persist_user(name: str, email: str): + ... +``` + +### 5. Public APIs + +**Question:** How do consumers interact with this? + +**API Design Checklist:** +- [ ] Clear function names (verbs for actions) +- [ ] Typed parameters (what goes in) +- [ ] Typed returns (what comes out) +- [ ] Error cases documented +- [ ] Idempotency specified (if relevant) +- [ ] Side effects stated + +**Example:** +```python +def transfer_funds( + from_account: AccountId, + to_account: AccountId, + amount: Decimal +) -> TransferResult: + """ + Transfer funds between accounts. + + Returns: + TransferResult with transaction ID + + Raises: + InsufficientFundsError: If from_account balance < amount + AccountNotFoundError: If either account doesn't exist + + Side Effects: + - Debits from_account + - Credits to_account + - Creates transaction record + + Idempotency: Safe to retry with same parameters + """ +``` + +### 6. Folder Structure + +**Question:** How is code organized on disk? + +**Principles:** +- Structure reflects architecture +- Co-locate related files +- Separate by layer or domain (choose one) + +**By Layer:** +``` +src/ +โ”œโ”€โ”€ controllers/ +โ”œโ”€โ”€ services/ +โ”œโ”€โ”€ repositories/ +โ””โ”€โ”€ models/ +``` + +**By Domain (Preferred for larger systems):** +``` +src/ +โ”œโ”€โ”€ users/ +โ”‚ โ”œโ”€โ”€ user_controller.py +โ”‚ โ”œโ”€โ”€ user_service.py +โ”‚ โ”œโ”€โ”€ user_repository.py +โ”‚ โ””โ”€โ”€ user_model.py +โ”œโ”€โ”€ orders/ +โ”‚ โ”œโ”€โ”€ order_controller.py +โ”‚ โ”œโ”€โ”€ order_service.py +โ”‚ โ””โ”€โ”€ ... +``` + +**Anti-Patterns:** +- `utils/` (too vague) +- `common/` (unclear ownership) +- `shared/` (becomes dumping ground) + +If you need shared code: +- Create specific modules: `validation/`, `auth/`, `formatting/` + +### 7. Files + +**Question:** What goes in each file? + +**One Reason to Change:** +``` +โœ“ user_repository.py (changes when data access changes) +โœ“ user_validator.py (changes when validation rules change) + +โœ— user_helpers.py (changes for multiple unrelated reasons) +``` + +**Naming:** +- Nouns for classes: `UserRepository`, `PaymentProcessor` +- Verbs for modules: `authenticate.py`, `validate.py` + +### 8. Functions + +**Question:** What are the atomic operations? + +**Function Design:** +- Single responsibility +- One level of abstraction +- Clear inputs/outputs +- Minimal side effects + +**Abstraction Levels:** +```python +# High level (orchestration) +def process_order(order): + validate_order(order) + charge_payment(order) + ship_order(order) + send_confirmation(order) + +# Mid level (business logic) +def validate_order(order): + check_inventory(order.items) + verify_address(order.shipping_address) + +# Low level (implementation) +def check_inventory(items): + for item in items: + if stock[item.id] < item.quantity: + raise OutOfStockError(item) +``` + +**Don't mix levels:** +```python +# Bad - mixing levels +def process_order(order): + # High level + validate_order(order) + # Low level - doesn't belong here + for item in order.items: + if stock[item.id] < item.quantity: + raise OutOfStockError(item) +``` + +### 9. Syntax + +**Question:** How is this expressed in code? + +**Only now** do you write actual implementation. + +If you start here, you'll build the wrong thing correctly. + +## Reasoning Example: Payment System + +**1. Responsibilities** +- PaymentService: Process payments +- PaymentGateway: Communicate with external processor +- PaymentRepository: Store payment records + +**2. Invariants** +- Payment amount must be positive +- Payment must have valid payment method +- Payment cannot be processed twice (idempotency) + +**3. Dependency Direction** +``` +PaymentController โ†’ PaymentService โ†’ PaymentGateway (interface) + โ†’ PaymentRepository +``` + +**4. Module Boundaries** +- Public: `PaymentService.process_payment()` +- Private: Gateway communication details, retry logic + +**5. Public API** +```python +def process_payment( + amount: Decimal, + payment_method: PaymentMethod +) -> PaymentResult +``` + +**6. Folder Structure** +``` +payments/ +โ”œโ”€โ”€ payment_service.py +โ”œโ”€โ”€ payment_gateway.py +โ”œโ”€โ”€ payment_repository.py +โ””โ”€โ”€ payment_models.py +``` + +**7. Files** +- `payment_service.py` - Business logic +- `payment_gateway.py` - External integration +- `payment_repository.py` - Data persistence + +**8. Functions** +```python +def process_payment(...) +def validate_payment_method(...) +def charge_payment_gateway(...) +def record_payment(...) +``` + +**9. Syntax** +*Now* write the Python code. + +## Forcing Function: Can You Answer These? + +Before writing code, answer: + +1. **What responsibilities does each component have?** +2. **What invariants must hold?** +3. **What depends on what? (Draw the graph)** +4. **What's public vs private?** +5. **How do consumers use this?** +6. **Where does this live in the folder structure?** +7. **What files exist and why?** +8. **What functions are needed?** + +If any answer is "I don't know" โ†’ **Stop. Don't guess.** + +## Anti-Pattern: Bottom-Up Thinking + +```python +# Wrong: Starting with syntax +def process_payment(amount, method): + # Wait, what should this do? + # Who calls this? + # What if amount is negative? + # Where does this go? +``` + +## Correct: Top-Down Thinking + +1. Responsibility: Process payment transactions +2. Invariant: amount > 0, method is valid +3. Depends on: PaymentGateway, PaymentRepository +4. Public API: `process_payment(amount, method) -> result` +5. Returns: Success/failure with transaction ID +6. Now write the code + +## Verification Questions + +After designing, ask: + +- **Can I explain this to a team member?** +- **Are dependencies testable/mockable?** +- **Can I change implementation without breaking consumers?** +- **Is the folder structure obvious?** +- **Would a new developer know where to add features?** + +If "no" to any โ†’ redesign before coding. + +## Summary + +**Architecture First = Correctness** + +Syntax is cheap. Wrong architecture is expensive. + +Spend time thinking before writing. + +**Ladder Enforcement:** +``` +Responsibilities โ†’ Invariants โ†’ Dependencies โ†’ Boundaries โ†’ APIs โ†’ +Folders โ†’ Files โ†’ Functions โ†’ Syntax +``` + +Skip a step = wrong design. diff --git a/src/plugins/engineering-discipline/snippets/references/architecture/negative-doubt.txt b/src/plugins/engineering-discipline/snippets/references/architecture/negative-doubt.txt new file mode 100755 index 0000000..1b95ce1 --- /dev/null +++ b/src/plugins/engineering-discipline/snippets/references/architecture/negative-doubt.txt @@ -0,0 +1,427 @@ +# Negative Doubt Bias (Self-Verification) + +## Purpose + +**Actively seek ways the solution can fail** before finalizing. + +This is a systematic routine to find bugs, edge cases, and flaws that optimistic reasoning misses. + +## When to Run + +**After producing any candidate solution, before finalizing.** + +This applies to: +- Architecture designs +- Code implementations +- Refactoring plans +- API designs + +## The Routine (8 Steps) + +### Step 1: Fail-Seeking Pass + +**Goal:** List concrete failure modes. + +**Process:** +Generate 5 ways the solution can fail: +1. **Bugs** - Logic errors, off-by-one, null pointers +2. **Edge Cases** - Empty input, max values, special characters +3. **Performance** - O(nยฒ) when n is large, memory leaks +4. **Security** - SQL injection, XSS, unauthorized access +5. **Maintainability** - Hard to change, tight coupling, unclear intent + +**For each failure mode, produce a minimal counterexample:** + +```python +# Solution: Calculate average +def average(numbers): + return sum(numbers) / len(numbers) + +# Fail-seeking: +# 1. Empty list โ†’ ZeroDivisionError +# 2. Non-numeric values โ†’ TypeError +# 3. Very large list โ†’ Memory overflow (unlikely but possible) +# 4. List with None values โ†’ TypeError in sum() +# 5. Integer division issues (Python 2) โ†’ Not applicable in Python 3 + +# Counterexamples: +average([]) # Fails: ZeroDivisionError +average([1, "two"]) # Fails: TypeError +average([1, None, 3]) # Fails: TypeError +``` + +### Step 2: Assumption Falsification + +**Goal:** Challenge every assumption. + +**Process:** +For each assumption: +1. Identify the assumption +2. Try to falsify it mentally +3. Find input/ordering/environment that breaks it + +**Example:** + +```python +def process_user(user): + """ + Assumptions: + 1. user is not None + 2. user.email is a string + 3. user.email contains '@' + 4. Database is available + """ + +# Falsification: +# Assumption 1: What if user=None? โ†’ Add guard +# Assumption 2: What if user.email=123? โ†’ Add type check +# Assumption 3: What if email="invalid"? โ†’ Add validation +# Assumption 4: What if DB is down? โ†’ Add retry/error handling + +# Updated code: +def process_user(user): + if user is None: + raise ValueError("User cannot be None") + if not isinstance(user.email, str): + raise TypeError("Email must be string") + if '@' not in user.email: + raise ValueError("Invalid email format") + + try: + save_to_db(user) + except DatabaseError as e: + log_error(e) + raise ProcessingError("Failed to save user") from e +``` + +### Step 3: Invariant Check + +**Goal:** Verify invariants are enforced. + +**Process:** +For each declared invariant: +1. Check if code enforces it +2. Add guards if missing +3. Add tests to verify + +**Example:** + +```python +# Invariant: Order total must equal sum of item prices + +class Order: + def __init__(self, items): + self.items = items + self.total = self._calculate_total() + + def _calculate_total(self): + return sum(item.price * item.quantity for item in self.items) + + def add_item(self, item): + self.items.append(item) + self.total = self._calculate_total() # Enforce invariant + + def validate(self): + # Runtime check + expected = sum(item.price * item.quantity for item in self.items) + if self.total != expected: + raise InvariantViolation("Order total mismatch") +``` + +**Test invariant:** +```python +def test_order_total_matches_items(): + order = Order([Item(price=10, quantity=2)]) + assert order.total == 20 + + order.add_item(Item(price=5, quantity=1)) + assert order.total == 25 # Invariant still holds +``` + +### Step 4: Dependency & Boundary Audit + +**Goal:** Verify clean architecture. + +**Checklist:** +- [ ] No circular dependencies introduced +- [ ] Module public surface is minimal +- [ ] Private internals not exposed +- [ ] Dependencies point in correct direction + +**Process:** + +**Draw dependency graph:** +``` +Module A โ†’ Module B + โ†’ Module C +Module C โ†’ Module D + +Any cycles? (e.g., B โ†’ A) +If YES โ†’ Refactor to break cycle +``` + +**Check public API:** +```python +# user_service.py + +# Public (should be exported) +def create_user(...): pass +def get_user(...): pass + +# Private (should NOT be exported) +def _validate_email(...): pass +def _hash_password(...): pass +``` + +**If consumers reach internals:** +```python +# WRONG: Consumer importing private function +from user_service import _validate_email + +# RIGHT: Add to public API or create facade +from validation import validate_email # Moved to proper module +``` + +### Step 5: Simpler-Alternative Challenge + +**Goal:** Find simpler solution that preserves behavior. + +**Process:** +1. Attempt to rewrite in 2-5 lines +2. If successful and acceptable, prefer it +3. If not, document why complexity is needed + +**Example:** + +```python +# Original (10 lines) +class UserValidator: + def __init__(self): + self.rules = [] + + def add_rule(self, rule): + self.rules.append(rule) + + def validate(self, user): + for rule in self.rules: + rule.check(user) + +# Simpler alternative (2 lines) +def validate_user(user, rules): + for rule in rules: + rule.check(user) + +# Decision: Use simpler version unless: +# - Need to cache rules +# - Need to add/remove rules dynamically +# - Need stateful validation +``` + +**Document if keeping complexity:** +```python +# Using class instead of function because: +# - Rules need to be cached and reused +# - Validators compose with other validators +# - Need to maintain validation state across calls +``` + +### Step 6: Test Injection + +**Goal:** Add tests for each failure mode. + +**Process:** +For each failure mode from Step 1: +1. Write test that would fail before fix +2. Verify test fails +3. Fix code +4. Verify test passes + +**Example:** + +```python +# Failure mode: Empty list causes ZeroDivisionError + +# Test (should fail initially) +def test_average_empty_list(): + with pytest.raises(ValueError): + average([]) + +# Fix code +def average(numbers): + if not numbers: + raise ValueError("Cannot calculate average of empty list") + return sum(numbers) / len(numbers) + +# Now test passes +``` + +### Step 7: Decision Revision + +**Goal:** Update design based on findings. + +**Process:** +If Steps 1-6 found issues: +1. Update architecture +2. Update code +3. Update tests +4. **Run routine again** (one more iteration) + +**Stopping Condition:** +- Routine finds no new issues, OR +- Issues are documented as known limitations + +### Step 8: Negative Doubt Log + +**Goal:** Document verification process. + +**Template:** + +```markdown +## Negative Doubt Log + +### Failure Modes Discovered +1. Empty input causes ZeroDivisionError +2. Non-numeric values cause TypeError +3. Large lists may cause memory issues + +### Tests Added +- test_average_empty_list() +- test_average_non_numeric() +- test_average_large_list() (performance test) + +### Assumptions Changed +Before: Assumed input is always non-empty +After: Explicitly validate input or document precondition + +### Design Changes +- Added input validation +- Added error handling +- Added defensive checks + +### Remaining Issues +- Very large lists (>10M items) may be slow + Decision: Document as known limitation, optimize if needed + +### Final Status +โœ“ All critical issues addressed +โš  Performance limitation documented +``` + +--- + +## Example: Complete Negative Doubt Routine + +**Original Code:** +```python +def transfer_funds(from_account, to_account, amount): + from_account.balance -= amount + to_account.balance += amount +``` + +**Step 1: Fail-Seeking** +1. Insufficient funds โ†’ Negative balance +2. Concurrent transfers โ†’ Race condition +3. Non-existent accounts โ†’ AttributeError +4. Negative amount โ†’ Allows theft +5. Same account transfer โ†’ No-op but still processes + +**Step 2: Assumption Falsification** +- Assumption: from_account.balance >= amount + Falsified: What if balance < amount? +- Assumption: Accounts exist + Falsified: What if from_account is None? + +**Step 3: Invariant Check** +- Invariant: Total money in system stays constant + Enforcement: Missing! Need transaction or rollback + +**Step 4: Dependency Audit** +- Direct balance manipulation โ†’ Breaks encapsulation +- Should use account methods + +**Step 5: Simpler Alternative** +```python +# No simpler alternative exists while preserving safety +# Complexity needed for atomicity and validation +``` + +**Step 6: Test Injection** +```python +def test_insufficient_funds(): + account = Account(balance=50) + with pytest.raises(InsufficientFundsError): + transfer_funds(account, other, 100) + +def test_negative_amount(): + with pytest.raises(ValueError): + transfer_funds(from_acc, to_acc, -50) +``` + +**Step 7: Revised Design** +```python +def transfer_funds(from_account, to_account, amount): + if from_account is None or to_account is None: + raise ValueError("Accounts cannot be None") + if amount <= 0: + raise ValueError("Amount must be positive") + if from_account == to_account: + return # No-op + if from_account.balance < amount: + raise InsufficientFundsError() + + # Atomic transaction + with transaction(): + from_account.withdraw(amount) + to_account.deposit(amount) +``` + +**Step 8: Negative Doubt Log** +``` +Failure Modes: 5 found, all addressed +Tests Added: 6 new tests +Assumptions Changed: Added explicit guards +Design Changes: Added transaction support, validation +Final Status: โœ“ Ready +``` + +--- + +## Hard Stop Condition + +**If any critical issue remains unaddressed:** + +**DO NOT finalize.** + +Return: +1. Revised design +2. Unmet requirements +3. Why they're unmet +4. What's needed to address them + +**Example:** +``` +HARD STOP + +Issue: Race condition in concurrent transfers +Status: NOT ADDRESSED +Reason: Requires database-level locking or transaction isolation +Needed: Database transaction support or pessimistic locking +Recommendation: Do not deploy without addressing +``` + +--- + +## Summary + +**Negative Doubt Bias is NOT optional.** + +**Default stance:** Your solution has bugs. Find them. + +Run this routine: +1. After every design +2. After every implementation +3. Before every review + +**It's faster to find bugs now than in production.** + +Pessimism in development = Reliability in production. diff --git a/src/plugins/engineering-discipline/snippets/references/architecture/output-format.txt b/src/plugins/engineering-discipline/snippets/references/architecture/output-format.txt new file mode 100755 index 0000000..c7e706f --- /dev/null +++ b/src/plugins/engineering-discipline/snippets/references/architecture/output-format.txt @@ -0,0 +1,49 @@ +# Standard Output Format + +Always structure Process Mode responses as: + +```markdown +## 1. Task Classification +[Feature | Refactor | Bug | Review | Docs] + +## 2. Assumptions +- Input assumptions +- State assumptions +- Environment assumptions + +## 3. Architecture / Design +- Responsibilities +- Invariants +- Dependencies +- Module boundaries + +## 4. Public APIs +- Function signatures +- Input/output contracts +- Error cases + +## 5. Code +[Implementation] + +## 6. Tests +[Test cases covering happy path, edge cases, errors] + +## 7. Risks & Trade-offs +- What can fail +- Performance considerations +- Maintainability impact + +## 8. Negative Doubt Log (Process Mode only) +- Failure modes discovered +- Tests added +- Assumptions changed +- Final decision changes +``` + +## Quick Reference Mode Format + +For direct pattern/principle lookups, respond concisely: +- State the answer directly +- Reference the relevant principle file +- Provide 2โ€“3 supporting points +- Note trade-offs if relevant diff --git a/src/plugins/engineering-discipline/snippets/references/architecture/task-classification.txt b/src/plugins/engineering-discipline/snippets/references/architecture/task-classification.txt new file mode 100755 index 0000000..c3ecdbe --- /dev/null +++ b/src/plugins/engineering-discipline/snippets/references/architecture/task-classification.txt @@ -0,0 +1,129 @@ +# Task Classification Framework + +## Purpose + +Before writing any code, classify the task to determine the appropriate engineering approach. This prevents mismatched solutions and establishes clear success criteria. + +## Classification Categories + +### 1. New Feature + +**Characteristics:** +- Adds new capability to the system +- Introduces new entry points or APIs +- May require new dependencies or infrastructure + +**Checklist:** +- [ ] Requirements clearly defined +- [ ] Success criteria established +- [ ] Dependencies identified +- [ ] Architecture impacts assessed +- [ ] Test strategy defined + +**Process:** +1. Define public API first +2. Identify module boundaries +3. Plan dependency direction +4. Design data flow +5. Implement with tests +6. Document behavior + +--- + +### 2. Refactor (Behavior Preserved) + +**Characteristics:** +- Changes internal structure +- **Zero** behavior change +- Improves maintainability, readability, or performance +- Must have tests locking existing behavior + +**Critical Rule:** No refactor without tests. If tests don't exist, the first step is writing them, not refactoring. + +**Process:** +1. **Verify tests exist** - If no tests, write them first +2. Make small, safe changes +3. Run tests after each change +4. Commit incrementally +5. Verify no behavior change + +--- + +### 3. Bug Fix + +**Characteristics:** +- Corrects incorrect behavior +- Has observable failure mode +- Should have reproduction test +- May require root cause analysis + +**Process:** +1. Write failing test that reproduces bug +2. Identify root cause (not just symptom) +3. Implement minimal fix +4. Verify test now passes +5. Check for similar bugs elsewhere +6. Document root cause and fix + +--- + +### 4. Review / Audit + +**Characteristics:** +- Evaluates existing code +- Identifies issues, risks, or improvements +- Does not modify code +- Produces recommendations + +**Process:** +1. Define review scope and criteria +2. Check against engineering principles +3. Identify risks and violations +4. Assess severity and impact +5. Provide prioritized recommendations +6. Suggest concrete improvements + +--- + +### 5. Documentation Only + +**Process:** +1. Identify what needs documentation +2. Determine appropriate level of detail +3. Use examples liberally +4. Link to related docs +5. Keep close to code +6. Verify accuracy + +--- + +## Classification Decision Tree + +``` +Is code changing? +โ”œโ”€ No โ†’ Documentation Only +โ””โ”€ Yes + โ”œโ”€ Does it add new capability? + โ”‚ โ””โ”€ Yes โ†’ New Feature + โ””โ”€ No + โ”œโ”€ Does it fix incorrect behavior? + โ”‚ โ””โ”€ Yes โ†’ Bug Fix + โ””โ”€ No + โ””โ”€ Does it change structure without behavior change? + โ””โ”€ Yes โ†’ Refactor +``` + +## When Classification is Unclear + +**Stop and clarify:** +- Request missing requirements +- Ask about success criteria +- Identify ambiguities +- Confirm behavior expectations +- Define scope boundaries + +**Never proceed with unclear classification.** + +## Golden Rule + +**If you can't classify it, don't start it.** diff --git a/src/plugins/engineering-discipline/snippets/references/architecture/verification-gates.txt b/src/plugins/engineering-discipline/snippets/references/architecture/verification-gates.txt new file mode 100755 index 0000000..d81ae37 --- /dev/null +++ b/src/plugins/engineering-discipline/snippets/references/architecture/verification-gates.txt @@ -0,0 +1,484 @@ +# Verification Gates & Safety Checks + +## Purpose + +Systematic checkpoints that prevent common engineering failures. Each gate must pass before proceeding. + +## Gate 0: Environment Verification + +**Before any reasoning or code generation.** + +### Checklist +- [ ] Runtime/language version specified +- [ ] Package manager identified +- [ ] Dependencies listed +- [ ] Build tool available +- [ ] Test framework present + +### Actions + +**If missing:** +```bash +# Example: Python project +python --version # Check runtime +pip list # Check installed packages +cat requirements.txt # Check dependencies +pytest --version # Check test framework +``` + +**Do NOT proceed without:** +1. Verified runtime environment +2. Required dependencies installed or listed +3. Build/test tools available + +**Output:** +``` +Environment Status: +โœ“ Python 3.11 +โœ“ Dependencies: requirements.txt present +โœ“ Test framework: pytest installed +โœ“ Proceed: YES +``` + +--- + +## Gate 1: Behavior Lock (Refactors Only) + +**Trigger:** Any refactoring task + +### Rule + +**NO REFACTOR WITHOUT PASSING TESTS** + +### Verification + +```python +# Step 1: Run existing tests +pytest tests/ + +# Step 2: Verify they pass +# All tests green? โ†’ Proceed +# Tests fail? โ†’ Fix first, then refactor +# No tests? โ†’ Write tests first +``` + +### If No Tests Exist + +**Write tests that lock current behavior:** + +```python +# Test current behavior (even if ugly) +def test_current_user_creation(): + # Lock the current behavior + user = create_user("Alice", "alice@example.com") + assert user.name == "Alice" + assert user.email == "alice@example.com" + assert user.created_at is not None +``` + +**Then refactor:** +```python +# Refactor internals +# Tests still pass? Good. +# Tests fail? Rollback and fix. +``` + +### Anti-Pattern + +```python +# WRONG: Refactoring without tests +"Let me clean this up..." +# Changes internal structure +# No tests to verify behavior preserved +# Introduces bugs silently +``` + +--- + +## Gate 2: Pattern Justification + +**Trigger:** Using any design pattern + +### Rule + +**Use pattern ONLY if:** +1. The force it resolves is named +2. The invariant it protects is stated +3. Simpler alternatives were rejected + +### Template + +``` +Pattern: [Factory | Strategy | Observer | etc.] + +Force Resolved: +- [Specific problem this solves] + +Invariant Protected: +- [What must remain true] + +Alternatives Rejected: +- Simple function: [Why insufficient] +- [Other alternative]: [Why insufficient] + +Justification: +- [Why this pattern is necessary] +``` + +### Example: Factory Pattern + +``` +Pattern: Factory + +Force Resolved: +- Object creation logic varies by user type (free, premium, enterprise) +- Don't want to expose creation complexity to clients + +Invariant Protected: +- All users must have valid email before creation +- Premium users must have payment method + +Alternatives Rejected: +- Simple constructor: Insufficient - creation logic too complex +- Multiple constructors: Insufficient - doesn't enforce validation order + +Justification: +- Factory centralizes validation and encapsulates creation complexity +``` + +### No Force โ†’ No Pattern + +```python +# WRONG: Pattern without justification +class UserFactory: # Why factory? + def create(self, name): + return User(name) # Just a wrapper + +# RIGHT: Simple function +def create_user(name): + return User(name) +``` + +--- + +## Gate 3: Circular Dependency Check + +**Trigger:** Adding new dependencies + +### Verification + +**Draw dependency graph:** +``` +A โ†’ B โ†’ C + โ†“ + D + +Is there a cycle? (A โ†’ B โ†’ ... โ†’ A) +If YES โ†’ STOP, redesign +If NO โ†’ Proceed +``` + +### Detection + +```python +# WRONG: Circular dependency +# user_service.py +from order_service import OrderService + +class UserService: + def get_user_orders(self, user_id): + return OrderService().get_orders(user_id) + +# order_service.py +from user_service import UserService # CIRCULAR! + +class OrderService: + def get_user_for_order(self, order_id): + return UserService().get_user(...) +``` + +### Fix Strategies + +**1. Extract common interface:** +```python +# models.py +class User: + pass + +class Order: + pass + +# user_service.py (depends on models) +# order_service.py (depends on models) +# No circular dependency +``` + +**2. Dependency Inversion:** +```python +# Define interface in high-level module +class IOrderRepository: + def get_orders(self, user_id): pass + +# Inject dependency +class UserService: + def __init__(self, order_repo: IOrderRepository): + self.orders = order_repo +``` + +--- + +## Gate 4: Public API Minimization + +**Trigger:** Before finalizing module interface + +### Rule + +**Expose only what's necessary.** + +### Checklist + +- [ ] Every public function has clear purpose +- [ ] Private helpers are actually private +- [ ] No implementation leakage +- [ ] Public API is documented + +### Example + +```python +# user_service.py + +# Public API (exported) +def create_user(name: str, email: str) -> User: + """Create a new user account.""" + _validate_email(email) + return _save_user(name, email) + +# Private (not exported) +def _validate_email(email: str): + if '@' not in email: + raise ValueError("Invalid email") + +def _save_user(name: str, email: str): + # Implementation detail + pass +``` + +### Anti-Pattern: Everything Public + +```python +# WRONG: Exposing internals +class UserService: + def create_user(self): pass + def validate_email(self): pass # Should be private + def save_to_db(self): pass # Should be private + def format_name(self): pass # Should be private +``` + +--- + +## Gate 5: Assumption Disclosure + +**Trigger:** Before finalizing any design/code + +### Rule + +**All assumptions must be explicit.** + +### Categories + +**Input Assumptions:** +```python +def calculate_discount(price: float) -> float: + """ + Assumptions: + - price is positive + - price is in USD + - customer is authenticated + """ +``` + +**State Assumptions:** +```python +def ship_order(order: Order): + """ + Assumptions: + - order.payment_status == 'paid' + - order.items is non-empty + - order.shipping_address is valid + """ +``` + +**Ordering Assumptions:** +```python +def finalize_checkout(): + """ + Assumptions: + - validate_cart() called first + - process_payment() called before this + """ +``` + +**Environment Assumptions:** +```python +def upload_to_s3(file_path: str): + """ + Assumptions: + - AWS credentials configured + - S3 bucket exists + - Network connectivity available + """ +``` + +### Verification + +Turn assumptions into **guards or tests:** + +```python +def calculate_discount(price: float) -> float: + # Guard against assumptions + if price <= 0: + raise ValueError("Price must be positive") + if not is_authenticated(): + raise AuthError("User must be authenticated") + + return price * 0.9 +``` + +--- + +## Gate 6: Test Coverage + +**Trigger:** Before marking code complete + +### Requirements + +**For each function:** +- [ ] Happy path tested +- [ ] Edge cases tested +- [ ] Error cases tested + +### Edge Cases to Test + +```python +def divide(a: float, b: float) -> float: + return a / b + +# Tests needed: +# - Normal case: divide(10, 2) == 5 +# - Zero divisor: divide(10, 0) โ†’ raises error +# - Negative numbers: divide(-10, 2) == -5 +# - Very large numbers: divide(1e100, 1e50) +# - Very small numbers: divide(0.0001, 0.0002) +``` + +### Coverage is NOT Enough + +```python +# 100% coverage, but bad test +def test_add(): + add(2, 3) # No assertion! Test passes but verifies nothing +``` + +**Good test:** +```python +def test_add_returns_sum(): + result = add(2, 3) + assert result == 5 +``` + +--- + +## Gate 7: Code Generation Rules + +**Trigger:** When writing implementation + +### Rules + +**Deletion over Abstraction:** +```python +# Prefer deleting unused code +# over abstracting "just in case" +``` + +**No Generic Folders:** +```python +# WRONG +utils/ +common/ +shared/ +helpers/ + +# RIGHT +validation/ +authentication/ +formatting/ +``` + +**One Reason to Change:** +```python +# user_manager.py +# โœ“ Changes when user management logic changes +# โœ— Changes when email logic, DB logic, AND user logic change +``` + +**Explicit Public API:** +```python +# __init__.py +from .user_service import create_user, get_user +# Only these are public +``` + +**Flat over Deep:** +```python +# WRONG: Excessive nesting +src/features/users/services/implementations/user_service_impl.py + +# RIGHT: Flat structure +src/users/user_service.py +``` + +**No Global State Without Justification:** +```python +# WRONG: Hidden global state +_cached_users = {} # Who manages this? When is it invalidated? + +# RIGHT: Explicit state management +class UserCache: + def __init__(self): + self._cache = {} + + def get(self, user_id): + return self._cache.get(user_id) +``` + +--- + +## Gate Enforcement Checklist + +Before finalizing any work: + +- [ ] **Gate 0:** Environment verified +- [ ] **Gate 1:** Tests pass (if refactoring) +- [ ] **Gate 2:** Patterns justified +- [ ] **Gate 3:** No circular dependencies +- [ ] **Gate 4:** Public API minimal +- [ ] **Gate 5:** Assumptions disclosed +- [ ] **Gate 6:** Tests cover edge cases +- [ ] **Gate 7:** Code generation rules followed + +**Any gate fails โ†’ Stop and fix.** + +--- + +## Summary + +Gates are **hard stops**, not suggestions. + +Bypassing gates leads to: +- Broken refactors (no tests) +- Over-engineered code (unjustified patterns) +- Tangled dependencies (circular refs) +- Brittle APIs (exposed internals) +- Hidden bugs (undisclosed assumptions) + +**Respect the gates.** diff --git a/src/plugins/engineering-discipline/snippets/references/misc/overview.txt b/src/plugins/engineering-discipline/snippets/references/misc/overview.txt new file mode 100755 index 0000000..5f29465 --- /dev/null +++ b/src/plugins/engineering-discipline/snippets/references/misc/overview.txt @@ -0,0 +1,450 @@ +# Engineering Discipline Skill + +A comprehensive Kiro skill combining design patterns, architectural reasoning, and systematic engineering verification for building production-quality software. + +## Overview + +This skill provides **two complementary modes**: + +### ๐Ÿ” Quick Reference Mode +Fast lookup for design patterns, principles, and best practices. + +**Use when:** +- Looking up specific patterns +- Checking naming conventions +- Quick code review questions +- Learning about principles + +### โš™๏ธ Process Mode +Full engineering workflow with verification gates for complex systems. + +**Use when:** +- Building production features +- Designing critical systems +- Security-sensitive code +- Complex architectures requiring rigor + +## Installation + +### Workspace Installation (Project-Specific) +```bash +mkdir -p .kiro/skills +cp -r engineering-discipline .kiro/skills/ +``` + +### Global Installation (All Projects) +```bash +mkdir -p ~/.kiro/skills +cp -r engineering-discipline ~/.kiro/skills/ +``` + +## Structure + +``` +engineering-discipline/ +โ”œโ”€โ”€ SKILL.md # Main skill with mode routing +โ”œโ”€โ”€ README.md # This file +โ””โ”€โ”€ references/ + โ”œโ”€โ”€ principles/ # Design patterns & best practices + โ”‚ โ”œโ”€โ”€ readability.md # Naming, formatting, documentation + โ”‚ โ”œโ”€โ”€ simplicity.md # KISS, DRY, YAGNI + โ”‚ โ”œโ”€โ”€ design-architecture.md # Patterns, SRP, composition + โ”‚ โ”œโ”€โ”€ testing.md # TDD, test pyramid, mocking + โ”‚ โ”œโ”€โ”€ error-handling.md # Validation, exceptions + โ”‚ โ””โ”€โ”€ maintainability.md # Refactoring, commits, tooling + โ””โ”€โ”€ process/ # Engineering workflow + โ”œโ”€โ”€ task-classification.md # Classify before coding + โ”œโ”€โ”€ architecture-reasoning.md # 9-layer thinking hierarchy + โ”œโ”€โ”€ verification-gates.md # Safety checkpoints + โ””โ”€โ”€ negative-doubt.md # Adversarial verification +``` + +## Quick Reference Mode Examples + +### Pattern Lookup +``` +User: "What pattern should I use for swappable payment processors?" + +Response: "Use the Strategy Pattern. It allows you to encapsulate +different payment algorithms (Stripe, PayPal, etc.) and make them +interchangeable at runtime..." +``` + +### Principle Check +``` +User: "Is this function too long?" + +Response: "Yes. At 45 lines with mixed abstraction levels, it violates +the Single Responsibility Principle. Consider extracting..." +``` + +### Best Practice +``` +User: "How should I name this variable?" + +Response: "Use a descriptive noun that indicates purpose. Instead of +'data', use 'user_records' or 'payment_transactions'..." +``` + +## Process Mode Examples + +### Building a System +``` +User: "Build a rate limiting service for our API" + +Response: +**Task Classification:** New Feature + +**Architecture:** +Responsibilities: +- RateLimiter: Track request counts per user +- Storage: Persist counter state +- Middleware: Intercept requests and enforce limits + +Invariants: +- Counter never negative +- Counter resets at time boundary +- Max requests per window enforced + +Dependencies: +RateLimiter โ†’ Storage (interface) +Middleware โ†’ RateLimiter + +[Full 9-layer architecture...] + +**Code:** [Implementation] + +**Tests:** [Comprehensive test suite] + +**Negative Doubt Log:** +- Failure mode: Race condition โ†’ Added atomic increment +- Failure mode: Time drift โ†’ Used monotonic clock +... +``` + +## Principle Categories + +### 1. Readability & Clarity +**Goal:** Code that reads like natural language + +- Descriptive naming (nouns for data, verbs for actions) +- Consistent formatting and style +- Self-documenting code (minimal comments) +- Small, focused functions (< 20 lines ideal) + +**Key Quote:** "Code is read 10x more than it's written" + +### 2. Simplicity & Efficiency +**Goal:** Minimal viable abstraction + +- KISS: Simplest solution that works +- DRY: No duplicated logic +- YAGNI: Build only what's needed now +- Defer optimization until profiling proves need + +**Key Quote:** "Premature optimization is the root of all evil" + +### 3. Design & Architecture +**Goal:** Modular, flexible, testable systems + +- Single Responsibility Principle +- Composition over Inheritance +- Depend on interfaces, not implementations +- 7 Essential Patterns: + - Factory, Strategy, Observer + - Decorator, Adapter, Command, Singleton + +**Key Quote:** "Architecture enables change velocity" + +### 4. Testing & Quality +**Goal:** Behavior locked by tests + +- Write tests first or alongside code +- Test pyramid: Many unit, fewer integration, minimal e2e +- One assertion per test (focused) +- Mock external dependencies + +**Key Quote:** "No refactor without tests" + +### 5. Error Handling +**Goal:** Fail fast and clearly + +- Validate inputs at entry points +- Use specific exception types +- Provide actionable error messages +- Guard clauses to reduce nesting + +**Key Quote:** "Explicit is better than implicit" + +### 6. Maintainability +**Goal:** Code that's easy to change + +- Boy Scout Rule: Leave it better +- Continuous small refactors +- Atomic commits with clear messages +- Automate quality checks (linting, formatting) + +**Key Quote:** "Technical debt compounds like financial debt" + +## Process Mode Workflow + +### Step 0: Environment Gate โš ๏ธ +**ALWAYS FIRST** + +Verify: +- Language version +- Package manager +- Dependencies +- Development tools + +### Step 1: Task Classification +Classify as ONE of: +- New Feature +- Refactor (behavior preserved) +- Bug Fix +- Review/Audit +- Documentation + +**If unclear โ†’ STOP** + +### Step 2: Load Engineering Constraints +Apply principles as hard rules: +- Single responsibility +- No circular dependencies +- Tests before refactoring +- No speculative features +- Patterns need stated forces + +### Step 3: Architecture-First Reasoning +Think in 9 layers (never skip): + +1. Responsibilities +2. Invariants +3. Dependencies +4. Module boundaries +5. Public APIs +6. Folder structure +7. Files +8. Functions +9. Syntax + +### Step 4: Behavior & Invariants +Document: +- Observable behavior +- Input/output contracts +- State invariants +- Ordering requirements + +### Step 5: Pattern Gate +Use pattern ONLY if: +- Force is stated +- Simpler alternative rejected +- Invariant being protected is clear + +### Step 6: Implementation +Generate code following: +- Explicit boundaries +- Minimal public surface +- No utils/common without ownership +- Flat structures over deep nesting + +### Step 7: Test-Driven +Include: +- Unit tests for logic +- Integration tests for dependencies +- Edge case coverage +- Error path tests + +### Step 8: Negative Doubt Routine +Adversarial verification: + +1. List 5 failure modes +2. Falsify assumptions +3. Check invariant enforcement +4. Audit dependencies +5. Try simpler alternative +6. Inject failure tests +7. Revise design +8. Document findings +9. Hard stop if unsafe + +### Step 9: Assumptions Disclosure +Always state: +- Input assumptions +- State invariants +- Ordering guarantees +- Non-goals + +## Mode Detection + +### Quick Reference Triggers +- "What pattern for...?" +- "How should I...?" +- "Best practice..." +- Pattern/principle names +- Short, focused questions + +### Process Mode Triggers +- "Build [system]" +- "Design [architecture]" +- "Production code for..." +- Keywords: critical, secure, complex +- Multi-component systems + +## Quick Decision Tables + +### When to Use Design Patterns? + +| Need | Pattern | Alternative | +|------|---------|-------------| +| Swap implementations | Strategy | If/else (for 2-3 variants) | +| Complex object creation | Factory | Direct constructor (simple objects) | +| Event notification | Observer | Direct calls (1-2 listeners) | +| Add capabilities | Decorator | Subclassing (stable hierarchy) | +| Interface mismatch | Adapter | Refactor interfaces | +| Undo/logging | Command | Direct methods (no history needed) | +| Single instance | Singleton | Dependency injection | + +### When to Refactor vs Rewrite? + +| Tests Exist? | Code Quality | Action | +|--------------|--------------|--------| +| โœ“ Yes | Poor structure | Incremental refactor | +| โœ— No | Poor structure | Write tests โ†’ refactor | +| โœ— No | Fundamentally wrong | Rewrite with TDD | +| โœ“ Yes | Security flaw | Fix โ†’ add regression test | + +### When to Add Abstraction? + +| Condition | Add? | Reason | +|-----------|------|--------| +| Duplicated in 3+ places | Yes | DRY principle | +| Future variation anticipated | No | YAGNI - wait for actual need | +| 2+ implementations exist | Yes | Interface for polymorphism | +| 1 implementation, simple | No | KISS - keep it simple | + +## Anti-Patterns Caught by This Skill + +### Common AI Code Generation Issues +- โŒ Generic variable names (`data`, `temp`, `result`) +- โŒ Over-commenting obvious code +- โŒ Skipping input validation +- โŒ Applying patterns without justification +- โŒ Monolithic functions (100+ lines) +- โŒ Copy-pasted code with minor changes +- โŒ Missing error handling + +### Engineering Anti-Patterns +- โŒ Coding before defining architecture +- โŒ Refactoring without tests +- โŒ Circular dependencies +- โŒ God objects (do everything) +- โŒ Premature optimization +- โŒ Unclear module boundaries +- โŒ Vague variable names + +## Example Usage + +### Quick Mode: Pattern Question +```bash +kiro "When should I use the Observer pattern instead of direct callbacks?" + +# Response includes: +# - Forces that justify Observer +# - When callbacks are simpler +# - Code example of both +# - Trade-offs +``` + +### Process Mode: Build Feature +```bash +kiro "Build authentication service with JWT tokens for production API" + +# Response includes: +# - Task classification (New Feature) +# - Architecture (9 layers) +# - Dependencies (interfaces) +# - Public API contracts +# - Full implementation +# - Comprehensive tests +# - Negative doubt log +# - Security considerations +``` + +## Verification Gates + +| Gate | Checks | Hard Stop If | +|------|--------|--------------| +| 0. Environment | Runtime, tools, deps | Missing required tools | +| 1. Requirements | Clarity, scope | Ambiguous or vague | +| 2. Architecture | Dependencies, boundaries | Circular deps | +| 3. Patterns | Justified forces | No force stated | +| 4. Tests | Coverage, edge cases | Critical paths untested | +| 5. Quality | Linting, formatting | Violations present | +| 6. Security | Validation, secrets | Vulnerabilities found | +| 7. Performance | Benchmarks (if critical) | Requirements not met | + +## Benefits + +### For Individual Developers +- Faster pattern selection +- Fewer bugs through verification +- Better architecture decisions +- Clearer code review criteria + +### For Teams +- Consistent engineering practices +- Shared vocabulary (patterns) +- Reduced technical debt +- Faster onboarding + +### For Production Systems +- Higher reliability (gates catch issues) +- Better maintainability (clear structure) +- Easier debugging (explicit invariants) +- Safer refactoring (tests lock behavior) + +## When NOT to Use Full Process Mode + +**Use lightweight reference mode for:** +- Prototypes and experiments +- Personal scripts +- Learning exercises +- Throwaway code + +**But always state:** "This is a prototype, skipping gates X, Y, Z" + +## Sources & Credits + +### Design Patterns & Principles +- *Clean Code* - Robert C. Martin +- *The Pragmatic Programmer* - Hunt & Thomas +- *Code Complete* - Steve McConnell +- *Refactoring* - Martin Fowler +- *Design Patterns* - Gang of Four + +### Engineering Process +- Senior SDE-3 industry practices +- Production systems methodology +- Safety-critical software engineering + +## Philosophy + +> **You are not an autocomplete engine.** +> **You are an engineering constraint solver.** + +This skill treats engineering as a discipline of **preserving correctness** through: +- Explicit constraints +- Verifiable invariants +- Systematic reasoning +- Adversarial verification + +Code is the output, not the input, of engineering. + +## Version + +2.0.0 - Combined design patterns + ProCoder engineering process + +## License + +Based on publicly available software engineering literature and industry best practices. diff --git a/src/plugins/engineering-discipline/snippets/references/patterns/design-architecture.txt b/src/plugins/engineering-discipline/snippets/references/patterns/design-architecture.txt new file mode 100755 index 0000000..fc1cd6c --- /dev/null +++ b/src/plugins/engineering-discipline/snippets/references/patterns/design-architecture.txt @@ -0,0 +1,477 @@ +# Design & Architecture Principles + +## Single Responsibility Principle (SRP) + +**Definition:** Each class or module should have only one reason to change. It should encapsulate one cohesive responsibility. + +**Supported by:** *Clean Code*, *The Pragmatic Programmer*, SOLID Principles + +### Examples + +```python +# Bad - Multiple responsibilities +class User: + def __init__(self, name, email): + self.name = name + self.email = email + + def save_to_database(self): + # Database logic + pass + + def send_welcome_email(self): + # Email logic + pass + + def generate_report(self): + # Reporting logic + pass + +# Good - Separated concerns +class User: + def __init__(self, name, email): + self.name = name + self.email = email + +class UserRepository: + def save(self, user): + # Database logic + pass + +class EmailService: + def send_welcome(self, user): + # Email logic + pass + +class UserReportGenerator: + def generate(self, user): + # Reporting logic + pass +``` + +### Do +- Encapsulate related data and behavior +- Separate concerns (data access, business logic, presentation) +- Create cohesive modules +- Make reasons for change explicit + +### Don't +- Mix data access, logic, and UI in one class +- Create "god objects" that do everything +- Couple unrelated functionality + +### AI Pitfalls +- Cramming multiple operations into one class +- Creating utility classes with unrelated methods +- Mixing infrastructure and domain logic + +--- + +## Composition Over Inheritance + +**Definition:** Prefer combining objects to form behavior over creating deep class hierarchies. Favor "has-a" relationships over "is-a". + +**Supported by:** *The Pragmatic Programmer*, *Design Patterns* + +### Examples + +```python +# Bad - Rigid inheritance hierarchy +class Bird: + def fly(self): + return "Flying" + +class Penguin(Bird): + def fly(self): + raise Exception("Penguins cannot fly") + +# Good - Composition with behavior injection +class FlyBehavior: + def fly(self): + pass + +class CanFly(FlyBehavior): + def fly(self): + return "Flying" + +class CannotFly(FlyBehavior): + def fly(self): + return "Cannot fly" + +class Bird: + def __init__(self, fly_behavior): + self.fly_behavior = fly_behavior + + def perform_fly(self): + return self.fly_behavior.fly() + +# Usage +sparrow = Bird(CanFly()) +penguin = Bird(CannotFly()) +``` + +### Do +- Use interfaces or protocols to define contracts +- Inject dependencies and behaviors +- Compose small, focused objects +- Favor delegation over inheritance + +### Don't +- Create deep inheritance hierarchies (>3 levels) +- Inherit just to override behavior +- Use inheritance for code reuse alone +- Force unnatural "is-a" relationships + +### AI Pitfalls +- Defaulting to inheritance for code reuse +- Creating rigid class hierarchies +- Not recognizing when composition is clearer + +--- + +## Program to an Interface, Not an Implementation + +**Definition:** Depend on abstractions (interfaces, protocols) rather than concrete implementations. This enables flexibility and testability. + +**Supported by:** *Design Patterns*, *Code Complete*, Dependency Inversion Principle + +### Examples + +```python +# Bad - Depends on concrete implementation +class OrderProcessor: + def __init__(self): + self.payment = StripePayment() # Hard-coded dependency + + def process(self, order): + self.payment.charge(order.total) + +# Good - Depends on abstraction +class PaymentProcessor: + def charge(self, amount): + raise NotImplementedError + +class StripePayment(PaymentProcessor): + def charge(self, amount): + # Stripe-specific logic + pass + +class PayPalPayment(PaymentProcessor): + def charge(self, amount): + # PayPal-specific logic + pass + +class OrderProcessor: + def __init__(self, payment_processor: PaymentProcessor): + self.payment = payment_processor + + def process(self, order): + self.payment.charge(order.total) + +# Usage - Easy to swap implementations +processor = OrderProcessor(StripePayment()) +# or +processor = OrderProcessor(PayPalPayment()) +``` + +### Do +- Define interfaces for key abstractions +- Inject dependencies via constructors +- Use dependency injection frameworks when appropriate +- Code against contracts, not implementations + +### Don't +- Hard-code concrete class names +- Use `isinstance()` checks to switch behavior +- Create tight coupling to specific implementations + +### AI Pitfalls +- Using fixed class names instead of interfaces +- Not recognizing opportunities for abstraction +- Creating concrete dependencies in constructors + +--- + +## Essential Design Patterns + +### Factory Pattern + +**Purpose:** Delegate object creation to factory methods or classes. Decouples client code from concrete instantiation. + +**Use when:** Object creation is complex or varies based on conditions. + +```python +# Example +class LoggerFactory: + @staticmethod + def get_logger(log_type): + if log_type == "file": + return FileLogger() + elif log_type == "console": + return ConsoleLogger() + elif log_type == "cloud": + return CloudLogger() + else: + raise ValueError(f"Unknown logger type: {log_type}") + +# Usage +logger = LoggerFactory.get_logger("file") +logger.log("Application started") +``` + +### Strategy Pattern + +**Purpose:** Define a family of interchangeable algorithms and make them swappable at runtime. + +**Use when:** You need different behaviors for the same operation. + +```python +# Example +class SortStrategy: + def sort(self, data): + raise NotImplementedError + +class QuickSort(SortStrategy): + def sort(self, data): + # Quick sort implementation + pass + +class MergeSort(SortStrategy): + def sort(self, data): + # Merge sort implementation + pass + +class DataProcessor: + def __init__(self, sort_strategy: SortStrategy): + self.sorter = sort_strategy + + def process(self, data): + sorted_data = self.sorter.sort(data) + return sorted_data + +# Usage +processor = DataProcessor(MergeSort()) +result = processor.process([3, 1, 4, 1, 5]) +``` + +### Observer Pattern + +**Purpose:** Define a one-to-many dependency where changes in one object notify all dependents automatically. + +**Use when:** Multiple objects need to react to state changes. + +```python +# Example +class Subject: + def __init__(self): + self._observers = [] + + def attach(self, observer): + self._observers.append(observer) + + def notify(self, event): + for observer in self._observers: + observer.update(event) + +class Observer: + def update(self, event): + raise NotImplementedError + +class EmailNotifier(Observer): + def update(self, event): + print(f"Sending email for: {event}") + +class SlackNotifier(Observer): + def update(self, event): + print(f"Posting to Slack: {event}") + +# Usage +order_system = Subject() +order_system.attach(EmailNotifier()) +order_system.attach(SlackNotifier()) +order_system.notify("Order #123 shipped") +``` + +### Decorator Pattern + +**Purpose:** Dynamically add responsibilities to objects without modifying their class. + +**Use when:** You need flexible, composable enhancements. + +```python +# Example +class Notifier: + def send(self, message): + raise NotImplementedError + +class BasicNotifier(Notifier): + def send(self, message): + print(f"Basic notification: {message}") + +class NotifierDecorator(Notifier): + def __init__(self, notifier: Notifier): + self._notifier = notifier + + def send(self, message): + self._notifier.send(message) + +class SlackDecorator(NotifierDecorator): + def send(self, message): + super().send(message) + print(f"Also sent to Slack: {message}") + +class EmailDecorator(NotifierDecorator): + def send(self, message): + super().send(message) + print(f"Also sent via email: {message}") + +# Usage - Compose behaviors +notifier = EmailDecorator(SlackDecorator(BasicNotifier())) +notifier.send("System alert") +``` + +### Adapter Pattern + +**Purpose:** Convert one interface into another that clients expect. Enables incompatible interfaces to work together. + +**Use when:** Integrating legacy systems or third-party libraries. + +```python +# Example +class LegacyPrinter: + def print_text(self, text): + print(f"[LEGACY] {text}") + +class ModernPrinter: + def print(self, content): + raise NotImplementedError + +class PrinterAdapter(ModernPrinter): + def __init__(self, legacy_printer: LegacyPrinter): + self.legacy = legacy_printer + + def print(self, content): + self.legacy.print_text(content) + +# Usage +old_printer = LegacyPrinter() +adapter = PrinterAdapter(old_printer) +adapter.print("Hello World") # Uses modern interface, delegates to legacy +``` + +### Command Pattern + +**Purpose:** Encapsulate a request as an object, enabling queuing, logging, or undoable operations. + +**Use when:** You need to queue operations, support undo/redo, or log actions. + +```python +# Example +class Command: + def execute(self): + raise NotImplementedError + + def undo(self): + raise NotImplementedError + +class Light: + def on(self): + print("Light is ON") + + def off(self): + print("Light is OFF") + +class LightOnCommand(Command): + def __init__(self, light: Light): + self.light = light + + def execute(self): + self.light.on() + + def undo(self): + self.light.off() + +class LightOffCommand(Command): + def __init__(self, light: Light): + self.light = light + + def execute(self): + self.light.off() + + def undo(self): + self.light.on() + +# Usage +living_room_light = Light() +light_on = LightOnCommand(living_room_light) +light_on.execute() # Light is ON +light_on.undo() # Light is OFF +``` + +### Singleton Pattern + +**Purpose:** Ensure only one instance of a class exists globally. + +**Use when:** You need a single point of access (e.g., config, logger, connection pool). + +**Caution:** Often overused. Consider dependency injection instead. + +```python +# Example +class Singleton: + _instance = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + +class ConfigManager(Singleton): + def __init__(self): + if not hasattr(self, 'initialized'): + self.config = {} + self.initialized = True + +# Usage +config1 = ConfigManager() +config2 = ConfigManager() +assert config1 is config2 # Same instance +``` + +--- + +## Pattern Usage Guidelines + +### Do +- Apply patterns when they improve clarity and flexibility +- Choose patterns based on structural fit +- Use patterns to communicate design intent +- Combine patterns when appropriate + +### Don't +- Force patterns into simple code +- Use patterns for the sake of patterns +- Apply patterns without understanding the problem +- Over-abstract with unnecessary pattern layers + +### AI Pitfalls +- Predicting patterns where none are needed +- Misnaming pattern roles (e.g., calling a simple factory a "Factory Pattern") +- Misapplying pattern intent (e.g., Singleton for everything) +- Creating pattern boilerplate without actual benefit + +--- + +## Summary + +Good architecture is: +- **Modular** - clear boundaries and responsibilities +- **Flexible** - uses composition and interfaces +- **Abstract** - depends on contracts, not implementations +- **Pattern-aware** - applies proven solutions appropriately + +When designing systems, ask: +- Does each module have one clear responsibility? +- Can I swap implementations easily? +- Am I using inheritance or composition? +- Does this pattern solve a real structural problem? diff --git a/src/plugins/engineering-discipline/snippets/references/patterns/error-handling.txt b/src/plugins/engineering-discipline/snippets/references/patterns/error-handling.txt new file mode 100755 index 0000000..1b1c25f --- /dev/null +++ b/src/plugins/engineering-discipline/snippets/references/patterns/error-handling.txt @@ -0,0 +1,364 @@ +# Error Handling & Input Validation + +## Handle Errors Clearly + +**Definition:** Use exceptions for unexpected states and provide clear error messages. Fail early and explicitly rather than allowing silent failures. + +**Supported by:** *Code Complete*, *Clean Code*, *The Pragmatic Programmer* + +### Examples + +```python +# Bad - Silent failure +def divide(a, b): + if b == 0: + return None # Caller has to check for None + return a / b + +# Good - Explicit error +def divide(a, b): + if b == 0: + raise ValueError("Cannot divide by zero") + return a / b +``` + +```python +# Bad - Vague error message +def process_user(user): + if not user: + raise Exception("Error") + +# Good - Descriptive error message +def process_user(user): + if user is None: + raise ValueError("User object cannot be None") + if not user.email: + raise ValueError(f"User {user.id} must have a valid email address") +``` + +### Do +- Use exceptions for exceptional conditions +- Provide descriptive error messages +- Include context (what failed, why, what was expected) +- Fail fast - validate inputs early +- Use specific exception types +- Document what exceptions can be raised + +### Don't +- Return None or -1 as error codes +- Swallow exceptions silently +- Use exceptions for control flow +- Provide generic error messages ("Error occurred") +- Catch exceptions you can't handle + +### AI Pitfalls +- Skipping edge-case validation +- Empty except blocks: `except: pass` +- Returning default values instead of raising errors +- Generic exception types instead of specific ones + +--- + +## Validate Inputs Early + +**Definition:** Check preconditions at the entry point of functions. Reject invalid input before processing. + +**Supported by:** *Code Complete*, *Clean Code* + +### Examples + +```python +# Bad - Late validation, partial processing +def register_user(username, email, age): + user = User(username, email, age) + save_to_database(user) + if age < 18: # Too late - already saved! + raise ValueError("User must be 18 or older") + +# Good - Early validation +def register_user(username, email, age): + if not username or len(username) < 3: + raise ValueError("Username must be at least 3 characters") + if not email or '@' not in email: + raise ValueError("Invalid email address") + if age < 18: + raise ValueError("User must be 18 or older") + + user = User(username, email, age) + save_to_database(user) +``` + +### Guard Clauses + +Use guard clauses to validate and exit early: + +```python +# Bad - Nested conditions +def process_order(order): + if order is not None: + if order.items: + if order.total > 0: + # Main logic here + charge_payment(order) + ship_order(order) + +# Good - Guard clauses +def process_order(order): + if order is None: + raise ValueError("Order cannot be None") + if not order.items: + raise ValueError("Order must contain at least one item") + if order.total <= 0: + raise ValueError("Order total must be positive") + + # Main logic - no nesting + charge_payment(order) + ship_order(order) +``` + +### Do +- Validate at function entry +- Use guard clauses to reduce nesting +- Check preconditions explicitly +- Validate types and ranges +- Use type hints and runtime validation + +### Don't +- Defer validation until deep in the logic +- Assume inputs are valid +- Mix validation with business logic + +--- + +## Exception Hierarchy + +**Definition:** Use specific exception types to allow targeted error handling. + +### Examples + +```python +# Bad - Generic exceptions +def fetch_user(user_id): + if user_id < 0: + raise Exception("Invalid ID") + user = db.get(user_id) + if not user: + raise Exception("Not found") + return user + +# Good - Specific exceptions +class InvalidUserIdError(ValueError): + pass + +class UserNotFoundError(LookupError): + pass + +def fetch_user(user_id): + if user_id < 0: + raise InvalidUserIdError(f"User ID must be positive, got {user_id}") + user = db.get(user_id) + if not user: + raise UserNotFoundError(f"User with ID {user_id} not found") + return user + +# Caller can handle specifically +try: + user = fetch_user(user_id) +except InvalidUserIdError as e: + return {"error": "bad_request", "message": str(e)} +except UserNotFoundError as e: + return {"error": "not_found", "message": str(e)} +``` + +### Do +- Create custom exception classes for domain errors +- Inherit from appropriate built-in exceptions +- Use exception hierarchies for related errors +- Document exception types in docstrings + +### Don't +- Raise generic `Exception` or `RuntimeError` +- Create exceptions for every possible error +- Use exceptions for non-exceptional cases + +--- + +## Error Recovery Strategies + +### Retry with Backoff + +```python +import time + +def fetch_with_retry(url, max_attempts=3): + for attempt in range(max_attempts): + try: + return http.get(url) + except TransientError as e: + if attempt == max_attempts - 1: + raise + wait_time = 2 ** attempt # Exponential backoff + time.sleep(wait_time) +``` + +### Fallback Mechanisms + +```python +def get_user_avatar(user_id): + try: + return cdn.fetch_avatar(user_id) + except CDNError: + # Fallback to default avatar + return DEFAULT_AVATAR_URL +``` + +### Circuit Breaker + +```python +class CircuitBreaker: + def __init__(self, failure_threshold=5): + self.failure_count = 0 + self.threshold = failure_threshold + self.state = "closed" # closed, open, half-open + + def call(self, func, *args): + if self.state == "open": + raise CircuitOpenError("Service is temporarily unavailable") + + try: + result = func(*args) + self.on_success() + return result + except Exception as e: + self.on_failure() + raise + + def on_success(self): + self.failure_count = 0 + self.state = "closed" + + def on_failure(self): + self.failure_count += 1 + if self.failure_count >= self.threshold: + self.state = "open" +``` + +--- + +## Logging vs. Exceptions + +**Definition:** Log for diagnostics, use exceptions for control flow. + +### When to Log + +```python +# Log operational info +logger.info(f"Processing order {order_id}") + +# Log warnings for recoverable issues +logger.warning(f"Slow query detected: {duration}ms") + +# Log errors with context +try: + process_payment(order) +except PaymentError as e: + logger.error(f"Payment failed for order {order.id}", exc_info=True) + raise # Re-raise after logging +``` + +### When to Raise Exceptions + +```python +# Invalid input - exception +def set_age(age): + if age < 0 or age > 150: + raise ValueError(f"Invalid age: {age}") + +# Business rule violation - exception +def withdraw(account, amount): + if account.balance < amount: + raise InsufficientFundsError(f"Balance: {account.balance}, requested: {amount}") + +# Operational issue - log + exception +def connect_to_database(): + try: + return db.connect() + except ConnectionError as e: + logger.error("Database connection failed", exc_info=True) + raise DatabaseUnavailableError("Cannot connect to database") from e +``` + +### Do +- Log context before re-raising +- Include exception traceback in logs +- Use structured logging for searchability +- Set appropriate log levels + +### Don't +- Log and swallow exceptions +- Log sensitive data (passwords, tokens) +- Over-log routine operations + +--- + +## Error Messages Best Practices + +### Good Error Messages + +**What went wrong:** +``` +"Invalid email address: 'user@domain' - missing top-level domain" +``` + +**What was expected:** +``` +"Order total must be positive, got -50.00" +``` + +**How to fix it:** +``` +"File not found: '/data/input.csv'. Check that the file exists and path is correct." +``` + +**Actionable context:** +``` +"User authentication failed: Invalid API key. Please check your credentials in the dashboard." +``` + +### Bad Error Messages + +``` +"Error" # Too vague +"Something went wrong" # Unhelpful +"Invalid input" # Missing details +"Error code: 42" # No explanation +``` + +### Do +- Explain what failed and why +- Include actual vs. expected values +- Suggest corrective actions +- Avoid technical jargon for user-facing errors +- Use clear, plain language + +### Don't +- Expose internal implementation details to end users +- Include stack traces in user-facing messages +- Use codes without explanations +- Be condescending ("You entered invalid data") + +--- + +## Summary + +Effective error handling: +- **Fails fast** - Validates early and explicitly +- **Provides clarity** - Error messages explain what and why +- **Uses exceptions correctly** - For exceptional conditions only +- **Enables recovery** - Appropriate retry and fallback strategies + +When handling errors, ask: +- Have I validated all inputs? +- Will the error message help someone fix the issue? +- Am I using the right exception type? +- Should this be logged, raised, or both? diff --git a/src/plugins/engineering-discipline/snippets/references/patterns/maintainability.txt b/src/plugins/engineering-discipline/snippets/references/patterns/maintainability.txt new file mode 100755 index 0000000..a085889 --- /dev/null +++ b/src/plugins/engineering-discipline/snippets/references/patterns/maintainability.txt @@ -0,0 +1,548 @@ +# Maintainability & Best Practices + +## Boy Scout Rule + +**Definition:** "Leave the code better than you found it." Make small improvements whenever you touch existing code. + +**Supported by:** *Clean Code*, *The Pragmatic Programmer* + +### Examples + +```python +# Before - Existing code you're modifying +def calc(a, b): + return a + b + +# After - Improved while making your change +def calculate_sum(a, b): + """Return the sum of two numbers.""" + return a + b +``` + +```python +# Before - Adding a feature to messy code +def processUser(u): + # Check age + if u.age<18:return False + db.save(u) + return True + +# After - Clean up while you're here +def process_user(user): + """Register an eligible user.""" + if not is_eligible_user(user): + return False + save_user(user) + return True + +def is_eligible_user(user): + return user.age >= 18 +``` + +### Do +- Improve variable names +- Extract magic numbers to constants +- Add missing docstrings +- Fix formatting inconsistencies +- Remove dead code +- Simplify complex conditions + +### Don't +- Make unrelated large refactors +- Change behavior without tests +- Add hacks or workarounds +- Ignore obvious issues ("not my code") + +### AI Pitfalls +- Regenerating dirty code without improvements +- Not suggesting cleanup opportunities +- Adding to technical debt instead of reducing it + +--- + +## Continuous Refactoring + +**Definition:** Improve code structure regularly through small, safe changes backed by tests. Refactoring should be ongoing, not a separate phase. + +**Supported by:** *Refactoring*, *Code Complete*, *Clean Code* + +### Common Refactorings + +**Extract Method** +```python +# Before +def process_order(order): + # Validate + if not order.items: + raise ValueError("Empty order") + + # Calculate total + total = 0 + for item in order.items: + total += item.price * item.quantity + + # Apply discount + if order.customer.is_premium: + total *= 0.9 + + return total + +# After +def process_order(order): + validate_order(order) + total = calculate_total(order) + return apply_discount(total, order.customer) + +def validate_order(order): + if not order.items: + raise ValueError("Empty order") + +def calculate_total(order): + return sum(item.price * item.quantity for item in order.items) + +def apply_discount(total, customer): + if customer.is_premium: + return total * 0.9 + return total +``` + +**Extract Variable** +```python +# Before +if (user.age >= 18 and user.has_verified_email and user.account_status == 'active'): + grant_access() + +# After +is_adult = user.age >= 18 +has_verified_email = user.has_verified_email +is_active = user.account_status == 'active' + +if is_adult and has_verified_email and is_active: + grant_access() +``` + +**Rename** +```python +# Before +def fn(x, y): + return x * y + +# After +def calculate_area(width, height): + return width * height +``` + +**Replace Magic Numbers** +```python +# Before +def calculate_price(quantity): + if quantity > 100: + return quantity * 9.99 * 0.85 + return quantity * 9.99 + +# After +UNIT_PRICE = 9.99 +BULK_DISCOUNT = 0.85 +BULK_THRESHOLD = 100 + +def calculate_price(quantity): + price = quantity * UNIT_PRICE + if quantity > BULK_THRESHOLD: + price *= BULK_DISCOUNT + return price +``` + +### Refactoring Workflow + +1. **Ensure tests pass** - Start with green tests +2. **Make one change** - Small, focused refactor +3. **Run tests** - Verify behavior unchanged +4. **Commit** - Save working state +5. **Repeat** - Iterate on improvements + +### Do +- Refactor in small steps +- Run tests after each change +- Commit frequently +- Use IDE refactoring tools +- Keep behavior identical + +### Don't +- Refactor without tests +- Mix refactoring with feature work +- Make multiple changes at once +- Skip running tests +- Delay commits + +### AI Pitfalls +- Suggesting large refactors without incremental steps +- Omitting test runs between changes +- Changing behavior during refactoring + +--- + +## Version Control & Incremental Work + +**Definition:** Commit code in logical, testable chunks. Each commit should represent a complete, working unit of change. + +**Supported by:** *Refactoring*, *The Pragmatic Programmer*, Agile practices + +### Good Commit Practices + +**Atomic Commits** +``` +โœ“ "Add user email validation" +โœ“ "Extract payment processing to service" +โœ“ "Fix off-by-one error in pagination" + +โœ— "Fixed stuff" +โœ— "WIP" +โœ— "Updated files" +``` + +**Commit Messages** +``` +# Good - Imperative mood, clear intent +Add password strength validation + +Implement validation rules: +- Minimum 8 characters +- At least one uppercase letter +- At least one number +- At least one special character + +Closes #123 + +# Bad +fixed login +``` + +### Commit Workflow + +```bash +# 1. Make a focused change +# 2. Run tests +pytest + +# 3. Review changes +git diff + +# 4. Stage related files +git add user_validator.py tests/test_validator.py + +# 5. Commit with clear message +git commit -m "Add email format validation" + +# 6. Repeat for next logical change +``` + +### Do +- Commit working, tested code +- Write descriptive commit messages +- Keep commits focused and atomic +- Use branches for features +- Commit frequently + +### Don't +- Commit broken code +- Mix unrelated changes in one commit +- Skip commit messages +- Commit sensitive data (API keys, passwords) +- Leave uncommitted changes overnight + +### AI Pitfalls +- Generating large changes without guiding commit boundaries +- Not suggesting logical commit points +- Creating code that can't be committed incrementally + +--- + +## Code Reviews + +**Definition:** Systematic examination of code changes by peers to catch issues, share knowledge, and maintain quality. + +### Review Checklist + +**Correctness** +- Does it solve the stated problem? +- Are edge cases handled? +- Is error handling appropriate? +- Are there off-by-one errors or race conditions? + +**Design** +- Is it in the right place? +- Does it follow existing patterns? +- Is complexity warranted? +- Could it be simpler? + +**Readability** +- Are names clear? +- Is logic easy to follow? +- Are comments helpful (not redundant)? +- Is formatting consistent? + +**Testing** +- Are tests included? +- Do tests cover edge cases? +- Are tests readable and maintainable? + +**Security** +- Is input validated? +- Are secrets hardcoded? +- Are SQL queries parameterized? +- Is authentication/authorization correct? + +### Review Etiquette + +**As Reviewer** +``` +โœ“ "Consider extracting this to a helper function for reusability" +โœ“ "Could we add a test for the empty list case?" +โœ“ "This is clever! Can we add a comment explaining the algorithm?" + +โœ— "This is terrible" +โœ— "Why didn't you just..." +โœ— "Obviously this is wrong" +``` + +**As Author** +- Respond to all feedback +- Ask for clarification +- Explain non-obvious decisions +- Be open to suggestions +- Thank reviewers + +### Do +- Review promptly +- Focus on substance over style +- Suggest improvements, don't demand +- Automate style checks +- Learn from reviews you receive + +### Don't +- Approve without reading +- Nitpick trivial issues +- Review your own PRs +- Take criticism personally +- Skip review for "small" changes + +--- + +## Automation and Tooling + +**Definition:** Automate repetitive tasks and use tools to maintain consistency and quality. + +**Supported by:** *The Pragmatic Programmer*, *Clean Code* + +### Essential Tools + +**Linters** - Catch common mistakes +```bash +# Python +pylint myapp/ +flake8 myapp/ + +# JavaScript +eslint src/ + +# Go +golangci-lint run +``` + +**Formatters** - Maintain consistent style +```bash +# Python +black myapp/ + +# JavaScript +prettier --write src/ + +# Rust +rustfmt src/ +``` + +**Type Checkers** - Catch type errors +```bash +# Python +mypy myapp/ + +# TypeScript +tsc --noEmit + +# Flow +flow check +``` + +**Test Runners** - Verify behavior +```bash +# Python +pytest + +# JavaScript +jest + +# Go +go test ./... +``` + +### Continuous Integration + +```yaml +# .github/workflows/ci.yml +name: CI +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install dependencies + run: pip install -r requirements.txt + - name: Lint + run: flake8 . + - name: Type check + run: mypy . + - name: Test + run: pytest --cov + - name: Security scan + run: bandit -r . +``` + +### Pre-commit Hooks + +```bash +# .pre-commit-config.yaml +repos: + - repo: https://github.com/psf/black + hooks: + - id: black + - repo: https://github.com/pycqa/flake8 + hooks: + - id: flake8 + - repo: https://github.com/pre-commit/pre-commit-hooks + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml +``` + +### Do +- Integrate tools into workflow +- Run checks locally before pushing +- Fail builds on violations +- Configure tools consistently +- Update tools regularly + +### Don't +- Rely on manual checks +- Ignore tool warnings +- Skip tools for "quick fixes" +- Disable checks without good reason + +### AI Pitfalls +- Producing code that doesn't pass linting +- Ignoring type annotations +- Generating code incompatible with project tools + +--- + +## Documentation + +**Definition:** Provide context and explanations where code alone isn't sufficient. + +### What to Document + +**APIs and Public Interfaces** +```python +def calculate_shipping_cost(weight_kg: float, destination: str) -> float: + """Calculate shipping cost based on weight and destination. + + Args: + weight_kg: Package weight in kilograms (must be positive) + destination: ISO 3166-1 alpha-2 country code + + Returns: + Shipping cost in USD + + Raises: + ValueError: If weight is negative or destination is invalid + + Example: + >>> calculate_shipping_cost(2.5, 'US') + 12.50 + """ +``` + +**Complex Algorithms** +```python +def dijkstra(graph, start): + """Find shortest paths using Dijkstra's algorithm. + + Time complexity: O((V + E) log V) where V is vertices, E is edges + Space complexity: O(V) + + See: https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm + """ +``` + +**Non-Obvious Decisions** +```python +# Using MD5 for cache keys only - NOT for security +# MD5 is fast and collision-resistant enough for this use case +cache_key = hashlib.md5(url.encode()).hexdigest() +``` + +**Setup and Configuration** +```markdown +# README.md + +## Installation + +pip install -r requirements.txt + +## Configuration + +Set environment variables: +- `DATABASE_URL`: PostgreSQL connection string +- `API_KEY`: Third-party service API key + +## Running + +python app.py +``` + +### Don't Document + +- Obvious code (let code be self-documenting) +- Implementation details that change frequently +- Duplicated information available elsewhere + +### Do +- Keep docs close to code +- Update docs with code changes +- Use examples liberally +- Link to external references + +### Don't +- Let docs become stale +- Over-document simple code +- Duplicate info across files + +--- + +## Summary + +Maintainable code: +- **Improves incrementally** - Boy Scout Rule +- **Refactors continuously** - Small, safe improvements +- **Commits logically** - Atomic, tested changes +- **Automates quality** - Linters, formatters, CI/CD +- **Documents appropriately** - Context where needed + +When maintaining code, ask: +- Can I improve this while I'm here? +- Is this change small and safe? +- Should I commit now? +- Are my tools catching issues? +- Does this need documentation? diff --git a/src/plugins/engineering-discipline/snippets/references/patterns/readability.txt b/src/plugins/engineering-discipline/snippets/references/patterns/readability.txt new file mode 100755 index 0000000..edf85e0 --- /dev/null +++ b/src/plugins/engineering-discipline/snippets/references/patterns/readability.txt @@ -0,0 +1,195 @@ +# Readability & Clarity Principles + +## Descriptive Naming + +**Definition:** Use clear, meaningful names for variables, functions, classes, etc., so code reads like natural language. Avoid vague, abbreviated, or encoded names. Good names explain intent without requiring comments. + +**Supported by:** *Clean Code*, *Code Complete* + +### Examples + +```python +# Bad +def calc(a, b): + return a * b + 3 + +# Good +def calculate_rectangle_area(width, height): + margin = 3 + return width * height + margin +``` + +### Do +- Use nouns for data structures and variables +- Use verbs for functions and methods +- Use consistent domain terminology +- Make names pronounceable and searchable +- Use solution/problem domain names + +### Don't +- Use single-letter names (except loop counters in small scopes) +- Create misleading names +- Use encodings or prefixes (Hungarian notation) +- Use abbreviations unless universally known +- Mix naming conventions in the same scope + +### AI Pitfalls +- Repeating generic names like `data`, `temp`, `foo`, `result` +- Inconsistent naming across similar concepts +- Using placeholder names and forgetting to rename +- Over-shortening meaningful names for brevity + +--- + +## Consistent Style & Formatting + +**Definition:** Follow a uniform coding style and project conventions. Consistency aids readability and reduces cognitive load. + +**Supported by:** *Clean Code*, *Code Complete* + +### Examples + +```javascript +// Bad - Inconsistent spacing, braces, indentation +if(x>0){ +y= x+10; + console.log(y);} + +// Good - Consistent formatting +if (x > 0) { + let result = x + 10; + console.log(result); +} +``` + +### Do +- Stick to one brace style (K&R, Allman, etc.) +- Use consistent indentation (2 or 4 spaces, never mix tabs/spaces) +- Follow language conventions (PEP 8 for Python, Airbnb for JS) +- Maintain consistent line length (80-120 characters) +- Use automated formatters (Prettier, Black, rustfmt) + +### Don't +- Mix different formatting styles in one file +- Ignore project linting rules +- Use inconsistent whitespace +- Create overly long lines + +### AI Pitfalls +- Producing inconsistent formatting across code blocks +- Mixing indentation styles +- Ignoring existing project formatting conventions + +--- + +## Self-Documenting Code (Minimize Comments) + +**Definition:** Write code so its intent is clear from the code itself. Comments should explain *why*, not *what*. + +**Supported by:** *Clean Code*, *Code Complete* + +### Examples + +```python +# Bad - Redundant comment +# Increment i by 1 +i = i + 1 + +# Good - No comment needed +i = i + 1 + +# Acceptable - Explains business rule +# Block access for users under minimum age requirement +if user.age < 13: + block_access() + +# Good - Explains non-obvious why +# Using exponential backoff to avoid API rate limits +retry_delay = base_delay * (2 ** attempt_count) +``` + +### Do +- Use clear naming and logic structure +- Comment complex algorithms or business rules +- Explain performance optimizations +- Document API contracts and side effects +- Add TODO comments for future work (with ticket IDs) + +### Don't +- Write comments that restate the code +- Leave commented-out code +- Write misleading or outdated comments +- Use comments to fix bad naming + +### AI Pitfalls +- Over-commenting obvious operations +- Leaving stale or contradictory comments +- Using comments instead of refactoring unclear code + +--- + +## Small Functions & Single Responsibility + +**Definition:** Functions and methods should do one thing and do it well. Small, cohesive units are easier to understand, test, and maintain. + +**Supported by:** *Clean Code*, *Code Complete* + +### Examples + +```python +# Bad - Function does too many things +def update_user(data): + validate(data) + update_database(data) + send_email(data) + log_activity(data) + invalidate_cache(data) + +# Good - Separated concerns +def update_user(data): + validated_data = validate(data) + save_user(validated_data) + notify_user(validated_data) + +def save_user(data): + update_database(data) + invalidate_cache(data) + +def notify_user(data): + send_email(data) + log_activity(data) +``` + +### Do +- Keep functions under 20-30 lines when possible +- Extract helper functions for complex logic +- Use descriptive function names that indicate purpose +- Limit function parameters (ideally โ‰ค 3) +- Make one level of abstraction per function + +### Don't +- Combine unrelated operations +- Create deeply nested logic +- Use flag arguments to control behavior +- Write functions that both query and modify state + +### AI Pitfalls +- Creating monolithic functions with multiple responsibilities +- Over-fragmenting into excessive tiny functions +- Mixing abstraction levels within one function +- Generating functions that modify global state unexpectedly + +--- + +## Summary + +Readable code is: +- **Self-explanatory** through naming +- **Consistent** in style and structure +- **Minimal in comments** - code speaks for itself +- **Small and focused** - easy to understand at a glance + +When writing or reviewing code, ask: +- Can I understand this without the author present? +- Would I want to debug this at 2 AM? +- Does this follow the team's conventions? diff --git a/src/plugins/engineering-discipline/snippets/references/patterns/simplicity.txt b/src/plugins/engineering-discipline/snippets/references/patterns/simplicity.txt new file mode 100755 index 0000000..f0533d2 --- /dev/null +++ b/src/plugins/engineering-discipline/snippets/references/patterns/simplicity.txt @@ -0,0 +1,279 @@ +# Simplicity & Efficiency Principles + +## KISS (Keep It Simple, Stupid) + +**Definition:** Use the simplest solution that solves the problem. Avoid unnecessary complexity, over-engineering, or premature optimization. + +**Supported by:** *Clean Code*, *The Pragmatic Programmer* + +### Examples + +```javascript +// Bad - Unnecessary abstraction +class SingleValueContainer { + constructor(value) { + this.values = [value]; + } + add(value) { + this.values.push(value); + } + getValue() { + return this.values[0]; + } +} + +// Good - Use built-in features +let numbers = [5]; +numbers.push(7); +let firstNumber = numbers[0]; +``` + +```python +# Bad - Over-complicated +def is_even(n): + return True if n % 2 == 0 else False + +# Good - Direct and clear +def is_even(n): + return n % 2 == 0 +``` + +### Do +- Use language built-ins and standard libraries +- Choose clear, direct solutions +- Optimize only when profiling shows need +- Prefer composition of simple parts +- Write code for the current requirement + +### Don't +- Create abstractions without clear benefit +- Add complexity for hypothetical future needs +- Use clever tricks that obscure intent +- Build custom solutions when standard ones exist + +### AI Pitfalls +- Using classes or design patterns unnecessarily +- Creating abstractions for single-use code +- Over-complicating simple conditional logic +- Generating enterprise patterns for simple scripts + +--- + +## DRY (Don't Repeat Yourself) + +**Definition:** Eliminate duplicated code and logic. Every piece of knowledge should have a single, authoritative representation. + +**Supported by:** *The Pragmatic Programmer*, *Clean Code* + +### Examples + +```python +# Bad - Duplicated logic +def circle_area(radius): + return 3.14159 * radius * radius + +def quarter_circle_area(radius): + return 3.14159 * radius * radius / 4 + +def sphere_volume(radius): + return (4/3) * 3.14159 * radius * radius * radius + +# Good - Extracted constant and reused logic +PI = 3.14159 + +def circle_area(radius): + return PI * radius ** 2 + +def quarter_circle_area(radius): + return circle_area(radius) / 4 + +def sphere_volume(radius): + return (4/3) * PI * radius ** 3 +``` + +```javascript +// Bad - Repeated validation +function createUser(name, email) { + if (!email.includes('@')) throw Error('Invalid email'); + // ... +} + +function updateEmail(userId, email) { + if (!email.includes('@')) throw Error('Invalid email'); + // ... +} + +// Good - Extracted validation +function validateEmail(email) { + if (!email.includes('@')) { + throw Error('Invalid email'); + } +} + +function createUser(name, email) { + validateEmail(email); + // ... +} + +function updateEmail(userId, email) { + validateEmail(email); + // ... +} +``` + +### Do +- Extract common logic into functions +- Use constants for repeated values +- Abstract similar patterns +- Share code across modules appropriately +- Keep abstractions at the right level + +### Don't +- Copy-paste code blocks +- Duplicate business rules +- Repeat validation logic +- Hard-code the same values multiple times +- Create premature abstractions (see Rule of Three) + +### Rule of Three +Wait until you see duplication **three times** before abstracting. Two instances might be coincidental; three suggests a pattern. + +### AI Pitfalls +- Producing repeated code structures from pattern prediction +- Duplicating similar functions instead of parameterizing +- Repeating validation or error handling logic +- Not recognizing when to extract shared utilities + +--- + +## YAGNI (You Aren't Gonna Need It) + +**Definition:** Don't implement features or infrastructure until you actually need them. Avoid speculative development. + +**Supported by:** *The Pragmatic Programmer*, Extreme Programming (XP) + +### Examples + +```python +# Bad - Building for hypothetical futures +def process_order(order): + prepare_invoice(order) + apply_future_discount_system(order) # Not used yet + schedule_loyalty_rewards(order) # Not needed now + prepare_for_blockchain_audit(order) # Speculative + +# Good - Only what's needed now +def process_order(order): + prepare_invoice(order) + charge_payment(order) + ship_order(order) +``` + +```javascript +// Bad - Over-engineered configuration +class DatabaseConfig { + constructor() { + this.primaryHost = 'localhost'; + this.replicaHosts = []; // Not using replication + this.shardingStrategy = null; // Not sharding + this.cacheLayer = null; // No cache yet + } +} + +// Good - Current requirements only +class DatabaseConfig { + constructor(host) { + this.host = host; + } +} +``` + +### Do +- Write code for current, known requirements +- Add features when they're actually requested +- Keep infrastructure minimal +- Refactor when new needs emerge +- Trust that future changes will be manageable + +### Don't +- Build "just in case" features +- Create extensibility points without use cases +- Add configuration for hypothetical scenarios +- Implement features before they're specified + +### AI Pitfalls +- Generating code for unspecified future features +- Adding unnecessary configuration options +- Creating extensibility hooks without current need +- Building infrastructure beyond MVP scope + +--- + +## Premature Optimization + +**Definition:** Don't optimize until you have evidence of a performance problem. Clarity and correctness come first. + +**Supported by:** *The Pragmatic Programmer*, Donald Knuth's famous quote + +> "Premature optimization is the root of all evil" โ€” Donald Knuth + +### Examples + +```python +# Bad - Premature optimization +def find_user(user_id): + # Using complex caching before knowing if it's needed + cache_key = f"user:{user_id}:v2" + if cache_key in cache: + return deserialize(decompress(cache[cache_key])) + user = db.query(user_id) + cache[cache_key] = compress(serialize(user)) + return user + +# Good - Start simple, optimize if needed +def find_user(user_id): + return db.query(user_id) + +# Later, if profiling shows this is slow: +def find_user(user_id): + cached = cache.get(f"user:{user_id}") + if cached: + return cached + user = db.query(user_id) + cache.set(f"user:{user_id}", user) + return user +``` + +### Do +- Write clear, correct code first +- Profile before optimizing +- Optimize only proven bottlenecks +- Measure impact of optimizations +- Document why optimizations were made + +### Don't +- Sacrifice readability for unmeasured performance +- Optimize without profiling data +- Use complex algorithms for small datasets +- Cache everything "just in case" + +### AI Pitfalls +- Adding caching layers without justification +- Using complex data structures for simple cases +- Micro-optimizing at the expense of clarity + +--- + +## Summary + +Simple code is: +- **Direct** - solves the problem at hand +- **DRY** - has no unnecessary duplication +- **Minimal** - contains only what's needed now +- **Clear** - prioritizes readability over premature optimization + +When writing code, ask: +- Is this the simplest approach that works? +- Am I repeating myself? +- Do I actually need this now? +- Am I optimizing based on evidence? diff --git a/src/plugins/engineering-discipline/snippets/references/patterns/testing.txt b/src/plugins/engineering-discipline/snippets/references/patterns/testing.txt new file mode 100755 index 0000000..6ddc615 --- /dev/null +++ b/src/plugins/engineering-discipline/snippets/references/patterns/testing.txt @@ -0,0 +1,309 @@ +# Testing & Quality Principles + +## Write Automated Tests Early + +**Definition:** Use tests to guide design, prevent regressions, and validate behavior. Testing should be part of the development process, not an afterthought. + +**Supported by:** *Refactoring*, *The Pragmatic Programmer*, Test-Driven Development (TDD) + +### Examples + +```python +# Test-first approach +def test_calculate_discount(): + # Arrange + price = 100 + discount_percent = 10 + + # Act + result = calculate_discount(price, discount_percent) + + # Assert + assert result == 90 + +def calculate_discount(price, discount_percent): + return price * (1 - discount_percent / 100) +``` + +```python +# Test edge cases +def test_user_age_validation(): + assert is_adult(18) == True + assert is_adult(17) == False + assert is_adult(0) == False + assert is_adult(150) == True # No upper bound check yet + +def is_adult(age): + return age >= 18 +``` + +### Do +- Write tests before or alongside code +- Test edge cases and boundary conditions +- Test business logic thoroughly +- Use descriptive test names +- Keep tests fast and independent +- Use test fixtures and setup/teardown appropriately + +### Don't +- Skip tests for "simple" code +- Test implementation details instead of behavior +- Write brittle tests that break on refactoring +- Ignore failing tests +- Write tests that depend on external state + +### AI Pitfalls +- Missing tests entirely +- Writing overly broad test functions +- Not testing edge cases or error paths +- Creating tests with vague assertions + +--- + +## One Assert Per Test (Focus) + +**Definition:** Keep tests focused on a single behavior or scenario. This makes failures easy to diagnose. + +**Supported by:** *Clean Code*, TDD best practices + +### Examples + +```python +# Bad - Multiple unrelated assertions +def test_user(): + user = User("Alice", 25) + assert user.name == "Alice" + assert user.age == 25 + assert user.is_adult() == True + assert user.can_vote() == True + assert user.get_greeting() == "Hello, Alice" + +# Good - Focused tests +def test_user_name_is_set_correctly(): + user = User("Alice", 25) + assert user.name == "Alice" + +def test_user_age_is_set_correctly(): + user = User("Alice", 25) + assert user.age == 25 + +def test_user_is_adult_when_age_18_or_above(): + user = User("Alice", 25) + assert user.is_adult() == True + +def test_user_is_not_adult_when_age_below_18(): + user = User("Bob", 17) + assert user.is_adult() == False +``` + +### Guideline Exceptions + +Multiple assertions are acceptable when: +- Testing object state after a single operation +- Verifying related properties of one concept +- Testing list/collection contents + +```python +# Acceptable - Related assertions on same concept +def test_order_creation(): + order = Order(items=[item1, item2]) + assert len(order.items) == 2 + assert order.total == 50.00 + assert order.status == OrderStatus.PENDING +``` + +### Do +- Use test names to describe expected behavior +- Group related tests in test classes +- Use parametrized tests for similar scenarios +- Make test intent crystal clear + +### Don't +- Group many checks together +- Test multiple behaviors in one test +- Create generic test names like `test_user()` + +### AI Pitfalls +- Combining multiple assertions in one test function +- Creating catch-all test functions +- Not using descriptive test names + +--- + +## Test Coverage Guidelines + +**Definition:** Aim for meaningful coverage of critical paths, not just high percentages. Focus on business logic, edge cases, and failure modes. + +### What to Test + +**High Priority:** +- Business logic and algorithms +- Input validation and error handling +- State transitions +- Integration points +- Security-critical code + +**Medium Priority:** +- Data transformations +- Configuration handling +- User-facing features + +**Low Priority:** +- Trivial getters/setters +- Framework-generated code +- External library wrappers + +### Coverage Anti-Patterns + +```python +# Bad - Testing for coverage, not correctness +def test_add(): + add(2, 3) # No assertion! + +# Good - Test actual behavior +def test_add_returns_sum(): + result = add(2, 3) + assert result == 5 +``` + +### Do +- Focus on critical code paths +- Test public interfaces, not private methods +- Use code coverage as a guide, not a goal +- Write tests that catch real bugs + +### Don't +- Aim for 100% coverage blindly +- Test trivial code just for metrics +- Ignore untested critical paths + +--- + +## Test Pyramid + +**Definition:** Balance different types of tests - many unit tests, fewer integration tests, even fewer end-to-end tests. + +``` + /\ + / \ Few E2E tests (slow, brittle) + /____\ + / \ More integration tests (moderate speed) + /________\ + / \ Many unit tests (fast, isolated) +``` + +### Unit Tests +- Test individual functions/classes in isolation +- Fast execution (milliseconds) +- Mock external dependencies +- High count (hundreds to thousands) + +### Integration Tests +- Test interactions between components +- Moderate speed (seconds) +- Use real dependencies where practical +- Medium count (dozens to hundreds) + +### End-to-End Tests +- Test complete user workflows +- Slow execution (minutes) +- Test through actual UI/API +- Low count (handful to dozens) + +### Do +- Rely primarily on unit tests +- Use integration tests for critical paths +- Reserve E2E tests for key user journeys + +### Don't +- Over-rely on E2E tests +- Skip unit tests in favor of integration tests +- Test everything through the UI + +--- + +## Test Quality Checklist + +Good tests are: + +- **Fast** - Run in milliseconds +- **Isolated** - No shared state or order dependency +- **Repeatable** - Same result every time +- **Self-validating** - Pass/fail is clear +- **Timely** - Written close to code + +### Do +- Use test fixtures for setup +- Clean up resources in teardown +- Use meaningful test data +- Avoid test interdependence + +### Don't +- Rely on external services without mocks +- Use production data +- Write flaky tests +- Commit commented-out tests + +--- + +## Mocking & Test Doubles + +**Definition:** Use test doubles (mocks, stubs, fakes) to isolate the code under test. + +### Types of Test Doubles + +**Stub** - Returns canned responses +```python +class StubPaymentGateway: + def charge(self, amount): + return {"status": "success", "transaction_id": "123"} +``` + +**Mock** - Verifies interactions +```python +def test_order_charges_payment(): + mock_gateway = Mock() + processor = OrderProcessor(mock_gateway) + processor.process(order) + mock_gateway.charge.assert_called_once_with(100.00) +``` + +**Fake** - Simplified working implementation +```python +class FakeDatabase: + def __init__(self): + self.data = {} + + def save(self, key, value): + self.data[key] = value + + def get(self, key): + return self.data.get(key) +``` + +### Do +- Mock external dependencies (APIs, databases, file systems) +- Use dependency injection to enable mocking +- Verify behavior, not implementation +- Keep mocks simple + +### Don't +- Mock everything (test real code when possible) +- Create complex mock hierarchies +- Over-specify mock expectations + +--- + +## Summary + +Effective testing: +- **Guides design** - Tests drive better architecture +- **Prevents regressions** - Catches bugs early +- **Documents behavior** - Tests are living specifications +- **Enables refactoring** - Confidence to improve code + +When writing tests, ask: +- Does this test verify actual behavior? +- Will this test catch real bugs? +- Is this test easy to understand and maintain? +- Can this test run quickly and reliably? diff --git a/src/plugins/excalidraw/index.ts b/src/plugins/excalidraw/index.ts new file mode 100644 index 0000000..63dff4f --- /dev/null +++ b/src/plugins/excalidraw/index.ts @@ -0,0 +1,3 @@ +import { createStaticSkillPlugin } from "../../shared/static-skill.js"; + +export const excalidrawPlugin = createStaticSkillPlugin("excalidraw", "The excalidraw skill."); diff --git a/src/plugins/excalidraw/snippets/README.txt b/src/plugins/excalidraw/snippets/README.txt new file mode 100755 index 0000000..ecdd744 --- /dev/null +++ b/src/plugins/excalidraw/snippets/README.txt @@ -0,0 +1,76 @@ +# Excalidraw - Architecture Diagram Generator + +Generate architecture diagrams as `.excalidraw` files from codebase analysis, with optional PNG and SVG export. + +--- + +## Installation + +```bash +# Add the ccc marketplace (if not already added) +/plugin marketplace add ooiyeefei/ccc + +# Install the skills collection +/plugin install ccc-skills@ccc +``` + +## Quick Start + +After installing, just ask Claude Code: + +``` +"Generate an architecture diagram for this project" +"Create an excalidraw diagram of the system" +"Visualize this codebase as an excalidraw file" +``` + +Claude Code will analyze any codebase (Node.js, Python, Java, Go, etc.), identify components, map relationships, and generate a valid `.excalidraw` JSON file. + +## Features + +- **Any codebase**: Discovers services, databases, APIs, and infrastructure from source code +- **No prerequisites**: Works without existing diagrams, Terraform, or specific file types +- **Proper arrows**: 90-degree elbow arrows with correct edge binding (not curved) +- **Color-coded**: Components styled by type (database, API, storage, AI/ML, etc.) +- **Cloud palettes**: AWS, Azure, GCP, and Kubernetes color schemes +- **Multiple layouts**: Vertical flow, horizontal pipeline, hub-and-spoke patterns +- **PNG/SVG export**: Optionally export to PNG and/or SVG via Playwright + +## PNG/SVG Export + +After generating a diagram, Claude Code will ask if you want to export to PNG, SVG, or both. + +The export uses `@excalidraw/utils` loaded in a Playwright browser โ€” fully programmatic, no manual upload to excalidraw.com needed. + +**Requirements:** Playwright MCP tools must be available. + +**Output:** Exported files are saved alongside the `.excalidraw` file: +``` +docs/architecture/ +โ”œโ”€โ”€ system-architecture.excalidraw # Editable diagram +โ”œโ”€โ”€ system-architecture.svg # Vector export +โ””โ”€โ”€ system-architecture.png # Raster export +``` + +## Output + +- **Location**: `docs/architecture/` or user-specified path +- **Format**: `.excalidraw` JSON (editable in [excalidraw.com](https://excalidraw.com) or VS Code extension) +- **Exports**: `.svg` and `.png` viewable directly or embeddable in documentation + +## Reference Files + +The skill includes detailed reference documentation: + +| File | Contents | +|------|----------| +| `references/json-format.md` | Element types, required properties, text bindings | +| `references/arrows.md` | Routing algorithm, patterns, bindings, staggering | +| `references/colors.md` | Default, AWS, Azure, GCP, K8s palettes | +| `references/examples.md` | Complete JSON examples, layout patterns | +| `references/validation.md` | Checklists, validation algorithm, bug fixes | +| `references/export.md` | PNG/SVG export procedure via Playwright | + +## License + +MIT diff --git a/src/plugins/excalidraw/snippets/SKILL.txt b/src/plugins/excalidraw/snippets/SKILL.txt new file mode 100755 index 0000000..26c8764 --- /dev/null +++ b/src/plugins/excalidraw/snippets/SKILL.txt @@ -0,0 +1,279 @@ +--- +name: excalidraw +description: Generate architecture diagrams as .excalidraw files from codebase analysis, with optional PNG/SVG export. Use when the user asks to create architecture diagrams, system diagrams, visualize codebase structure, generate excalidraw files, export excalidraw diagrams to PNG or SVG, or convert .excalidraw files to image formats. +--- + +# Excalidraw Diagram Generator + +Generate architecture diagrams as `.excalidraw` files directly from codebase analysis, with optional export to PNG and SVG. + +--- + +## Quick Start + +**User just asks:** +``` +"Generate an architecture diagram for this project" +"Create an excalidraw diagram of the system" +"Visualize this codebase as an excalidraw file" +``` + +**Claude Code will:** +1. Analyze the codebase (any language/framework) +2. Identify components, services, databases, APIs +3. Map relationships and data flows +4. Generate valid `.excalidraw` JSON with dynamic IDs and labels +5. Optionally export to PNG and/or SVG using Playwright + +**No prerequisites:** Works without existing diagrams, Terraform, or specific file types. + +--- + +## Critical Rules + +### 1. NEVER Use Diamond Shapes + +Diamond arrow connections are broken in raw Excalidraw JSON. Use styled rectangles instead: + +| Semantic Meaning | Rectangle Style | +|------------------|-----------------| +| Orchestrator/Hub | Coral (`#ffa8a8`/`#c92a2a`) + strokeWidth: 3 | +| Decision Point | Orange (`#ffd8a8`/`#e8590c`) + dashed stroke | + +### 2. Labels Require TWO Elements + +The `label` property does NOT work in raw JSON. Every labeled shape needs: + +```json +// 1. Shape with boundElements reference +{ + "id": "my-box", + "type": "rectangle", + "boundElements": [{ "type": "text", "id": "my-box-text" }] +} + +// 2. Separate text element with containerId +{ + "id": "my-box-text", + "type": "text", + "containerId": "my-box", + "text": "My Label" +} +``` + +### 3. Elbow Arrows Need Three Properties + +For 90-degree corners (not curved): + +```json +{ + "type": "arrow", + "roughness": 0, // Clean lines + "roundness": null, // Sharp corners + "elbowed": true // 90-degree mode +} +``` + +### 4. Arrow Edge Calculations + +Arrows must start/end at shape edges, not centers: + +| Edge | Formula | +|------|---------| +| Top | `(x + width/2, y)` | +| Bottom | `(x + width/2, y + height)` | +| Left | `(x, y + height/2)` | +| Right | `(x + width, y + height/2)` | + +**Detailed arrow routing:** See `references/arrows.md` + +--- + +## Element Types + +| Type | Use For | +|------|---------| +| `rectangle` | Services, databases, containers, orchestrators | +| `ellipse` | Users, external systems, start/end points | +| `text` | Labels inside shapes, titles, annotations | +| `arrow` | Data flow, connections, dependencies | +| `line` | Grouping boundaries, separators | + +**Full JSON format:** See `references/json-format.md` + +--- + +## Workflow + +### Step 1: Analyze Codebase + +Discover components by looking for: + +| Codebase Type | What to Look For | +|---------------|------------------| +| Monorepo | `packages/*/package.json`, workspace configs | +| Microservices | `docker-compose.yml`, k8s manifests | +| IaC | Terraform/Pulumi resource definitions | +| Backend API | Route definitions, controllers, DB models | +| Frontend | Component hierarchy, API calls | + +**Use tools:** +- `Glob` โ†’ `**/package.json`, `**/Dockerfile`, `**/*.tf` +- `Grep` โ†’ `app.get`, `@Controller`, `CREATE TABLE` +- `Read` โ†’ README, config files, entry points + +### Step 2: Plan Layout + +**Vertical flow (most common):** +``` +Row 1: Users/Entry points (y: 100) +Row 2: Frontend/Gateway (y: 230) +Row 3: Orchestration (y: 380) +Row 4: Services (y: 530) +Row 5: Data layer (y: 680) + +Columns: x = 100, 300, 500, 700, 900 +Element size: 160-200px x 80-90px +``` + +**Other patterns:** See `references/examples.md` + +### Step 3: Generate Elements + +For each component: +1. Create shape with unique `id` +2. Add `boundElements` referencing text +3. Create text with `containerId` +4. Choose color based on type + +**Color palettes:** See `references/colors.md` + +### Step 4: Add Connections + +For each relationship: +1. Calculate source edge point +2. Plan elbow route (avoid overlaps) +3. Create arrow with `points` array +4. Match stroke color to destination type + +**Arrow patterns:** See `references/arrows.md` + +### Step 5: Add Grouping (Optional) + +For logical groupings: +- Large transparent rectangle with `strokeStyle: "dashed"` +- Standalone text label at top-left + +### Step 6: Validate and Write + +Run validation before writing. Save to `docs/` or user-specified path. + +**Validation checklist:** See `references/validation.md` + +### Step 7: Export to PNG/SVG (Optional) + +After writing the `.excalidraw` file, ask the user if they want PNG, SVG, or both exports. + +Uses Playwright MCP tools and `@excalidraw/utils` to programmatically render the diagram โ€” no manual upload to excalidraw.com needed. + +**Requires:** Playwright MCP tools (`browser_navigate`, `browser_run_code`, `browser_close`). + +**Full export procedure:** See `references/export.md` + +--- + +## Quick Arrow Reference + +**Straight down:** +```json +{ "points": [[0, 0], [0, 110]], "x": 590, "y": 290 } +``` + +**L-shape (left then down):** +```json +{ "points": [[0, 0], [-325, 0], [-325, 125]], "x": 525, "y": 420 } +``` + +**U-turn (callback):** +```json +{ "points": [[0, 0], [50, 0], [50, -125], [20, -125]], "x": 710, "y": 440 } +``` + +**Arrow width/height** = bounding box of points: +``` +points [[0,0], [-440,0], [-440,70]] โ†’ width=440, height=70 +``` + +**Multiple arrows from same edge** - stagger positions: +``` +5 arrows: 20%, 35%, 50%, 65%, 80% across edge width +``` + +--- + +## Default Color Palette + +| Component | Background | Stroke | +|-----------|------------|--------| +| Frontend | `#a5d8ff` | `#1971c2` | +| Backend/API | `#d0bfff` | `#7048e8` | +| Database | `#b2f2bb` | `#2f9e44` | +| Storage | `#ffec99` | `#f08c00` | +| AI/ML | `#e599f7` | `#9c36b5` | +| External APIs | `#ffc9c9` | `#e03131` | +| Orchestration | `#ffa8a8` | `#c92a2a` | +| Message Queue | `#fff3bf` | `#fab005` | +| Cache | `#ffe8cc` | `#fd7e14` | +| Users | `#e7f5ff` | `#1971c2` | + +**Cloud-specific palettes:** See `references/colors.md` + +--- + +## Quick Validation Checklist + +Before writing file: +- [ ] Every shape with label has boundElements + text element +- [ ] Text elements have containerId matching shape +- [ ] Multi-point arrows have `elbowed: true`, `roundness: null` +- [ ] Arrow x,y = source shape edge point +- [ ] Arrow final point offset reaches target edge +- [ ] No diamond shapes +- [ ] No duplicate IDs + +**Full validation algorithm:** See `references/validation.md` + +--- + +## Common Issues + +| Issue | Fix | +|-------|-----| +| Labels don't appear | Use TWO elements (shape + text), not `label` property | +| Arrows curved | Add `elbowed: true`, `roundness: null`, `roughness: 0` | +| Arrows floating | Calculate x,y from shape edge, not center | +| Arrows overlapping | Stagger start positions across edge | + +**Detailed bug fixes:** See `references/validation.md` + +--- + +## Reference Files + +| File | Contents | +|------|----------| +| `references/json-format.md` | Element types, required properties, text bindings | +| `references/arrows.md` | Routing algorithm, patterns, bindings, staggering | +| `references/colors.md` | Default, AWS, Azure, GCP, K8s palettes | +| `references/examples.md` | Complete JSON examples, layout patterns | +| `references/validation.md` | Checklists, validation algorithm, bug fixes | +| `references/export.md` | PNG/SVG export procedure via Playwright | + +--- + +## Output + +- **Location:** `docs/architecture/` or user-specified +- **Filename:** Descriptive, e.g., `system-architecture.excalidraw` +- **Exports (optional):** `system-architecture.svg` and/or `system-architecture.png` in same directory +- **Testing:** Open `.excalidraw` in https://excalidraw.com or VS Code extension; `.svg` and `.png` can be viewed directly or embedded in documentation diff --git a/src/plugins/excalidraw/snippets/references/arrows.txt b/src/plugins/excalidraw/snippets/references/arrows.txt new file mode 100755 index 0000000..9ce666d --- /dev/null +++ b/src/plugins/excalidraw/snippets/references/arrows.txt @@ -0,0 +1,288 @@ +# Arrow Routing Reference + +Complete guide for creating elbow arrows with proper connections. + +--- + +## Critical: Elbow Arrow Properties + +Three required properties for 90-degree corners: + +```json +{ + "type": "arrow", + "roughness": 0, // Clean lines + "roundness": null, // Sharp corners (not curved) + "elbowed": true // Enables elbow mode +} +``` + +**Without these, arrows will be curved, not 90-degree elbows.** + +--- + +## Edge Calculation Formulas + +| Shape Type | Edge | Formula | +|------------|------|---------| +| Rectangle | Top | `(x + width/2, y)` | +| Rectangle | Bottom | `(x + width/2, y + height)` | +| Rectangle | Left | `(x, y + height/2)` | +| Rectangle | Right | `(x + width, y + height/2)` | +| Ellipse | Top | `(x + width/2, y)` | +| Ellipse | Bottom | `(x + width/2, y + height)` | + +--- + +## Universal Arrow Routing Algorithm + +``` +FUNCTION createArrow(source, target, sourceEdge, targetEdge): + // Step 1: Get source edge point + sourcePoint = getEdgePoint(source, sourceEdge) + + // Step 2: Get target edge point + targetPoint = getEdgePoint(target, targetEdge) + + // Step 3: Calculate offsets + dx = targetPoint.x - sourcePoint.x + dy = targetPoint.y - sourcePoint.y + + // Step 4: Determine routing pattern + IF sourceEdge == "bottom" AND targetEdge == "top": + IF abs(dx) < 10: // Nearly aligned + points = [[0, 0], [0, dy]] + ELSE: // Need L-shape + points = [[0, 0], [dx, 0], [dx, dy]] + + ELSE IF sourceEdge == "right" AND targetEdge == "left": + IF abs(dy) < 10: + points = [[0, 0], [dx, 0]] + ELSE: + points = [[0, 0], [0, dy], [dx, dy]] + + ELSE IF sourceEdge == targetEdge: // U-turn + clearance = 50 + IF sourceEdge == "right": + points = [[0, 0], [clearance, 0], [clearance, dy], [dx, dy]] + ELSE IF sourceEdge == "bottom": + points = [[0, 0], [0, clearance], [dx, clearance], [dx, dy]] + + // Step 5: Calculate bounding box + width = max(abs(p[0]) for p in points) + height = max(abs(p[1]) for p in points) + + RETURN {x: sourcePoint.x, y: sourcePoint.y, points, width, height} + +FUNCTION getEdgePoint(shape, edge): + SWITCH edge: + "top": RETURN (shape.x + shape.width/2, shape.y) + "bottom": RETURN (shape.x + shape.width/2, shape.y + shape.height) + "left": RETURN (shape.x, shape.y + shape.height/2) + "right": RETURN (shape.x + shape.width, shape.y + shape.height/2) +``` + +--- + +## Arrow Patterns Reference + +| Pattern | Points | Use Case | +|---------|--------|----------| +| Down | `[[0,0], [0,h]]` | Vertical connection | +| Right | `[[0,0], [w,0]]` | Horizontal connection | +| L-left-down | `[[0,0], [-w,0], [-w,h]]` | Go left, then down | +| L-right-down | `[[0,0], [w,0], [w,h]]` | Go right, then down | +| L-down-left | `[[0,0], [0,h], [-w,h]]` | Go down, then left | +| L-down-right | `[[0,0], [0,h], [w,h]]` | Go down, then right | +| S-shape | `[[0,0], [0,h1], [w,h1], [w,h2]]` | Navigate around obstacles | +| U-turn | `[[0,0], [w,0], [w,-h], [0,-h]]` | Callback/return arrows | + +--- + +## Worked Examples + +### Vertical Connection (Bottom to Top) + +``` +Source: x=500, y=200, width=180, height=90 +Target: x=500, y=400, width=180, height=90 + +source_bottom = (500 + 180/2, 200 + 90) = (590, 290) +target_top = (500 + 180/2, 400) = (590, 400) + +Arrow x = 590, y = 290 +Distance = 400 - 290 = 110 +Points = [[0, 0], [0, 110]] +``` + +### Fan-out (One to Many) + +``` +Orchestrator: x=570, y=400, width=140, height=80 +Target: x=120, y=550, width=160, height=80 + +orchestrator_bottom = (570 + 140/2, 400 + 80) = (640, 480) +target_top = (120 + 160/2, 550) = (200, 550) + +Arrow x = 640, y = 480 +Horizontal offset = 200 - 640 = -440 +Vertical offset = 550 - 480 = 70 + +Points = [[0, 0], [-440, 0], [-440, 70]] // Left first, then down +``` + +### U-turn (Callback) + +``` +Source: x=570, y=400, width=140, height=80 +Target: x=550, y=270, width=180, height=90 +Connection: Right of source -> Right of target + +source_right = (570 + 140, 400 + 80/2) = (710, 440) +target_right = (550 + 180, 270 + 90/2) = (730, 315) + +Arrow x = 710, y = 440 +Vertical distance = 315 - 440 = -125 +Final x offset = 730 - 710 = 20 + +Points = [[0, 0], [50, 0], [50, -125], [20, -125]] +// Right 50px (clearance), up 125px, left 30px +``` + +--- + +## Staggering Multiple Arrows + +When N arrows leave from same edge, spread evenly: + +``` +FUNCTION getStaggeredPositions(shape, edge, numArrows): + positions = [] + FOR i FROM 0 TO numArrows-1: + percentage = 0.2 + (0.6 * i / (numArrows - 1)) + + IF edge == "bottom" OR edge == "top": + x = shape.x + shape.width * percentage + y = (edge == "bottom") ? shape.y + shape.height : shape.y + ELSE: + x = (edge == "right") ? shape.x + shape.width : shape.x + y = shape.y + shape.height * percentage + + positions.append({x, y}) + RETURN positions + +// Examples: +// 2 arrows: 20%, 80% +// 3 arrows: 20%, 50%, 80% +// 5 arrows: 20%, 35%, 50%, 65%, 80% +``` + +--- + +## Arrow Bindings + +For better visual attachment, use `startBinding` and `endBinding`: + +```json +{ + "id": "arrow-workflow-convert", + "type": "arrow", + "x": 525, + "y": 420, + "width": 325, + "height": 125, + "points": [[0, 0], [-325, 0], [-325, 125]], + "roughness": 0, + "roundness": null, + "elbowed": true, + "startBinding": { + "elementId": "cloud-workflows", + "focus": 0, + "gap": 1, + "fixedPoint": [0.5, 1] + }, + "endBinding": { + "elementId": "convert-pdf-service", + "focus": 0, + "gap": 1, + "fixedPoint": [0.5, 0] + }, + "startArrowhead": null, + "endArrowhead": "arrow" +} +``` + +### fixedPoint Values + +- Top center: `[0.5, 0]` +- Bottom center: `[0.5, 1]` +- Left center: `[0, 0.5]` +- Right center: `[1, 0.5]` + +### Update Shape boundElements + +```json +{ + "id": "cloud-workflows", + "boundElements": [ + { "type": "text", "id": "cloud-workflows-text" }, + { "type": "arrow", "id": "arrow-workflow-convert" } + ] +} +``` + +--- + +## Bidirectional Arrows + +For two-way data flows: + +```json +{ + "type": "arrow", + "startArrowhead": "arrow", + "endArrowhead": "arrow" +} +``` + +Arrowhead options: `null`, `"arrow"`, `"bar"`, `"dot"`, `"triangle"` + +--- + +## Arrow Labels + +Position standalone text near arrow midpoint: + +```json +{ + "id": "arrow-api-db-label", + "type": "text", + "x": 305, // Arrow x + offset + "y": 245, // Arrow midpoint + "text": "SQL", + "fontSize": 12, + "containerId": null, + "backgroundColor": "#ffffff" +} +``` + +**Positioning formula:** +- Vertical: `label.y = arrow.y + (total_height / 2)` +- Horizontal: `label.x = arrow.x + (total_width / 2)` +- L-shaped: Position at corner or longest segment midpoint + +--- + +## Width/Height Calculation + +Arrow `width` and `height` = bounding box of path: + +``` +points = [[0, 0], [-440, 0], [-440, 70]] +width = abs(-440) = 440 +height = abs(70) = 70 + +points = [[0, 0], [50, 0], [50, -125], [20, -125]] +width = max(abs(50), abs(20)) = 50 +height = abs(-125) = 125 +``` diff --git a/src/plugins/excalidraw/snippets/references/colors.txt b/src/plugins/excalidraw/snippets/references/colors.txt new file mode 100755 index 0000000..0f7eec0 --- /dev/null +++ b/src/plugins/excalidraw/snippets/references/colors.txt @@ -0,0 +1,91 @@ +# Color Palettes Reference + +Color schemes for different platforms and component types. + +--- + +## Default Palette (Platform-Agnostic) + +| Component Type | Background | Stroke | Example | +|----------------|------------|--------|---------| +| Frontend/UI | `#a5d8ff` | `#1971c2` | Next.js, React apps | +| Backend/API | `#d0bfff` | `#7048e8` | API servers, processors | +| Database | `#b2f2bb` | `#2f9e44` | PostgreSQL, MySQL, MongoDB | +| Storage | `#ffec99` | `#f08c00` | Object storage, file systems | +| AI/ML Services | `#e599f7` | `#9c36b5` | ML models, AI APIs | +| External APIs | `#ffc9c9` | `#e03131` | Third-party services | +| Orchestration | `#ffa8a8` | `#c92a2a` | Workflows, schedulers | +| Validation | `#ffd8a8` | `#e8590c` | Validators, checkers | +| Network/Security | `#dee2e6` | `#495057` | VPC, IAM, firewalls | +| Classification | `#99e9f2` | `#0c8599` | Routers, classifiers | +| Users/Actors | `#e7f5ff` | `#1971c2` | User ellipses | +| Message Queue | `#fff3bf` | `#fab005` | Kafka, RabbitMQ, SQS | +| Cache | `#ffe8cc` | `#fd7e14` | Redis, Memcached | +| Monitoring | `#d3f9d8` | `#40c057` | Prometheus, Grafana | + +--- + +## AWS Palette + +| Service Category | Background | Stroke | +|-----------------|------------|--------| +| Compute (EC2, Lambda, ECS) | `#ff9900` | `#cc7a00` | +| Storage (S3, EBS) | `#3f8624` | `#2d6119` | +| Database (RDS, DynamoDB) | `#3b48cc` | `#2d3899` | +| Networking (VPC, Route53) | `#8c4fff` | `#6b3dcc` | +| Security (IAM, KMS) | `#dd344c` | `#b12a3d` | +| Analytics (Kinesis, Athena) | `#8c4fff` | `#6b3dcc` | +| ML (SageMaker, Bedrock) | `#01a88d` | `#017d69` | + +--- + +## Azure Palette + +| Service Category | Background | Stroke | +|-----------------|------------|--------| +| Compute | `#0078d4` | `#005a9e` | +| Storage | `#50e6ff` | `#3cb5cc` | +| Database | `#0078d4` | `#005a9e` | +| Networking | `#773adc` | `#5a2ca8` | +| Security | `#ff8c00` | `#cc7000` | +| AI/ML | `#50e6ff` | `#3cb5cc` | + +--- + +## GCP Palette + +| Service Category | Background | Stroke | +|-----------------|------------|--------| +| Compute (GCE, Cloud Run) | `#4285f4` | `#3367d6` | +| Storage (GCS) | `#34a853` | `#2d8e47` | +| Database (Cloud SQL, Firestore) | `#ea4335` | `#c53929` | +| Networking | `#fbbc04` | `#d99e04` | +| AI/ML (Vertex AI) | `#9334e6` | `#7627b8` | + +--- + +## Kubernetes Palette + +| Component | Background | Stroke | +|-----------|------------|--------| +| Pod | `#326ce5` | `#2756b8` | +| Service | `#326ce5` | `#2756b8` | +| Deployment | `#326ce5` | `#2756b8` | +| ConfigMap/Secret | `#7f8c8d` | `#626d6e` | +| Ingress | `#00d4aa` | `#00a888` | +| Node | `#303030` | `#1a1a1a` | +| Namespace | `#f0f0f0` | `#c0c0c0` (dashed) | + +--- + +## Diagram Type Suggestions + +| Diagram Type | Recommended Layout | Key Elements | +|--------------|-------------------|--------------| +| Microservices | Vertical flow | Services, databases, queues, API gateway | +| Data Pipeline | Horizontal flow | Sources, transformers, sinks, storage | +| Event-Driven | Hub-and-spoke | Event bus center, producers/consumers | +| Kubernetes | Layered groups | Namespace boxes, pods inside deployments | +| CI/CD | Horizontal flow | Source -> Build -> Test -> Deploy -> Monitor | +| Network | Hierarchical | Internet -> LB -> VPC -> Subnets -> Instances | +| User Flow | Swimlanes | User actions, system responses, external calls | diff --git a/src/plugins/excalidraw/snippets/references/examples.txt b/src/plugins/excalidraw/snippets/references/examples.txt new file mode 100755 index 0000000..0574b60 --- /dev/null +++ b/src/plugins/excalidraw/snippets/references/examples.txt @@ -0,0 +1,381 @@ +# Complete Examples Reference + +Full JSON examples showing proper element structure. + +--- + +## 3-Tier Architecture Example + +This is a REFERENCE showing JSON structure. Replace IDs, labels, positions, and colors based on discovered components. + +```json +{ + "type": "excalidraw", + "version": 2, + "source": "claude-code-excalidraw-skill", + "elements": [ + { + "id": "user", + "type": "ellipse", + "x": 150, + "y": 50, + "width": 100, + "height": 60, + "angle": 0, + "strokeColor": "#1971c2", + "backgroundColor": "#e7f5ff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { "type": 2 }, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [{ "type": "text", "id": "user-text" }], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "user-text", + "type": "text", + "x": 175, + "y": 67, + "width": 50, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 2, + "version": 1, + "versionNonce": 2, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "User", + "fontSize": 16, + "fontFamily": 1, + "textAlign": "center", + "verticalAlign": "middle", + "baseline": 14, + "containerId": "user", + "originalText": "User", + "lineHeight": 1.25 + }, + { + "id": "frontend", + "type": "rectangle", + "x": 100, + "y": 180, + "width": 200, + "height": 80, + "angle": 0, + "strokeColor": "#1971c2", + "backgroundColor": "#a5d8ff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { "type": 3 }, + "seed": 3, + "version": 1, + "versionNonce": 3, + "isDeleted": false, + "boundElements": [{ "type": "text", "id": "frontend-text" }], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "frontend-text", + "type": "text", + "x": 105, + "y": 195, + "width": 190, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 4, + "version": 1, + "versionNonce": 4, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "Frontend\nNext.js", + "fontSize": 16, + "fontFamily": 1, + "textAlign": "center", + "verticalAlign": "middle", + "baseline": 14, + "containerId": "frontend", + "originalText": "Frontend\nNext.js", + "lineHeight": 1.25 + }, + { + "id": "database", + "type": "rectangle", + "x": 100, + "y": 330, + "width": 200, + "height": 80, + "angle": 0, + "strokeColor": "#2f9e44", + "backgroundColor": "#b2f2bb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { "type": 3 }, + "seed": 5, + "version": 1, + "versionNonce": 5, + "isDeleted": false, + "boundElements": [{ "type": "text", "id": "database-text" }], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "database-text", + "type": "text", + "x": 105, + "y": 345, + "width": 190, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 6, + "version": 1, + "versionNonce": 6, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "Database\nPostgreSQL", + "fontSize": 16, + "fontFamily": 1, + "textAlign": "center", + "verticalAlign": "middle", + "baseline": 14, + "containerId": "database", + "originalText": "Database\nPostgreSQL", + "lineHeight": 1.25 + }, + { + "id": "arrow-user-frontend", + "type": "arrow", + "x": 200, + "y": 115, + "width": 0, + "height": 60, + "angle": 0, + "strokeColor": "#1971c2", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 7, + "version": 1, + "versionNonce": 7, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "points": [[0, 0], [0, 60]], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": true + }, + { + "id": "arrow-frontend-database", + "type": "arrow", + "x": 200, + "y": 265, + "width": 0, + "height": 60, + "angle": 0, + "strokeColor": "#2f9e44", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 8, + "version": 1, + "versionNonce": 8, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "points": [[0, 0], [0, 60]], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": true + } + ], + "appState": { + "gridSize": 20, + "viewBackgroundColor": "#ffffff" + }, + "files": {} +} +``` + +--- + +## Layout Patterns + +### Vertical Flow (Most Common) + +``` +Grid positioning: +- Column width: 200-250px +- Row height: 130-150px +- Element size: 160-200px x 80-90px +- Spacing: 40-50px between elements + +Row positions (y): + Row 0: 20 (title) + Row 1: 100 (users/entry points) + Row 2: 230 (frontend/gateway) + Row 3: 380 (orchestration) + Row 4: 530 (services) + Row 5: 680 (data layer) + Row 6: 830 (external services) + +Column positions (x): + Col 0: 100 + Col 1: 300 + Col 2: 500 + Col 3: 700 + Col 4: 900 +``` + +### Horizontal Flow (Pipelines) + +``` +Stage positions (x): + Stage 0: 100 (input/source) + Stage 1: 350 (transform 1) + Stage 2: 600 (transform 2) + Stage 3: 850 (transform 3) + Stage 4: 1100 (output/sink) + +All stages at same y: 200 +Arrows: "right" -> "left" connections +``` + +### Hub-and-Spoke + +``` +Center hub: x=500, y=350 +8 positions at 45ยฐ increments: + N: (500, 150) + NE: (640, 210) + E: (700, 350) + SE: (640, 490) + S: (500, 550) + SW: (360, 490) + W: (300, 350) + NW: (360, 210) +``` + +--- + +## Complex Architecture Layout + +``` +Row 0: Title/Header (y: 20) +Row 1: Users/Clients (y: 80) +Row 2: Frontend/Gateway (y: 200) +Row 3: Orchestration (y: 350) +Row 4: Processing Services (y: 550) +Row 5: Data Layer (y: 680) +Row 6: External Services (y: 830) + +Columns (x): + Col 0: 120 + Col 1: 320 + Col 2: 520 + Col 3: 720 + Col 4: 920 +``` + +--- + +## Diagram Complexity Guidelines + +| Complexity | Max Elements | Max Arrows | Approach | +|------------|-------------|------------|----------| +| Simple | 5-10 | 5-10 | Single file, no groups | +| Medium | 10-25 | 15-30 | Use grouping rectangles | +| Complex | 25-50 | 30-60 | Split into multiple diagrams | +| Very Complex | 50+ | 60+ | Multiple focused diagrams | + +**When to split:** +- More than 50 elements +- Create: `architecture-overview.excalidraw`, `architecture-data-layer.excalidraw` + +**When to use groups:** +- 3+ related services +- Same deployment unit +- Logical boundaries (VPC, Security Zone) diff --git a/src/plugins/excalidraw/snippets/references/export.txt b/src/plugins/excalidraw/snippets/references/export.txt new file mode 100755 index 0000000..f059aa6 --- /dev/null +++ b/src/plugins/excalidraw/snippets/references/export.txt @@ -0,0 +1,124 @@ +# Export to PNG/SVG + +Export `.excalidraw` files to PNG and/or SVG using Playwright MCP tools and `@excalidraw/utils`. + +## Prerequisites + +- Playwright MCP tools available: `browser_navigate`, `browser_run_code`, `browser_close` +- Python 3 installed (for local HTTP server) + +## Procedure + +### 1. Start a Local HTTP Server + +A browser origin is required for dynamic ESM imports. Start a temporary server on any available port: + +```bash +python3 -m http.server 8765 & +SERVER_PID=$! +``` + +### 2. Navigate Playwright to the Server + +``` +browser_navigate โ†’ http://localhost:8765/ +``` + +The 404 page is fine โ€” we only need the HTTP origin for the dynamic import to work. + +### 3. Read the .excalidraw File + +Use the Read tool to get the `.excalidraw` file contents as a string. This JSON string will be passed into the browser context in the next steps. + +### 4. Export SVG + +Use `browser_run_code` with the following pattern. Replace `EXCALIDRAW_JSON_HERE` with the actual JSON string from Step 3: + +```javascript +async (page) => { + const excalidrawJson = `EXCALIDRAW_JSON_HERE`; + + const svgString = await page.evaluate(async (json) => { + const utils = await import('https://esm.sh/@excalidraw/utils@0.1.2'); + const { exportToSvg } = utils.default; + const data = JSON.parse(json); + const svg = await exportToSvg({ + elements: data.elements, + appState: { ...data.appState, exportBackground: true }, + files: data.files || {} + }); + return svg.outerHTML; + }, excalidrawJson); + + return svgString; +} +``` + +Write the returned SVG string directly to `.svg` using the Write tool. + +### 5. Export PNG + +Use `browser_run_code` with the following pattern: + +```javascript +async (page) => { + const excalidrawJson = `EXCALIDRAW_JSON_HERE`; + + const pngBase64 = await page.evaluate(async (json) => { + const utils = await import('https://esm.sh/@excalidraw/utils@0.1.2'); + const { exportToBlob } = utils.default; + const data = JSON.parse(json); + const blob = await exportToBlob({ + elements: data.elements, + appState: { ...data.appState, exportBackground: true }, + files: data.files || {}, + mimeType: 'image/png' + }); + const reader = new FileReader(); + return new Promise((resolve) => { + reader.onloadend = () => resolve(reader.result); + reader.readAsDataURL(blob); + }); + }, excalidrawJson); + + return pngBase64; +} +``` + +The result is a base64 data URL. Decode and write to `.png`: + +```bash +echo "" | base64 -d > .png +``` + +Strip the `data:image/png;base64,` prefix before decoding. + +### 6. Clean Up + +Close the browser and kill the HTTP server: + +``` +browser_close +``` + +```bash +kill $SERVER_PID +``` + +## Key Details + +- **Import path**: Export functions are on `utils.default`, not named exports โ€” this is how `esm.sh` wraps the `@excalidraw/utils` package +- **Console errors**: ` attribute y: Expected length` warnings are cosmetic โ€” exports are valid +- **Background**: `exportBackground: true` includes the white background in exports +- **Output location**: Save exported files alongside the `.excalidraw` file with matching filename (e.g., `system-architecture.excalidraw` โ†’ `system-architecture.svg`, `system-architecture.png`) +- **Visual fidelity**: Both exports produce the same visual output as opening in excalidraw.com + +## Troubleshooting + +| Issue | Fix | +|-------|-----| +| Port already in use | Try a different port: `python3 -m http.server 9876 &` | +| Dynamic import fails | Check network connectivity; `esm.sh` CDN must be reachable | +| Playwright tools not available | Ensure Playwright MCP server is configured and running | +| PNG is blank/corrupted | Verify the base64 prefix was stripped before decoding | +| SVG missing text | Cosmetic only โ€” text renders correctly when opened in a browser | diff --git a/src/plugins/excalidraw/snippets/references/json-format.txt b/src/plugins/excalidraw/snippets/references/json-format.txt new file mode 100755 index 0000000..213ada4 --- /dev/null +++ b/src/plugins/excalidraw/snippets/references/json-format.txt @@ -0,0 +1,210 @@ +# Excalidraw JSON Format Reference + +Complete reference for Excalidraw JSON structure and element types. + +--- + +## File Structure + +```json +{ + "type": "excalidraw", + "version": 2, + "source": "claude-code-excalidraw-skill", + "elements": [], + "appState": { + "gridSize": 20, + "viewBackgroundColor": "#ffffff" + }, + "files": {} +} +``` + +--- + +## Element Types + +| Type | Use For | Arrow Reliability | +|------|---------|-------------------| +| `rectangle` | Services, components, databases, containers, orchestrators, decision points | Excellent | +| `ellipse` | Users, external systems, start/end points | Good | +| `text` | Labels inside shapes, titles, annotations | N/A | +| `arrow` | Data flow, connections, dependencies | N/A | +| `line` | Grouping boundaries, separators | N/A | + +### BANNED: Diamond Shapes + +**NEVER use `type: "diamond"` in generated diagrams.** + +Diamond arrow connections are fundamentally broken in raw Excalidraw JSON: +- Excalidraw applies `roundness` to diamond vertices during rendering +- Visual edges appear offset from mathematical edge points +- No offset formula reliably compensates +- Arrows appear disconnected/floating + +**Use styled rectangles instead** for visual distinction: + +| Semantic Meaning | Rectangle Style | +|------------------|-----------------| +| Orchestrator/Hub | Coral (`#ffa8a8`/`#c92a2a`) + strokeWidth: 3 | +| Decision Point | Orange (`#ffd8a8`/`#e8590c`) + dashed stroke | +| Central Router | Larger size + bold color | + +--- + +## Required Element Properties + +Every element MUST have these properties: + +```json +{ + "id": "unique-id-string", + "type": "rectangle", + "x": 100, + "y": 100, + "width": 200, + "height": 80, + "angle": 0, + "strokeColor": "#1971c2", + "backgroundColor": "#a5d8ff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": { "type": 3 }, + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false +} +``` + +--- + +## Text Inside Shapes (Labels) + +**Every labeled shape requires TWO elements:** + +### Shape with boundElements + +```json +{ + "id": "{component-id}", + "type": "rectangle", + "x": 500, + "y": 200, + "width": 200, + "height": 90, + "strokeColor": "#1971c2", + "backgroundColor": "#a5d8ff", + "boundElements": [{ "type": "text", "id": "{component-id}-text" }], + // ... other required properties +} +``` + +### Text with containerId + +```json +{ + "id": "{component-id}-text", + "type": "text", + "x": 505, // shape.x + 5 + "y": 220, // shape.y + (shape.height - text.height) / 2 + "width": 190, // shape.width - 10 + "height": 50, + "text": "{Component Name}\n{Subtitle}", + "fontSize": 16, + "fontFamily": 1, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "{component-id}", + "originalText": "{Component Name}\n{Subtitle}", + "lineHeight": 1.25, + // ... other required properties +} +``` + +### DO NOT Use the `label` Property + +The `label` property is for the JavaScript API, NOT raw JSON files: + +```json +// WRONG - will show empty boxes +{ "type": "rectangle", "label": { "text": "My Label" } } + +// CORRECT - requires TWO elements +// 1. Shape with boundElements reference +// 2. Separate text element with containerId +``` + +### Text Positioning + +- Text `x` = shape `x` + 5 +- Text `y` = shape `y` + (shape.height - text.height) / 2 +- Text `width` = shape `width` - 10 +- Use `\n` for multi-line labels +- Always use `textAlign: "center"` and `verticalAlign: "middle"` + +### ID Naming Convention + +Always use pattern: `{shape-id}-text` for text element IDs. + +--- + +## Dynamic ID Generation + +IDs and labels are generated from codebase analysis: + +| Discovered Component | Generated ID | Generated Label | +|---------------------|--------------|-----------------| +| Express API server | `express-api` | `"API Server\nExpress.js"` | +| PostgreSQL database | `postgres-db` | `"PostgreSQL\nDatabase"` | +| Redis cache | `redis-cache` | `"Redis\nCache Layer"` | +| S3 bucket for uploads | `s3-uploads` | `"S3 Bucket\nuploads/"` | +| Lambda function | `lambda-processor` | `"Lambda\nProcessor"` | +| React frontend | `react-frontend` | `"React App\nFrontend"` | + +--- + +## Grouping with Dashed Rectangles + +For logical groupings (namespaces, VPCs, pipelines): + +```json +{ + "id": "group-ai-pipeline", + "type": "rectangle", + "x": 100, + "y": 500, + "width": 1000, + "height": 280, + "strokeColor": "#9c36b5", + "backgroundColor": "transparent", + "strokeStyle": "dashed", + "roughness": 0, + "roundness": null, + "boundElements": null +} +``` + +Group labels are standalone text (no containerId) at top-left: + +```json +{ + "id": "group-ai-pipeline-label", + "type": "text", + "x": 120, + "y": 510, + "text": "AI Processing Pipeline (Cloud Run)", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null +} +``` diff --git a/src/plugins/excalidraw/snippets/references/validation.txt b/src/plugins/excalidraw/snippets/references/validation.txt new file mode 100755 index 0000000..774e2c9 --- /dev/null +++ b/src/plugins/excalidraw/snippets/references/validation.txt @@ -0,0 +1,182 @@ +# Validation Reference + +Checklists, validation algorithms, and common bug fixes. + +--- + +## Pre-Flight Validation Algorithm + +Run BEFORE writing the file: + +``` +FUNCTION validateDiagram(elements): + errors = [] + + // 1. Validate shape-text bindings + FOR each shape IN elements WHERE shape.boundElements != null: + FOR each binding IN shape.boundElements: + textElement = findById(elements, binding.id) + IF textElement == null: + errors.append("Shape {shape.id} references missing text {binding.id}") + ELSE IF textElement.containerId != shape.id: + errors.append("Text containerId doesn't match shape") + + // 2. Validate arrow connections + FOR each arrow IN elements WHERE arrow.type == "arrow": + sourceShape = findShapeNear(elements, arrow.x, arrow.y) + IF sourceShape == null: + errors.append("Arrow {arrow.id} doesn't start from shape edge") + + finalPoint = arrow.points[arrow.points.length - 1] + endX = arrow.x + finalPoint[0] + endY = arrow.y + finalPoint[1] + targetShape = findShapeNear(elements, endX, endY) + IF targetShape == null: + errors.append("Arrow {arrow.id} doesn't end at shape edge") + + IF arrow.points.length > 2: + IF arrow.elbowed != true: + errors.append("Arrow {arrow.id} missing elbowed:true") + IF arrow.roundness != null: + errors.append("Arrow {arrow.id} should have roundness:null") + + // 3. Validate unique IDs + ids = [el.id for el in elements] + duplicates = findDuplicates(ids) + IF duplicates.length > 0: + errors.append("Duplicate IDs: {duplicates}") + + // 4. Validate bounding boxes + FOR each arrow IN elements WHERE arrow.type == "arrow": + maxX = max(abs(p[0]) for p in arrow.points) + maxY = max(abs(p[1]) for p in arrow.points) + IF arrow.width < maxX OR arrow.height < maxY: + errors.append("Arrow {arrow.id} bounding box too small") + + RETURN errors + +FUNCTION findShapeNear(elements, x, y, tolerance=15): + FOR each shape IN elements WHERE shape.type IN ["rectangle", "ellipse"]: + edges = [ + (shape.x + shape.width/2, shape.y), // top + (shape.x + shape.width/2, shape.y + shape.height), // bottom + (shape.x, shape.y + shape.height/2), // left + (shape.x + shape.width, shape.y + shape.height/2) // right + ] + FOR each edge IN edges: + IF abs(edge.x - x) < tolerance AND abs(edge.y - y) < tolerance: + RETURN shape + RETURN null +``` + +--- + +## Checklists + +### Before Generating + +- [ ] Identified all components from codebase +- [ ] Mapped all connections/data flows +- [ ] Chose layout pattern (vertical, horizontal, hub-and-spoke) +- [ ] Selected color palette (default, AWS, Azure, K8s) +- [ ] Planned grid positions +- [ ] Created ID naming scheme + +### During Generation + +- [ ] Every labeled shape has BOTH shape AND text elements +- [ ] Shape has `boundElements: [{ "type": "text", "id": "{id}-text" }]` +- [ ] Text has `containerId: "{shape-id}"` +- [ ] Multi-point arrows have `elbowed: true`, `roundness: null`, `roughness: 0` +- [ ] Arrows have `startBinding` and `endBinding` +- [ ] No diamond shapes used +- [ ] Applied staggering formula for multiple arrows + +### Arrow Validation (Every Arrow) + +- [ ] Arrow `x,y` calculated from shape edge +- [ ] Final point offset = `targetEdge - sourceEdge` +- [ ] Arrow `width` = `max(abs(point[0]))` +- [ ] Arrow `height` = `max(abs(point[1]))` +- [ ] U-turn arrows have 40-60px clearance + +### After Generation + +- [ ] All `boundElements` IDs reference valid text elements +- [ ] All `containerId` values reference valid shapes +- [ ] All arrows start within 15px of shape edge +- [ ] All arrows end within 15px of shape edge +- [ ] No duplicate IDs +- [ ] Arrow bounding boxes match points +- [ ] File is valid JSON + +--- + +## Common Bugs and Fixes + +### Bug: Arrow appears disconnected/floating + +**Cause**: Arrow `x,y` not calculated from shape edge. + +**Fix**: +``` +Rectangle bottom: arrow_x = shape.x + shape.width/2 + arrow_y = shape.y + shape.height +``` + +### Bug: Arrow endpoint doesn't reach target + +**Cause**: Final point offset calculated incorrectly. + +**Fix**: +``` +target_edge = (target.x + target.width/2, target.y) +offset_x = target_edge.x - arrow.x +offset_y = target_edge.y - arrow.y +Final point = [offset_x, offset_y] +``` + +### Bug: Multiple arrows from same source overlap + +**Cause**: All arrows start from identical `x,y`. + +**Fix**: Stagger start positions: +``` +For 5 arrows from bottom edge: + arrow1.x = shape.x + shape.width * 0.2 + arrow2.x = shape.x + shape.width * 0.35 + arrow3.x = shape.x + shape.width * 0.5 + arrow4.x = shape.x + shape.width * 0.65 + arrow5.x = shape.x + shape.width * 0.8 +``` + +### Bug: Callback arrow doesn't loop correctly + +**Cause**: U-turn path lacks clearance. + +**Fix**: Use 4-point path: +``` +Points = [[0, 0], [clearance, 0], [clearance, -vert], [final_x, -vert]] +clearance = 40-60px +``` + +### Bug: Labels don't appear inside shapes + +**Cause**: Using `label` property instead of separate text element. + +**Fix**: Create TWO elements: +1. Shape with `boundElements` referencing text +2. Text with `containerId` referencing shape + +### Bug: Arrows are curved, not 90-degree + +**Cause**: Missing elbow properties. + +**Fix**: Add all three: +```json +{ + "roughness": 0, + "roundness": null, + "elbowed": true +} +``` diff --git a/src/plugins/frame-animator/index.ts b/src/plugins/frame-animator/index.ts new file mode 100644 index 0000000..b8b5ba2 --- /dev/null +++ b/src/plugins/frame-animator/index.ts @@ -0,0 +1,3 @@ +import { createStaticSkillPlugin } from "../../shared/static-skill.js"; + +export const frameAnimatorPlugin = createStaticSkillPlugin("frame-animator", "The frame-animator skill."); diff --git a/src/plugins/frame-animator/snippets/SKILL.txt b/src/plugins/frame-animator/snippets/SKILL.txt new file mode 100755 index 0000000..e9a5eff --- /dev/null +++ b/src/plugins/frame-animator/snippets/SKILL.txt @@ -0,0 +1,163 @@ +--- +name: frame-animator +description: Design and improve frame-based character animations for avatar/expression systems. Use when working on tick-based animation arrays, expression timing, blink cycles, idle loops, or mouth movement for a desktop character or mascot. Applies Disney's 12 animation principles concretely: asymmetric blinks, secondary actions, slow-in/slow-out, speaking rhythm, and idle personality. +metadata: + author: orkait + version: "1.0" +--- + +# Frame Animator + +Systematic process for designing natural-feeling frame animations for character avatar systems. Covers idle loops, speaking, listening, and all emotional stances. + +Assumes a frame-array model: each entry in the array = one tick. The array loops. No math โ€” just explicit frames. + +## When to Use + +- Creating a new stance animation from scratch +- Improving an existing animation that "feels off" +- Auditing why an animation looks robotic or dead +- Designing a full set (idle + speaking + listening) for a new emotional state + +## Core Principles + +See [references/principles.md](references/principles.md) for the full reference. The rules that matter most in practice: + +**1. Asymmetric blinks** +Closing is faster than opening. Always use an intermediate (half-open) frame. +``` +open โ†’ half-open โ†’ half-open โ†’ closed โ†’ half-open โ†’ half-open โ†’ half-open โ†’ open + [ 2 frames close ] [hold] [ 3 frames open (slower) ] +``` +A blink that closes and opens at the same speed reads as mechanical. + +**2. Secondary actions** +Idle loops need something happening beyond the main expression. Ear twitches, subtle eye shifts โ€” 1-2 frame actions that don't change the emotional read but suggest life. Place them at intervals that feel irregular (not every N frames exactly). + +**3. Uneven speaking rhythm** +Vowels hold 3 frames. Consonant transitions 1-2. The mouth should not open and close at a perfect even interval. Personality leaks through how much rest sits between phrases. + +**4. Blink frequency matches personality** +- Hyper-alert/focused: rare blinks (every 24-32 ticks) +- Calm/warm: moderate (every 16-24 ticks) +- Tired/sad: no blink needed (static expression reads correctly) +- Playful: happy-close acts as a natural blink, no separate sequence needed + +**5. Static vs live** +Not every stance needs animation in idle. Locked/frozen expressions (stern, guarded, sad, tired) work better as static 1-frame arrays. Motion on a frozen face reads as wrong. Reserve live idle loops for stances with emotional energy. + +## Process + +### Phase 1 โ€” Classify the stance + +Before writing any frames, answer: + +| Question | Guides | +|----------|--------| +| Is this stance energetic or locked? | Energetic โ†’ live idle. Locked โ†’ static 1-frame idle. | +| What's the dominant eye asset? | That's frame 0. Everything else is variation from it. | +| Does the stance perk ears? | Sharp ears during listening. Rounded = stays soft. | +| How does this character speak โ€” expansive or restrained? | Wide open vs barely opens mouth. | + +### Phase 2 โ€” Design idle + +For **live** stances: +1. Establish base frame (dominant eye + mouth + ears) +2. Place blink โ€” choose tick (not too early in the loop, not too late) +3. Build asymmetric blink sequence (2 close, 1 hold, 3 open) using half-open intermediate +4. Add 1 secondary action (ear twitch, or subtle eye shift) at a different position in the loop +5. Pad with base frames to reach 24-32 total ticks + +For **static** stances: +```rust +pub const IDLE: &[Frame] = &[ + Frame { face: "...", eyes: "...", mouth: "...", ears: "..." }, +]; +``` +`tick % 1 = 0` always. One frame, no animation. + +### Phase 3 โ€” Design speaking + +12-frame loop is enough. Pattern: + +``` +rest rest rest | open open open | close | rest rest | open open | close rest + [pause] [vowel hold] [trans] [gap] [next phrase] +``` + +Vary by personality: +- **Warm/playful**: more open frames, expressive, fast transitions +- **Neutral/focused**: even timing, moderate open hold +- **Guarded/stern**: less open (barely opens), more rest, long pauses +- **Tired/sad**: heavy rest before opening, sluggish, late open + +For the open position โ€” pick the mouth asset that fits the emotional register: +- `mouth_open_flat` โ†’ neutral/large open +- `mouth_tiny_triangle` โ†’ tight/guarded open +- `mouth_open_tongue` โ†’ playful/excited open +- `mouth_tiny_triangle` โ†’ curious/tense open + +### Phase 4 โ€” Design listening + +12-frame loop. No mouth movement. Show attentiveness through eyes and ears. + +- Ears go sharp for most stances (perk up to listen), stay rounded for soft stances (warm, playful, sad, tired) +- Add 1-2 frames of a slightly different eye state mid-loop (attentive glance, slight processing dip) +- Otherwise hold the dominant eye asset + +### Phase 5 โ€” Review checklist + +Before writing the files: + +- [ ] Blink uses half-open intermediate (not instant openโ†’closed) +- [ ] Blink close is faster than open (2 frames down, 3 frames up) +- [ ] Speaking rhythm is uneven (not perfectly even open/close intervals) +- [ ] Idle loop has at least one secondary action +- [ ] Static stances stay static (no unnecessary blinking on locked expressions) +- [ ] Ear state is correct for listening (sharp or intentionally rounded) +- [ ] Loop length feels right โ€” 24-32 for live idle, 12 for speaking/listening + +## Frame Structure + +```rust +pub struct Frame { + pub face: &'static str, // face_fill_blush | face_fill_rose + pub eyes: &'static str, // see references/principles.md for full list + pub mouth: &'static str, // see references/principles.md for full list + pub ears: &'static str, // ears_style_rounded | ears_style_sharp +} +``` + +Every field explicit on every frame. Duplicates are fine โ€” Rust is fast enough. + +## File Layout + +``` +src/animations/ + mod.rs โ€” Frame struct + pub mod declarations + neutral.rs โ€” IDLE, SPEAKING, LISTENING + warm.rs + playful.rs + ... (one file per stance) +``` + +Each file exports three const arrays: `IDLE`, `SPEAKING`, `LISTENING`. + +Resolver in `avatar.rs`: +```rust +fn select_frames(state: &WhiteboxState) -> &'static [Frame] { + let frame = &frames[state.tick_count as usize % frames.len()]; + // ... +} +``` + +## Common Mistakes + +| Symptom | Cause | Fix | +|---------|-------|-----| +| Blink looks like flickering | Instant openโ†’closed, no intermediate | Add half-open frames on both sides | +| Expression feels dead/static | No secondary actions in idle | Add ear twitch or eye variation 2/3 into the loop | +| Speaking looks like a metronome | Perfectly even open/close | Vary open hold length, add extra rest before some phrases | +| Sad/tired looks wrong when it blinks | Static expression shouldn't animate | Remove blink, use 1-frame static IDLE | +| Blink timing feels off | Placed too close to start or end of loop | Put blink around tick 8 in a 32-tick loop | +| Listening looks the same as idle | Ears not changed, no eye variation | Set ears to sharp, add 1-2 frame eye shift at mid-loop | diff --git a/src/plugins/frame-animator/snippets/references/principles.txt b/src/plugins/frame-animator/snippets/references/principles.txt new file mode 100755 index 0000000..e70e611 --- /dev/null +++ b/src/plugins/frame-animator/snippets/references/principles.txt @@ -0,0 +1,189 @@ +# Animation Principles Reference + +Research-backed timing and design rules for tick-based avatar animation at ~12fps (83ms per tick). + +## Available Assets (whitebox) + +### Eyes (11 variants) +| Asset | Reads as | +|-------|----------| +| `eyes_open_blush` | Neutral open, blush tint | +| `eyes_open_rose` | Open, warm/rose tint | +| `eyes_half_open_blush` | Half-open, blush โ€” intermediate for blink, or concentrated base | +| `eyes_half_open_rose` | Half-open, rose โ€” intermediate for blink on warm/curious | +| `eyes_soft_closed` | Gently closed โ€” blink hold state | +| `eyes_happy_closed` | Closed with joy โ€” playful blink / warm secondary | +| `eyes_excited_squint` | Squinted excitement โ€” playful dominant | +| `eyes_worried_angled` | Angled worry โ€” guarded dominant | +| `eyes_serious_angry` | Hard, locked โ€” stern/angry dominant | +| `eyes_sleepy_flat` | Flat drooped โ€” tired dominant | +| `eyes_teary` | Teary โ€” sad dominant | + +**Blink pairs:** +- Blush stances: `eyes_open_blush` โ†” `eyes_half_open_blush` โ†” `eyes_soft_closed` +- Rose stances: `eyes_open_rose` โ†” `eyes_half_open_rose` โ†” `eyes_soft_closed` +- Playful: `eyes_excited_squint` โ†” `eyes_happy_closed` (squint is already mostly closed) +- Focused: `eyes_half_open_blush` โ†” `eyes_soft_closed` (already halfway, 1 frame down) +- Angry/stern: `eyes_serious_angry` โ†’ `eyes_soft_closed` (no intermediate, instant tense blink) + +### Mouths (10 variants) +| Asset | Reads as | Use for | +|-------|----------|---------| +| `mouth_flat_neutral` | Resting closed | Neutral/focused/tired rest position | +| `mouth_open_flat` | Open, neutral | General speech open, angry open | +| `mouth_soft_smile` | Closed smile | Warm rest position | +| `mouth_tiny_triangle` | Small tight open | Curious/guarded open, tense speech | +| `mouth_cat_smile` | Curved cat smile | Playful rest | +| `mouth_wavy_cat` | Wavy playful | Playful variation | +| `mouth_open_tongue` | Wide open, tongue | Playful excited open | +| `mouth_small_frown` | Downturned | Guarded/sad rest position | +| `mouth_chevron_serious` | Chevron tight | Stern rest position | +| `mouth_pout_loop` | Pout | Angry rest position | + +**Speaking open position by stance:** +- Neutral, Alert, Focused, Angry: `mouth_open_flat` +- Warm: `mouth_open_flat` (with soft_smile as rest) +- Playful: `mouth_open_tongue` or `mouth_open_flat` +- Curious: `mouth_open_flat` (with tiny_triangle as rest) +- Guarded: `mouth_tiny_triangle` (barely opens) +- Stern: `mouth_tiny_triangle` (tight, deliberate) +- Tired: `mouth_open_flat` (sluggish, arrives late) +- Sad: `mouth_open_flat` (heavy, reluctant) + +### Ears (2 variants) +| Asset | Use | +|-------|-----| +| `ears_style_rounded` | Default for soft/warm/sad/tired stances | +| `ears_style_sharp` | Alert, curious, focused, stern, guarded, angry. Also: all stances during listening except warm/playful/sad/tired | + +### Face (2 variants) +| Asset | Use | +|-------|-----| +| `face_fill_blush` | Most stances | +| `face_fill_rose` | Warm, playful, curious โ€” warmer/softer emotional palette | + +--- + +## Disney's 12 Principles โ€” Applied + +### 1. Slow In and Slow Out +Movement feels natural when it accelerates into and decelerates out of held positions. + +**For blinks**: closing is a fast acceleration (2 frames), holding is a brief stop (1 frame), opening is a slow deceleration (3 frames). Total: 6 frames. + +**For speaking**: mouth accelerates open (1-2 frames transition), holds at the vowel (2-3 frames), decelerates closed (1-2 frames). + +At 12fps: 2 frames = 166ms (fast), 3 frames = 249ms (noticeable slow). + +### 2. Secondary Action +The main action (idle expression, speaking mouth) should be accompanied by a supporting action that adds personality without stealing focus. + +**Examples:** +- Ear twitching 2/3 of the way through an idle loop +- Eyes staying slightly more open during speech (engaged) +- A brief happy-close during warm speaking (joy leaks through) + +Rule: secondary actions should be 1-3 frames, not draw attention on their own. + +### 3. Timing +Frame count = character weight and personality. + +| Personality trait | Speaking rhythm | Idle blink frequency | +|-------------------|-----------------|----------------------| +| Alert, urgent | Short vowel holds (2 frames), fast transitions | Very rare (1 blink per 32-tick loop) | +| Warm, expressive | Longer holds (3 frames), smooth | Moderate (blink at tick 8 of 24) | +| Focused, deliberate | Even, controlled | Slow (blink at tick 16 of 32) | +| Tired, sluggish | Long rest before opening, late open | No blink (static) | +| Stern, measured | Extra rest before each phrase | No blink (static) | +| Sad, heavy | Heavy pause before opening, frown returns fast | No blink (static) | +| Playful, energetic | Fast transitions, varied mouth shapes | Joy squish instead of blink | + +### 4. Anticipation +A small preparatory action before the main action makes it read more intentionally. + +For this avatar system, anticipation is implicit in the half-open blink frames โ€” the eyes begin closing before they're fully closed, so the brain reads "a blink is happening" a frame before it completes. + +### 5. Exaggeration +At small avatar scale, subtle differences in expression vanish. Lean into distinct eye/mouth combinations per stance. Don't use the same open-mouth asset for all stances โ€” let the tight `mouth_tiny_triangle` signal guarded/tense, and `mouth_open_tongue` signal playful. + +### 6. Appeal +Each stance should be immediately readable at a glance. Avoid expressions that look "in between" two stances โ€” they read as broken, not subtle. + +Static stances (guarded, stern, tired, sad) achieve appeal through stillness. Motion on a naturally still face breaks the read. + +--- + +## Blink Timing by Stance + +| Stance | Blink position | Intermediate | Total blink frames | +|--------|---------------|--------------|-------------------| +| Neutral | Tick 7 of 32 | `eyes_half_open_blush` | 6 (2+1+3) | +| Warm | Tick 8 of 32 | `eyes_half_open_rose` | 6 (2+1+3) | +| Playful | Tick 9-10 of 24 | `eyes_happy_closed` acts as blink | 2 | +| Curious | Tick 8 of 32 | `eyes_half_open_rose` | 6 (2+1+3) | +| Alert | Tick 24 of 32 | `eyes_half_open_rose` | 4 (1+1+2) โ€” very brief | +| Focused | Tick 16 of 32 | (already half-open, 1 frame close) | 4 (1+1+2) | +| Angry | Tick 20 of 24 | None โ€” instant tense blink | 1 | +| Guarded | None | โ€” | Static | +| Stern | None | โ€” | Static | +| Tired | None | โ€” | Static | +| Sad | None | โ€” | Static | + +--- + +## Speaking Rhythm Patterns + +**Standard (neutral, alert):** +``` +rest rest | open open open | rest rest | open open | rest rest +``` +Uneven phrase lengths. 3-frame vowel holds. + +**Expressive (warm, playful):** +``` +rest | open open open | secondary | open open | rest +``` +Faster pace, secondary action mid-speech (happy close for warm, excited squint for playful). + +**Restrained (guarded, stern):** +``` +rest rest rest rest | tiny-open tiny-open | rest rest | tiny-open | rest rest +``` +More rest than open. Mouth barely parts. + +**Sluggish (tired):** +``` +rest rest rest rest rest rest rest rest rest | open open open +``` +8 frames rest before the mouth opens at all. Heavy reluctance. + +**Heavy (sad):** +``` +rest rest rest rest | open open open | rest rest rest rest +``` +Opens late, closes fast, then sits in long rest. + +--- + +## Idle Loop Structure + +``` +[base frames] โ†’ [blink 6 frames] โ†’ [base frames] โ†’ [secondary action 2 frames] โ†’ [base frames] +``` + +Positioning: +- Blink: ~1/4 into the loop (tick 7-8 of 32) +- Secondary action: ~2/3 into the loop (tick 19-22 of 32) +- Both positions should feel irregular โ€” not exactly at the midpoint or end + +Total loop length: 24 for playful/energetic, 32 for most stances. + +--- + +## Sources + +- Disney's 12 Principles of Animation (Johnston & Thomas, 1981) +- Programming natural idle character animations โ€” dev.to +- Sprite animation frame timing โ€” sprite-ai.art +- Breathing life into idle animations โ€” AnimSchool Blog +- Duolingo character animation with Rive โ€” dev.to diff --git a/src/plugins/golang-design-pattern/index.ts b/src/plugins/golang-design-pattern/index.ts new file mode 100644 index 0000000..d01baf8 --- /dev/null +++ b/src/plugins/golang-design-pattern/index.ts @@ -0,0 +1,3 @@ +import { createStaticSkillPlugin } from "../../shared/static-skill.js"; + +export const golangDesignPatternPlugin = createStaticSkillPlugin("golang-design-pattern", "The golang-design-pattern skill."); diff --git a/src/plugins/golang-design-pattern/snippets/SKILL.txt b/src/plugins/golang-design-pattern/snippets/SKILL.txt new file mode 100755 index 0000000..3f6a085 --- /dev/null +++ b/src/plugins/golang-design-pattern/snippets/SKILL.txt @@ -0,0 +1,141 @@ +--- +name: golang-design-pattern +description: Design patterns adapted for Go's philosophy. Use when writing Go code, reviewing Go architecture, translating OOP patterns to idiomatic Go, or applying design patterns in a non-OOP language. +triggers: + - "Go design pattern" + - "Go architecture" + - "idiomatic Go" + - "OOP to Go" + - "Go interface" + - "Go concurrency pattern" + - "Go struct pattern" + - "Go anti-pattern" +activation: + mode: fuzzy + priority: normal + triggers: + - "Go design pattern" + - "Go architecture" + - "idiomatic Go" + - "OOP to Go" + - "Go interface" + - "Go concurrency pattern" + - "Go struct pattern" + - "Go anti-pattern" +compatibility: "Go 1.18+" +metadata: + version: "1.0.0" +references: + - references/patterns/anti-patterns.md + - references/patterns/full-patterns-guide.md + - references/examples/creational-patterns.md + - references/examples/structural-behavioral-patterns.md + - references/examples/concurrency-error-testing.md + - references/examples/anti-patterns.md +--- + +# Go Design Patterns & Principles + +Apply design patterns idiomatically in Go by respecting composition over inheritance, implicit interfaces, and simplicity over abstraction. + +## When to Use This Skill + +- Writing or reviewing Go code that needs structural patterns +- Translating OOP design patterns to Go +- Architecting Go services with clean patterns +- Avoiding common anti-patterns from OOP backgrounds + +--- + +## Core Philosophy + +Go rejects traditional OOP in favor of: + +1. **Composition over Inheritance** โ€” No class hierarchies. Use embedding and interfaces. +2. **Implicit Interfaces** โ€” Types satisfy interfaces automatically. No `implements` keyword. +3. **Small Interfaces** โ€” Prefer single-method interfaces (`io.Reader`, `http.Handler`, `error`). +4. **Explicit Dependencies** โ€” Dependency injection over globals. No magic. +5. **Concurrency via Communication** โ€” Use channels, not shared memory locks. + +--- + +## Pattern Quick Reference + +### Creational +| Pattern | When to Use | Reference | +|---------|-------------|-----------| +| Constructor `New()` | Any struct needing validation | `references/examples/creational-patterns.md` | +| Functional Options | 5+ optional config params | `references/examples/creational-patterns.md` | +| Factory Function | Conditional object creation | `references/examples/creational-patterns.md` | +| Avoid Singleton | Use dependency injection | `references/examples/creational-patterns.md` | + +### Structural +| Pattern | When to Use | Reference | +|---------|-------------|-----------| +| Adapter | Wrap external/legacy types | `references/examples/structural-behavioral-patterns.md` | +| Decorator (Middleware) | Add behavior without changing type | `references/examples/structural-behavioral-patterns.md` | +| Composition/Embedding | Promote methods, NOT inheritance | `references/examples/structural-behavioral-patterns.md` | +| Consumer-Side Interface | Define interface where consumed | `references/examples/structural-behavioral-patterns.md` | + +### Behavioral +| Pattern | When to Use | Reference | +|---------|-------------|-----------| +| Strategy | Interchangeable algorithms | `references/examples/structural-behavioral-patterns.md` | +| Observer | Decouple event producers/consumers | `references/examples/structural-behavioral-patterns.md` | +| Command | Job queues, undo, task pipelines | `references/examples/structural-behavioral-patterns.md` | + +### Concurrency +| Pattern | When to Use | Reference | +|---------|-------------|-----------| +| Worker Pool | Bounded parallelism | `references/examples/concurrency-error-testing.md` | +| Pipeline | Stage-by-stage stream processing | `references/examples/concurrency-error-testing.md` | +| Fan-Out/Fan-In | Parallel work + result collection | `references/examples/concurrency-error-testing.md` | + +--- + +## Go Idioms vs OOP + +| Traditional OOP | Go Idiom | +|-----------------|----------| +| Inheritance | Composition via embedding | +| Abstract classes | Interfaces with multiple implementations | +| Factory classes | Constructor functions (`NewX()`) | +| Singletons | Dependency injection + `sync.Once` (avoid) | +| Template method | Function/interface injection | +| Observer | Channels or callback functions | +| Decorator | Middleware functions | +| Strategy | Interfaces or function types | + +--- + +## Best Practices + +**DO:** +- Use small, focused interfaces (1โ€“3 methods max) +- Define interfaces at consumer side (where used) +- Accept interfaces, return concrete structs +- Use table-driven tests for all test cases +- Pass `context.Context` for cancellation +- Wrap errors with `fmt.Errorf("...: %w", err)` +- Close channels to signal completion +- Use `defer` for cleanup + +**DON'T:** +- Create class hierarchies with embedding +- Use `init()` for dependency setup +- Create global mutable state +- Ignore errors with `_` blank identifier +- Use `panic` for business logic errors +- Create interfaces before having 2+ implementations +- Start goroutines without cancellation mechanism + +--- + +## Critical Anti-Patterns + +See `references/examples/anti-patterns.md` for code examples of: +- OOP inheritance simulation +- Global state +- Ignored errors +- Goroutine leaks +- Premature abstraction diff --git a/src/plugins/golang-design-pattern/snippets/references/examples/anti-patterns.txt b/src/plugins/golang-design-pattern/snippets/references/examples/anti-patterns.txt new file mode 100755 index 0000000..5937524 --- /dev/null +++ b/src/plugins/golang-design-pattern/snippets/references/examples/anti-patterns.txt @@ -0,0 +1,77 @@ +# Go Anti-Patterns โ€” What NOT to Do + +## Don't Simulate OOP Inheritance + +```go +// โŒ BAD: Embedding as inheritance is confusing +type Animal struct { Name string } +func (a *Animal) Speak() string { return "..." } + +type Dog struct { + Animal +} +func (d *Dog) Speak() string { return "Woof" } // Overrides parent โ€” confusing! + +// โœ… GOOD: Explicit composition +type Dog struct { name string } +func (d *Dog) Speak() string { return "Woof" } +``` + +## Don't Create Global State + +```go +// โŒ BAD +var db *sql.DB +func init() { db, _ = sql.Open("postgres", dsn) } + +// โœ… GOOD: Dependency injection +type Service struct { db *sql.DB } +func NewService(db *sql.DB) *Service { return &Service{db: db} } +``` + +## Don't Ignore Errors + +```go +// โŒ BAD +file, _ := os.Open("data.txt") + +// โœ… GOOD +file, err := os.Open("data.txt") +if err != nil { + return fmt.Errorf("failed to open file: %w", err) +} +defer file.Close() +``` + +## Don't Create Goroutine Leaks + +```go +// โŒ BAD: No cancellation +func worker() { + for { /* runs forever */ } +} + +// โœ… GOOD: Context-aware +func worker(ctx context.Context) { + for { + select { + case <-ctx.Done(): + return + default: + // work + } + } +} +``` + +## Don't Use Premature Abstraction + +```go +// โŒ BAD: Interface with single implementation +type UserRepo interface { Get(id string) (*User, error) } +type PostgresUserRepo struct {} // Only implementation + +// โœ… GOOD: Start concrete, extract when 2+ implementations exist +type UserStore struct { db *sql.DB } +func (s *UserStore) Get(id string) (*User, error) { /* ... */ } +``` diff --git a/src/plugins/golang-design-pattern/snippets/references/examples/concurrency-error-testing.txt b/src/plugins/golang-design-pattern/snippets/references/examples/concurrency-error-testing.txt new file mode 100755 index 0000000..cf2061f --- /dev/null +++ b/src/plugins/golang-design-pattern/snippets/references/examples/concurrency-error-testing.txt @@ -0,0 +1,148 @@ +# Concurrency, Error Handling & Testing Patterns โ€” Go Examples + +## Concurrency Patterns + +### Worker Pool โ€” Bounded goroutine execution + +```go +type WorkerPool struct { + workers int + jobs chan Job + wg sync.WaitGroup +} + +func (p *WorkerPool) Start(ctx context.Context) { + for i := 0; i < p.workers; i++ { + p.wg.Add(1) + go p.worker(ctx) + } +} +``` + +### Pipeline โ€” Chain processing stages with channels + +```go +func generator(nums ...int) <-chan int { + out := make(chan int) + go func() { + defer close(out) + for _, n := range nums { out <- n } + }() + return out +} + +func square(in <-chan int) <-chan int { + out := make(chan int) + go func() { + defer close(out) + for n := range in { out <- n * n } + }() + return out +} +``` + +### Fan-Out/Fan-In โ€” Distribute work, collect results + +```go +func fanIn(channels ...<-chan int) <-chan int { + out := make(chan int) + var wg sync.WaitGroup + + for _, ch := range channels { + wg.Add(1) + go func(c <-chan int) { + defer wg.Done() + for n := range c { out <- n } + }(ch) + } + + go func() { wg.Wait(); close(out) }() + return out +} +``` + +## Error Handling Patterns + +### Sentinel Errors โ€” Pre-defined error constants + +```go +var ( + ErrNotFound = errors.New("not found") + ErrUnauthorized = errors.New("unauthorized") +) + +if errors.Is(err, ErrNotFound) { /* handle */ } +``` + +### Error Wrapping โ€” Add context with `%w` + +```go +func ProcessOrder(id string) error { + order, err := fetchOrder(id) + if err != nil { + return fmt.Errorf("failed to fetch order %s: %w", id, err) + } + return nil +} +``` + +### Custom Error Types โ€” For structured error data + +```go +type ValidationError struct { + Field string + Issue string +} + +func (e *ValidationError) Error() string { + return fmt.Sprintf("validation failed: %s - %s", e.Field, e.Issue) +} + +var validationErr *ValidationError +if errors.As(err, &validationErr) { /* use fields */ } +``` + +## Testing Patterns + +### Table-Driven Tests + +```go +func TestAdd(t *testing.T) { + tests := []struct { + name string + a, b int + want int + }{ + {"positive", 1, 2, 3}, + {"negative", -1, -1, -2}, + {"zero", 0, 0, 0}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := Add(tt.a, tt.b) + if got != tt.want { + t.Errorf("got %d, want %d", got, tt.want) + } + }) + } +} +``` + +### Interface Mocking โ€” Hand-written mocks with function fields + +```go +type MockUserRepo struct { + GetByIDFunc func(ctx context.Context, id string) (*User, error) +} + +func (m *MockUserRepo) GetByID(ctx context.Context, id string) (*User, error) { + if m.GetByIDFunc != nil { + return m.GetByIDFunc(ctx, id) + } + return nil, errors.New("not implemented") +} + +// Verify interface at compile time +var _ UserRepository = (*MockUserRepo)(nil) +``` diff --git a/src/plugins/golang-design-pattern/snippets/references/examples/creational-patterns.txt b/src/plugins/golang-design-pattern/snippets/references/examples/creational-patterns.txt new file mode 100755 index 0000000..5675407 --- /dev/null +++ b/src/plugins/golang-design-pattern/snippets/references/examples/creational-patterns.txt @@ -0,0 +1,60 @@ +# Creational Patterns โ€” Go Examples + +## Constructor Pattern + +Use `New()` or `NewType()` functions with validation: + +```go +func NewClient(timeout time.Duration) (*Client, error) { + if timeout <= 0 { + return nil, fmt.Errorf("timeout must be positive") + } + return &Client{timeout: timeout}, nil +} +``` + +## Functional Options + +For complex configuration (5+ optional params): + +```go +type Option func(*Server) + +func WithTimeout(d time.Duration) Option { + return func(s *Server) { s.timeout = d } +} + +func NewServer(addr string, opts ...Option) *Server { + s := &Server{addr: addr, timeout: 30 * time.Second} + for _, opt := range opts { + opt(s) + } + return s +} +``` + +## Factory Functions + +Conditional object creation (not factory classes): + +```go +func NewLogger(typ string) (Logger, error) { + switch typ { + case "file": return &FileLogger{}, nil + case "console": return &ConsoleLogger{}, nil + default: return nil, fmt.Errorf("unknown type: %s", typ) + } +} +``` + +## Avoid Singleton โ€” Use Dependency Injection + +```go +// โŒ BAD: Global singleton +var instance *Database +func GetDB() *Database { return instance } + +// โœ… GOOD: Explicit dependency +type Service struct { db *Database } +func NewService(db *Database) *Service { return &Service{db: db} } +``` diff --git a/src/plugins/golang-design-pattern/snippets/references/examples/structural-behavioral-patterns.txt b/src/plugins/golang-design-pattern/snippets/references/examples/structural-behavioral-patterns.txt new file mode 100755 index 0000000..39de3dd --- /dev/null +++ b/src/plugins/golang-design-pattern/snippets/references/examples/structural-behavioral-patterns.txt @@ -0,0 +1,105 @@ +# Structural & Behavioral Patterns โ€” Go Examples + +## Structural Patterns + +### Adapter โ€” Wrap external types to match your interface + +```go +type Storage interface { + Save(ctx context.Context, key string, data []byte) error +} + +type RedisAdapter struct { client *RedisClient } + +func (a *RedisAdapter) Save(ctx context.Context, key string, data []byte) error { + return a.client.Set(key, data, 5*time.Minute) +} +``` + +### Decorator โ€” Middleware pattern for HTTP handlers + +```go +func WithLogging(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log.Printf("%s %s", r.Method, r.URL.Path) + next.ServeHTTP(w, r) + }) +} +``` + +### Composition โ€” Embed structs to promote fields/methods + +```go +type Logger struct { mu sync.Mutex; file *os.File } + +type Service struct { + Logger // Embedded โ€” promotes Logger's methods + db *sql.DB +} +``` + +### Consumer-Side Interfaces โ€” Define where used, not where implemented + +```go +// โœ… GOOD: Interface in consumer package +package service + +type UserGetter interface { + GetByID(ctx context.Context, id string) (*User, error) +} + +type UserService struct { + users UserGetter // Only needs GetByID +} +``` + +## Behavioral Patterns + +### Strategy โ€” Interface or function type + +```go +type CompressionStrategy interface { + Compress([]byte) ([]byte, error) +} + +type FileStorage struct { compression CompressionStrategy } + +// Or simpler: function type for stateless strategies +type CompressFunc func([]byte) ([]byte, error) +``` + +### Observer โ€” Use channels (more idiomatic than callbacks) + +```go +type EventBus struct { + subscribers []chan Event +} + +func (b *EventBus) Subscribe() <-chan Event { + ch := make(chan Event, 10) + b.subscribers = append(b.subscribers, ch) + return ch +} +``` + +### Command โ€” Function closures for job queues + +```go +type Job func(context.Context) error + +type Worker struct { jobs chan Job } + +func (w *Worker) Submit(job Job) { w.jobs <- job } +``` + +### Template Method โ€” Injected functions (no inheritance) + +```go +type ProcessingSteps struct { + Load func() ([]byte, error) + Transform func([]byte) ([]byte, error) + Save func([]byte) error +} + +type Processor struct { steps ProcessingSteps } +``` diff --git a/src/plugins/golang-design-pattern/snippets/references/misc/overview.txt b/src/plugins/golang-design-pattern/snippets/references/misc/overview.txt new file mode 100755 index 0000000..79c1152 --- /dev/null +++ b/src/plugins/golang-design-pattern/snippets/references/misc/overview.txt @@ -0,0 +1,122 @@ +# Go Design Patterns Skill + +A Kiro skill for applying design patterns idiomatically in Go, respecting composition over inheritance and Go's minimalist philosophy. + +## Structure + +``` +golang-design-patterns/ +โ”œโ”€โ”€ SKILL.md # Main skill definition (load this) +โ”œโ”€โ”€ references/ +โ”‚ โ”œโ”€โ”€ full-patterns-guide.md # Comprehensive pattern catalog +โ”‚ โ””โ”€โ”€ anti-patterns.md # What NOT to do +โ”œโ”€โ”€ scripts/ +โ”‚ โ””โ”€โ”€ detect-antipatterns.sh # Scan code for common issues +โ””โ”€โ”€ README.md # This file +``` + +## Usage + +### As a Kiro Skill + +This skill activates when you: +- Mention "Go design patterns" +- Ask about translating OOP patterns to Go +- Request architectural guidance for Go services +- Need to review Go code for pattern usage + +### Manual Reference + +Read `SKILL.md` for quick guidance on: +- When to use each pattern category +- Go-idiomatic implementations +- Quick anti-pattern checklist + +Consult `references/` for: +- **full-patterns-guide.md**: Detailed explanations with examples +- **anti-patterns.md**: Common mistakes and corrections + +### Anti-Pattern Detection + +Run the provided script to scan your codebase: + +```bash +# Scan current directory +./scripts/detect-antipatterns.sh + +# Scan specific directory +./scripts/detect-antipatterns.sh /path/to/project +``` + +The script detects: +- Global mutable state +- init() function usage +- Ignored errors (`_ = err`) +- panic() in non-init code +- Goroutines without context +- Large interfaces (>5 methods) + +## Key Principles + +1. **No Inheritance**: Go doesn't have class hierarchies. Use composition. +2. **Implicit Interfaces**: Types automatically satisfy interfaces. +3. **Small Interfaces**: Prefer 1-3 methods per interface. +4. **Consumer-Side Interfaces**: Define interfaces where used, not implemented. +5. **Explicit Dependencies**: Inject dependencies, avoid globals. +6. **Context Everywhere**: Pass `context.Context` for cancellation. +7. **Return Errors**: Don't use `panic` for business logic. + +## Pattern Categories + +### Creational +- Constructor Pattern (`New()` functions) +- Functional Options (flexible configuration) +- Factory Functions (conditional creation) + +### Structural +- Adapter (interface wrapping) +- Decorator (middleware, io wrappers) +- Composite (recursive structures) + +### Behavioral +- Strategy (interchangeable algorithms) +- Observer (channels, callbacks) +- Command (job queues) + +### Concurrency +- Worker Pool (bounded goroutines) +- Pipeline (staged processing) +- Fan-Out/Fan-In (parallel processing) + +### Error Handling +- Sentinel Errors (predefined constants) +- Error Wrapping (`%w` format) +- Custom Error Types (structured data) + +### Testing +- Table-Driven Tests (data-driven) +- Interface Mocking (hand-written mocks) +- Testable Constructors (accept interfaces) + +## Integration + +This skill complements: +- **golang.md**: Core language standards +- **Clean Architecture**: Domain-driven design in Go +- **Standard Library**: Following stdlib patterns + +## Version + +1.0.0 - Initial release + +## License + +Public domain / CC0 + +## Contributing + +To extend this skill: +1. Add new patterns to `references/full-patterns-guide.md` +2. Update `SKILL.md` summary +3. Add detection rules to `scripts/detect-antipatterns.sh` +4. Keep examples simple and idiomatic diff --git a/src/plugins/golang-design-pattern/snippets/references/patterns/anti-patterns.txt b/src/plugins/golang-design-pattern/snippets/references/patterns/anti-patterns.txt new file mode 100755 index 0000000..091c527 --- /dev/null +++ b/src/plugins/golang-design-pattern/snippets/references/patterns/anti-patterns.txt @@ -0,0 +1,775 @@ +# Go Anti-Patterns: What NOT to Do + +Common mistakes when coming from OOP languages or misapplying design patterns in Go. + +--- + +## 1. Inheritance via Embedding + +**Problem:** Treating embedding as inheritance/polymorphism. + +```go +// โŒ BAD: Simulating inheritance +type Animal struct { + Name string +} + +func (a *Animal) Speak() string { + return "generic sound" +} + +type Dog struct { + Animal // Embedded to "inherit" +} + +func (d *Dog) Speak() string { + return "Woof" // Trying to "override" +} + +// Confusing behavior: +var animal Animal = Dog{Animal{Name: "Rex"}} // Compile error! +// Embedding doesn't create an "is-a" relationship + +// โœ… GOOD: Explicit composition +type Dog struct { + name string +} + +func (d *Dog) Speak() string { + return "Woof" +} + +type Cat struct { + name string +} + +func (c *Cat) Speak() string { + return "Meow" +} + +// Use interface for polymorphism +type Speaker interface { + Speak() string +} + +func MakeNoise(s Speaker) { + fmt.Println(s.Speak()) +} +``` + +**Why it's bad:** +- Embedding is for field/method promotion, not inheritance +- Creates confusion about method resolution +- Violates Go's composition philosophy + +--- + +## 2. Global Mutable State + +**Problem:** Using global variables for shared state. + +```go +// โŒ BAD: Global database connection +var db *sql.DB + +func init() { + var err error + db, err = sql.Open("postgres", os.Getenv("DSN")) + if err != nil { + log.Fatal(err) + } +} + +func GetUser(id string) (*User, error) { + // Hidden dependency on global db + return queryUser(db, id) +} + +// Testing is nightmare: +// - Can't run tests in parallel +// - Must mock global state +// - Hidden dependencies + +// โœ… GOOD: Explicit dependency injection +type UserService struct { + db *sql.DB +} + +func NewUserService(db *sql.DB) *UserService { + return &UserService{db: db} +} + +func (s *UserService) GetUser(id string) (*User, error) { + return queryUser(s.db, id) +} + +// Testing: +func TestUserService_GetUser(t *testing.T) { + mockDB := &MockDB{} + service := NewUserService(mockDB) + // Easy to test with mock +} + +// Main: +func main() { + db, _ := sql.Open("postgres", dsn) + defer db.Close() + + userService := NewUserService(db) + orderService := NewOrderService(db) + // Explicit dependencies visible +} +``` + +**Why it's bad:** +- Makes testing difficult (can't run parallel tests) +- Creates hidden dependencies +- Violates single responsibility (init does too much) +- Hard to trace data flow + +--- + +## 3. Init() Abuse + +**Problem:** Using `init()` for complex setup or dependency initialization. + +```go +// โŒ BAD: Complex init +var ( + config *Config + logger *Logger + cache *Cache +) + +func init() { + config = loadConfig() // File I/O + logger = setupLogger(config) + cache = newCache(config.CacheSize) + // Hard to control initialization order + // Can't handle errors properly + // Creates global state +} + +// โœ… GOOD: Explicit initialization +type App struct { + config *Config + logger *Logger + cache *Cache +} + +func NewApp() (*App, error) { + config, err := loadConfig() + if err != nil { + return nil, fmt.Errorf("load config: %w", err) + } + + logger, err := setupLogger(config) + if err != nil { + return nil, fmt.Errorf("setup logger: %w", err) + } + + cache := newCache(config.CacheSize) + + return &App{ + config: config, + logger: logger, + cache: cache, + }, nil +} + +func main() { + app, err := NewApp() + if err != nil { + log.Fatal(err) + } + defer app.Close() + + app.Run() +} +``` + +**Legitimate uses of init():** +- Registering drivers: `database/sql` +- Setting up package-level constants +- One-time computations with no side effects + +--- + +## 4. Ignoring Errors + +**Problem:** Using blank identifier `_` for errors or not checking them. + +```go +// โŒ BAD: Ignoring errors +file, _ := os.Open("config.json") +defer file.Close() +json.NewDecoder(file).Decode(&config) + +// Potential panic if file is nil! + +// โŒ BAD: Silent failures +func saveUser(user *User) { + _ = db.Save(user) // Error lost +} + +// โœ… GOOD: Always check errors +file, err := os.Open("config.json") +if err != nil { + return fmt.Errorf("failed to open config: %w", err) +} +defer file.Close() + +if err := json.NewDecoder(file).Decode(&config); err != nil { + return fmt.Errorf("failed to decode config: %w", err) +} + +// โœ… GOOD: At minimum, log errors +func saveUser(user *User) error { + if err := db.Save(user); err != nil { + log.Printf("WARNING: failed to save user %s: %v", user.ID, err) + return err + } + return nil +} +``` + +**Exception:** Explicitly documented intentional ignore +```go +// Ignore error because fallback is acceptable +_ = cache.Set(key, value) // Cache miss is acceptable + +// But better: +if err := cache.Set(key, value); err != nil { + log.Printf("cache set failed (non-critical): %v", err) +} +``` + +--- + +## 5. Goroutine Leaks + +**Problem:** Starting goroutines without termination mechanism. + +```go +// โŒ BAD: No way to stop +func streamData() <-chan Data { + ch := make(chan Data) + go func() { + for { + ch <- fetchData() // Runs forever + time.Sleep(time.Second) + } + }() + return ch +} + +// If consumer stops reading, goroutine blocks forever + +// โœ… GOOD: Context-aware cancellation +func streamData(ctx context.Context) <-chan Data { + ch := make(chan Data) + go func() { + defer close(ch) + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + select { + case ch <- fetchData(): + case <-ctx.Done(): + return + } + case <-ctx.Done(): + return + } + } + }() + return ch +} + +// Usage +ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) +defer cancel() + +for data := range streamData(ctx) { + process(data) +} +``` + +**Common leak patterns:** +```go +// โŒ Unbuffered channel with no receiver +ch := make(chan int) +go func() { + ch <- 1 // Blocks forever if no receiver +}() + +// โŒ Infinite loop with no exit +go func() { + for { + work() // No way to stop + } +}() + +// โŒ Blocked on channel send +go func() { + for item := range input { + output <- process(item) // Blocks if output is full + } +}() +``` + +--- + +## 6. Panic for Control Flow + +**Problem:** Using `panic` for business logic errors. + +```go +// โŒ BAD: Panic for expected errors +func GetUser(id string) *User { + user := db.Find(id) + if user == nil { + panic("user not found") // Don't use panic! + } + return user +} + +// โœ… GOOD: Return errors +func GetUser(id string) (*User, error) { + user := db.Find(id) + if user == nil { + return nil, ErrUserNotFound + } + return user, nil +} + +// Panic is only for: +// 1. Unrecoverable programmer errors +func validateConfig(cfg *Config) { + if cfg == nil { + panic("nil config") // Programmer error + } +} + +// 2. Init-time failures +func init() { + if err := loadCriticalData(); err != nil { + panic(fmt.Sprintf("failed to load critical data: %v", err)) + } +} +``` + +--- + +## 7. Premature Interface Abstraction + +**Problem:** Creating interfaces before they're needed. + +```go +// โŒ BAD: Interface with single implementation +package user + +type Repository interface { + Get(id string) (*User, error) + Create(user *User) error + Update(user *User) error + Delete(id string) error +} + +type PostgresRepository struct { + db *sql.DB +} +// Only implementation in entire codebase + +// โœ… GOOD: Start with concrete type +package user + +type Repository struct { + db *sql.DB +} + +func (r *Repository) Get(id string) (*User, error) { ... } +func (r *Repository) Create(user *User) error { ... } + +// Extract interface when you have 2+ implementations +// or when consumer package needs to define it +``` + +**When to create interfaces:** +- Consumer package needs to mock dependency (testing) +- 2+ implementations exist +- Defining contract between packages +- Standard library interfaces (`io.Reader`, `http.Handler`) + +--- + +## 8. Large Interfaces + +**Problem:** Creating interfaces with many methods. + +```go +// โŒ BAD: God interface +type UserManager interface { + Create(user *User) error + Update(user *User) error + Delete(id string) error + Get(id string) (*User, error) + List(filters Filters) ([]*User, error) + Authenticate(email, password string) (*User, error) + ResetPassword(id string) error + SendWelcomeEmail(id string) error + UpdatePreferences(id string, prefs Preferences) error +} + +// Hard to mock, violates interface segregation + +// โœ… GOOD: Small, focused interfaces +type UserReader interface { + Get(id string) (*User, error) + List(filters Filters) ([]*User, error) +} + +type UserWriter interface { + Create(user *User) error + Update(user *User) error + Delete(id string) error +} + +type Authenticator interface { + Authenticate(email, password string) (*User, error) +} + +// Services depend only on what they need +type ProfileService struct { + users UserReader // Only needs read access +} + +type AdminService struct { + users UserWriter // Only needs write access +} +``` + +**Go's guideline:** Interfaces should have 1-3 methods + +--- + +## 9. Context Misuse + +**Problem:** Not passing context or creating long-lived contexts. + +```go +// โŒ BAD: No context +func FetchData() ([]byte, error) { + resp, err := http.Get("http://api.example.com") + // Can't cancel, no timeout +} + +// โŒ BAD: Creating context in function +func FetchData() ([]byte, error) { + ctx := context.Background() // Should be passed in + req, _ := http.NewRequestWithContext(ctx, "GET", url, nil) + // ... +} + +// โŒ BAD: Storing context in struct +type Fetcher struct { + ctx context.Context // Don't store context +} + +// โœ… GOOD: Pass context as first parameter +func FetchData(ctx context.Context) ([]byte, error) { + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, err + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + return io.ReadAll(resp.Body) +} + +// Usage with timeout +ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) +defer cancel() + +data, err := FetchData(ctx) +``` + +**Context rules:** +1. First parameter: `func DoSomething(ctx context.Context, ...)` +2. Never store in structs +3. Pass down the call chain +4. Create once at top level (main, HTTP handler) + +--- + +## 10. Mutex in Value Types + +**Problem:** Copying structs that contain mutexes. + +```go +// โŒ BAD: Mutex in value type +type Cache struct { + mu sync.Mutex // Copied when Cache is copied! + items map[string]interface{} +} + +func (c Cache) Get(key string) interface{} { // Value receiver + c.mu.Lock() // Locking a copy! + defer c.mu.Unlock() + return c.items[key] +} + +cache := Cache{items: make(map[string]interface{})} +cache2 := cache // Copies the mutex - breaks thread safety! + +// โœ… GOOD: Pointer receiver for types with mutexes +type Cache struct { + mu sync.Mutex + items map[string]interface{} +} + +func (c *Cache) Get(key string) interface{} { // Pointer receiver + c.mu.Lock() + defer c.mu.Unlock() + return c.items[key] +} + +// Or use sync.RWMutex for better read performance +type Cache struct { + mu sync.RWMutex + items map[string]interface{} +} + +func (c *Cache) Get(key string) interface{} { + c.mu.RLock() + defer c.mu.RUnlock() + return c.items[key] +} +``` + +**Rule:** Types containing sync primitives must use pointer receivers + +--- + +## 11. String Concatenation in Loops + +**Problem:** Using `+` for string building in loops. + +```go +// โŒ BAD: O(nยฒ) performance +func buildQuery(columns []string) string { + query := "SELECT " + for i, col := range columns { + query += col // Creates new string each iteration + if i < len(columns)-1 { + query += ", " + } + } + return query + " FROM users" +} + +// โœ… GOOD: Use strings.Builder +func buildQuery(columns []string) string { + var b strings.Builder + b.WriteString("SELECT ") + + for i, col := range columns { + b.WriteString(col) + if i < len(columns)-1 { + b.WriteString(", ") + } + } + + b.WriteString(" FROM users") + return b.String() +} + +// Or strings.Join for simple cases +func buildQuery(columns []string) string { + return "SELECT " + strings.Join(columns, ", ") + " FROM users" +} +``` + +--- + +## 12. Not Closing Resources + +**Problem:** Forgetting to close files, connections, response bodies. + +```go +// โŒ BAD: Resource leak +func readConfig() (*Config, error) { + file, err := os.Open("config.json") + if err != nil { + return nil, err + } + // Missing: defer file.Close() + + var cfg Config + err = json.NewDecoder(file).Decode(&cfg) + return &cfg, err +} + +// โŒ BAD: Not checking Close error +defer file.Close() // Error ignored + +// โœ… GOOD: Always defer Close +func readConfig() (*Config, error) { + file, err := os.Open("config.json") + if err != nil { + return nil, err + } + defer file.Close() // Guaranteed cleanup + + var cfg Config + if err := json.NewDecoder(file).Decode(&cfg); err != nil { + return nil, fmt.Errorf("decode config: %w", err) + } + + return &cfg, nil +} + +// โœ… GOOD: Check Close error when it matters +func writeData(data []byte) (err error) { + file, err := os.Create("output.dat") + if err != nil { + return err + } + + defer func() { + if cerr := file.Close(); cerr != nil && err == nil { + err = cerr // Return close error if no other error + } + }() + + _, err = file.Write(data) + return err +} +``` + +**Common resources to close:** +- `*os.File` +- `*sql.Rows` +- `http.Response.Body` +- Network connections +- Custom types with `Close()` method + +--- + +## 13. Testing Anti-Patterns + +### Not Using Table-Driven Tests + +```go +// โŒ BAD: Separate test for each case +func TestAdd_PositiveNumbers(t *testing.T) { + result := Add(1, 2) + if result != 3 { + t.Errorf("expected 3, got %d", result) + } +} + +func TestAdd_NegativeNumbers(t *testing.T) { + result := Add(-1, -1) + if result != -2 { + t.Errorf("expected -2, got %d", result) + } +} + +// โœ… GOOD: Table-driven +func TestAdd(t *testing.T) { + tests := []struct { + name string + a, b int + want int + }{ + {"positive", 1, 2, 3}, + {"negative", -1, -1, -2}, + {"zero", 0, 0, 0}, + {"mixed", 5, -3, 2}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := Add(tt.a, tt.b) + if got != tt.want { + t.Errorf("got %d, want %d", got, tt.want) + } + }) + } +} +``` + +### Testing Implementation Instead of Behavior + +```go +// โŒ BAD: Testing internal implementation +func TestCache_InternalMap(t *testing.T) { + cache := NewCache() + cache.items["key"] = "value" // Accessing internal field + + if len(cache.items) != 1 { + t.Error("expected 1 item in map") + } +} + +// โœ… GOOD: Test public behavior +func TestCache_SetAndGet(t *testing.T) { + cache := NewCache() + cache.Set("key", "value") + + got, ok := cache.Get("key") + if !ok { + t.Fatal("expected key to exist") + } + if got != "value" { + t.Errorf("got %v, want %v", got, "value") + } +} +``` + +--- + +## Quick Anti-Pattern Checklist + +โŒ **AVOID:** +- Embedding for inheritance +- Global mutable state +- Complex `init()` functions +- Ignoring errors with `_` +- Starting goroutines without cancellation +- Using `panic` for business logic +- Interfaces before you need them +- Large interfaces (>3 methods) +- Not passing `context.Context` +- Storing mutexes in value types +- String concatenation in loops +- Not closing resources +- Separate tests instead of table-driven + +โœ… **DO:** +- Use composition explicitly +- Inject dependencies +- Return errors from constructors +- Always check errors +- Use `context.Context` for cancellation +- Return errors, not panic +- Extract interfaces when needed +- Keep interfaces small (1-3 methods) +- Pass context as first parameter +- Use pointer receivers for mutex types +- Use `strings.Builder` for loops +- `defer` resource cleanup +- Write table-driven tests + +--- + +This anti-patterns guide helps you avoid common pitfalls when transitioning from OOP to Go or applying design patterns incorrectly. diff --git a/src/plugins/golang-design-pattern/snippets/references/patterns/full-patterns-guide.txt b/src/plugins/golang-design-pattern/snippets/references/patterns/full-patterns-guide.txt new file mode 100755 index 0000000..503888a --- /dev/null +++ b/src/plugins/golang-design-pattern/snippets/references/patterns/full-patterns-guide.txt @@ -0,0 +1,779 @@ +# Complete Go Design Patterns Reference + +This document provides comprehensive explanations, examples, and use cases for all design patterns adapted to Go. + +**Note:** This is reference material. For quick guidance, see the main `../SKILL.md`. + +--- + +## Table of Contents + +1. [Creational Patterns](#creational-patterns) +2. [Structural Patterns](#structural-patterns) +3. [Behavioral Patterns](#behavioral-patterns) +4. [Concurrency Patterns](#concurrency-patterns) +5. [Error Handling Patterns](#error-handling-patterns) +6. [Testing Patterns](#testing-patterns) + +--- + +## Creational Patterns + +### Constructor Pattern + +**Purpose:** Create instances with validation and initialization logic. + +**When to use:** +- Object requires validation during creation +- Default values need to be set +- Resource allocation needed (connections, files) + +**Implementation:** + +```go +// Basic constructor +func NewClient(timeout time.Duration) (*Client, error) { + if timeout <= 0 { + return nil, fmt.Errorf("timeout must be positive") + } + + return &Client{ + timeout: timeout, + client: &http.Client{Timeout: timeout}, + }, nil +} + +// Constructor with compile-time interface check +var _ http.Handler = (*MyHandler)(nil) + +func NewHandler(logger Logger) *MyHandler { + return &MyHandler{logger: logger} +} +``` + +**Common pitfalls:** +- Using `panic` instead of returning errors +- Not validating inputs +- Creating constructors for zero-value-useful types unnecessarily + +--- + +### Functional Options Pattern + +**Purpose:** Provide flexible configuration without telescoping constructors. + +**When to use:** +- 5+ optional configuration parameters +- Need backward-compatible API evolution +- Clear default values exist + +**Implementation:** + +```go +type Server struct { + addr string + timeout time.Duration + maxConn int + logger Logger +} + +type Option func(*Server) + +func WithTimeout(d time.Duration) Option { + return func(s *Server) { s.timeout = d } +} + +func WithMaxConnections(n int) Option { + return func(s *Server) { s.maxConn = n } +} + +func WithLogger(l Logger) Option { + return func(s *Server) { s.logger = l } +} + +func NewServer(addr string, opts ...Option) *Server { + s := &Server{ + addr: addr, + timeout: 30 * time.Second, // Sensible defaults + maxConn: 100, + logger: defaultLogger, + } + + for _, opt := range opts { + opt(s) + } + + return s +} + +// Usage +srv := NewServer(":8080", + WithTimeout(60 * time.Second), + WithMaxConnections(200), +) +``` + +**Advanced: Options with validation** + +```go +func WithTimeout(d time.Duration) Option { + return func(s *Server) error { + if d <= 0 { + return fmt.Errorf("timeout must be positive") + } + s.timeout = d + return nil + } +} + +// Signature changes to return error +type Option func(*Server) error + +func NewServer(addr string, opts ...Option) (*Server, error) { + s := &Server{/* defaults */} + + for _, opt := range opts { + if err := opt(s); err != nil { + return nil, fmt.Errorf("option failed: %w", err) + } + } + + return s, nil +} +``` + +**When NOT to use:** +- Simple constructors with <3 parameters +- When all parameters are required +- Struct literal initialization works fine + +--- + +### Factory Pattern + +**Purpose:** Create objects based on runtime conditions without exposing creation logic. + +**Traditional OOP vs Go:** + +```go +// โŒ Traditional OOP style (don't do this in Go) +type LoggerFactory struct{} + +func (f *LoggerFactory) CreateLogger(typ string) Logger { + // Factory class is unnecessary +} + +// โœ… Go style: simple function +func NewLogger(typ string) (Logger, error) { + switch typ { + case "file": + return &FileLogger{}, nil + case "console": + return &ConsoleLogger{}, nil + case "remote": + return &RemoteLogger{}, nil + default: + return nil, fmt.Errorf("unknown logger type: %s", typ) + } +} + +// โœ… Factory with configuration +func NewDatabase(cfg Config) (Database, error) { + switch cfg.Driver { + case "postgres": + return postgres.New(cfg.DSN) + case "mysql": + return mysql.New(cfg.DSN) + default: + return nil, fmt.Errorf("unsupported driver: %s", cfg.Driver) + } +} +``` + +**Registry pattern variation:** + +```go +type Factory func(config Config) (Plugin, error) + +var registry = make(map[string]Factory) + +func Register(name string, factory Factory) { + registry[name] = factory +} + +func Create(name string, cfg Config) (Plugin, error) { + factory, ok := registry[name] + if !ok { + return nil, fmt.Errorf("unknown plugin: %s", name) + } + return factory(cfg) +} + +// Plugins register themselves +func init() { + Register("aws", newAWSPlugin) + Register("gcp", newGCPPlugin) +} +``` + +--- + +### Builder Pattern + +**Purpose:** Construct complex objects step-by-step with validation. + +**When to use:** +- Object construction requires multiple steps +- Need to enforce construction order +- Want to validate intermediate states + +**Implementation:** + +```go +type QueryBuilder struct { + table string + columns []string + where []string + orderBy string + limit int + err error // Accumulate errors +} + +func NewQueryBuilder(table string) *QueryBuilder { + return &QueryBuilder{table: table} +} + +func (b *QueryBuilder) Select(columns ...string) *QueryBuilder { + if b.err != nil { + return b + } + if len(columns) == 0 { + b.err = fmt.Errorf("no columns specified") + return b + } + b.columns = columns + return b +} + +func (b *QueryBuilder) Where(condition string) *QueryBuilder { + if b.err != nil { + return b + } + if condition == "" { + b.err = fmt.Errorf("empty where condition") + return b + } + b.where = append(b.where, condition) + return b +} + +func (b *QueryBuilder) OrderBy(column string) *QueryBuilder { + if b.err != nil { + return b + } + b.orderBy = column + return b +} + +func (b *QueryBuilder) Limit(n int) *QueryBuilder { + if b.err != nil { + return b + } + if n <= 0 { + b.err = fmt.Errorf("limit must be positive") + return b + } + b.limit = n + return b +} + +func (b *QueryBuilder) Build() (string, error) { + if b.err != nil { + return "", b.err + } + + if b.table == "" { + return "", fmt.Errorf("table name required") + } + + // Build query string + cols := "*" + if len(b.columns) > 0 { + cols = strings.Join(b.columns, ", ") + } + + query := fmt.Sprintf("SELECT %s FROM %s", cols, b.table) + + if len(b.where) > 0 { + query += " WHERE " + strings.Join(b.where, " AND ") + } + + if b.orderBy != "" { + query += " ORDER BY " + b.orderBy + } + + if b.limit > 0 { + query += fmt.Sprintf(" LIMIT %d", b.limit) + } + + return query, nil +} + +// Usage +query, err := NewQueryBuilder("users"). + Select("id", "email", "name"). + Where("age > 18"). + Where("active = true"). + OrderBy("created_at DESC"). + Limit(10). + Build() + +if err != nil { + log.Fatal(err) +} +fmt.Println(query) +``` + +**When to use Builder vs Functional Options:** +- **Builder:** Complex construction with validation at each step +- **Functional Options:** Configuration with independent optional parameters + +--- + +### Singleton Pattern (Avoid!) + +**Purpose:** Ensure only one instance exists. + +**Why to avoid:** Global state makes testing difficult and creates hidden dependencies. + +**If you must use it:** + +```go +// Thread-safe lazy initialization +var ( + instance *Config + once sync.Once +) + +func GetConfig() *Config { + once.Do(func() { + instance = &Config{ + // Load from file/env + } + }) + return instance +} + +// โœ… BETTER: Dependency injection +type Service struct { + config *Config +} + +func NewService(cfg *Config) *Service { + return &Service{config: cfg} +} + +// In main() +func main() { + cfg := loadConfig() // Create once + + userService := NewUserService(cfg) + orderService := NewOrderService(cfg) + // Explicit dependencies +} +``` + +**Legitimate use cases:** +- Application configuration loaded once at startup +- Global logger (though context-based logging is better) + +--- + +## Structural Patterns + +### Adapter Pattern + +**Purpose:** Convert one interface to another to make incompatible interfaces work together. + +**When to use:** +- Integrating third-party libraries +- Wrapping legacy code +- Converting between different data representations + +**Implementation:** + +```go +// Your interface +type Storage interface { + Save(ctx context.Context, key string, value []byte) error + Load(ctx context.Context, key string) ([]byte, error) + Delete(ctx context.Context, key string) error +} + +// External library (Redis client) +type RedisClient struct { + client *redis.Client +} + +func (r *RedisClient) Set(key string, val []byte, ttl time.Duration) error { + return r.client.Set(context.Background(), key, val, ttl).Err() +} + +func (r *RedisClient) Get(key string) ([]byte, error) { + return r.client.Get(context.Background(), key).Bytes() +} + +func (r *RedisClient) Del(key string) error { + return r.client.Del(context.Background(), key).Err() +} + +// Adapter +type RedisStorageAdapter struct { + client *RedisClient + ttl time.Duration +} + +func NewRedisStorageAdapter(client *RedisClient, ttl time.Duration) *RedisStorageAdapter { + return &RedisStorageAdapter{ + client: client, + ttl: ttl, + } +} + +func (a *RedisStorageAdapter) Save(ctx context.Context, key string, value []byte) error { + return a.client.Set(key, value, a.ttl) +} + +func (a *RedisStorageAdapter) Load(ctx context.Context, key string) ([]byte, error) { + return a.client.Get(key) +} + +func (a *RedisStorageAdapter) Delete(ctx context.Context, key string) error { + return a.client.Del(key) +} + +// Compile-time interface verification +var _ Storage = (*RedisStorageAdapter)(nil) + +// Usage +storage := NewRedisStorageAdapter(redisClient, 5*time.Minute) +err := storage.Save(ctx, "user:123", userData) +``` + +**Best practices:** +- Keep adapters thin - only translation, no business logic +- Use compile-time interface checks: `var _ Interface = (*Adapter)(nil)` +- Document what's being adapted and why + +--- + +### Decorator Pattern + +**Purpose:** Add behavior to objects dynamically without modifying them. + +**Go's most common use:** HTTP middleware + +**Implementation:** + +```go +// Handler signature +type Handler func(http.ResponseWriter, *http.Request) + +// Decorator: Logging +func WithLogging(next Handler) Handler { + return func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + log.Printf("Started %s %s", r.Method, r.URL.Path) + + next(w, r) + + log.Printf("Completed %s in %v", r.URL.Path, time.Since(start)) + } +} + +// Decorator: Authentication +func WithAuth(next Handler) Handler { + return func(w http.ResponseWriter, r *http.Request) { + token := r.Header.Get("Authorization") + if token == "" { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + // Validate token + if !isValidToken(token) { + http.Error(w, "Invalid token", http.StatusForbidden) + return + } + + next(w, r) + } +} + +// Decorator: Rate limiting (with state) +func WithRateLimit(limit int) func(Handler) Handler { + limiter := rate.NewLimiter(rate.Limit(limit), limit) + + return func(next Handler) Handler { + return func(w http.ResponseWriter, r *http.Request) { + if !limiter.Allow() { + http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests) + return + } + next(w, r) + } + } +} + +// Decorator: Error recovery +func WithRecovery(next Handler) Handler { + return func(w http.ResponseWriter, r *http.Request) { + defer func() { + if err := recover(); err != nil { + log.Printf("Panic: %v\n%s", err, debug.Stack()) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + } + }() + + next(w, r) + } +} + +// Stack decorators (applied bottom-up) +handler := WithRecovery( + WithLogging( + WithAuth( + WithRateLimit(100)( + actualHandler, + ), + ), + ), +) + +// Execution order: Recovery โ†’ Logging โ†’ Auth โ†’ RateLimit โ†’ Handler +``` + +**io.Reader/Writer decorators:** + +```go +// Counting decorator +type CountingReader struct { + reader io.Reader + count int64 +} + +func (r *CountingReader) Read(p []byte) (n int, err error) { + n, err = r.reader.Read(p) + atomic.AddInt64(&r.count, int64(n)) + return n, err +} + +func (r *CountingReader) BytesRead() int64 { + return atomic.LoadInt64(&r.count) +} + +// Compression decorator +type GzipReader struct { + reader io.Reader + gzReader *gzip.Reader +} + +func NewGzipReader(r io.Reader) (*GzipReader, error) { + gr, err := gzip.NewReader(r) + if err != nil { + return nil, err + } + return &GzipReader{reader: r, gzReader: gr}, nil +} + +func (g *GzipReader) Read(p []byte) (n int, err error) { + return g.gzReader.Read(p) +} + +func (g *GzipReader) Close() error { + return g.gzReader.Close() +} + +// Usage: Stack decorators +file, _ := os.Open("data.txt.gz") +gzipReader, _ := NewGzipReader(file) +counter := &CountingReader{reader: gzipReader} + +io.Copy(os.Stdout, counter) +fmt.Printf("Read %d bytes\n", counter.BytesRead()) +``` + +--- + +### Proxy Pattern + +**Purpose:** Control access to an object through a surrogate. + +**Use cases:** +- Lazy initialization +- Access control +- Caching +- Logging + +**Implementation: Caching Proxy** + +```go +type Database interface { + Query(ctx context.Context, sql string) ([]Row, error) + Execute(ctx context.Context, sql string) error +} + +type CachingDatabaseProxy struct { + db Database + cache map[string]cachedResult + mu sync.RWMutex + ttl time.Duration +} + +type cachedResult struct { + data []Row + timestamp time.Time +} + +func NewCachingProxy(db Database, ttl time.Duration) *CachingDatabaseProxy { + return &CachingDatabaseProxy{ + db: db, + cache: make(map[string]cachedResult), + ttl: ttl, + } +} + +func (p *CachingDatabaseProxy) Query(ctx context.Context, sql string) ([]Row, error) { + // Try cache first + p.mu.RLock() + if cached, ok := p.cache[sql]; ok { + if time.Since(cached.timestamp) < p.ttl { + p.mu.RUnlock() + return cached.data, nil // Cache hit + } + } + p.mu.RUnlock() + + // Cache miss - query database + rows, err := p.db.Query(ctx, sql) + if err != nil { + return nil, err + } + + // Update cache + p.mu.Lock() + p.cache[sql] = cachedResult{ + data: rows, + timestamp: time.Now(), + } + p.mu.Unlock() + + return rows, nil +} + +func (p *CachingDatabaseProxy) Execute(ctx context.Context, sql string) error { + // Invalidate cache on writes + p.mu.Lock() + p.cache = make(map[string]cachedResult) // Simple invalidation + p.mu.Unlock() + + return p.db.Execute(ctx, sql) +} + +// Verify interface +var _ Database = (*CachingDatabaseProxy)(nil) +``` + +**Implementation: Lazy Loading Proxy** + +```go +type ConnectionProxy struct { + once sync.Once + conn *sql.DB + err error + dsn string +} + +func NewConnectionProxy(dsn string) *ConnectionProxy { + return &ConnectionProxy{dsn: dsn} +} + +func (p *ConnectionProxy) getConnection() (*sql.DB, error) { + p.once.Do(func() { + p.conn, p.err = sql.Open("postgres", p.dsn) + }) + return p.conn, p.err +} + +func (p *ConnectionProxy) Query(ctx context.Context, query string) (*sql.Rows, error) { + conn, err := p.getConnection() + if err != nil { + return nil, fmt.Errorf("connection failed: %w", err) + } + return conn.QueryContext(ctx, query) +} +``` + +--- + +### Composite Pattern + +**Purpose:** Treat individual objects and compositions uniformly. + +**Go adaptation:** Interfaces with recursive structures + +```go +// Component interface +type FileSystemNode interface { + Name() string + Size() int64 + IsDir() bool +} + +// Leaf: File +type File struct { + name string + size int64 +} + +func (f *File) Name() string { return f.name } +func (f *File) Size() int64 { return f.size } +func (f *File) IsDir() bool { return false } + +// Composite: Directory +type Directory struct { + name string + children []FileSystemNode +} + +func (d *Directory) Name() string { return d.name } +func (d *Directory) IsDir() bool { return true } + +func (d *Directory) Size() int64 { + var total int64 + for _, child := range d.children { + total += child.Size() + } + return total +} + +func (d *Directory) Add(node FileSystemNode) { + d.children = append(d.children, node) +} + +// Usage +root := &Directory{name: "/"} +home := &Directory{name: "home"} +root.Add(home) + +home.Add(&File{name: "doc.txt", size: 1024}) +home.Add(&File{name: "image.png", size: 2048}) + +fmt.Printf("Total size: %d bytes\n", root.Size()) // 3072 +``` + +--- + +[Content continues with Behavioral Patterns, Concurrency Patterns, Error Handling, and Testing sections - truncated for length] + +This reference is meant to be comprehensive. For day-to-day work, use the quick reference in `../SKILL.md`. diff --git a/src/plugins/golang-design-pattern/snippets/scripts/detect-antipatterns.sh b/src/plugins/golang-design-pattern/snippets/scripts/detect-antipatterns.sh new file mode 100755 index 0000000..2620231 --- /dev/null +++ b/src/plugins/golang-design-pattern/snippets/scripts/detect-antipatterns.sh @@ -0,0 +1,101 @@ +#!/bin/bash +# Pattern Detector: Scan Go files for common anti-patterns + +set -euo pipefail + +# Colors for output +RED='\033[0;31m' +YELLOW='\033[1;33m' +GREEN='\033[0;32m' +NC='\033[0m' # No Color + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TARGET_DIR="${1:-.}" + +echo "๐Ÿ” Scanning Go files in: $TARGET_DIR" +echo "" + +# Initialize counters +issues_found=0 + +# Check for global variables +echo "Checking for global mutable state..." +if grep -rn --include="*.go" "^var.*=.*\(sql.DB\|http.Client\|redis.Client\)" "$TARGET_DIR" 2>/dev/null; then + echo -e "${RED}โš  Found global database/client connections${NC}" + echo " Consider dependency injection instead" + ((issues_found++)) +fi + +# Check for init() usage +echo "" +echo "Checking for init() functions..." +init_count=$(grep -rn --include="*.go" "^func init()" "$TARGET_DIR" 2>/dev/null | wc -l) +if [ "$init_count" -gt 0 ]; then + echo -e "${YELLOW}โš  Found $init_count init() functions${NC}" + grep -rn --include="*.go" "^func init()" "$TARGET_DIR" 2>/dev/null || true + echo " Consider explicit initialization in constructors" + ((issues_found++)) +fi + +# Check for ignored errors +echo "" +echo "Checking for ignored errors..." +if grep -rn --include="*.go" '^\s*_\s*=.*\(Error\|err\)' "$TARGET_DIR" 2>/dev/null; then + echo -e "${RED}โš  Found ignored errors with blank identifier${NC}" + echo " Always check and handle errors" + ((issues_found++)) +fi + +# Check for panic in non-init code +echo "" +echo "Checking for panic usage..." +if grep -rn --include="*.go" 'panic(' "$TARGET_DIR" 2>/dev/null | grep -v "func init()" | head -5; then + echo -e "${YELLOW}โš  Found panic() outside init()${NC}" + echo " Consider returning errors instead" + ((issues_found++)) +fi + +# Check for goroutines without context +echo "" +echo "Checking for goroutines without context..." +if grep -rn --include="*.go" 'go func()' "$TARGET_DIR" 2>/dev/null | grep -v "context.Context" | head -5; then + echo -e "${YELLOW}โš  Found goroutines potentially without cancellation${NC}" + echo " Pass context.Context for proper cancellation" + ((issues_found++)) +fi + +# Check for large interfaces (>5 methods) +echo "" +echo "Checking for large interfaces..." +while IFS= read -r file; do + awk ' + /^type .* interface {/ { + interface_name = $2 + method_count = 0 + in_interface = 1 + } + in_interface && /^[[:space:]]+[A-Z].*\(/ { + method_count++ + } + in_interface && /^}/ { + if (method_count > 5) { + print FILENAME ":" interface_name " has " method_count " methods (consider splitting)" + } + in_interface = 0 + } + ' "$file" +done < <(find "$TARGET_DIR" -name "*.go" 2>/dev/null) + +# Summary +echo "" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +if [ $issues_found -eq 0 ]; then + echo -e "${GREEN}โœ“ No obvious anti-patterns detected${NC}" +else + echo -e "${RED}Found $issues_found potential issues${NC}" + echo "" + echo "Review the warnings above and consult:" + echo " - references/anti-patterns.md for detailed explanations" + echo " - SKILL.md for idiomatic alternatives" +fi +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" diff --git a/src/plugins/pinchtab/index.ts b/src/plugins/pinchtab/index.ts new file mode 100644 index 0000000..6b53ff4 --- /dev/null +++ b/src/plugins/pinchtab/index.ts @@ -0,0 +1,3 @@ +import { createStaticSkillPlugin } from "../../shared/static-skill.js"; + +export const pinchtabPlugin = createStaticSkillPlugin("pinchtab", "The pinchtab skill."); diff --git a/src/plugins/pinchtab/snippets/SKILL.txt b/src/plugins/pinchtab/snippets/SKILL.txt new file mode 100644 index 0000000..dc52671 --- /dev/null +++ b/src/plugins/pinchtab/snippets/SKILL.txt @@ -0,0 +1,494 @@ +--- +name: pinchtab +description: "Use this skill when a task needs browser automation through PinchTab: open a website, inspect interactive elements, click through flows, fill out forms, scrape page text, log into sites with a persistent profile, export screenshots or PDFs, manage multiple browser instances, or fall back to the HTTP API when the CLI is unavailable. Prefer this skill for token-efficient browser work driven by stable accessibility refs such as `e5` and `e12`." +metadata: + openclaw: + requires: + bins: + - pinchtab + anyBins: + - google-chrome + - google-chrome-stable + - chromium + - chromium-browser + env: + - PINCHTAB_TOKEN + - PINCHTAB_CONFIG + homepage: https://github.com/pinchtab/pinchtab + install: + - kind: brew + formula: pinchtab/tap/pinchtab + bins: [pinchtab] + - kind: go + package: github.com/pinchtab/pinchtab/cmd/pinchtab@latest + bins: [pinchtab] +--- + +# Browser Automation with PinchTab + +PinchTab gives agents a browser they can drive through stable accessibility refs, low-token text extraction, and persistent profiles or instances. Treat it as a CLI-first browser skill; use the HTTP API only when the CLI is unavailable or you need profile-management routes that do not exist in the CLI yet. + +Preferred tool surface: + +- Use `pinchtab` CLI commands first. +- Use `curl` for profile-management routes or non-shell/API fallback flows. +- Use `jq` only when you need structured parsing from JSON responses. + +## Safety Defaults + +- Default to `http://localhost` targets. Only use a remote PinchTab server when the user explicitly provides it and, if needed, a token. +- Prefer read-only operations first: `text`, `snap -i -c`, `snap -d`, `find`, `click`, `fill`, `type`, `press`, `select`, `hover`, `scroll`. +- Do not evaluate arbitrary JavaScript unless a simpler PinchTab command cannot answer the question. +- Do not upload local files unless the user explicitly names the file to upload and the destination flow requires it. +- Do not save screenshots, PDFs, or downloads to arbitrary paths. Use a user-specified path or a safe temporary/workspace path. +- Never use PinchTab to inspect unrelated local files, browser secrets, stored credentials, or system configuration outside the task. + +## Core Workflow + +Every PinchTab automation follows this pattern: + +1. Ensure the correct server, profile, or instance is available for the task. +2. Navigate with `pinchtab nav ` or `pinchtab instance navigate `. +3. Observe with `pinchtab snap -i -c`, `pinchtab snap --text`, or `pinchtab text`, then collect the current refs such as `e5`. +4. Interact with those fresh refs using `click`, `fill`, `type`, `press`, `select`, `hover`, or `scroll`. +5. Re-snapshot or re-read text after any navigation, submit, modal open, accordion expand, or other DOM-changing action. + +Rules: + +- Never act on stale refs after the page changes. +- Default to `pinchtab text` when you need content, not layout. +- Default to `pinchtab snap -i -c` when you need actionable elements. +- Use screenshots only for visual verification, UI diffs, or debugging. +- Start multi-site or parallel work by choosing the right instance or profile first. + +## Selectors + +PinchTab uses a unified selector system. Any command that targets an element accepts these formats: + +| Selector | Example | Resolves via | +|---|---|---| +| Ref | `e5` | Snapshot cache (fastest) | +| CSS | `#login`, `.btn`, `[data-testid="x"]` | `document.querySelector` | +| XPath | `xpath://button[@id="submit"]` | CDP search | +| Text | `text:Sign In` | Visible text match | +| Semantic | `find:login button` | Natural language query via `/find` | + +Auto-detection: bare `e5` โ†’ ref, `#id` / `.class` / `[attr]` โ†’ CSS, `//path` โ†’ XPath. Use explicit prefixes (`css:`, `xpath:`, `text:`, `find:`) when auto-detection is ambiguous. + +```bash +pinchtab click e5 # ref +pinchtab click "#submit" # CSS (auto-detected) +pinchtab click "text:Sign In" # text match +pinchtab click "xpath://button[@type]" # XPath +pinchtab fill "#email" "user@test.com" # CSS +pinchtab fill e3 "user@test.com" # ref +``` + +The same syntax works in the HTTP API via the `selector` field: + +```json +{"kind": "click", "selector": "text:Sign In"} +{"kind": "fill", "selector": "#email", "text": "user@test.com"} +{"kind": "click", "selector": "e5"} +``` + +Legacy `ref` field is still accepted for backward compatibility. + +## Command Chaining + +Use `&&` only when you do not need to inspect intermediate output before deciding the next step. + +Good: + +```bash +pinchtab nav https://example.com && pinchtab snap -i -c +pinchtab click --wait-nav e5 && pinchtab snap -i -c +pinchtab nav https://example.com --block-images && pinchtab text +``` + +Run commands separately when you must read the snapshot output first: + +```bash +pinchtab nav https://example.com +pinchtab snap -i -c +# Read refs, choose the correct e# +pinchtab click e7 +pinchtab snap -i -c +``` + +## Challenge Solving + +PinchTab includes a pluggable solver framework that auto-detects and resolves browser challenges (Cloudflare Turnstile, CAPTCHAs, interstitials). Use this **after navigation** when the page shows a challenge instead of the expected content. + +**Important:** Solvers work best with `stealthLevel: "full"` in the PinchTab config (or `instanceDefaults.stealthLevel: "full"`). Full stealth mode patches CDP detection vectors, rotates fingerprints, and masks automation signals โ€” all of which challenge providers like Cloudflare check before and after the checkbox click. Without full stealth, the solver may click correctly but the challenge can still fail fingerprint verification. + +```bash +# Auto-detect and solve any challenge on the current page +curl -X POST http://localhost:9867/solve \ + -H 'Content-Type: application/json' \ + -d '{"maxAttempts": 3, "timeout": 30000}' + +# Use a specific solver +curl -X POST http://localhost:9867/solve/cloudflare \ + -H 'Content-Type: application/json' \ + -d '{"maxAttempts": 3}' + +# Tab-scoped solve +curl -X POST http://localhost:9867/tabs/TAB_ID/solve \ + -H 'Content-Type: application/json' \ + -d '{}' + +# List available solvers +curl http://localhost:9867/solvers +``` + +**When to use solve:** + +- Page title is "Just a moment..." or similar challenge indicator +- `pinchtab text` returns empty or challenge-page text after navigation +- A Cloudflare Turnstile widget blocks the target content + +**Workflow pattern:** + +```bash +pinchtab nav https://protected-site.com +pinchtab text # Check if page loaded or shows challenge +# If challenge detected: +curl -X POST http://localhost:9867/solve \ + -H 'Content-Type: application/json' -d '{}' +pinchtab text # Verify: should now show real page content +``` + +**Response fields:** `solver` (which solver handled it), `solved` (bool), `challengeType` (e.g. "managed"), `attempts`, `title` (final page title). + +The auto-detect mode (`POST /solve` without specifying a solver) tries each registered solver in order and returns immediately with `solved: true, attempts: 0` if no challenge is present. This makes it safe to call speculatively after any navigation. + +## Handling Authentication and State + +Pick one of these five patterns before you start interacting with the site. + +### 1. One-off public browsing + +Use a temporary instance for public pages, scraping, or tasks that do not need login persistence. + +```bash +pinchtab instance start +pinchtab instances +# Point CLI commands at the instance port you want to use. +pinchtab --server http://localhost:9868 nav https://example.com +pinchtab --server http://localhost:9868 text +``` + +### 2. Reuse an existing named profile + +Use this for recurring tasks against the same authenticated site. + +```bash +pinchtab profiles +pinchtab instance start --profile work --mode headed +pinchtab --server http://localhost:9868 nav https://mail.google.com +``` + +If the login is already stored in that profile, you can switch to headless later: + +```bash +pinchtab instance stop inst_ea2e747f +pinchtab instance start --profile work --mode headless +``` + +### 3. Create a dedicated auth profile over HTTP + +Use this when you need a durable profile and it does not exist yet. + +```bash +curl -X POST http://localhost:9867/profiles \ + -H "Content-Type: application/json" \ + -d '{"name":"billing","description":"Billing portal automation","useWhen":"Use for billing tasks"}' + +curl -X POST http://localhost:9867/profiles/billing/start \ + -H "Content-Type: application/json" \ + -d '{"headless":false}' +``` + +Then target the returned port with `--server`. + +### 4. Human-assisted headed login, then agent reuse + +Use this for CAPTCHA, MFA, or first-time setup. + +```bash +pinchtab instance start --profile work --mode headed +# Human completes login in the visible Chrome window. +pinchtab --server http://localhost:9868 nav https://app.example.com/dashboard +pinchtab --server http://localhost:9868 snap -i -c +``` + +Once the session is stored, reuse the same profile for later tasks. + +### 5. Remote or non-shell agent with tokenized HTTP API + +Use this when the agent cannot call the CLI directly. + +```bash +curl http://localhost:9867/health +curl -X POST http://localhost:9867/profiles \ + -H "Content-Type: application/json" \ + -d '{"name":"work"}' +curl -X POST http://localhost:9867/instances/start \ + -H "Content-Type: application/json" \ + -d '{"profileId":"work","mode":"headless"}' +curl -X POST http://localhost:9868/action \ + -H "Content-Type: application/json" \ + -d '{"kind":"click","selector":"e5"}' +``` + +If the server is exposed beyond localhost, require a token and use a dedicated automation profile. See [TRUST.md](./TRUST.md) and [config.md](../../docs/reference/config.md). + +## Essential Commands + +### Server and targeting + +```bash +pinchtab server # Start server foreground +pinchtab daemon install # Install as system service +pinchtab health # Check server status +pinchtab instances # List running instances +pinchtab profiles # List available profiles +pinchtab --server http://localhost:9868 snap -i -c # Target specific instance +``` + +### Navigation and tabs + +```bash +pinchtab nav +pinchtab nav --new-tab +pinchtab nav --tab +pinchtab nav --block-images +pinchtab nav --block-ads +pinchtab back # Navigate back in history +pinchtab forward # Navigate forward +pinchtab reload # Reload current page +pinchtab tab # List tabs or focus by ID +pinchtab tab new +pinchtab tab close +pinchtab instance navigate +``` + +### Observation + +```bash +pinchtab snap +pinchtab snap -i # Interactive elements only +pinchtab snap -i -c # Interactive + compact +pinchtab snap -d # Diff from previous snapshot +pinchtab snap --selector # Scope to CSS selector +pinchtab snap --max-tokens # Token budget limit +pinchtab snap --text # Text output format +pinchtab text # Page text content +pinchtab text --raw # Raw text extraction +pinchtab find # Semantic element search +pinchtab find --ref-only # Return refs only +``` + +Guidance: + +- `snap -i -c` is the default for finding actionable refs. +- `snap -d` is the default follow-up snapshot for multi-step flows. +- `text` is the default for reading articles, dashboards, reports, or confirmation messages. +- `find --ref-only` is useful when the page is large and you already know the semantic target. + +### Interaction + +All interaction commands accept unified selectors (refs, CSS, XPath, text, semantic). See the Selectors section above. + +```bash +pinchtab click # Click element +pinchtab click --wait-nav # Click and wait for navigation +pinchtab click --x 100 --y 200 # Click by coordinates +pinchtab dblclick # Double-click element +pinchtab type # Type with keystrokes +pinchtab fill # Set value directly +pinchtab press # Press key (Enter, Tab, Escape...) +pinchtab hover # Hover element +pinchtab select # Select dropdown option +pinchtab scroll # Scroll element or page +``` + +Rules: + +- Prefer `fill` for deterministic form entry. +- Prefer `type` only when the site depends on keystroke events. +- Prefer `click --wait-nav` when a click is expected to navigate. +- Re-snapshot immediately after `click`, `press Enter`, `select`, or `scroll` if the UI can change. + +### Export, debug, and verification + +```bash +pinchtab screenshot +pinchtab screenshot -o /tmp/pinchtab-page.png # Format driven by extension +pinchtab screenshot -q 60 # JPEG quality +pinchtab pdf +pinchtab pdf -o /tmp/pinchtab-report.pdf +pinchtab pdf --landscape +``` + +### Advanced operations: explicit opt-in only + +Use these only when the task explicitly requires them and safer commands are insufficient. + +```bash +pinchtab eval "document.title" +pinchtab download -o /tmp/pinchtab-download.bin +pinchtab upload /absolute/path/provided-by-user.ext -s +``` + +Rules: + +- `eval` is for narrow, read-only DOM inspection unless the user explicitly asks for a page mutation. +- `download` should prefer a safe temporary or workspace path over an arbitrary filesystem location. +- `upload` requires a file path the user explicitly provided or clearly approved for the task. + +### HTTP API fallback + +```bash +curl -X POST http://localhost:9868/navigate \ + -H "Content-Type: application/json" \ + -d '{"url":"https://example.com"}' + +curl "http://localhost:9868/snapshot?filter=interactive&format=compact" + +curl -X POST http://localhost:9868/action \ + -H "Content-Type: application/json" \ + -d '{"kind":"fill","selector":"e3","text":"ada@example.com"}' + +curl http://localhost:9868/text + +## Instance-scoped solve (instance port, not server port) +curl -X POST http://localhost:9868/solve \ + -H "Content-Type: application/json" \ + -d '{"maxAttempts": 3}' + +curl http://localhost:9868/solvers +``` + +Use the API when: + +- the agent cannot shell out, +- profile creation or mutation is required, +- or you need explicit instance- and tab-scoped routes. + +## Common Patterns + +### Open a page and inspect actions + +```bash +pinchtab nav https://pinchtab.com && pinchtab snap -i -c +``` + +### Fill and submit a form + +```bash +pinchtab nav https://example.com/login +pinchtab snap -i -c +pinchtab fill e3 "user@example.com" +pinchtab fill e4 "correct horse battery staple" +pinchtab click --wait-nav e5 +pinchtab text +``` + +### Search, then extract the result page cheaply + +```bash +pinchtab nav https://example.com +pinchtab snap -i -c +pinchtab fill e2 "quarterly report" +pinchtab press Enter +pinchtab text +``` + +### Use diff snapshots in a multi-step flow + +```bash +pinchtab nav https://example.com/checkout +pinchtab snap -i -c +pinchtab click e8 +pinchtab snap -d -i -c +``` + +### Target elements without a snapshot + +When you know the page structure, skip the snapshot and use CSS or text selectors directly: + +```bash +pinchtab click "text:Accept Cookies" +pinchtab fill "#search" "quarterly report" +pinchtab click "xpath://button[@type='submit']" +``` + +### Navigate through a Cloudflare-protected site + +```bash +pinchtab nav https://protected-site.com +# Page may show CF challenge ("Just a moment...") +curl -X POST http://localhost:9867/solve \ + -H 'Content-Type: application/json' -d '{"maxAttempts": 3}' +# Now the real page is loaded โ€” proceed normally +pinchtab snap -i -c +pinchtab text +``` + +### Bootstrap an authenticated profile + +```bash +pinchtab profiles +pinchtab instance start --profile work --mode headed +# Human signs in once. +pinchtab --server http://localhost:9868 text +``` + +### Run separate instances for separate sites + +```bash +pinchtab instance start --profile work --mode headless +pinchtab instance start --profile staging --mode headless +pinchtab instances +``` + +Then point each command stream at its own port using `--server`. + +## Security and Token Economy + +- Use a dedicated automation profile, not a daily browsing profile. +- If PinchTab is reachable off-machine, require a token and bind conservatively. +- Prefer `text`, `snap -i -c`, and `snap -d` before screenshots, PDFs, eval, downloads, or uploads. +- Use `--block-images` for read-heavy tasks that do not need visual assets. +- Stop or isolate instances when switching between unrelated accounts or environments. + +## Diffing and Verification + +- Use `pinchtab snap -d` after each state-changing action in long workflows. +- Use `pinchtab text` to confirm success messages, table updates, or navigation outcomes. +- Use `pinchtab screenshot` only when visual regressions, CAPTCHA, or layout-specific confirmation matters. +- If a ref disappears after a change, treat that as expected and fetch fresh refs instead of retrying the stale one. + +## Privacy and Security + +PinchTab is a fully open-source, local-only browser automation tool: + +- **Runs on localhost only.** The server binds to `127.0.0.1` by default. No external network calls are made by PinchTab itself. +- **No telemetry or analytics.** The binary makes zero outbound connections. +- **Single Go binary (~16 MB).** Fully verifiable โ€” anyone can build from source at [github.com/pinchtab/pinchtab](https://github.com/pinchtab/pinchtab). +- **Local Chrome profiles.** Persistent profiles store cookies and sessions on your machine only. This enables agents to reuse authenticated sessions without re-entering credentials, similar to how a human reuses their browser profile. +- **Token-efficient by design.** Uses the accessibility tree (structured text) instead of screenshots, keeping agent context windows small. Comparable to Playwright but purpose-built for AI agents. +- **Multi-instance isolation.** Each browser instance runs in its own profile directory with tab-level locking for safe multi-agent use. + +## References + +- Command surface: [commands.md](../../docs/commands.md) +- CLI overview: [cli.md](../../docs/reference/cli.md) +- Profiles: [profiles.md](../../docs/reference/profiles.md) +- Instances: [instances.md](../../docs/reference/instances.md) +- Full API: [api.md](./references/api.md) +- Minimal env vars: [env.md](./references/env.md) +- Config reference: [config.md](../../docs/reference/config.md) +- Security model: [TRUST.md](./TRUST.md) diff --git a/src/plugins/pinchtab/snippets/TRUST.txt b/src/plugins/pinchtab/snippets/TRUST.txt new file mode 100644 index 0000000..d378a42 --- /dev/null +++ b/src/plugins/pinchtab/snippets/TRUST.txt @@ -0,0 +1,83 @@ +# Pinchtab Security & Trust + +**TL;DR**: Pinchtab is a local, sandboxed browser control tool. It does not phone home, steal credentials, or exfiltrate data. Source code is public; binaries are signed and published via GitHub. + +## What Pinchtab Does + +- Launches a Chrome browser (local, under your control) +- Exposes navigation, clicking, typing, and page inspection via HTTP API +- Extracts the page's accessibility tree (for AI agents) +- Runs screenshots, PDFs, and JavaScript evaluation + +High-risk operations such as JavaScript evaluation, local-file upload, and direct file writes should be treated as explicit opt-in actions for the current task, not the default workflow. + +**All of this stays local.** No telemetry. No external API calls (except to sites you navigate to). + +## What Pinchtab Does NOT Do + +- โŒ Doesn't access your saved passwords/credentials (Chrome sandboxing) +- โŒ Doesn't exfiltrate data to remote servers +- โŒ Doesn't inject ads, malware, or miners +- โŒ Doesn't track browsing or send analytics +- โŒ Doesn't modify system files outside its state directory (`~/.pinchtab`) + +## Builds & Verification + +Every release includes **checksums** alongside binaries: + +```bash +# After downloading, verify: +sha256sum -c checksums.txt +``` + +Binaries are built automatically from tagged commits via GitHub Actions (publicly visible at https://github.com/pinchtab/pinchtab/actions). + +## Open Source + +- **Source**: https://github.com/pinchtab/pinchtab (MIT) +- **Releases**: https://github.com/pinchtab/pinchtab/releases +- **Latest**: v0.8.4 (Mar 2026) + +If you're concerned, audit the sourceโ€”it's ~15MB, zero external dependencies, mostly Go stdlib. + +## VirusTotal Flag + +Pinchtab may trigger heuristic scanners on VirusTotal because: + +- โœ“ It launches Chrome (subprocess execution โ€” flagged by AV heuristics) +- โœ“ It runs JavaScript evaluation (eval-like operations) +- โœ“ It makes HTTP requests (network activity) + +These are **intentional design features**, not security flaws. Your browser does all three things by default. + +**False positives are common for development tools.** The VT flag is a known false positive for chromedp-based tools (subprocess + HTTP server). Always verify SHA256 checksums from GitHub releases before running. + +For maximum confidence, use the npm package (`npm install -g pinchtab`) or Docker image, which undergo additional validation. + +## Sandboxing + +Pinchtab runs a separate Chrome process with: + +- Isolated profile directory (default: `~/.pinchtab`) +- No access to your user's home files (unless you explicitly navigate to `file://` URLs) +- Standard Chrome security model (site isolation, CSP, etc.) + +Use `profiles.baseDir`, `profiles.defaultProfile`, or `PINCHTAB_CONFIG` if you need to control where PinchTab stores browser state. + +## Security History + +| CVE | Severity | Affected | Fixed In | Endpoint | +| --- | --- | --- | --- | --- | +| [CVE-2026-30834](https://github.com/advisories/GHSA-rw8p-c6hf-q3pg) | High (7.5) | < 0.7.7 | 0.7.7 | `/download` | + +**Type:** Server-Side Request Forgery (SSRF) โ€” allowed exfiltration of internal files and network probing via crafted download URLs. + +**Fix PRs:** [#135](https://github.com/pinchtab/pinchtab/pull/135) (SafePath validation), [#288](https://github.com/pinchtab/pinchtab/pull/288) (expanded URL validation). + +**Minimum recommended version:** 0.8.3+ (includes full SSRF hardening). + +## Questions? + +- Source code: https://github.com/pinchtab/pinchtab +- Issues/security reports: https://github.com/pinchtab/pinchtab/issues +- Docs: https://pinchtab.com diff --git a/src/plugins/pinchtab/snippets/references/agent-optimization.txt b/src/plugins/pinchtab/snippets/references/agent-optimization.txt new file mode 100644 index 0000000..529b940 --- /dev/null +++ b/src/plugins/pinchtab/snippets/references/agent-optimization.txt @@ -0,0 +1,174 @@ +# Agent Optimization Playbook + +Practical guidance for running token-efficient, resilient PinchTab agent workflows. + +--- + +## Cheapest-Path Decision Tree + +Choose the lowest-cost tool that satisfies your goal: + +``` +Need to check page state? +โ”œโ”€ Know the element ref already? โ†’ skip snap, use click/type directly +โ”œโ”€ Need to find interactive elements? โ†’ snap -i -c (cheapest) +โ”œโ”€ Need to read text/data only? โ†’ pinchtab text (no tree overhead) +โ”œโ”€ Need to find a specific element? โ†’ pinchtab find "" +โ”œโ”€ Need full page structure? โ†’ snap -c (compact tree) +โ”œโ”€ Need to debug visually? โ†’ screenshot (use sparingly, large output) +โ””โ”€ Need to run a JS check? โ†’ eval (precise, zero visual overhead) +``` + +**Token cost ranking (cheapest โ†’ most expensive):** +1. `eval` โ€” single value, no DOM traversal output +2. `find` โ€” targeted element list only +3. `text` โ€” readable text only +4. `snap -i -c` โ€” interactive elements, compact format +5. `snap -c` โ€” full tree, compact +6. `snap -i` โ€” interactive elements, verbose +7. `snap` โ€” full tree, verbose +8. `screenshot` โ€” image payload, highest token cost + +**Rule of thumb:** Reach for `snap -i -c` as your default snapshot. Only escalate to `screenshot` when visual layout matters (CAPTCHA, canvas, complex CSS). + +--- + +## Diff Snapshots for Follow-Up Reads + +After an interaction, use `snap -d` to see only what changed โ€” not the full tree. + +```bash +pinchtab click e5 # trigger action +pinchtab snap -d # only the delta โ€” much smaller +``` + +**When to use `-d`:** +- After clicks that update part of the UI (e.g. accordion opens, toast appears) +- After form submissions that show inline validation +- During multi-step wizards where only one section changes + +**When NOT to use `-d`:** +- After full page navigations (diff will be the entire new page) +- After `nav` โ€” always take a fresh `snap` instead +- First snapshot of a session (no baseline exists) + +--- + +## Lite Engine + +Start PinchTab with `--engine lite` for minimal rendering overhead. + +```bash +pinchtab start --engine lite +``` + +**Lite engine capabilities:** +- Faster page loads (no CSS animations, reduced JS execution) +- Lower memory footprint โ€” useful for multi-tab fleet workflows +- Accessibility tree (`snap`) works fully +- `text`, `find`, `eval` all work as normal + +**Lite engine limitations:** +- `screenshot` output may not reflect full visual styling +- Pages that depend on CSS transitions for state changes may behave differently +- Some canvas/WebGL content will not render +- Not suitable for visual regression testing + +**Best for:** Form automation, data extraction, API-heavy SPAs, scraping workflows where visual fidelity is not required. + +--- + +## Recovery Patterns + +### 403 Forbidden +**Cause:** `eval` called without `security.allowEvaluate: true`, or a page blocked the request. + +**Recovery:** +```bash +# Option 1: enable eval in config, restart server +# Option 2: switch to snap + find instead of eval +pinchtab find "target text" # avoids eval entirely +``` + +--- + +### 401 Unauthorized +**Cause:** Session expired, auth cookie gone, or protected resource. + +**Recovery:** +1. `pinchtab screenshot` โ€” confirm login page is showing +2. Re-authenticate: `pinchtab nav `, then fill credentials +3. If using a profile: `pinchtab profile use ` may restore the session + +--- + +### Connection Refused +**Cause:** PinchTab server is not running or crashed. + +**Recovery:** +```bash +pinchtab health # confirm down +pinchtab start # restart +pinchtab health # confirm up before continuing +``` + +For fleet workflows: check `pinchtab instances` to confirm the right instance is running. + +--- + +### Stale Element Refs +**Cause:** A `snap` was taken, then the page re-rendered (navigation, dynamic update). Old refs (`e5`, `e12`) are no longer valid. + +**Symptoms:** Interaction returns "ref not found" or acts on the wrong element. + +**Recovery:** +```bash +pinchtab snap -i -c # fresh snapshot โ†’ new refs +# Now use the new refs from this response +``` + +**Prevention:** Never cache refs across navigations. Always re-snap after a page load or major DOM update. + +--- + +### Bot Detection / CAPTCHA / Cloudflare +**Cause:** Target site detected automated behavior or uses a challenge gateway. + +**Recovery options:** +1. Try `POST /solve` first โ€” it auto-detects Cloudflare Turnstile and solves it: + ```bash + curl -X POST http://localhost:9867/solve \ + -H 'Content-Type: application/json' -d '{"maxAttempts": 3}' + ``` +2. If solve returns `solved: false`, try with more attempts or check `challengeType` +3. Slow down: add `pinchtab wait --ms 1500` between interactions +4. Switch to a profile with existing session cookies (CF cookies persist) +5. If unsupported CAPTCHA (not Cloudflare): report to user for manual intervention +6. Check `GET /solvers` to see which solver types are available +7. Verify `stealthLevel: "full"` is active โ€” solvers depend on it. Check with `GET /stealth/status` + +--- + +### Timeout on Navigation +**Cause:** Page load exceeded default timeout (usually 30s). + +**Recovery:** +```bash +pinchtab nav --timeout 90 # extend timeout +``` + +If the page consistently times out, consider `--block-images` to speed up load: +```bash +pinchtab nav --block-images --timeout 60 +``` + +--- + +## General Efficiency Rules + +- **Batch reads before writes.** Snap once, extract all refs, then act. Avoid snap โ†’ act โ†’ snap โ†’ act loops when you can snap โ†’ act ร— N โ†’ snap once to verify. +- **Use `text` for extraction tasks.** If you only need to read content (not interact), `text` is cheaper than `snap` + parsing. +- **Scope snapshots.** Use `snap -s ` to target a specific section of the page when you know where the element is. +- **Prefer `fill` over `type` for framework forms.** Saves retries caused by React/Vue not detecting raw keystroke events. +- **Check health before long workflows.** Run `pinchtab health` at the start of a multi-step task to fail fast if the server is down. +- **Export network traces after sessions.** `pinchtab network-export -o session.har` captures every request. For live capture: `pinchtab network-export --stream -o live.har`. diff --git a/src/plugins/pinchtab/snippets/references/api.txt b/src/plugins/pinchtab/snippets/references/api.txt new file mode 100644 index 0000000..fa30c60 --- /dev/null +++ b/src/plugins/pinchtab/snippets/references/api.txt @@ -0,0 +1,426 @@ +# Pinchtab API Reference + +Base URL for all examples: `http://localhost:9867` + +> **CLI alternative:** All endpoints have CLI equivalents. Use `pinchtab help` for the full list. Examples are shown as `# CLI:` comments below. + +## Navigate + +```bash +# CLI: pinchtab nav https://pinchtab.com [--new-tab] [--block-images] +curl -X POST /navigate \ + -H 'Content-Type: application/json' \ + -d '{"url": "https://pinchtab.com"}' + +# With options: custom timeout, block images, open in new tab +curl -X POST /navigate \ + -H 'Content-Type: application/json' \ + -d '{"url": "https://pinchtab.com", "timeout": 60, "blockImages": true, "newTab": true}' +``` + +## Snapshot (accessibility tree) + +```bash +# CLI: pinchtab snap [-i] [-c] [-d] [-s main] [--max-tokens 2000] +# Full tree +curl /snapshot + +# Interactive elements only (buttons, links, inputs) โ€” much smaller +curl "/snapshot?filter=interactive" + +# Limit depth +curl "/snapshot?depth=5" + +# Smart diff โ€” only changes since last snapshot (massive token savings) +curl "/snapshot?diff=true" + +# Text format โ€” indented tree, ~40-60% fewer tokens than JSON +curl "/snapshot?format=text" + +# Compact format โ€” one-line-per-node, 56-64% fewer tokens than JSON (recommended) +curl "/snapshot?format=compact" + +# YAML format +curl "/snapshot?format=yaml" + +# Scope to CSS selector (e.g. main content only) +curl "/snapshot?selector=main" + +# Truncate to ~N tokens +curl "/snapshot?maxTokens=2000" + +# Combine for maximum efficiency +curl "/snapshot?format=compact&selector=main&maxTokens=2000&filter=interactive" + +# Disable animations before capture +curl "/snapshot?noAnimations=true" + +# Write to file +curl "/snapshot?output=file&path=/tmp/snapshot.json" +``` + +Returns flat JSON array of nodes with `ref`, `role`, `name`, `depth`, `value`, `nodeId`. + +**Token optimization**: Use `?format=compact` for best token efficiency. Add `?filter=interactive` for action-oriented tasks (~75% fewer nodes). Use `?selector=main` to scope to relevant content. Use `?maxTokens=2000` to cap output. Use `?diff=true` on multi-step workflows to see only changes. Combine all params freely. + +## Act on elements + +```bash +# CLI: pinchtab click e5 / pinchtab type e12 hello / pinchtab press Enter +# Click by ref +curl -X POST /action -H 'Content-Type: application/json' \ + -d '{"kind": "click", "ref": "e5"}' + +# Type into focused element (click first, then type) +curl -X POST /action -H 'Content-Type: application/json' \ + -d '{"kind": "click", "ref": "e12"}' +curl -X POST /action -H 'Content-Type: application/json' \ + -d '{"kind": "type", "ref": "e12", "text": "hello world"}' + +# Press a key +curl -X POST /action -H 'Content-Type: application/json' \ + -d '{"kind": "press", "key": "Enter"}' + +# Focus an element +curl -X POST /action -H 'Content-Type: application/json' \ + -d '{"kind": "focus", "ref": "e3"}' + +# Fill (set value directly, no keystrokes) +curl -X POST /action -H 'Content-Type: application/json' \ + -d '{"kind": "fill", "selector": "#email", "text": "user@pinchtab.com"}' + +# Hover (trigger dropdowns/tooltips) +curl -X POST /action -H 'Content-Type: application/json' \ + -d '{"kind": "hover", "ref": "e8"}' + +# Select dropdown option (by value or visible text) +curl -X POST /action -H 'Content-Type: application/json' \ + -d '{"kind": "select", "ref": "e10", "value": "option2"}' + +# Scroll to element +curl -X POST /action -H 'Content-Type: application/json' \ + -d '{"kind": "scroll", "ref": "e20"}' + +# Scroll by pixels (infinite scroll pages) +curl -X POST /action -H 'Content-Type: application/json' \ + -d '{"kind": "scroll", "scrollY": 800}' + +# Click and wait for navigation (link clicks) +curl -X POST /action -H 'Content-Type: application/json' \ + -d '{"kind": "click", "ref": "e5", "waitNav": true}' +``` + +## Batch actions + +```bash +# Execute multiple actions in sequence +curl -X POST /actions -H 'Content-Type: application/json' \ + -d '{"actions":[{"kind":"click","ref":"e3"},{"kind":"type","ref":"e3","text":"hello"},{"kind":"press","key":"Enter"}]}' + +# Stop on first error (default: false) +curl -X POST /actions -H 'Content-Type: application/json' \ + -d '{"tabId":"TARGET_ID","actions":[...],"stopOnError":true}' +``` + +## Extract text + +```bash +# CLI: pinchtab text [--raw] +# Readability mode (default) โ€” strips nav/footer/ads +curl /text + +# Raw innerText +curl "/text?mode=raw" +``` + +Returns `{url, title, text}`. Cheapest option (~1K tokens for most pages). + +## PDF export + +Prefer returning base64 or raw bytes unless the user explicitly wants a file written to disk. +When writing to disk, use a safe temporary or workspace path. + +```bash +# CLI: pinchtab pdf --tab TAB_ID [-o file.pdf] [--landscape] [--scale 0.8] +# Returns base64 JSON +curl "/tabs/TAB_ID/pdf" + +# Raw PDF bytes +curl "/tabs/TAB_ID/pdf?raw=true" -o page.pdf + +# Save to disk in a safe temp location +curl "/tabs/TAB_ID/pdf?output=file&path=/tmp/pinchtab-page.pdf" + +# Landscape with custom scale +curl "/tabs/TAB_ID/pdf?landscape=true&scale=0.8&raw=true" -o page.pdf + +# Custom paper size (Letter: 8.5x11, A4: 8.27x11.69) +curl "/tabs/TAB_ID/pdf?paperWidth=8.5&paperHeight=11&marginTop=0.5&marginLeft=0.5&raw=true" -o custom.pdf + +# Export specific pages +curl "/tabs/TAB_ID/pdf?pageRanges=1-5&raw=true" -o pages.pdf + +# With header/footer +curl "/tabs/TAB_ID/pdf?displayHeaderFooter=true&headerTemplate=%3Cspan%20class=title%3E%3C/span%3E&raw=true" -o header.pdf + +# Accessible PDF with document outline +curl "/tabs/TAB_ID/pdf?generateTaggedPDF=true&generateDocumentOutline=true&raw=true" -o accessible.pdf + +# Honor CSS page size +curl "/tabs/TAB_ID/pdf?preferCSSPageSize=true&raw=true" -o css-sized.pdf +``` + +**Query Parameters:** + +| Param | Type | Default | Description | +|-------|------|---------|-------------| +| `paperWidth` | float | 8.5 | Paper width in inches | +| `paperHeight` | float | 11.0 | Paper height in inches | +| `landscape` | bool | false | Landscape orientation | +| `marginTop` | float | 0.4 | Top margin in inches | +| `marginBottom` | float | 0.4 | Bottom margin in inches | +| `marginLeft` | float | 0.4 | Left margin in inches | +| `marginRight` | float | 0.4 | Right margin in inches | +| `scale` | float | 1.0 | Print scale (0.1โ€“2.0) | +| `pageRanges` | string | all | Pages to export (e.g., `1-3,5`) | +| `displayHeaderFooter` | bool | false | Show header and footer | +| `headerTemplate` | string | โ€” | HTML template for header | +| `footerTemplate` | string | โ€” | HTML template for footer | +| `preferCSSPageSize` | bool | false | Honor CSS `@page` size | +| `generateTaggedPDF` | bool | false | Generate accessible/tagged PDF | +| `generateDocumentOutline` | bool | false | Embed document outline | +| `output` | string | JSON | `file` to save to disk, default returns base64 | +| `path` | string | auto | Destination path (prefer temp or workspace paths with `output=file`) | +| `raw` | bool | false | Return raw PDF bytes instead of JSON | + +Wraps `Page.printToPDF`. Prints background graphics by default. + +## Download files + +Prefer raw bytes or base64 responses unless the user explicitly asks for a saved file. + +```bash +# Returns base64 JSON by default (uses browser session/cookies/stealth) +curl "/download?url=https://site.com/report.pdf" + +# Raw bytes (pipe to file) +curl "/download?url=https://site.com/image.jpg&raw=true" -o image.jpg + +# Save directly to disk in a safe temp location +curl "/download?url=https://site.com/export.csv&output=file&path=/tmp/pinchtab-export.csv" +``` + +## Upload files + +Only upload local files the user explicitly provided or approved for the task. + +```bash +# Upload a local file to a file input +curl -X POST "/upload?tabId=TAB_ID" -H "Content-Type: application/json" \ + -d '{"selector": "input[type=file]", "paths": ["/tmp/user-approved-photo.jpg"]}' + +# Upload base64-encoded data +curl -X POST /upload -H "Content-Type: application/json" \ + -d '{"selector": "#avatar-input", "files": ["data:image/png;base64,iVBOR..."]}' +``` + +Sets files on `` elements via CDP. Fires `change` events. Selector defaults to `input[type=file]` if omitted. + +## Screenshot + +```bash +# CLI: pinchtab ss [-o file.jpg] [-q 80] +# Returns raw JPEG (default) +curl "/screenshot?raw=true" -o screenshot.jpg +curl "/screenshot?raw=true&quality=50" -o screenshot.jpg + +# Returns raw PNG +curl "/screenshot?raw=true&format=png" -o screenshot.png +``` + +## Evaluate JavaScript + +Use this sparingly. Prefer `text`, `snapshot`, and normal actions first. +Default to read-only DOM inspection and avoid reading cookies, localStorage, or unrelated page secrets unless the user explicitly asks for that behavior. + +```bash +# CLI: pinchtab eval "document.title" +curl -X POST /evaluate -H 'Content-Type: application/json' \ + -d '{"expression": "document.title"}' +``` + +## Tab management + +```bash +# CLI: pinchtab tabs / pinchtab tabs new / pinchtab tabs close +# List tabs +curl /tabs + +# Open new tab +curl -X POST /tab -H 'Content-Type: application/json' \ + -d '{"action": "new", "url": "https://pinchtab.com"}' + +# Close tab +curl -X POST /tab -H 'Content-Type: application/json' \ + -d '{"action": "close", "tabId": "TARGET_ID"}' +``` + +Multi-tab: pass `?tabId=TARGET_ID` to snapshot/screenshot/text, or `"tabId"` in POST body. + +## Tab-specific endpoints + +All read/action endpoints have tab-scoped variants using `/tabs/{id}/...`: + +```bash +# Navigate a specific tab +curl -X POST /tabs/TARGET_ID/navigate \ + -H 'Content-Type: application/json' \ + -d '{"url": "https://pinchtab.com"}' + +# Snapshot a specific tab +curl "/tabs/TARGET_ID/snapshot" +curl "/tabs/TARGET_ID/snapshot?filter=interactive&format=compact" + +# Screenshot a specific tab +curl "/tabs/TARGET_ID/screenshot?raw=true" -o tab-screenshot.jpg + +# Extract text from a specific tab +curl "/tabs/TARGET_ID/text" + +# Action on a specific tab +curl -X POST /tabs/TARGET_ID/action \ + -H 'Content-Type: application/json' \ + -d '{"kind": "click", "ref": "e5"}' + +# Batch actions on a specific tab +curl -X POST /tabs/TARGET_ID/actions \ + -H 'Content-Type: application/json' \ + -d '{"actions": [{"kind": "click", "ref": "e3"}, {"kind": "type", "ref": "e3", "text": "hello"}]}' +``` + +These are equivalent to using `?tabId=TARGET_ID` on top-level endpoints but follow REST conventions. The tab ID comes from `/tabs` or from the `tabId` field in navigate/tab creation responses. + +## Tab locking (multi-agent) + +```bash +# Lock a tab (default 30s timeout, max 5min) +curl -X POST /tab/lock -H 'Content-Type: application/json' \ + -d '{"tabId": "TARGET_ID", "owner": "agent-1", "timeoutSec": 60}' + +# Unlock +curl -X POST /tab/unlock -H 'Content-Type: application/json' \ + -d '{"tabId": "TARGET_ID", "owner": "agent-1"}' +``` + +Locked tabs show `owner` and `lockedUntil` in `/tabs`. Returns 409 on conflict. + +## Cookies + +```bash +# Get cookies for current page +curl /cookies + +# Set cookies +curl -X POST /cookies -H 'Content-Type: application/json' \ + -d '{"url":"https://pinchtab.com","cookies":[{"name":"session","value":"abc123"}]}' +``` + +## Solve challenges + +PinchTab includes a pluggable solver framework for browser challenges (Cloudflare Turnstile, CAPTCHAs, interstitials). Solvers auto-detect the challenge type and resolve it using human-like interaction. + +```bash +# List available solvers +curl /solvers + +# Auto-detect and solve (tries each solver in order) +curl -X POST /solve -H 'Content-Type: application/json' \ + -d '{"maxAttempts": 3, "timeout": 30000}' + +# Use a specific solver by name +curl -X POST /solve/cloudflare -H 'Content-Type: application/json' \ + -d '{"maxAttempts": 3}' + +# Solve on a specific tab +curl -X POST /tabs/TAB_ID/solve -H 'Content-Type: application/json' \ + -d '{"solver": "cloudflare"}' + +# Solve on a specific tab with path-based solver +curl -X POST /tabs/TAB_ID/solve/cloudflare -H 'Content-Type: application/json' \ + -d '{}' +``` + +**Request fields:** + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `solver` | string | โ€” | Solver name (omit for auto-detect) | +| `tabId` | string | โ€” | Target tab (omit for default tab) | +| `maxAttempts` | int | 3 | Maximum solve attempts | +| `timeout` | float | 30000 | Overall timeout in ms | + +**Response:** + +```json +{ + "tabId": "DEADBEEF", + "solver": "cloudflare", + "solved": true, + "challengeType": "managed", + "attempts": 1, + "title": "Example Site" +} +``` + +Returns `solved: true, attempts: 0` when no challenge is detected โ€” safe to call speculatively. + +**Built-in solvers:** `cloudflare` (Turnstile/interstitial โ€” detects via page title, clicks checkbox with human-like input). + +**Stealth requirement:** Solvers work best with `stealthLevel: "full"`. Cloudflare checks browser fingerprints before and after the checkbox click. Verify stealth is active with `GET /stealth/status`. + +## Network Export + +```bash +# Export as HAR 1.2 (stream to response) +curl /network/export?format=har + +# Export as NDJSON (one JSON per line) +curl /network/export?format=ndjson + +# Save to server-side file +curl "/network/export?format=har&output=file&path=session.har" + +# Include response bodies (10 MB cap per entry) +curl "/network/export?format=har&body=true" + +# Include raw sensitive headers (Cookie, Authorization) +curl "/network/export?format=har&redact=false" + +# Live streaming export (entries written to file as they arrive) +curl -N "/network/export/stream?format=ndjson&path=live.ndjson" + +# Tab-scoped +curl /tabs/TAB_ID/network/export?format=har +``` + +All standard network filters apply: `filter`, `method`, `status`, `type`, `limit`. + +Formats are pluggable. `GET /network/export?format=unknown` returns `{"available": ["har", "ndjson"]}`. + +## Stealth + +```bash +# Check stealth status and score +curl /stealth/status + +# Rotate browser fingerprint +curl -X POST /fingerprint/rotate -H 'Content-Type: application/json' \ + -d '{"os":"windows"}' +# os: "windows", "mac", or omit for random +``` + +## Health check + +```bash +curl /health +``` diff --git a/src/plugins/pinchtab/snippets/references/commands.txt b/src/plugins/pinchtab/snippets/references/commands.txt new file mode 100644 index 0000000..de9c403 --- /dev/null +++ b/src/plugins/pinchtab/snippets/references/commands.txt @@ -0,0 +1,206 @@ +# CLI Commands Reference โ€” Pinchtab 0.8.x + +> **Quick tip:** Use `pinchtab help` or `pinchtab --help` for full flag lists. + +--- + +## Control Plane + +### `pinchtab start` +Start the PinchTab server (default port 9867). + +```bash +pinchtab start +pinchtab start --port 9868 +pinchtab start --profile work --headless +``` + +### `pinchtab stop` +Stop the running server. + +### `pinchtab status` / `pinchtab health` +Check if the server is running and healthy. + +--- + +## Browser Commands + +### `pinchtab nav ` +Navigate the current tab to a URL. + +```bash +pinchtab nav https://example.com +pinchtab nav https://example.com --new-tab +pinchtab nav https://example.com --block-images +pinchtab nav https://example.com --timeout 60 +``` + +| Flag | Description | +|------|-------------| +| `--new-tab` | Open in a new tab instead of current | +| `--block-images` | Block image loading (faster, fewer tokens) | +| `--timeout ` | Navigation timeout in seconds | +| `--profile ` | Target a named profile | + +> โš ๏ธ **Quirk:** Use `--profile`, not `--profileId`. The long-form flag is required. + +### `pinchtab tab` (not `tabs`) +Manage browser tabs. + +```bash +pinchtab tab list # List all open tabs +pinchtab tab close # Close current tab +pinchtab tab close # Close specific tab +``` + +> โš ๏ธ **Quirk:** The command is `tab` (singular), not `tabs`. + +--- + +## Interaction Commands + +### `pinchtab click ` +Click an element by its accessibility ref (from `snap`). + +```bash +pinchtab click e5 +pinchtab click e5 --tab +``` + +### `pinchtab type ` +Type text into an input element. + +```bash +pinchtab type e12 "hello world" +``` + +### `pinchtab fill ` +Fill a form field using JS event dispatch. Prefer over `type` for React/Vue/Angular forms. + +```bash +pinchtab fill e12 "hello world" +``` + +### `pinchtab press ` +Press a named keyboard key. + +```bash +pinchtab press Enter +pinchtab press Tab +pinchtab press Escape +``` + +### `pinchtab hover ` +Hover over an element to trigger tooltips or hover styles. + +### `pinchtab scroll [ref]` +Scroll the page or a specific element. + +```bash +pinchtab scroll # scroll page down 300px +pinchtab scroll --pixels -300 # scroll up +pinchtab scroll e20 --pixels 500 +``` + +### `pinchtab select ` +Select an option from a ` +
+``` + +### Flag These (React-Specific) + +```jsx +// XSS - Explicit unsafe rendering +
// FLAG: Critical +// Only safe if userInput is sanitized with DOMPurify or similar + +// URL-based XSS +Link // FLAG: Check for javascript: protocol +