From 649eb01d803bc70430bbff96bf07b013938098f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Gie=C5=82da?= Date: Thu, 9 Apr 2026 11:13:30 +0200 Subject: [PATCH 1/5] feat(skills): add filters skill --- .github/agents/tsh-software-engineer.agent.md | 1 + .github/prompts/tsh-implement-ui.prompt.md | 1 + .../skills/tsh-implementing-filters/SKILL.md | 309 ++++++++++++++++++ .../references/nextjs-patterns.md | 111 +++++++ .../references/react-patterns.md | 139 ++++++++ .../skills/tsh-implementing-forms/SKILL.md | 1 + .../skills/tsh-implementing-frontend/SKILL.md | 1 + .github/skills/tsh-writing-hooks/SKILL.md | 1 + website/docs/skills/implementing-filters.md | 35 ++ 9 files changed, 599 insertions(+) create mode 100644 .github/skills/tsh-implementing-filters/SKILL.md create mode 100644 .github/skills/tsh-implementing-filters/references/nextjs-patterns.md create mode 100644 .github/skills/tsh-implementing-filters/references/react-patterns.md create mode 100644 website/docs/skills/implementing-filters.md diff --git a/.github/agents/tsh-software-engineer.agent.md b/.github/agents/tsh-software-engineer.agent.md index a797a26d..a39c2517 100644 --- a/.github/agents/tsh-software-engineer.agent.md +++ b/.github/agents/tsh-software-engineer.agent.md @@ -59,6 +59,7 @@ Before starting any task, you check all available skills and decide which one is - `tsh-sql-and-database-understanding` - when writing SQL queries, designing database schemas, creating migrations, implementing ORM-based data access, optimising query performance, or working with transactions and locking. Applies to PostgreSQL, MySQL, MariaDB, SQL Server, and Oracle. - `tsh-implementing-frontend` - for UI tasks: component patterns, composition, design tokens, barrel files, and Figma-to-code workflow. - `tsh-implementing-forms` - for form tasks: schema validation, field composition, error handling, multi-step form flows. +- `tsh-implementing-filters` - for filter tasks: type-safe URL filter synchronization, bracket notation serialization, filter sync hooks, push/replace navigation strategies. - `tsh-writing-hooks` - for custom hooks: naming, composition, stable returns, effect cleanup, testing. - `tsh-ensuring-accessibility` - for WCAG 2.1 AA compliance: semantic HTML, ARIA, keyboard navigation, focus management, screen readers. - `tsh-optimizing-frontend` - for frontend performance: code splitting, memoization, bundle size, rendering optimization, memory management. diff --git a/.github/prompts/tsh-implement-ui.prompt.md b/.github/prompts/tsh-implement-ui.prompt.md index 0049783c..f97ba922 100644 --- a/.github/prompts/tsh-implement-ui.prompt.md +++ b/.github/prompts/tsh-implement-ui.prompt.md @@ -13,6 +13,7 @@ Implement the UI feature according to the **research context** and **implementat Before starting, load and follow these skills: - `tsh-implementing-frontend` - for component patterns, design system usage, composition, and performance guidelines +- `tsh-implementing-filters` - for URL filter synchronization, bracket notation serialization, filter sync hooks, and navigation strategies when implementing filterable lists - `tsh-ui-verifying` - for understanding verification criteria, tolerances, and what constitutes PASS/FAIL - `tsh-ensuring-accessibility` - for WCAG 2.1 AA compliance, semantic HTML, ARIA, and automated axe-core verification - `tsh-technical-context-discovering` - to establish project conventions before implementing diff --git a/.github/skills/tsh-implementing-filters/SKILL.md b/.github/skills/tsh-implementing-filters/SKILL.md new file mode 100644 index 00000000..2d0a979a --- /dev/null +++ b/.github/skills/tsh-implementing-filters/SKILL.md @@ -0,0 +1,309 @@ +--- +name: tsh-implementing-filters +description: "Type-safe URL filter synchronization with clean path and query string routing for React Router. Headless filter logic — schema definition, bracket notation serialization, URL sync hooks, and navigation strategy patterns. Use when implementing filterable lists, search pages, faceted navigation, or any UI that persists filter state in the URL." +--- + +# Implementing Filters + +Provides headless patterns for type-safe URL filter synchronization — schema definition, bracket notation serialization/deserialization, router-bound hooks, and push/replace navigation strategies for React Router v6+. + + + + +Filter state lives in the URL, not in component state. The URL is the single source of truth — components derive their state by parsing the URL. This enables shareable links, back/forward navigation, and SSR hydration without state mismatch. Never store canonical filter state in `useState` or a store and then sync it to the URL — read from the URL, write to the URL. + + + +If removing a parameter changes what page or resource you are looking at, it belongs in the path. If removing it just narrows or broadens the same list, it belongs in a query string. Path = identity; query string = modifier. Category hierarchies go in the path (`/products/shoes/running`). Filters, sort, pagination, and search terms go in query strings (`?filter[color]=blue&sort[price]=ASC&page=2`). + + + +Define a TypeScript schema for every filter set. Parse URL params through the schema on read (deserialize), and serialize through the schema on write. Bracket notation (`filter[field]=value`) is the standard serialization format — it aligns with the backend API contract. Never pass raw `string | null` from `searchParams` into application code — always validate and coerce to the schema type. Invalid or missing values fall back to schema defaults. + + + +The URL serialization format must match the API it talks to. Bracket notation (`filter[field]=value`) is the default for TSH-owned APIs. When consuming an external API that uses a different convention (flat keys like `color=blue`, JSON-encoded params, comma-separated values, or any other format), adapt the serialization layer to that API's expected format. The filter schema, hook shape, and navigation strategy remain the same — only the serialize/deserialize functions change. + + + + +## Filter Implementation Process + +Use the checklist below and track progress: + +``` +Progress: +- [ ] Step 1: Define the filter schema +- [ ] Step 2: Choose the URL structure +- [ ] Step 3: Implement serialization/deserialization +- [ ] Step 4: Create the filter sync hook +- [ ] Step 5: Wire up navigation strategy +``` + +**Step 1: Define the filter schema** + +Define a TypeScript type or interface for each filter set. Every filter parameter must have an explicit type and a default value. + +Supported filter parameter types: + +| Type | Example | Serialized | +| -------------- | ------------------------------------ | ------------------------------------------------------ | +| Single value | `color: string` | `filter[color]=blue` | +| Multi-select | `tags: string[]` | `filter[tags]=a&filter[tags]=b` (repeated bracket key) | +| Range (flat) | `priceMin: number; priceMax: number` | `filter[priceMin]=10&filter[priceMax]=50` | +| Boolean toggle | `inStock: boolean` | `filter[in_stock]=true` | +| Numeric | `page: number` | `page=2` (top-level, not bracketed) | + +Rules: + +- Filters use bracket notation: `filter[fieldName]=value`. Range filters use flat keys: `filter[priceMin]=10&filter[priceMax]=50`. +- Use `snake_case` for bracket key names — readable in URLs. +- Use `camelCase` for TypeScript property names. +- Define defaults for every param — defaults are used when a param is absent from the URL. + +Example schema shape: + +```typescript +interface ProductFilters { + color: string; // default: "" → filter[color]=blue + tags: string[]; // default: [] → filter[tags]=a&filter[tags]=b + priceMin: number; // default: 0 → filter[priceMin]=0 + priceMax: number; // default: 10000 → filter[priceMax]=10000 + inStock: boolean; // default: false → filter[in_stock]=true + sort: string; // default: "relevance" → sort[relevance]=ASC + page: number; // default: 1 → page=1 +} +``` + +**Step 2: Choose the URL structure** + +Apply the path-vs-query golden rule to classify each piece of your URL: + +| Element | URL part | Example | Rationale | +| ----------------------- | ------------ | ------------------------------------- | ------------------------------------------------ | +| Resource category | Path segment | `/products/shoes` | Removing "shoes" changes what you are looking at | +| Subcategory / hierarchy | Path segment | `/products/shoes/running` | Still defines identity | +| Filter (color, size) | Query string | `?filter[color]=blue&filter[size]=10` | Removing it shows the same page, broader list | +| Sort order | Query string | `?sort[price]=ASC` | Display modifier, not identity | +| Pagination | Query string | `?page=2&limit=20` | Display modifier | +| Search term | Query string | `?search=lightweight` | Display modifier | + +**API alignment rule**: The URL serialization must match the API it communicates with. For TSH-owned APIs, this means bracket notation (`filter[field]=value`). For external APIs with different conventions, adapt the serialize/deserialize functions to match the external format. Common external patterns: + +| External API Pattern | Example | Adaptation | +| ---------------------- | -------------------------- | ------------------------------------------------------------------- | +| Flat keys | `color=blue&price_min=10` | Serialize filters as top-level params without `filter[...]` wrapper | +| Comma-separated values | `colors=blue,red,green` | Join arrays with commas instead of repeated keys | +| JSON-encoded | `filters={"color":"blue"}` | JSON-stringify the filter object into a single param | +| Custom prefix | `f_color=blue&f_size=10` | Use the API's prefix convention in serialize/deserialize | + +The filter schema, hook shape (`{ filters, updateFilters, resetFilters }`), and navigation strategy (push/replace) are API-agnostic — only the serialization layer changes per API. + +**Step 3: Implement serialization/deserialization** + +Build two functions tied to the filter schema: one to serialize the typed filter object into `URLSearchParams`, and one to deserialize `URLSearchParams` back into the typed filter object. + +**Serialize** (typed object → `URLSearchParams`): + +- Iterate over schema keys. +- Skip params whose value matches the default — keeps the URL clean. +- Wrap filter params in bracket notation: `filter[fieldName]=value`. Use `snake_case` inside brackets. +- Ranges use flat keys: `filter[priceMin]=10&filter[priceMax]=50`. +- Arrays use repeated bracket keys: `filter[tags]=a&filter[tags]=b`. +- Sort params use `sort[field]=direction` format. +- Booleans serialize as `"true"` / `"false"`. + +**Deserialize** (`URLSearchParams` → typed object): + +- Parse `filter[...]` keys — extract field names from brackets. +- For flat range keys (`filter[priceMin]`, `filter[priceMax]`), parse directly as bracket keys. +- Coerce types: `Number()` for numerics, `=== 'true'` for booleans, `searchParams.getAll()` for repeated bracket keys. +- Apply defaults for any missing or invalid param. +- Reject out-of-range or malformed values by falling back to the schema default — never propagate garbage into application code. + +> **Prefer schema validation**: Use a library like [Zod](https://zod.dev) or [Valibot](https://valibot.dev) to parse, coerce, and validate URL parameters in one step. Define a schema that mirrors the filter interface, with `.default()` for fallbacks and `.coerce` for type conversions. Fall back to manual `Number()` / `=== 'true'` only when adding a validation library is not feasible. + +Key rules: + +- Bracket notation for all filter params: `filter[field]=value`. +- Repeated bracket keys for multi-value filters: `filter[field]=a&filter[field]=b`. +- Omit default values from URL — cleaner URLs, same result. +- Never trust raw URL params — always parse through the schema. + +**Step 4: Create the filter sync hook** + +Create a custom hook (e.g., `useFilters`) that connects the filter schema to the router. The hook: + +1. Reads current URL search params via the router. +2. Deserializes them into the typed filter state using the schema. +3. Provides an `updateFilters(partial)` function that merges partial updates with current filters, serializes the result, and navigates. +4. Provides a `resetFilters()` function that navigates to the URL with all defaults (effectively clearing query params). + +The hook accepts configuration: + +- The filter schema defaults object. +- The serialization/deserialization functions. + +The hook returns: + +- `filters` — the current typed filter state, derived from the URL. +- `updateFilters(partial, options?)` — merges partial filter updates and navigates. +- `resetFilters()` — clears all filters back to defaults. + +Framework binding: + +| Framework | Read params | Navigate | +| ---------------- | ------------------- | -------------------------------------- | +| React Router v6+ | `useSearchParams()` | `setSearchParams()` or `useNavigate()` | + +The hook must: + +- Handle partial updates — merge incoming changes with current filter state, not replace entirely. Use flat properties for ranges (e.g., `priceMin`, `priceMax`) so that partial updates merge correctly with shallow spread. +- Be type-safe end-to-end — input partial → serialization → URL → deserialization → output typed. +- Derive state from the URL on every render — never cache filter state in local `useState`. + +**Step 5: Wire up navigation strategy** + +Choose `push` or `replace` navigation based on the type of filter change: + +| Action | Strategy | Rationale | +| ----------------------- | ---------------------------------------- | ---------------------------------------------------- | +| Category toggle (major) | `push` | User can press Back to undo | +| Sort change | `push` | Intentional user action, should be undoable | +| Search-as-you-type | `replace` | Don't flood history with every keystroke | +| Pagination | `push` | User expects Back to go to previous page | +| Filter toggle (facet) | `push` | User expects Back to undo filter | +| Debounced range slider | `replace` during drag, `push` on release | Balance between history cleanliness and undo-ability | + +The `updateFilters` function should accept an optional `{ replace?: boolean }` option. Default to `push`. Callers override to `replace` for as-you-type or continuous inputs. + +```typescript +// Push (default) — for discrete filter actions +updateFilters({ color: "blue" }); + +// Replace — for as-you-type search input +updateFilters({ search: searchTerm }, { replace: true }); +``` + +For debounced inputs (range sliders, search fields), use `replace` during rapid changes and `push` on the final committed value. + +## Serialization Quick Reference + +| TypeScript type | Serialized format | Deserialize with | +| -------------------------------- | ----------------------------------- | ------------------------------- | +| `string` | `filter[key]=value` | Parse bracket key, `get()` | +| `number` | `filter[key]=123` | Parse bracket key, `Number()` | +| `boolean` | `filter[key]=true` | Parse bracket key, `=== 'true'` | +| `string[]` | `filter[key]=a&filter[key]=b` | Parse bracket key, `getAll()` | +| `keyMin: number; keyMax: number` | `filter[keyMin]=1&filter[keyMax]=9` | `Number()` for each key | +| sort | `sort[field]=ASC` | Parse bracket key | +| search | `search=text` | `get('search')` | +| pagination | `page=1&limit=20` | `get('page')`, `get('limit')` | + +## API Contract + +The conventions below describe the **default TSH backend API contract**. When the API provider follows a different pattern (external APIs, third-party services, legacy backends), adapt the serialization layer to match that API's expected format — the filter schema and hook architecture remain unchanged. + +### Query Parameter Structure + +| Concern | Format | Example | +| ----------------------- | ----------------------- | ---------------------------------------------- | +| Filters | `filter[field]=value` | `filter[firstName]=Ewa` | +| Multi-value filter (OR) | Repeated bracket key | `filter[firstName]=Ewa&filter[firstName]=Adam` | +| Range filter | Flat keys | `filter[priceMin]=10&filter[priceMax]=50` | +| Text search | Partial match value | `filter[firstName]=Nowak` | +| Pagination | Top-level keys | `page=1&limit=100` | +| Sort | `sort[field]=direction` | `sort[lastName]=ASC` | +| Full-text search | Top-level key | `search=test` | + +### Filter Logic + +- **Same field, multiple values → OR**: `filter[firstName]=Ewa&filter[firstName]=Adam` — same field with multiple values implies OR — the response includes items matching any of the values. +- **Different fields → AND**: `filter[firstName]=Ewa&filter[lastName]=Kowalska` — different fields imply AND — the response includes only items matching all field conditions. +- **Text search**: `filter[firstName]=Nowak` — backend determines the matching strategy (e.g., prefix, contains, fuzzy). + +### Example API Response Envelope + +APIs vary — the following is one common envelope structure. Adapt to match your project's actual API response format: + +```typescript +interface ApiResponse { + meta: { + pagination: { + page: number; + total: number; + limit: number; + totalPages: number; + }; + filter: Record; + sort: Record; + search: string; + }; + data: T[]; +} +``` + +The `meta` object echoes back the applied filters, sort, search, and pagination — use it to verify that the URL state matches what the backend applied. + +If your API echoes applied filters in the response meta, verify that it matches the deserialized URL state to ensure frontend-backend sync. + +### Backend Considerations + +- **Pagination resets**: When filters change, reset the page parameter to 1 to avoid requesting pages beyond the new result set. +- **Filter echoing**: If the API returns applied filters in the response (e.g., in a `meta` object), compare them against the URL state to detect sync issues. + +## Filter Quality Checklist + +``` +Filter: +- [ ] Filter schema defined with TypeScript types and defaults +- [ ] URL structure follows path-vs-query golden rule +- [ ] Filters use bracket notation (filter[field]=value) +- [ ] Sort uses bracket notation (sort[field]=ASC|DESC) +- [ ] Serialization omits default values from URL +- [ ] Deserialization validates and coerces types +- [ ] Range filters use flat keys (filter[priceMin]=10&filter[priceMax]=50) +- [ ] Multi-value filters use repeated bracket keys (OR logic) +- [ ] If external API, serialization adapted to match API's expected format +- [ ] Cross-field filters combine as AND +- [ ] Text search values contain only the raw search term — no backend-specific operators or wildcards +- [ ] Hook returns typed filter state, updateFilters, resetFilters +- [ ] Push/Replace strategy documented for each filter action +- [ ] Search-as-you-type inputs use Replace navigation +- [ ] No raw string params leak into application code +- [ ] Back/Forward navigation correctly restores filter state +- [ ] Shareable URL reproduces the exact filter state +- [ ] API response meta echoed back matches URL state +``` + +## Anti-Patterns + +| Anti-Pattern | Instead Do | +| ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- | +| Storing filter state in `useState` and syncing to URL | Derive filter state FROM the URL — URL is the source of truth | +| Using flat keys for filters (`color=blue&price_min=10`) | Use bracket notation (`filter[color]=blue&filter[priceMin]=10`) | +| Hardcoding filter params as magic strings | Define a filter schema type and derive param names from it | +| Using `push` for search-as-you-type | Use `replace` to avoid flooding browser history | +| Putting optional filters in path segments (`/products/color/blue`) | Put filters in query strings — path is for resource identity only | +| Serializing default values in URL (`?page=1&sort[relevance]=ASC`) | Omit defaults — cleaner URLs, same behavior | +| Parsing URL params without type coercion | Validate with a schema library (Zod, Valibot) or manually coerce: `Number()`, `=== 'true'`, with fallback to defaults | +| Building one monolithic filter hook for all pages | Create filter schemas per-page/per-feature, share the serialization utility | +| Treating multi-value filter as AND | Same-field repeated values are OR; different fields are AND | +| Ignoring API response `meta` object | Verify URL filter state matches what the backend echoed in `meta` | +| Using bracket notation when the external API expects flat keys | Adapt serialization to match the API contract — bracket notation is the TSH default, not a universal rule | + +## Framework-Specific Patterns + +The patterns above are framework-agnostic. For framework-specific implementation guidance, load the appropriate reference: + +- **React Router v6+**: See `./references/react-patterns.md` — `useSearchParams`, `setSearchParams()` with functional updater, `useNavigate()` integration, hook binding. +- **Next.js App Router**: See `./references/nextjs-patterns.md` — `useSearchParams`, `usePathname`, `router.push()`/`router.replace()` integration, server component considerations. + +## Connected Skills + +- `tsh-implementing-frontend` — for component patterns that consume the headless filter logic +- `tsh-implementing-forms` — for form-based filter UIs that wire into the filter hook +- `tsh-writing-hooks` — for hook composition patterns and return shape conventions applicable to filter hooks +- `tsh-optimizing-frontend` — for rendering optimization for filter-heavy pages +- `tsh-ensuring-accessibility` — for accessible filter controls (keyboard navigation, ARIA attributes for active filters) +- `tsh-sql-and-database-understanding` — for backend query optimization behind the API contract diff --git a/.github/skills/tsh-implementing-filters/references/nextjs-patterns.md b/.github/skills/tsh-implementing-filters/references/nextjs-patterns.md new file mode 100644 index 00000000..d9050783 --- /dev/null +++ b/.github/skills/tsh-implementing-filters/references/nextjs-patterns.md @@ -0,0 +1,111 @@ +# Next.js Filter Patterns + +Next.js App Router-specific patterns for the tsh-implementing-filters skill. Load this reference when the project uses Next.js. + +## Table of Contents + +- [Reading Search Params](#reading-search-params) +- [Navigation (Push/Replace)](#navigation-pushreplace) +- [Server Component Constraints](#server-component-constraints) +- [Bracket Notation with Next.js](#bracket-notation-with-nextjs) +- [Anti-Patterns](#anti-patterns) + +## Reading Search Params + +Next.js App Router provides different APIs for reading URL search params depending on the context: + +| Context | API | Notes | +| ---------------- | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | +| Client Component | `useSearchParams()` from `next/navigation` | Returns `ReadonlyURLSearchParams`. Only available in Client Components. | +| Server Component | `searchParams` prop from page/layout (async in Next.js 15+ — must be `await`ed) | Passed as a prop to `page.tsx`. Access `searchParams.filter` etc. | +| Route Handler | `request.nextUrl.searchParams` | In `route.ts` handlers. | + +Key difference from React Router: `useSearchParams()` returns a **read-only** object. You cannot call `setSearchParams()` — you must construct a new URL and navigate. + +## Navigation (Push/Replace) + +How to navigate with updated filters in Next.js App Router: + +```typescript +import { useRouter, usePathname, useSearchParams } from "next/navigation"; + +const useFilterNavigation = () => { + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + + const navigate = ( + newParams: URLSearchParams, + options?: { replace?: boolean }, + ) => { + const url = `${pathname}?${newParams.toString()}`; + if (options?.replace) { + router.replace(url); + } else { + router.push(url); + } + }; + + return { navigate, searchParams, pathname }; +}; +``` + +`router.push()` and `router.replace()` use **soft navigation** by default — no full page reload, which is the desired behavior for filter changes. + +## Server Component Constraints + +- Server Components **cannot** use `useSearchParams()` — they receive `searchParams` as a page prop. +- Deserialization logic (bracket notation parsing, type coercion) works identically in both contexts — only the source of raw params differs. +- Pattern: Define serialization/deserialization as plain functions (not hooks). The `useFilters` hook wraps them for Client Components. Server Components call the deserialization function directly. + +```typescript +// Shared — works in both contexts +const deserializeFilters = (params: URLSearchParams): ProductFilters => { + /* ... */ +}; + +// Client Component — via hook +const useFilters = (defaults: ProductFilters) => { + const searchParams = useSearchParams(); + const filters = deserializeFilters( + new URLSearchParams(searchParams.toString()), + ); + // ... +}; + +// Server Component — direct call +export default async function ProductsPage({ + searchParams, +}: { + searchParams: Promise>; +}) { + const resolvedParams = await searchParams; + const params = new URLSearchParams(); + // Convert resolvedParams record to URLSearchParams... + const filters = deserializeFilters(params); + // ... +} +``` + +## Bracket Notation with Next.js + +Next.js does not natively parse bracket notation (`filter[color]=blue`) into structured objects. The `searchParams` prop gives flat key-value pairs where the key is the literal string `filter[color]`. + +The deserialization function must parse bracket keys manually. This logic is framework-agnostic — identical to the React Router approach. Only the source of `URLSearchParams` differs. + +```typescript +// searchParams in Next.js page: { "filter[color]": "blue", "filter[tags]": ["a", "b"] } +// Parse bracket keys to extract field names +``` + +## Anti-Patterns + +| Anti-Pattern | Instead Do | +| ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ | +| Using `useSearchParams()` in Server Components | Use the `searchParams` page prop in Server Components | +| Calling `setSearchParams()` like React Router | Construct new URL string, use `router.push()` / `router.replace()` | +| Wrapping entire page in Client Component for filter access | Keep filter display in Server Components via `searchParams` prop; only filter controls need Client Component | +| Using `router.push()` with full URL including origin | Use pathname-relative URLs: `` `${pathname}?${params}` `` | +| Using nested objects for range filters (e.g., `{ price: { min, max } }`) | Use flat properties (`priceMin`, `priceMax`) so partial updates with shallow spread merge correctly | + +> **Note:** When composing filter update functions, use flat properties for ranges (e.g., `priceMin`, `priceMax` instead of nested `{ price: { min, max } }`) so that partial updates with shallow spread merge correctly. diff --git a/.github/skills/tsh-implementing-filters/references/react-patterns.md b/.github/skills/tsh-implementing-filters/references/react-patterns.md new file mode 100644 index 00000000..e950eafc --- /dev/null +++ b/.github/skills/tsh-implementing-filters/references/react-patterns.md @@ -0,0 +1,139 @@ +# React Filter Patterns + +React Router v6+-specific patterns for the `tsh-implementing-filters` skill. Load this reference when the project uses React Router. + +## Reading Search Params + +| API | Usage | Notes | +| -------------------------- | ----------------------------------------- | ---------------------------------------------------------------------- | +| `useSearchParams()` | Returns `[searchParams, setSearchParams]` | Read-write tuple. `searchParams` is a `URLSearchParams` instance. | +| `searchParams.get(key)` | Single value | Returns `string \| null` | +| `searchParams.getAll(key)` | Multi-value (arrays) | Returns `string[]` — use for repeated bracket keys like `filter[tags]` | +| `searchParams.toString()` | Full query string | For URL construction | + +Key difference from Next.js: React Router's `useSearchParams()` returns a **read-write** tuple — you can use `setSearchParams()` directly to update the URL without constructing a full URL string. + +## Navigation (Push/Replace) + +**Approach 1: `setSearchParams()` (preferred for filter updates)** + +```typescript +import { useSearchParams } from "react-router-dom"; + +const useFilterNavigation = () => { + const [searchParams, setSearchParams] = useSearchParams(); + + const navigate = ( + newParams: URLSearchParams, + options?: { replace?: boolean }, + ) => { + setSearchParams(newParams, { replace: options?.replace }); + }; + + return { navigate, searchParams }; +}; +``` + +`setSearchParams` also supports a functional updater for deriving from the previous params: + +```typescript +setSearchParams( + (prev) => { + const next = new URLSearchParams(prev); + next.set("filter[color]", "blue"); + return next; + }, + { replace: true }, +); +``` + +**Approach 2: `useNavigate()` (when path + search need to change together)** + +```typescript +import { useNavigate, useSearchParams } from "react-router-dom"; + +const useFilterNavigation = () => { + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + + const navigateWithFilters = ( + newParams: URLSearchParams, + options?: { replace?: boolean; pathname?: string }, + ) => { + navigate( + { pathname: options?.pathname ?? ".", search: newParams.toString() }, + { replace: options?.replace }, + ); + }; + + return { navigateWithFilters, searchParams }; +}; +``` + +Use `setSearchParams()` when only filters change. Use `useNavigate()` when both the path segment and query string need to update simultaneously (e.g., changing category + resetting filters). `setSearchParams()` with `{ replace: true }` maps directly to the skill's navigation strategy — use it for search-as-you-type, omit it (defaults to push) for discrete filter actions. + +## Hook Binding + +Concrete React Router binding for the generic `useFilters` hook described in SKILL.md Step 4: + +```typescript +import { useSearchParams } from "react-router-dom"; + +const useFilters = >({ + defaults, + serialize, + deserialize, +}: { + defaults: T; + serialize: (filters: T) => URLSearchParams; + deserialize: (params: URLSearchParams) => T; +}) => { + const [searchParams, setSearchParams] = useSearchParams(); + + const filters = deserialize(searchParams); + + const updateFilters = ( + partial: Partial, + options?: { replace?: boolean }, + ) => { + setSearchParams( + (prev) => { + const current = deserialize(prev); + const next = { ...current, ...partial }; + return serialize(next); + }, + { replace: options?.replace }, + ); + }; + + const resetFilters = () => { + setSearchParams(serialize(defaults)); + }; + + return { filters, updateFilters, resetFilters }; +}; +``` + +The hook reads `searchParams` for the current filter state and uses `setSearchParams` with a functional updater for writes. No `useEffect` or `useState` needed. + +Note: `{ ...filters, ...partial }` is a shallow merge. Use flat properties for ranges (e.g., `priceMin`, `priceMax` instead of nested `{ price: { min, max } }`) so that partial updates merge correctly. + +## Bracket Notation Parsing + +React Router does not parse bracket notation (`filter[color]=blue`) into structured objects — the `searchParams` instance treats `filter[color]` as a literal key string. The deserialization function must parse bracket keys manually. This logic is framework-agnostic. See SKILL.md Step 3 for the serialization/deserialization pattern. + +```typescript +// searchParams.get('filter[color]') → 'blue' +// searchParams.getAll('filter[tags]') → ['a', 'b'] +// Parse bracket keys to extract field names and structure +``` + +## Anti-Patterns + +| Anti-Pattern | Instead Do | +| ------------------------------------------------------------------- | --------------------------------------------------------------------------- | +| Using `useNavigate()` when only filters change | Use `setSearchParams()` — simpler, doesn't require path construction | +| Mutating the `searchParams` object directly and expecting re-render | Create new `URLSearchParams` via `serialize()`, pass to `setSearchParams()` | +| Using `setSearchParams` with string instead of `URLSearchParams` | Pass `URLSearchParams` object for type safety and correct encoding | +| Wrapping `setSearchParams` in `useEffect` to sync state | Call `setSearchParams` directly in event handlers — no effect sync needed | +| Using `navigate(-1)` to "undo" filter changes | Rely on browser Back button — push navigation already handles this | diff --git a/.github/skills/tsh-implementing-forms/SKILL.md b/.github/skills/tsh-implementing-forms/SKILL.md index acc00ed3..a2b96caf 100644 --- a/.github/skills/tsh-implementing-forms/SKILL.md +++ b/.github/skills/tsh-implementing-forms/SKILL.md @@ -149,3 +149,4 @@ Form: - `tsh-ensuring-accessibility` — for WCAG compliance in form fields, labels, and error announcements - `tsh-writing-hooks` — for custom form-related hooks/composables (useFormField, useMultiStepForm) - `tsh-reviewing-frontend` — for form-specific review criteria during code review +- `tsh-implementing-filters` — for filter UIs that wire form inputs into URL-synced filter hooks diff --git a/.github/skills/tsh-implementing-frontend/SKILL.md b/.github/skills/tsh-implementing-frontend/SKILL.md index 009ef83d..986e1808 100644 --- a/.github/skills/tsh-implementing-frontend/SKILL.md +++ b/.github/skills/tsh-implementing-frontend/SKILL.md @@ -148,5 +148,6 @@ The patterns above are framework-agnostic. For framework-specific implementation - `tsh-ensuring-accessibility` — to ensure components meet WCAG 2.1 AA standards - `tsh-optimizing-frontend` — for performance considerations during component implementation - `tsh-implementing-forms` — for form-specific component patterns and validation +- `tsh-implementing-filters` — for URL filter synchronization patterns consumed by list and search components - `tsh-writing-hooks` — for custom hook patterns used within components - `tsh-reviewing-frontend` — for frontend-specific code review of implemented components diff --git a/.github/skills/tsh-writing-hooks/SKILL.md b/.github/skills/tsh-writing-hooks/SKILL.md index 72660d02..24f428db 100644 --- a/.github/skills/tsh-writing-hooks/SKILL.md +++ b/.github/skills/tsh-writing-hooks/SKILL.md @@ -222,3 +222,4 @@ The patterns above are framework-agnostic. For framework-specific hook/composabl - `tsh-optimizing-frontend` — for memoization strategies and performance patterns in hooks - `tsh-reviewing-frontend` — for hook-specific code review criteria - `tsh-implementing-forms` — for form-related custom hooks (field state, validation triggers) +- `tsh-implementing-filters` — for the filter sync hook pattern (useFilters) that applies hook composition conventions diff --git a/website/docs/skills/implementing-filters.md b/website/docs/skills/implementing-filters.md new file mode 100644 index 00000000..f510242b --- /dev/null +++ b/website/docs/skills/implementing-filters.md @@ -0,0 +1,35 @@ +--- +sidebar_position: 29 +title: Implementing Filters +--- + +# Implementing Filters + +**Folder:** `.github/skills/tsh-implementing-filters/` +**Used by:** Software Engineer + +Type-safe URL filter synchronization with clean path and query string routing. Headless filter logic — schema definition, bracket notation serialization, URL sync hooks, and navigation strategy patterns. + +## Key Areas + +| Area | Coverage | +| ----------------------- | ---------------------------------------------------------------------------------------- | +| **Filter Schema** | TypeScript types, defaults, supported param types (single, multi-select, range, boolean) | +| **URL Structure** | Path-vs-query golden rule, bracket notation (`filter[field]=value`) | +| **Serialization** | Serialize/deserialize functions, bracket notation, snake_case keys, default omission | +| **Filter Sync Hook** | `useFilters` hook — typed state from URL, `updateFilters`, `resetFilters` | +| **Navigation Strategy** | Push vs replace per action type, debounced inputs, search-as-you-type | +| **API Contract** | TSH bracket notation default, external API adaptation, response envelope | + +## When to Use + +- Implementing filterable lists, search pages, or faceted navigation +- Persisting filter state in the URL for shareable links and back/forward navigation +- Building filter sync hooks that derive state from URL search params +- Adapting serialization for external APIs with non-bracket conventions + +## Connected Skills + +- `tsh-implementing-frontend` — component patterns that consume headless filter logic +- `tsh-implementing-forms` — form-based filter UIs +- `tsh-writing-hooks` — hook composition patterns for the filter sync hook From 881f900d20eefdb8bc52cc9549abed5b4b300a0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Gie=C5=82da?= Date: Tue, 21 Apr 2026 10:50:17 +0200 Subject: [PATCH 2/5] feat(skills): add filters skill - apply CR changes, part 1 --- .../skills/tsh-implementing-filters/SKILL.md | 75 +++++++++---------- 1 file changed, 37 insertions(+), 38 deletions(-) diff --git a/.github/skills/tsh-implementing-filters/SKILL.md b/.github/skills/tsh-implementing-filters/SKILL.md index 2d0a979a..26418652 100644 --- a/.github/skills/tsh-implementing-filters/SKILL.md +++ b/.github/skills/tsh-implementing-filters/SKILL.md @@ -1,11 +1,11 @@ --- name: tsh-implementing-filters -description: "Type-safe URL filter synchronization with clean path and query string routing for React Router. Headless filter logic — schema definition, bracket notation serialization, URL sync hooks, and navigation strategy patterns. Use when implementing filterable lists, search pages, faceted navigation, or any UI that persists filter state in the URL." +description: "Type-safe URL filter synchronization with clean path and query string routing. Headless filter logic — schema definition, bracket notation serialization, URL sync hooks, and navigation strategy patterns. Use when implementing filterable lists, search pages, faceted navigation, or any UI that persists filter state in the URL." --- # Implementing Filters -Provides headless patterns for type-safe URL filter synchronization — schema definition, bracket notation serialization/deserialization, router-bound hooks, and push/replace navigation strategies for React Router v6+. +Provides patterns for type-safe URL filter synchronization — schema definition, bracket notation serialization/deserialization, router-bound hooks, and push/replace navigation strategies. @@ -50,13 +50,13 @@ Supported filter parameter types: | -------------- | ------------------------------------ | ------------------------------------------------------ | | Single value | `color: string` | `filter[color]=blue` | | Multi-select | `tags: string[]` | `filter[tags]=a&filter[tags]=b` (repeated bracket key) | -| Range (flat) | `priceMin: number; priceMax: number` | `filter[priceMin]=10&filter[priceMax]=50` | +| Range | `priceMin: number; priceMax: number` | `filter[price_min]=10&filter[price_max]=50` (separate bracket keys) | | Boolean toggle | `inStock: boolean` | `filter[in_stock]=true` | | Numeric | `page: number` | `page=2` (top-level, not bracketed) | Rules: -- Filters use bracket notation: `filter[fieldName]=value`. Range filters use flat keys: `filter[priceMin]=10&filter[priceMax]=50`. +- Filters use bracket notation: `filter[field_name]=value`. Range filters use separate bracket keys: `filter[price_min]=10&filter[price_max]=50`. - Use `snake_case` for bracket key names — readable in URLs. - Use `camelCase` for TypeScript property names. - Define defaults for every param — defaults are used when a param is absent from the URL. @@ -67,8 +67,8 @@ Example schema shape: interface ProductFilters { color: string; // default: "" → filter[color]=blue tags: string[]; // default: [] → filter[tags]=a&filter[tags]=b - priceMin: number; // default: 0 → filter[priceMin]=0 - priceMax: number; // default: 10000 → filter[priceMax]=10000 + priceMin: number; // default: 0 → filter[price_min]=0 + priceMax: number; // default: 10000 → filter[price_max]=10000 inStock: boolean; // default: false → filter[in_stock]=true sort: string; // default: "relevance" → sort[relevance]=ASC page: number; // default: 1 → page=1 @@ -107,21 +107,20 @@ Build two functions tied to the filter schema: one to serialize the typed filter - Iterate over schema keys. - Skip params whose value matches the default — keeps the URL clean. -- Wrap filter params in bracket notation: `filter[fieldName]=value`. Use `snake_case` inside brackets. -- Ranges use flat keys: `filter[priceMin]=10&filter[priceMax]=50`. +- Wrap filter params in bracket notation: `filter[field_name]=value`. Use `snake_case` inside brackets. +- Ranges use separate bracket keys: `filter[price_min]=10&filter[price_max]=50`. - Arrays use repeated bracket keys: `filter[tags]=a&filter[tags]=b`. - Sort params use `sort[field]=direction` format. - Booleans serialize as `"true"` / `"false"`. **Deserialize** (`URLSearchParams` → typed object): -- Parse `filter[...]` keys — extract field names from brackets. -- For flat range keys (`filter[priceMin]`, `filter[priceMax]`), parse directly as bracket keys. -- Coerce types: `Number()` for numerics, `=== 'true'` for booleans, `searchParams.getAll()` for repeated bracket keys. -- Apply defaults for any missing or invalid param. -- Reject out-of-range or malformed values by falling back to the schema default — never propagate garbage into application code. +Deserialization is a two-phase pipeline: -> **Prefer schema validation**: Use a library like [Zod](https://zod.dev) or [Valibot](https://valibot.dev) to parse, coerce, and validate URL parameters in one step. Define a schema that mirrors the filter interface, with `.default()` for fallbacks and `.coerce` for type conversions. Fall back to manual `Number()` / `=== 'true'` only when adding a validation library is not feasible. +1. **Extract bracket keys** — parse `filter[...]` and `sort[...]` keys from `URLSearchParams`, producing a flat key-value object with `camelCase` property names. Handle repeated bracket keys (`getAll()`) for multi-value filters and separate range keys (`filter[price_min]`, `filter[price_max]`). This phase is URL-format-aware but type-unaware. +2. **Validate and coerce** — pass the flat object through the filter schema. Coerce types (`Number()` for numerics, `=== 'true'` for booleans), apply defaults for missing params, and reject out-of-range or malformed values by falling back to schema defaults. Never propagate garbage into application code. + +> **Prefer schema validation**: Use a library like [Zod](https://zod.dev) or [Valibot](https://valibot.dev) for phase 2 — define a schema that mirrors the filter interface, with `.default()` for fallbacks and `.coerce` for type conversions. The schema validates domain types (`color: string`, `priceMin: number`) without knowing anything about URL serialization format. Fall back to manual `Number()` / `=== 'true'` only when adding a validation library is not feasible. Key rules: @@ -189,16 +188,16 @@ For debounced inputs (range sliders, search fields), use `replace` during rapid ## Serialization Quick Reference -| TypeScript type | Serialized format | Deserialize with | -| -------------------------------- | ----------------------------------- | ------------------------------- | -| `string` | `filter[key]=value` | Parse bracket key, `get()` | -| `number` | `filter[key]=123` | Parse bracket key, `Number()` | -| `boolean` | `filter[key]=true` | Parse bracket key, `=== 'true'` | -| `string[]` | `filter[key]=a&filter[key]=b` | Parse bracket key, `getAll()` | -| `keyMin: number; keyMax: number` | `filter[keyMin]=1&filter[keyMax]=9` | `Number()` for each key | -| sort | `sort[field]=ASC` | Parse bracket key | -| search | `search=text` | `get('search')` | -| pagination | `page=1&limit=20` | `get('page')`, `get('limit')` | +| TypeScript type | Serialized format | Deserialize with | +| -------------------------------- | ------------------------------------- | ------------------------------- | +| `string` | `filter[key]=value` | Parse bracket key, `get()` | +| `number` | `filter[key]=123` | Parse bracket key, `Number()` | +| `boolean` | `filter[key]=true` | Parse bracket key, `=== 'true'` | +| `string[]` | `filter[key]=a&filter[key]=b` | Parse bracket key, `getAll()` | +| `keyMin: number; keyMax: number` | `filter[key_min]=1&filter[key_max]=9` | `Number()` for each key | +| sort | `sort[field]=ASC` | Parse bracket key | +| search | `search=text` | `get('search')` | +| pagination | `page=1&limit=20` | `get('page')`, `get('limit')` | ## API Contract @@ -206,21 +205,21 @@ The conventions below describe the **default TSH backend API contract**. When th ### Query Parameter Structure -| Concern | Format | Example | -| ----------------------- | ----------------------- | ---------------------------------------------- | -| Filters | `filter[field]=value` | `filter[firstName]=Ewa` | -| Multi-value filter (OR) | Repeated bracket key | `filter[firstName]=Ewa&filter[firstName]=Adam` | -| Range filter | Flat keys | `filter[priceMin]=10&filter[priceMax]=50` | -| Text search | Partial match value | `filter[firstName]=Nowak` | -| Pagination | Top-level keys | `page=1&limit=100` | -| Sort | `sort[field]=direction` | `sort[lastName]=ASC` | -| Full-text search | Top-level key | `search=test` | +| Concern | Format | Example | +| ----------------------- | ----------------------- | ------------------------------------------------ | +| Filters | `filter[field]=value` | `filter[first_name]=Ewa` | +| Multi-value filter (OR) | Repeated bracket key | `filter[first_name]=Ewa&filter[first_name]=Adam` | +| Range filter | Separate bracket keys | `filter[price_min]=10&filter[price_max]=50` | +| Text search | Partial match value | `filter[first_name]=Nowak` | +| Pagination | Top-level keys | `page=1&limit=100` | +| Sort | `sort[field]=direction` | `sort[last_name]=ASC` | +| Full-text search | Top-level key | `search=test` | ### Filter Logic -- **Same field, multiple values → OR**: `filter[firstName]=Ewa&filter[firstName]=Adam` — same field with multiple values implies OR — the response includes items matching any of the values. -- **Different fields → AND**: `filter[firstName]=Ewa&filter[lastName]=Kowalska` — different fields imply AND — the response includes only items matching all field conditions. -- **Text search**: `filter[firstName]=Nowak` — backend determines the matching strategy (e.g., prefix, contains, fuzzy). +- **Same field, multiple values → OR**: `filter[first_name]=Ewa&filter[first_name]=Adam` — same field with multiple values implies OR — the response includes items matching any of the values. +- **Different fields → AND**: `filter[first_name]=Ewa&filter[last_name]=Kowalska` — different fields imply AND — the response includes only items matching all field conditions. +- **Text search**: `filter[first_name]=Nowak` — backend determines the matching strategy (e.g., prefix, contains, fuzzy). ### Example API Response Envelope @@ -262,7 +261,7 @@ Filter: - [ ] Sort uses bracket notation (sort[field]=ASC|DESC) - [ ] Serialization omits default values from URL - [ ] Deserialization validates and coerces types -- [ ] Range filters use flat keys (filter[priceMin]=10&filter[priceMax]=50) +- [ ] Range filters use separate bracket keys (filter[price_min]=10&filter[price_max]=50) - [ ] Multi-value filters use repeated bracket keys (OR logic) - [ ] If external API, serialization adapted to match API's expected format - [ ] Cross-field filters combine as AND @@ -281,7 +280,7 @@ Filter: | Anti-Pattern | Instead Do | | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- | | Storing filter state in `useState` and syncing to URL | Derive filter state FROM the URL — URL is the source of truth | -| Using flat keys for filters (`color=blue&price_min=10`) | Use bracket notation (`filter[color]=blue&filter[priceMin]=10`) | +| Using flat keys for filters (`color=blue&price_min=10`) | Use bracket notation (`filter[color]=blue&filter[price_min]=10`) | | Hardcoding filter params as magic strings | Define a filter schema type and derive param names from it | | Using `push` for search-as-you-type | Use `replace` to avoid flooding browser history | | Putting optional filters in path segments (`/products/color/blue`) | Put filters in query strings — path is for resource identity only | From af182de9ebe61578bd45baa72f9a7626bc4d3514 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Gie=C5=82da?= Date: Fri, 24 Apr 2026 15:04:07 +0200 Subject: [PATCH 3/5] feat(skills): add filters skill - apply CR changes, part 2 --- .../skills/tsh-implementing-filters/SKILL.md | 47 ++++++++++++------- .../references/react-patterns.md | 8 ++++ 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/.github/skills/tsh-implementing-filters/SKILL.md b/.github/skills/tsh-implementing-filters/SKILL.md index 26418652..452e6d7b 100644 --- a/.github/skills/tsh-implementing-filters/SKILL.md +++ b/.github/skills/tsh-implementing-filters/SKILL.md @@ -7,6 +7,21 @@ description: "Type-safe URL filter synchronization with clean path and query str Provides patterns for type-safe URL filter synchronization — schema definition, bracket notation serialization/deserialization, router-bound hooks, and push/replace navigation strategies. +## Table of Contents + +- [Principles](#principles) +- [Filter Implementation Process](#filter-implementation-process) +- [Serialization Quick Reference](#serialization-quick-reference) +- [API Contract](#api-contract) + - [Query Parameter Structure](#query-parameter-structure) + - [Filter Logic](#filter-logic) + - [Example API Response Envelope](#example-api-response-envelope) + - [Backend Considerations](#backend-considerations) +- [Filter Quality Checklist](#filter-quality-checklist) +- [Anti-Patterns](#anti-patterns) +- [Framework-Specific Patterns](#framework-specific-patterns) +- [Connected Skills](#connected-skills) + @@ -46,13 +61,13 @@ Define a TypeScript type or interface for each filter set. Every filter paramete Supported filter parameter types: -| Type | Example | Serialized | -| -------------- | ------------------------------------ | ------------------------------------------------------ | -| Single value | `color: string` | `filter[color]=blue` | -| Multi-select | `tags: string[]` | `filter[tags]=a&filter[tags]=b` (repeated bracket key) | +| Type | Example | Serialized | +| -------------- | ------------------------------------ | ------------------------------------------------------------------- | +| Single value | `color: string` | `filter[color]=blue` | +| Multi-select | `tags: string[]` | `filter[tags]=a&filter[tags]=b` (repeated bracket key) | | Range | `priceMin: number; priceMax: number` | `filter[price_min]=10&filter[price_max]=50` (separate bracket keys) | -| Boolean toggle | `inStock: boolean` | `filter[in_stock]=true` | -| Numeric | `page: number` | `page=2` (top-level, not bracketed) | +| Boolean toggle | `inStock: boolean` | `filter[in_stock]=true` | +| Numeric | `page: number` | `page=2` (top-level, not bracketed) | Rules: @@ -205,21 +220,21 @@ The conventions below describe the **default TSH backend API contract**. When th ### Query Parameter Structure -| Concern | Format | Example | -| ----------------------- | ----------------------- | ------------------------------------------------ | -| Filters | `filter[field]=value` | `filter[first_name]=Ewa` | -| Multi-value filter (OR) | Repeated bracket key | `filter[first_name]=Ewa&filter[first_name]=Adam` | -| Range filter | Separate bracket keys | `filter[price_min]=10&filter[price_max]=50` | -| Text search | Partial match value | `filter[first_name]=Nowak` | -| Pagination | Top-level keys | `page=1&limit=100` | -| Sort | `sort[field]=direction` | `sort[last_name]=ASC` | -| Full-text search | Top-level key | `search=test` | +| Concern | Format | Example | +| ------------------------- | ----------------------- | ------------------------------------------------ | +| Filters | `filter[field]=value` | `filter[first_name]=Ewa` | +| Multi-value filter (OR) | Repeated bracket key | `filter[first_name]=Ewa&filter[first_name]=Adam` | +| Range filter | Separate bracket keys | `filter[price_min]=10&filter[price_max]=50` | +| Partial-text match filter | Partial match value | `filter[first_name]=Nowak` | +| Pagination | Top-level keys | `page=1&limit=100` | +| Sort | `sort[field]=direction` | `sort[last_name]=ASC` | +| Full-text search | Top-level key | `search=test` | ### Filter Logic - **Same field, multiple values → OR**: `filter[first_name]=Ewa&filter[first_name]=Adam` — same field with multiple values implies OR — the response includes items matching any of the values. - **Different fields → AND**: `filter[first_name]=Ewa&filter[last_name]=Kowalska` — different fields imply AND — the response includes only items matching all field conditions. -- **Text search**: `filter[first_name]=Nowak` — backend determines the matching strategy (e.g., prefix, contains, fuzzy). +- **Partial-text match filter**: `filter[first_name]=Nowak` — backend determines the matching strategy (e.g., prefix, contains, fuzzy). ### Example API Response Envelope diff --git a/.github/skills/tsh-implementing-filters/references/react-patterns.md b/.github/skills/tsh-implementing-filters/references/react-patterns.md index e950eafc..f88d9391 100644 --- a/.github/skills/tsh-implementing-filters/references/react-patterns.md +++ b/.github/skills/tsh-implementing-filters/references/react-patterns.md @@ -2,6 +2,14 @@ React Router v6+-specific patterns for the `tsh-implementing-filters` skill. Load this reference when the project uses React Router. +## Table of Contents + +- [Reading Search Params](#reading-search-params) +- [Navigation (Push/Replace)](#navigation-pushreplace) +- [Hook Binding](#hook-binding) +- [Bracket Notation Parsing](#bracket-notation-parsing) +- [Anti-Patterns](#anti-patterns) + ## Reading Search Params | API | Usage | Notes | From e3675b8ae0373a9f9141e7c9d32650e54aaf6517 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Gie=C5=82da?= Date: Fri, 24 Apr 2026 15:19:52 +0200 Subject: [PATCH 4/5] feat(skills): add filters skill - apply CR changes, part 3 --- .../skills/tsh-implementing-filters/SKILL.md | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/.github/skills/tsh-implementing-filters/SKILL.md b/.github/skills/tsh-implementing-filters/SKILL.md index 452e6d7b..53fe06c5 100644 --- a/.github/skills/tsh-implementing-filters/SKILL.md +++ b/.github/skills/tsh-implementing-filters/SKILL.md @@ -79,14 +79,21 @@ Rules: Example schema shape: ```typescript +type SortDirection = "ASC" | "DESC"; + +interface ProductSort { + field: "relevance" | "price" | "createdAt"; + direction: SortDirection; +} + interface ProductFilters { - color: string; // default: "" → filter[color]=blue - tags: string[]; // default: [] → filter[tags]=a&filter[tags]=b - priceMin: number; // default: 0 → filter[price_min]=0 - priceMax: number; // default: 10000 → filter[price_max]=10000 - inStock: boolean; // default: false → filter[in_stock]=true - sort: string; // default: "relevance" → sort[relevance]=ASC - page: number; // default: 1 → page=1 + color: string; // default: "" → filter[color]=blue + tags: string[]; // default: [] → filter[tags]=a&filter[tags]=b + priceMin: number; // default: 0 → filter[price_min]=0 + priceMax: number; // default: 10000 → filter[price_max]=10000 + inStock: boolean; // default: false → filter[in_stock]=true + sort: ProductSort; // default: { field: "relevance", direction: "ASC" } → sort[relevance]=ASC + page: number; // default: 1 → page=1 } ``` From 974a0cbb7b2a7a1c6fc72f5650387be35534a24d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Gie=C5=82da?= Date: Fri, 24 Apr 2026 16:15:52 +0200 Subject: [PATCH 5/5] feat(skills): add filters skill - apply CR changes, part 4 --- website/docs/skills/optimizing-frontend.md | 2 +- website/docs/skills/overview.md | 30 ++++++++++--------- website/docs/skills/prompt-engineering.md | 34 +++++++++++----------- website/docs/skills/reviewing-frontend.md | 2 +- website/docs/skills/writing-hooks.md | 2 +- website/package-lock.json | 30 +------------------ 6 files changed, 37 insertions(+), 63 deletions(-) diff --git a/website/docs/skills/optimizing-frontend.md b/website/docs/skills/optimizing-frontend.md index 7e8513ef..468b5bcb 100644 --- a/website/docs/skills/optimizing-frontend.md +++ b/website/docs/skills/optimizing-frontend.md @@ -1,5 +1,5 @@ --- -sidebar_position: 29 +sidebar_position: 30 title: Frontend Optimization --- diff --git a/website/docs/skills/overview.md b/website/docs/skills/overview.md index d32f17b6..34317877 100644 --- a/website/docs/skills/overview.md +++ b/website/docs/skills/overview.md @@ -5,7 +5,7 @@ title: Skills Overview # Skills Overview -Copilot Collections includes **31 reusable skills** — knowledge modules that provide specialized domain expertise, structured processes, and quality templates. They encode tested best practices for every phase of the product lifecycle. Skills are stored in `.github/skills/` and loaded automatically by agents when their domain applies to the current task. +Copilot Collections includes **32 reusable skills** — knowledge modules that provide specialized domain expertise, structured processes, and quality templates. They encode tested best practices for every phase of the product lifecycle. Skills are stored in `.github/skills/` and loaded automatically by agents when their domain applies to the current task. ## How Skills Work @@ -31,19 +31,20 @@ When an agent starts a task, it checks all available skills and decides which on ### 🛠 Development Skills -| Skill | Description | Used By | -| ------------------------------------------------------------------ | ------------------------------------------------------------- | -------------------------- | -| [tsh-architecture-designing](./architecture-design) | Solution architecture design and implementation plan creation | Architect | -| [tsh-technical-context-discovering](./technical-context-discovery) | Project conventions and pattern discovery | Architect, CR, SE, E2E, CE | -| [tsh-implementing-frontend](./frontend-implementation) | UI component patterns, composition, design tokens | Software Engineer | -| [tsh-implementing-forms](./implementing-forms) | Form architecture, schema validation, multi-step flows | Software Engineer | -| [tsh-writing-hooks](./writing-hooks) | Custom hook/composable patterns, lifecycle, testing | Software Engineer | -| [tsh-ensuring-accessibility](./ensuring-accessibility) | WCAG 2.1 AA compliance, semantic HTML, ARIA, keyboard nav | Software Engineer | -| [tsh-optimizing-frontend](./optimizing-frontend) | Rendering optimization, code splitting, bundle size | Software Engineer | -| [tsh-implementation-gap-analysing](./implementation-gap-analysis) | Gap analysis between plan and current state | Architect, CR, SE | -| [tsh-sql-and-database-understanding](./sql-and-database) | Database engineering standards and ORM integration | Architect, CR, SE | -| [tsh-codebase-analysing](./codebase-analysis) | Deep codebase analysis and dependency mapping | Architect, BA, CE, SE | -| [tsh-engineering-prompts](./prompt-engineering) | LLM prompt design, optimization, security, and evaluation | PE, SE, Architect, CR | +| Skill | Description | Used By | +| ------------------------------------------------------------------ | ------------------------------------------------------------------ | -------------------------- | +| [tsh-architecture-designing](./architecture-design) | Solution architecture design and implementation plan creation | Architect | +| [tsh-technical-context-discovering](./technical-context-discovery) | Project conventions and pattern discovery | Architect, CR, SE, E2E, CE | +| [tsh-implementing-frontend](./frontend-implementation) | UI component patterns, composition, design tokens | Software Engineer | +| [tsh-implementing-forms](./implementing-forms) | Form architecture, schema validation, multi-step flows | Software Engineer | +| [tsh-implementing-filters](./implementing-filters) | Type-safe URL filter sync, bracket notation, navigation strategies | Software Engineer | +| [tsh-writing-hooks](./writing-hooks) | Custom hook/composable patterns, lifecycle, testing | Software Engineer | +| [tsh-ensuring-accessibility](./ensuring-accessibility) | WCAG 2.1 AA compliance, semantic HTML, ARIA, keyboard nav | Software Engineer | +| [tsh-optimizing-frontend](./optimizing-frontend) | Rendering optimization, code splitting, bundle size | Software Engineer | +| [tsh-implementation-gap-analysing](./implementation-gap-analysis) | Gap analysis between plan and current state | Architect, CR, SE | +| [tsh-sql-and-database-understanding](./sql-and-database) | Database engineering standards and ORM integration | Architect, CR, SE | +| [tsh-codebase-analysing](./codebase-analysis) | Deep codebase analysis and dependency mapping | Architect, BA, CE, SE | +| [tsh-engineering-prompts](./prompt-engineering) | LLM prompt design, optimization, security, and evaluation | PE, SE, Architect, CR | ### ☁️ Cloud & Infrastructure Skills @@ -92,6 +93,7 @@ When an agent starts a task, it checks all available skills and decides which on | tsh-ensuring-accessibility | | | | ✅ | | | | | | | | tsh-implementing-ci-cd | | | ✅ | | | | | | ✅ | | | tsh-implementing-forms | | | | ✅ | | | | | | | +| tsh-implementing-filters | | | | ✅ | | | | | | | | tsh-implementing-frontend | | | | ✅ | | | | | | | | tsh-implementing-kubernetes | | | ✅ | | | | | | ✅ | | | tsh-implementing-observability | | | ✅ | | | | | | ✅ | | diff --git a/website/docs/skills/prompt-engineering.md b/website/docs/skills/prompt-engineering.md index 3d8858e8..5599ea3b 100644 --- a/website/docs/skills/prompt-engineering.md +++ b/website/docs/skills/prompt-engineering.md @@ -1,5 +1,5 @@ --- -sidebar_position: 32 +sidebar_position: 33 title: Prompt Engineering --- @@ -16,14 +16,14 @@ Reliable prompt structure separates concerns into distinct sections: system prom ## Optimization Techniques -| Technique | Purpose | -|---|---| -| **Clarity and specificity** | Replace vague instructions with exact behavior, constraints, and output format | -| **Constraint specification** | Explicit MUST / MUST NOT rules for model behavior | -| **Output format control** | JSON schemas, structured output modes, explicit format instructions | -| **Token efficiency** | Remove filler, use tables over paragraphs, cache static content in system prompts | -| **Negative prompting** | Tell the model what NOT to do for better compliance | -| **Temperature guidance** | Match temperature to task type (0.0–0.2 for extraction, 0.7–1.0 for creative) | +| Technique | Purpose | +| ---------------------------- | --------------------------------------------------------------------------------- | +| **Clarity and specificity** | Replace vague instructions with exact behavior, constraints, and output format | +| **Constraint specification** | Explicit MUST / MUST NOT rules for model behavior | +| **Output format control** | JSON schemas, structured output modes, explicit format instructions | +| **Token efficiency** | Remove filler, use tables over paragraphs, cache static content in system prompts | +| **Negative prompting** | Tell the model what NOT to do for better compliance | +| **Temperature guidance** | Match temperature to task type (0.0–0.2 for extraction, 0.7–1.0 for creative) | ## Security Patterns @@ -54,14 +54,14 @@ Reusable templates for common patterns: ## Anti-Patterns -| Anti-Pattern | Instead Do | -|---|---| -| Vague instructions ("be helpful") | Specify exact behavior and output format | -| User input in system prompt | Separate with delimiters in dedicated section | -| No output format specification | Define explicit schema or structure | -| Prompt-only validation | Parse into typed models, validate schemas | -| Hardcoded secrets in templates | Use environment variables or secret managers | -| Testing only happy paths | Include adversarial, empty, long, and multilingual inputs | +| Anti-Pattern | Instead Do | +| --------------------------------- | --------------------------------------------------------- | +| Vague instructions ("be helpful") | Specify exact behavior and output format | +| User input in system prompt | Separate with delimiters in dedicated section | +| No output format specification | Define explicit schema or structure | +| Prompt-only validation | Parse into typed models, validate schemas | +| Hardcoded secrets in templates | Use environment variables or secret managers | +| Testing only happy paths | Include adversarial, empty, long, and multilingual inputs | ## Connected Skills diff --git a/website/docs/skills/reviewing-frontend.md b/website/docs/skills/reviewing-frontend.md index 933f2029..95f2ba3e 100644 --- a/website/docs/skills/reviewing-frontend.md +++ b/website/docs/skills/reviewing-frontend.md @@ -1,5 +1,5 @@ --- -sidebar_position: 30 +sidebar_position: 31 title: Frontend Review --- diff --git a/website/docs/skills/writing-hooks.md b/website/docs/skills/writing-hooks.md index de96abc4..49e6a879 100644 --- a/website/docs/skills/writing-hooks.md +++ b/website/docs/skills/writing-hooks.md @@ -1,5 +1,5 @@ --- -sidebar_position: 31 +sidebar_position: 32 title: Writing Hooks --- diff --git a/website/package-lock.json b/website/package-lock.json index 29bbca82..6dc385eb 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -161,7 +161,6 @@ "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.49.0.tgz", "integrity": "sha512-uGv2P3lcviuaZy8ZOAyN60cZdhOVyjXwaDC27a1qdp3Pb5Azn+lLSJwkHU4TNRpphHmIei9HZuUxwQroujdPjw==", "license": "MIT", - "peer": true, "dependencies": { "@algolia/client-common": "5.49.0", "@algolia/requester-browser-xhr": "5.49.0", @@ -287,7 +286,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -2090,7 +2088,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -2113,7 +2110,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -2223,7 +2219,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -2645,7 +2640,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -3626,7 +3620,6 @@ "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.9.2.tgz", "integrity": "sha512-C5wZsGuKTY8jEYsqdxhhFOe1ZDjH0uIYJ9T/jebHwkyxqnr4wW0jTkB72OMqNjsoQRcb0JN3PcSeTwFlVgzCZg==", "license": "MIT", - "peer": true, "dependencies": { "@docusaurus/core": "3.9.2", "@docusaurus/logger": "3.9.2", @@ -3895,7 +3888,6 @@ "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.9.2.tgz", "integrity": "sha512-6c4DAbR6n6nPbnZhY2V3tzpnKnGL+6aOsLvFL26VRqhlczli9eWG0VDUNoCQEPnGwDMhPS42UhSAnz5pThm5Ag==", "license": "MIT", - "peer": true, "dependencies": { "@docusaurus/mdx-loader": "3.9.2", "@docusaurus/module-type-aliases": "3.9.2", @@ -4779,7 +4771,6 @@ "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", "license": "MIT", - "peer": true, "dependencies": { "@types/mdx": "^2.0.0" }, @@ -5508,7 +5499,6 @@ "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -5881,7 +5871,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -6223,7 +6212,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6291,7 +6279,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -6337,7 +6324,6 @@ "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.49.0.tgz", "integrity": "sha512-Tse7vx7WOvbU+kpq/L3BrBhSWTPbtMa59zIEhMn+Z2NoxZlpcCRUDCRxQ7kDFs1T3CHxDgvb+mDuILiBBpBaAA==", "license": "MIT", - "peer": true, "dependencies": { "@algolia/abtesting": "1.15.0", "@algolia/client-abtesting": "5.49.0", @@ -6817,7 +6803,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -7789,7 +7774,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -9179,7 +9163,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -13643,7 +13626,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -14188,7 +14170,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -15092,7 +15073,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -15909,7 +15889,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -15922,7 +15901,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -15979,7 +15957,6 @@ "resolved": "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz", "integrity": "sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ==", "license": "MIT", - "peer": true, "dependencies": { "@types/react": "*" }, @@ -16008,7 +15985,6 @@ "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.12.13", "history": "^4.9.0", @@ -17773,8 +17749,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tsyringe": { "version": "4.10.0", @@ -17855,7 +17830,6 @@ "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -18206,7 +18180,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -18405,7 +18378,6 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.2.tgz", "integrity": "sha512-dRXm0a2qcHPUBEzVk8uph0xWSjV/xZxenQQbLwnwP7caQCYpqG1qddwlyEkIDkYn0K8tvmcrZ+bOrzoQ3HxCDw==", "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8",