diff --git a/.agent/skills/frontend-rules/SKILL.md b/.agent/skills/frontend-rules/SKILL.md
new file mode 100644
index 0000000..ce1a1c6
--- /dev/null
+++ b/.agent/skills/frontend-rules/SKILL.md
@@ -0,0 +1,544 @@
+---
+name: frontend-rules
+description: >-
+ Frontend code quality reviewer for HTML, CSS, JS, images, fonts, network,
+ Core Web Vitals, design principles, accessibility, and animation. Audits code
+ against ruleskit standards and returns structured, actionable findings.
+---
+
+# SKILL: Frontend Agent
+
+# Generated by ruleskit — https://ruleskit.dev
+
+# Pack: Frontend | Framework: Framework-agnostic
+
+# Source: roadmap.sh/frontend-performance-best-practices
+
+## Role
+
+You are a Frontend code quality reviewer.
+When a developer shares code, audit it against the rules below
+and return structured, actionable findings only.
+
+## Rules
+
+## HTML Performance Rules
+
+- Deliver critical above-the-fold HTML first; use SSR or SSG to reduce TTFB
+- Place all in
— never below the fold
+- Place
+```
+
+- **Never** use raw `fetch()` in `
+```
+
+### Middleware (Client-Side Route Guards)
+
+```typescript
+// middleware/auth.ts — named middleware
+export default defineNuxtRouteMiddleware((to, from) => {
+ const user = useSupabaseUser(); // or your auth composable
+ if (!user.value && to.path !== "/login") {
+ return navigateTo("/login");
+ }
+});
+
+// middleware/auth.global.ts — runs on EVERY navigation (expensive — use sparingly)
+```
+
+- Named middleware → applied via `definePageMeta({ middleware: ["auth"] })`
+- Global middleware (`.global.ts`) → applied everywhere; keep logic minimal
+
+### Performance
+
+```typescript
+// Payload extraction — avoid re-fetching on client hydration
+// Nuxt serializes useAsyncData/useFetch results into the HTML payload automatically
+// Ensure you're NOT disabling this with { server: false } unintentionally
+
+// useHead for per-page preload hints
+useHead({
+ link: [{ rel: "preload", as: "font", href: "/fonts/inter.woff2", crossorigin: "" }],
+});
+
+// Enable Brotli in nuxt.config.ts
+export default defineNuxtConfig({
+ nitro: { compressPublicAssets: { brotli: true } },
+ routeRules: {
+ "/": { prerender: true }, // static
+ "/products/**": { isr: 3600 }, // ISR — revalidate every hour
+ "/dashboard/**": { ssr: true }, // always SSR (auth-gated)
+ },
+});
+```
diff --git a/packs/frontend/blocks/frameworks/sveltekit.md b/packs/frontend/blocks/frameworks/sveltekit.md
index a8366c5..293d08b 100644
--- a/packs/frontend/blocks/frameworks/sveltekit.md
+++ b/packs/frontend/blocks/frameworks/sveltekit.md
@@ -1,9 +1,207 @@
## SvelteKit Specific Rules
-- Use load() in +page.server.js for SSR data — avoid client-side fetch waterfalls
-- Use preload hints via inside
-- Prefer +page.server.js over +page.js when data doesn't need to be reactive
-- Use import() for heavy client-only components with {#await}
-- Leverage built-in prerendering for static routes (prerender = true)
-- Avoid hydration mismatches — keep server and client data consistent
-- Use $app/environment browser check before accessing window or document
+### Project & Folder Structure
+
+```
+src/
+├── routes/
+│ ├── +layout.svelte ← root layout; wraps all pages
+│ ├── +layout.server.ts ← load() runs on server; data flows into layout
+│ ├── +page.svelte ← / route
+│ ├── +page.server.ts ← server-only load; never in client bundle
+│ ├── products/
+│ │ ├── +page.svelte
+│ │ ├── +page.server.ts
+│ │ └── [id]/
+│ │ ├── +page.svelte
+│ │ └── +page.server.ts
+│ └── api/
+│ └── webhooks/
+│ └── +server.ts ← endpoint: POST /api/webhooks
+├── lib/
+│ ├── server/
+│ │ └── db.ts ← server-only (never importable on client)
+│ ├── components/
+│ │ └── ProductCard.svelte
+│ └── index.ts ← re-exports from $lib
+└── app.html ← root HTML template
+```
+
+- `+page.server.ts` — server-only load; returned data serialized via `JSON.stringify`
+- `+page.ts` — runs on both server and client (good for client-navigations)
+- `src/lib/server/` — SvelteKit enforces this cannot be imported in client code
+- `$lib` alias → `src/lib/` — use this everywhere instead of relative `../../lib`
+
+### Load Functions — Server vs Universal
+
+```typescript
+// +page.server.ts — runs ONLY on the server (DB access, auth, secrets)
+import type { PageServerLoad } from "./$types";
+import { db } from "$lib/server/db";
+import { error } from "@sveltejs/kit";
+
+export const load: PageServerLoad = async ({ params, locals }) => {
+ const product = await db.product.findUnique({ where: { id: params.id } });
+ if (!product) error(404, "Product not found");
+
+ return { product }; // serialized to the page; Svelte types this for you
+};
+
+// +page.ts — runs on server (initial load) AND client (navigation)
+// Use for: public API calls that don't need secrets
+import type { PageLoad } from "./$types";
+
+export const load: PageLoad = async ({ fetch, params }) => {
+ // Use the provided fetch — it's enhanced with SvelteKit's fetch interceptor
+ const res = await fetch(`/api/products/${params.id}`);
+ return { product: await res.json() };
+};
+```
+
+**Rule**: Prefer `+page.server.ts` for anything touching a database, session,
+or secret. Use `+page.ts` only for public API calls that must re-run on
+client-side navigation.
+
+### Form Actions — Server Mutations
+
+```typescript
+// +page.server.ts
+import type { Actions } from "./$types";
+import { fail, redirect } from "@sveltejs/kit";
+import { z } from "zod";
+
+export const actions: Actions = {
+ default: async ({ request, locals }) => {
+ const data = Object.fromEntries(await request.formData());
+ const result = schema.safeParse(data);
+ if (!result.success) return fail(422, { errors: result.error.flatten() });
+
+ await db.product.create({ data: result.data });
+ redirect(303, "/products");
+ },
+};
+```
+
+```svelte
+
+
+
+
+```
+
+- `use:enhance` — progressively enhances forms: no full page reload, but
+ gracefully degrades to native form if JS fails
+- Always `fail(422, data)` for validation errors — never throw
+- Always `redirect(303, path)` after a successful mutation (Post/Redirect/Get)
+
+### Reactive State — Svelte 5 Runes
+
+```svelte
+
+```
+
+- Do not mix Svelte 4 syntax (`$:`, reactive `let`) with runes in the same file
+- `$state` is deeply reactive — mutate arrays/objects directly (`items.push(x)`)
+- Use `$state.snapshot(value)` to get a non-reactive plain copy for serialization
+
+### Server Endpoints
+
+```typescript
+// src/routes/api/products/+server.ts
+import type { RequestHandler } from "./$types";
+import { json, error } from "@sveltejs/kit";
+
+export const GET: RequestHandler = async ({ url, locals }) => {
+ const limit = Number(url.searchParams.get("limit")) || 20;
+ const products = await db.product.findMany({ take: limit });
+ return json(products);
+};
+
+export const POST: RequestHandler = async ({ request, locals }) => {
+ const body = await request.json();
+ // validate with zod
+ const product = await db.product.create({ data: body });
+ return json(product, { status: 201 });
+};
+```
+
+### Hooks & Middleware
+
+```typescript
+// src/hooks.server.ts — runs on every server request
+import type { Handle } from "@sveltejs/kit";
+
+export const handle: Handle = async ({ event, resolve }) => {
+ // Auth: populate event.locals before load functions run
+ const session = await getSession(event.cookies.get("session"));
+ event.locals.user = session?.user ?? null;
+
+ return resolve(event, {
+ // Inject HTML attributes into tag
+ transformPageChunk: ({ html }) => html.replace("%lang%", "en"),
+ });
+};
+```
+
+### Route Protection Pattern
+
+```typescript
+// src/routes/(dashboard)/+layout.server.ts
+import { redirect } from "@sveltejs/kit";
+import type { LayoutServerLoad } from "./$types";
+
+export const load: LayoutServerLoad = async ({ locals }) => {
+ if (!locals.user) redirect(303, "/login");
+ return { user: locals.user };
+};
+```
+
+### Performance
+
+```typescript
+// svelte.config.js — prerender and SSR options
+import adapter from "@sveltejs/adapter-auto";
+
+export default {
+ kit: {
+ adapter: adapter(),
+ prerender: {
+ handleMissingId: "warn",
+ },
+ },
+};
+
+// Per-route rendering options in +page.ts or +page.server.ts
+export const prerender = true; // static generation
+export const ssr = false; // CSR only (use sparingly — auth dashboards)
+export const csr = false; // no client JS (pure HTML — forms still work)
+```
+
+- `browser` check: `import { browser } from "$app/environment"` before accessing `window`
+- Never `await import("module")` at the top level in a universal load — it will
+ block the server render; use dynamic imports inside functions instead
diff --git a/packs/frontend/blocks/frameworks/vanilla.md b/packs/frontend/blocks/frameworks/vanilla.md
new file mode 100644
index 0000000..33c5c87
--- /dev/null
+++ b/packs/frontend/blocks/frameworks/vanilla.md
@@ -0,0 +1,227 @@
+## Vanilla JS / Framework-Agnostic Rules
+
+These rules apply when working without a framework — plain HTML, CSS, and
+JavaScript, or when building a framework-agnostic library/component.
+
+### Project & Folder Structure
+
+```
+src/
+├── index.html
+├── main.js ← entry point; minimal — just wires things together
+├── components/
+│ ├── modal.js ← one class/function per file
+│ └── carousel.js
+├── lib/
+│ ├── dom.js ← DOM helpers ($, $$, createElement)
+│ ├── events.js ← event bus or pub/sub
+│ └── fetch.js ← typed fetch wrapper
+├── state/
+│ └── store.js ← lightweight state container
+└── styles/
+ ├── base.css
+ ├── components/
+ └── utilities.css
+```
+
+### DOM Query Helpers — Write Once, Use Everywhere
+
+```javascript
+// lib/dom.js
+const $ = (selector, root = document) => root.querySelector(selector);
+const $$ = (selector, root = document) => [...root.querySelectorAll(selector)];
+
+function createElement(tag, attrs = {}, ...children) {
+ const el = document.createElement(tag);
+ Object.entries(attrs).forEach(([k, v]) => {
+ if (k === "class") el.className = v;
+ else if (k.startsWith("on")) el.addEventListener(k.slice(2).toLowerCase(), v);
+ else el.setAttribute(k, v);
+ });
+ children.flat().forEach(c =>
+ el.appendChild(typeof c === "string" ? document.createTextNode(c) : c)
+ );
+ return el;
+}
+
+export { $, $$, createElement };
+```
+
+### Component Pattern — Class with Lifecycle
+
+```javascript
+// components/modal.js
+export class Modal {
+ #root;
+ #onClose;
+
+ constructor(selector, { onClose } = {}) {
+ this.#root = document.querySelector(selector);
+ this.#onClose = onClose ?? (() => {});
+ this.#bind();
+ }
+
+ open() {
+ this.#root.removeAttribute("hidden");
+ this.#root.setAttribute("aria-hidden", "false");
+ document.body.style.overflow = "hidden";
+ this.#root.querySelector("[data-autofocus]")?.focus();
+ }
+
+ close() {
+ this.#root.setAttribute("hidden", "");
+ this.#root.setAttribute("aria-hidden", "true");
+ document.body.style.overflow = "";
+ this.#onClose();
+ }
+
+ #bind() {
+ this.#root.addEventListener("click", e => {
+ if (e.target === this.#root) this.close(); // backdrop click
+ });
+ document.addEventListener("keydown", e => {
+ if (e.key === "Escape" && !this.#root.hasAttribute("hidden")) this.close();
+ });
+ }
+
+ destroy() {
+ // remove event listeners before removing from DOM
+ this.#root.replaceWith(this.#root.cloneNode(true));
+ }
+}
+```
+
+### State Management — Lightweight Store
+
+```javascript
+// state/store.js
+export function createStore(initialState) {
+ let state = { ...initialState };
+ const listeners = new Set();
+
+ return {
+ getState: () => ({ ...state }),
+ setState(updater) {
+ const next = typeof updater === "function" ? updater(state) : updater;
+ state = { ...state, ...next };
+ listeners.forEach(fn => fn(state));
+ },
+ subscribe(fn) {
+ listeners.add(fn);
+ return () => listeners.delete(fn); // return unsubscribe
+ },
+ };
+}
+
+// Usage
+const store = createStore({ cart: [], user: null });
+const unsubscribe = store.subscribe(state => renderCart(state.cart));
+store.setState(s => ({ cart: [...s.cart, newItem] }));
+```
+
+### Event Delegation — Never Attach to Individual List Items
+
+```javascript
+// BAD — attaches N listeners for N items
+document.querySelectorAll(".product-card").forEach(card => {
+ card.addEventListener("click", handleClick);
+});
+
+// GOOD — one listener on the parent
+document.querySelector(".product-list").addEventListener("click", e => {
+ const card = e.target.closest(".product-card");
+ if (!card) return;
+ handleClick(card.dataset.id);
+});
+```
+
+### Fetch Wrapper — Typed, Error-Handled
+
+```javascript
+// lib/fetch.js
+export async function apiFetch(path, options = {}) {
+ const res = await fetch(`/api${path}`, {
+ ...options,
+ headers: { "Content-Type": "application/json", ...options.headers },
+ body: options.body ? JSON.stringify(options.body) : undefined,
+ });
+
+ if (!res.ok) {
+ const error = await res.json().catch(() => ({ message: res.statusText }));
+ throw Object.assign(new Error(error.message), { status: res.status, data: error });
+ }
+
+ return res.status === 204 ? null : res.json();
+}
+```
+
+### Async Patterns
+
+```javascript
+// Load data without blocking render — Intersection Observer for lazy load
+const observer = new IntersectionObserver(
+ entries => entries.forEach(entry => {
+ if (!entry.isIntersecting) return;
+ loadContent(entry.target);
+ observer.unobserve(entry.target);
+ }),
+ { rootMargin: "200px" }
+);
+
+document.querySelectorAll("[data-lazy]").forEach(el => observer.observe(el));
+
+// Web Worker for CPU-heavy tasks
+const worker = new Worker(new URL("./workers/sort.js", import.meta.url), { type: "module" });
+worker.postMessage({ items: largeArray });
+worker.addEventListener("message", e => renderSorted(e.data));
+```
+
+### Custom Element Pattern (Web Components)
+
+```javascript
+class ProductCard extends HTMLElement {
+ static observedAttributes = ["product-id", "name"];
+
+ connectedCallback() {
+ this.render();
+ }
+
+ attributeChangedCallback() {
+ this.render();
+ }
+
+ render() {
+ this.innerHTML = `
+
+ ${this.getAttribute("name")}
+
+
+ `;
+ }
+}
+
+customElements.define("product-card", ProductCard);
+// Usage:
+```
+
+### Performance Rules Specific to Vanilla JS
+
+- Use `requestAnimationFrame` for all visual updates — never `setTimeout` for animation
+- Use `requestIdleCallback` for non-urgent background work (analytics, prefetch)
+- Batch DOM reads before DOM writes — never interleave (causes forced reflow):
+
+```javascript
+// BAD — alternates read/write → layout thrash
+boxes.forEach(box => {
+ const h = box.offsetHeight; // read → force layout
+ box.style.height = `${h * 2}px`; // write
+});
+
+// GOOD — batch reads, then batch writes
+const heights = boxes.map(box => box.offsetHeight); // all reads
+boxes.forEach((box, i) => (box.style.height = `${heights[i] * 2}px`)); // all writes
+```
+
+- Use `AbortController` to cancel fetch requests when the user navigates away
+- Use `MutationObserver` instead of polling to react to DOM changes
+- Remove all event listeners and observers in cleanup logic (SPA unmount, custom element `disconnectedCallback`)
diff --git a/packs/frontend/pack.config.ts b/packs/frontend/pack.config.ts
index a463fd3..fb10c74 100644
--- a/packs/frontend/pack.config.ts
+++ b/packs/frontend/pack.config.ts
@@ -16,17 +16,163 @@ const config: PackConfig = {
{ id: "vanilla", label: "Vanilla JS", frameworkFile: null },
],
blocks: [
- "html",
- "css",
- "javascript",
- "comments",
- "healing",
- "images",
- "fonts",
- "network",
- "web-vitals",
+ // ------------------------------------------------------------------
+ // Always-on base blocks (domain: frontend/performance)
+ // ------------------------------------------------------------------
+ {
+ id: "frontend/performance/html",
+ label: "HTML Performance",
+ file: "blocks/html.md",
+ packId: "frontend",
+ tags: ["performance", "html", "browser"],
+ inclusion: "always",
+ },
+ {
+ id: "frontend/performance/css",
+ label: "CSS Performance",
+ file: "blocks/css.md",
+ packId: "frontend",
+ tags: ["performance", "css", "browser"],
+ inclusion: "always",
+ },
+ {
+ id: "frontend/performance/javascript",
+ label: "JavaScript Performance",
+ file: "blocks/javascript.md",
+ packId: "frontend",
+ tags: ["performance", "javascript", "browser"],
+ inclusion: "always",
+ },
+ {
+ id: "frontend/quality/comments",
+ label: "Code Comment Rules",
+ file: "blocks/comments.md",
+ packId: "frontend",
+ tags: ["quality", "comments"],
+ inclusion: "always",
+ },
+ {
+ id: "frontend/healing/base",
+ label: "Self-Healing System",
+ file: "blocks/healing.md",
+ packId: "frontend",
+ tags: ["healing", "dx"],
+ inclusion: "always",
+ },
+ {
+ id: "frontend/performance/images",
+ label: "Image Performance",
+ file: "blocks/images.md",
+ packId: "frontend",
+ tags: ["performance", "images", "browser"],
+ inclusion: "always",
+ },
+ {
+ id: "frontend/performance/fonts",
+ label: "Font Performance",
+ file: "blocks/fonts.md",
+ packId: "frontend",
+ tags: ["performance", "fonts", "browser"],
+ inclusion: "always",
+ },
+ {
+ id: "frontend/performance/network",
+ label: "Network & Caching",
+ file: "blocks/network.md",
+ packId: "frontend",
+ tags: ["performance", "network", "caching"],
+ inclusion: "always",
+ },
+ {
+ id: "frontend/performance/web-vitals",
+ label: "Core Web Vitals",
+ file: "blocks/web-vitals.md",
+ packId: "frontend",
+ tags: ["performance", "vitals", "browser"],
+ inclusion: "always",
+ },
+
+ // ------------------------------------------------------------------
+ // Optional blocks
+ // ------------------------------------------------------------------
+ {
+ id: "frontend/design/base",
+ label: "Design Principles",
+ file: "blocks/design.md",
+ packId: "frontend",
+ tags: ["design", "a11y", "ux"],
+ inclusion: "optional",
+ },
+ {
+ id: "frontend/quality/anti-slop",
+ label: "Anti-Slop Frontend Rules",
+ file: "blocks/anti-slop.md",
+ packId: "frontend",
+ tags: ["quality", "design", "anti-slop", "aesthetics"],
+ inclusion: "optional",
+ },
+ {
+ // Child of anti-slop — resolver guarantees base block appears first.
+ // If a user selects tooling without the base, the resolver pulls the
+ // base in automatically via the parentId ancestry expansion.
+ id: "frontend/quality/anti-slop-tooling",
+ label: "Anti-Slop Tooling: Library Selection Guide",
+ file: "blocks/anti-slop-tooling.md",
+ packId: "frontend",
+ parentId: "frontend/quality/anti-slop",
+ tags: ["quality", "tooling", "anti-slop", "libraries"],
+ inclusion: "optional",
+ },
+
+ // ------------------------------------------------------------------
+ // Framework-specific blocks (children of javascript performance base)
+ // ------------------------------------------------------------------
+ {
+ id: "frontend/framework/nextjs",
+ label: "Next.js Specific Rules",
+ file: "blocks/frameworks/nextjs.md",
+ packId: "frontend",
+ parentId: "frontend/performance/javascript",
+ tags: ["framework", "nextjs", "react"],
+ inclusion: "framework",
+ },
+ {
+ id: "frontend/framework/nuxt",
+ label: "Nuxt Specific Rules",
+ file: "blocks/frameworks/nuxt.md",
+ packId: "frontend",
+ parentId: "frontend/performance/javascript",
+ tags: ["framework", "nuxt", "vue"],
+ inclusion: "framework",
+ },
+ {
+ id: "frontend/framework/sveltekit",
+ label: "SvelteKit Specific Rules",
+ file: "blocks/frameworks/sveltekit.md",
+ packId: "frontend",
+ parentId: "frontend/performance/javascript",
+ tags: ["framework", "sveltekit", "svelte"],
+ inclusion: "framework",
+ },
+ {
+ id: "frontend/framework/angular",
+ label: "Angular Specific Rules",
+ file: "blocks/frameworks/angular.md",
+ packId: "frontend",
+ parentId: "frontend/performance/javascript",
+ tags: ["framework", "angular"],
+ inclusion: "framework",
+ },
+ {
+ id: "frontend/framework/vanilla",
+ label: "Vanilla JS Specific Rules",
+ file: "blocks/frameworks/vanilla.md",
+ packId: "frontend",
+ parentId: "frontend/performance/javascript",
+ tags: ["framework", "vanilla", "no-framework"],
+ inclusion: "framework",
+ },
],
- optionalBlocks: [{ id: "design", label: "Design principles", default: true }],
extras: [
{ id: "husky", label: "Husky + lint-staged", default: true },
{ id: "eslint", label: "ESLint", default: true },
diff --git a/packs/fullstack/pack.config.ts b/packs/fullstack/pack.config.ts
index 4326831..a506cdf 100644
--- a/packs/fullstack/pack.config.ts
+++ b/packs/fullstack/pack.config.ts
@@ -1,32 +1,288 @@
import type { PackConfig } from "@/core/types";
+/**
+ * Fullstack pack — the complete union of frontend + backend blocks.
+ *
+ * Every block is referenced by its canonical id from the owning pack.
+ * The resolver deduplicates automatically: if a user also selects the
+ * standalone frontend or backend pack, no content appears twice.
+ */
const config: PackConfig = {
id: "fullstack",
label: "Full-Stack",
status: "stable",
- description: "End-to-end rules bridging UI performance, API design, security, database hygiene, and AI self-healing",
+ description:
+ "End-to-end rules bridging UI performance, API design, security, database hygiene, and AI self-healing",
icon: "layers",
source: "roadmap.sh/full-stack",
frameworks: [
- { id: "agnostic", label: "Framework-agnostic", frameworkFile: null },
- { id: "nextjs", label: "Next.js (React)", frameworkFile: "nextjs.md" },
- { id: "nuxt", label: "Nuxt (Vue)", frameworkFile: "nuxt.md" },
- { id: "sveltekit", label: "SvelteKit", frameworkFile: "sveltekit.md" },
- { id: "laravel", label: "Laravel (PHP)", frameworkFile: "laravel.md" },
+ { id: "agnostic", label: "Framework-agnostic", frameworkFile: null },
+ { id: "nextjs", label: "Next.js (React)", frameworkFile: "nextjs.md" },
+ { id: "nuxt", label: "Nuxt (Vue)", frameworkFile: "nuxt.md" },
+ { id: "sveltekit", label: "SvelteKit", frameworkFile: "sveltekit.md" },
+ { id: "angular", label: "Angular", frameworkFile: null },
+ { id: "vanilla", label: "Vanilla JS", frameworkFile: null },
+ { id: "laravel", label: "Laravel (PHP)", frameworkFile: "laravel.md" },
+ { id: "express", label: "Express.js", frameworkFile: null },
+ { id: "nestjs", label: "NestJS", frameworkFile: null },
+ { id: "fastify", label: "Fastify", frameworkFile: null },
+ { id: "django", label: "Django", frameworkFile: null },
],
blocks: [
- "general",
- "frontend",
- "backend",
- "database",
- ],
- optionalBlocks: [
- { id: "healing", label: "AI self-healing workflow", default: true },
+ // ------------------------------------------------------------------
+ // Fullstack-owned: general cross-cutting principles
+ // ------------------------------------------------------------------
+ {
+ id: "fullstack/general/principles",
+ label: "General Principles",
+ file: "blocks/general.md",
+ packId: "fullstack",
+ tags: ["general", "principles", "quality"],
+ inclusion: "always",
+ },
+
+ // ------------------------------------------------------------------
+ // FRONTEND — always-on (full set, same as frontend pack)
+ // ------------------------------------------------------------------
+ {
+ id: "frontend/performance/html",
+ label: "HTML Performance",
+ file: "blocks/html.md",
+ packId: "frontend",
+ tags: ["performance", "html", "browser"],
+ inclusion: "always",
+ },
+ {
+ id: "frontend/performance/css",
+ label: "CSS Performance",
+ file: "blocks/css.md",
+ packId: "frontend",
+ tags: ["performance", "css", "browser"],
+ inclusion: "always",
+ },
+ {
+ id: "frontend/performance/javascript",
+ label: "JavaScript Performance",
+ file: "blocks/javascript.md",
+ packId: "frontend",
+ tags: ["performance", "javascript", "browser"],
+ inclusion: "always",
+ },
+ {
+ id: "frontend/quality/comments",
+ label: "Code Comment Rules",
+ file: "blocks/comments.md",
+ packId: "frontend",
+ tags: ["quality", "comments"],
+ inclusion: "always",
+ },
+ {
+ id: "frontend/healing/base",
+ label: "Self-Healing System",
+ file: "blocks/healing.md",
+ packId: "frontend",
+ tags: ["healing", "dx"],
+ inclusion: "always",
+ },
+ {
+ id: "frontend/performance/images",
+ label: "Image Performance",
+ file: "blocks/images.md",
+ packId: "frontend",
+ tags: ["performance", "images", "browser"],
+ inclusion: "always",
+ },
+ {
+ id: "frontend/performance/fonts",
+ label: "Font Performance",
+ file: "blocks/fonts.md",
+ packId: "frontend",
+ tags: ["performance", "fonts", "browser"],
+ inclusion: "always",
+ },
+ {
+ id: "frontend/performance/network",
+ label: "Network & Caching",
+ file: "blocks/network.md",
+ packId: "frontend",
+ tags: ["performance", "network", "caching"],
+ inclusion: "always",
+ },
+ {
+ id: "frontend/performance/web-vitals",
+ label: "Core Web Vitals",
+ file: "blocks/web-vitals.md",
+ packId: "frontend",
+ tags: ["performance", "vitals", "browser"],
+ inclusion: "always",
+ },
+
+ // ------------------------------------------------------------------
+ // BACKEND — always-on (full set, same as backend pack)
+ // ------------------------------------------------------------------
+ {
+ id: "backend/api/design",
+ label: "API Design",
+ file: "blocks/api-design.md",
+ packId: "backend",
+ tags: ["api", "design", "rest"],
+ inclusion: "always",
+ },
+ {
+ id: "backend/security/base",
+ label: "Security",
+ file: "blocks/security.md",
+ packId: "backend",
+ tags: ["security", "auth"],
+ inclusion: "always",
+ },
+ {
+ id: "backend/data/database",
+ label: "Database",
+ file: "blocks/database.md",
+ packId: "backend",
+ tags: ["database", "sql"],
+ inclusion: "always",
+ },
+ {
+ id: "backend/reliability/error-handling",
+ label: "Error Handling",
+ file: "blocks/error-handling.md",
+ packId: "backend",
+ tags: ["error-handling", "reliability"],
+ inclusion: "always",
+ },
+ {
+ id: "backend/performance/base",
+ label: "Backend Performance",
+ file: "blocks/performance.md",
+ packId: "backend",
+ tags: ["performance", "server"],
+ inclusion: "always",
+ },
+
+ // ------------------------------------------------------------------
+ // OPTIONAL — full set from both frontend and backend
+ // ------------------------------------------------------------------
+ {
+ id: "frontend/design/base",
+ label: "Design Principles",
+ file: "blocks/design.md",
+ packId: "frontend",
+ tags: ["design", "a11y", "ux"],
+ inclusion: "optional",
+ },
+ {
+ id: "frontend/quality/anti-slop",
+ label: "Anti-Slop Frontend Rules",
+ file: "blocks/anti-slop.md",
+ packId: "frontend",
+ tags: ["quality", "design", "anti-slop", "aesthetics"],
+ inclusion: "optional",
+ },
+ {
+ id: "frontend/quality/anti-slop-tooling",
+ label: "Anti-Slop Tooling: Library Selection Guide",
+ file: "blocks/anti-slop-tooling.md",
+ packId: "frontend",
+ parentId: "frontend/quality/anti-slop",
+ tags: ["quality", "tooling", "anti-slop", "libraries"],
+ inclusion: "optional",
+ },
+ {
+ id: "backend/data/caching",
+ label: "Caching Strategies",
+ file: "blocks/caching.md",
+ packId: "backend",
+ tags: ["caching", "performance", "server"],
+ inclusion: "optional",
+ },
+ {
+ id: "backend/architecture/base",
+ label: "Code Architecture",
+ file: "blocks/architecture.md",
+ packId: "backend",
+ tags: ["architecture", "patterns"],
+ inclusion: "optional",
+ },
+
+ // ------------------------------------------------------------------
+ // FRAMEWORK — frontend frameworks (point at canonical frontend files)
+ // ------------------------------------------------------------------
+ {
+ id: "frontend/framework/nextjs",
+ label: "Next.js Rules",
+ file: "blocks/frameworks/nextjs.md",
+ packId: "frontend",
+ parentId: "frontend/performance/javascript",
+ tags: ["framework", "nextjs", "react"],
+ inclusion: "framework",
+ },
+ {
+ id: "frontend/framework/nuxt",
+ label: "Nuxt Rules",
+ file: "blocks/frameworks/nuxt.md",
+ packId: "frontend",
+ parentId: "frontend/performance/javascript",
+ tags: ["framework", "nuxt", "vue"],
+ inclusion: "framework",
+ },
+ {
+ id: "frontend/framework/sveltekit",
+ label: "SvelteKit Rules",
+ file: "blocks/frameworks/sveltekit.md",
+ packId: "frontend",
+ parentId: "frontend/performance/javascript",
+ tags: ["framework", "sveltekit", "svelte"],
+ inclusion: "framework",
+ },
+ {
+ id: "frontend/framework/angular",
+ label: "Angular Rules",
+ file: "blocks/frameworks/angular.md",
+ packId: "frontend",
+ parentId: "frontend/performance/javascript",
+ tags: ["framework", "angular"],
+ inclusion: "framework",
+ },
+ {
+ id: "frontend/framework/vanilla",
+ label: "Vanilla JS Rules",
+ file: "blocks/frameworks/vanilla.md",
+ packId: "frontend",
+ parentId: "frontend/performance/javascript",
+ tags: ["framework", "vanilla", "no-framework"],
+ inclusion: "framework",
+ },
+
+ // ------------------------------------------------------------------
+ // FRAMEWORK — backend frameworks (point at canonical backend files)
+ // ------------------------------------------------------------------
+ {
+ id: "backend/framework/laravel",
+ label: "Laravel Rules",
+ file: "blocks/frameworks/laravel.md",
+ packId: "backend",
+ parentId: "backend/security/base",
+ tags: ["framework", "laravel", "php"],
+ inclusion: "framework",
+ },
+ {
+ id: "backend/framework/express",
+ label: "Express.js Rules",
+ file: "blocks/frameworks/express.md",
+ packId: "backend",
+ parentId: "backend/security/base",
+ tags: ["framework", "express", "nodejs"],
+ inclusion: "framework",
+ },
],
extras: [
- { id: "husky", label: "Husky pre-commit hook", default: true },
- { id: "linter", label: "Linter (ESLint/Flake8/etc.)", default: true },
- { id: "prettier", label: "Prettier formatter", default: true },
+ { id: "husky", label: "Husky pre-commit hook", default: true },
+ { id: "linter", label: "Linter (ESLint / Flake8 / etc.)", default: true },
+ { id: "prettier", label: "Prettier formatter", default: true },
+ { id: "backend-formatter", label: "Backend formatter (config file)", default: false },
+ { id: "ai-prompt", label: "AI generation prompt", default: false },
],
};
diff --git a/src/components/Generator.tsx b/src/components/Generator.tsx
index 84450d6..22c830d 100644
--- a/src/components/Generator.tsx
+++ b/src/components/Generator.tsx
@@ -1,7 +1,7 @@
import { useMemo, useState, useEffect } from "react";
import { registry } from "@/core/registry";
import { generate, buildCliCommand } from "@/core/generator";
-import type { OutputFormat, GeneratedFile, PackConfig } from "@/core/types";
+import type { OutputFormat, GeneratedFile, PackConfig, RuleBlock } from "@/core/types";
const FORMAT_OPTIONS: { id: OutputFormat; label: string; ext: string }[] = [
{ id: "cursorrules", label: "Cursor", ext: ".cursorrules" },
@@ -10,6 +10,14 @@ const FORMAT_OPTIONS: { id: OutputFormat; label: string; ext: string }[] = [
{ id: "claude", label: "Claude Code", ext: "CLAUDE.md" },
];
+/**
+ * Derives the list of togglable optional rule blocks from the new RuleBlock[]
+ * structure. Previously this read from the deprecated `pack.optionalBlocks` field.
+ */
+function getOptionalRuleBlocks(pack: PackConfig): RuleBlock[] {
+ return pack.blocks.filter((b) => b.inclusion === "optional");
+}
+
interface Props {
pack: PackConfig;
}
@@ -18,7 +26,9 @@ export function Generator({ pack }: Props) {
const [formats, setFormats] = useState(["cursorrules"]);
const [frameworkId, setFrameworkId] = useState(pack.frameworks[0]?.id ?? "agnostic");
const [optionalBlocks, setOptionalBlocks] = useState(
- (pack.optionalBlocks ?? []).filter((b) => b.default).map((b) => b.id),
+ // Read from the new RuleBlock[] structure — blocks with inclusion: "optional" and default: true
+ // (Note: RuleBlock has no "default" field; we default-enable none and let the user choose)
+ [],
);
const [extras, setExtras] = useState(
pack.extras.filter((e) => e.default).map((e) => e.id),
@@ -26,6 +36,8 @@ export function Generator({ pack }: Props) {
const [activeTab, setActiveTab] = useState(0);
const [copied, setCopied] = useState(null);
+ const optionalRuleBlocks = getOptionalRuleBlocks(pack);
+
const files: GeneratedFile[] = useMemo(
() =>
generate({
@@ -40,7 +52,7 @@ export function Generator({ pack }: Props) {
useEffect(() => {
setFrameworkId(pack.frameworks[0]?.id ?? "agnostic");
- setOptionalBlocks((pack.optionalBlocks ?? []).filter((b) => b.default).map((b) => b.id));
+ setOptionalBlocks([]); // reset optional blocks on pack change
setExtras(pack.extras.filter((e) => e.default).map((e) => e.id));
setActiveTab(0);
setCopied(null);
@@ -134,7 +146,7 @@ export function Generator({ pack }: Props) {
{/* Step 3: Extras */}
- {(pack.optionalBlocks ?? []).map((b) => (
+ {optionalRuleBlocks.map((b) => (