diff --git a/content/docs/charts/index.mdx b/content/docs/charts/index.mdx
new file mode 100644
index 0000000..eb44174
--- /dev/null
+++ b/content/docs/charts/index.mdx
@@ -0,0 +1,270 @@
+---
+title: Charts
+description: Reactive ECharts bridge with lazy loading, auto-detection, and typed options for Pyreon.
+---
+
+`@pyreon/charts` provides a reactive bridge to [Apache ECharts](https://echarts.apache.org/) for Pyreon applications. Chart modules are lazy-loaded on demand -- zero bundle cost until a chart actually renders. The Canvas renderer is used by default, with SVG available as an option.
+
+
+
+## Installation
+
+```package-install
+@pyreon/charts
+```
+
+## Quick Start
+
+Use the `` component to render a chart. Pass an options function that returns a standard ECharts configuration -- signal reads inside the function are tracked for reactivity.
+
+```tsx
+import { signal } from '@pyreon/reactivity'
+import { Chart } from '@pyreon/charts'
+
+function SalesChart() {
+ const months = signal(['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'])
+ const revenue = signal([120, 200, 150, 80, 70, 110])
+
+ return (
+ ({
+ xAxis: { type: 'category', data: months() },
+ yAxis: { type: 'value' },
+ tooltip: { trigger: 'axis' },
+ series: [{ name: 'Revenue', type: 'bar', data: revenue() }],
+ })}
+ style="height: 400px"
+ />
+ )
+}
+```
+
+The `options` prop accepts a function (not a plain object) so that signal reads are tracked. When any signal inside the function changes, the chart re-renders automatically.
+
+## API Reference
+
+### ``
+
+The primary component for rendering charts.
+
+| Prop | Type | Description |
+|------|------|-------------|
+| `options` | `() => EChartsOption` | Function returning ECharts configuration. Signal reads are tracked for reactivity. |
+| `style` | `string` | Inline style string. Must include a height (ECharts requires a sized container). |
+| `class` | `string` | CSS class name for the container element. |
+| `renderer` | `'canvas' \| 'svg'` | Rendering mode. Defaults to `'canvas'`. |
+| `onChartReady` | `(instance: ECharts) => void` | Callback fired after the chart instance is initialized. |
+| `on*` | Event handlers | ECharts event bindings, e.g. `onClick`, `onMouseover`, `onLegendSelectChanged`. |
+
+```tsx
+ ({ /* ... */ })}
+ style="height: 300px"
+ renderer="svg"
+ onClick={(params) => console.log('Clicked:', params.name)}
+ onChartReady={(instance) => console.log('Chart ready:', instance)}
+/>
+```
+
+### `useChart(optionsFn, config?)`
+
+A lower-level hook for programmatic control. Returns reactive signals for the chart instance and error state.
+
+```tsx
+import { useChart } from '@pyreon/charts'
+
+function MyChart() {
+ const { containerRef, instance, error } = useChart(() => ({
+ xAxis: { type: 'category', data: ['A', 'B', 'C'] },
+ yAxis: { type: 'value' },
+ series: [{ type: 'bar', data: [10, 20, 30] }],
+ }))
+
+ return (
+
+ {() => error() ?
{error()!.message}
: null}
+
containerRef.set(el)} style="height: 400px" />
+
+ )
+}
+```
+
+**Config options:**
+
+| Option | Type | Default | Description |
+|--------|------|---------|-------------|
+| `renderer` | `'canvas' \| 'svg'` | `'canvas'` | Rendering mode |
+| `notMerge` | `boolean` | `false` | Replace options entirely instead of merging |
+| `lazyUpdate` | `boolean` | `false` | Defer chart update to next frame |
+
+**Return value:**
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `containerRef` | `Signal` | Bind to a DOM element via `ref` |
+| `instance` | `Signal` | The underlying ECharts instance (available after init) |
+| `error` | `Signal` | Error signal for init or setOption failures |
+
+### Types
+
+`@pyreon/charts` re-exports all ECharts option types for strict typing:
+
+```tsx
+import type {
+ ComposeOption,
+ BarSeriesOption,
+ LineSeriesOption,
+ PieSeriesOption,
+ ScatterSeriesOption,
+ RadarSeriesOption,
+ HeatmapSeriesOption,
+ TreemapSeriesOption,
+ SankeySeriesOption,
+ GaugeSeriesOption,
+ FunnelSeriesOption,
+ CandlestickSeriesOption,
+ GraphSeriesOption,
+} from '@pyreon/charts'
+```
+
+## Auto-Detection
+
+When you provide an options function, `@pyreon/charts` inspects the configuration to determine which ECharts modules are needed:
+
+- **Series types** (`bar`, `line`, `pie`, etc.) are detected from `series[].type`
+- **Components** (`tooltip`, `legend`, `dataZoom`, etc.) are detected from top-level keys
+- **Axis types** (`category`, `value`, `time`, `log`) are detected from axis config
+
+Only the required modules are dynamically imported. A chart with `type: 'bar'` and `tooltip` will only load the bar series renderer and tooltip component -- not the full ECharts bundle.
+
+```tsx
+// Only loads: BarChart, TooltipComponent, GridComponent, CanvasRenderer
+ ({
+ tooltip: { trigger: 'axis' },
+ xAxis: { type: 'category', data: labels() },
+ yAxis: { type: 'value' },
+ series: [{ type: 'bar', data: values() }],
+ })}
+ style="height: 300px"
+/>
+```
+
+## Strict Typing with ComposeOption
+
+For type-safe chart configurations, use `ComposeOption<>` to narrow the options type to only the series types you use:
+
+```tsx
+import { useChart } from '@pyreon/charts'
+import type { ComposeOption, BarSeriesOption, LineSeriesOption } from '@pyreon/charts'
+
+type DashboardOption = ComposeOption
+
+function Dashboard() {
+ const chart = useChart(() => ({
+ xAxis: { type: 'category', data: ['Q1', 'Q2', 'Q3', 'Q4'] },
+ yAxis: { type: 'value' },
+ series: [
+ { type: 'bar', data: [100, 200, 150, 300] },
+ { type: 'line', data: [80, 170, 130, 280] },
+ ],
+ }))
+
+ return
chart.containerRef.set(el)} style="height: 400px" />
+}
+```
+
+This gives you autocomplete and type checking for the specific series options you declared.
+
+## Manual Registration
+
+For maximum tree-shaking control, use the `@pyreon/charts/manual` entry point. This disables auto-detection and requires you to register ECharts modules explicitly:
+
+```tsx
+import { useChart, registerModules } from '@pyreon/charts/manual'
+import { BarChart, LineChart } from 'echarts/charts'
+import { TooltipComponent, GridComponent, LegendComponent } from 'echarts/components'
+import { CanvasRenderer } from 'echarts/renderers'
+
+// Register once at app startup
+registerModules([
+ BarChart,
+ LineChart,
+ TooltipComponent,
+ GridComponent,
+ LegendComponent,
+ CanvasRenderer,
+])
+
+// Then use useChart / as normal
+function MyChart() {
+ const chart = useChart(() => ({
+ series: [{ type: 'bar', data: [1, 2, 3] }],
+ }))
+
+ return
chart.containerRef.set(el)} style="height: 300px" />
+}
+```
+
+Use manual registration when you need deterministic bundle sizes or are building a library that should not auto-import ECharts modules.
+
+## Bundle Size
+
+| Import | Approximate Size (gzipped) |
+|--------|---------------------------|
+| `@pyreon/charts` (wrapper only) | ~2 KB |
+| + Bar chart | ~15 KB |
+| + Line chart | ~18 KB |
+| + Pie chart | ~12 KB |
+| + Tooltip + Legend | ~8 KB |
+| Full ECharts (all modules) | ~300 KB |
+
+Auto-detection ensures you only pay for what you use. A typical dashboard with 2-3 chart types loads ~40-50 KB of ECharts code.
+
+## Error Handling
+
+Both `` and `useChart()` expose an `error` signal that captures initialization and rendering failures:
+
+```tsx
+import { Chart } from '@pyreon/charts'
+
+function SafeChart() {
+ return (
+ ({
+ series: [{ type: 'bar', data: chartData() }],
+ })}
+ style="height: 300px"
+ onError={(err) => console.error('Chart error:', err)}
+ />
+ )
+}
+```
+
+With `useChart()`, check the error signal directly:
+
+```tsx
+import { useChart } from '@pyreon/charts'
+
+function SafeChart() {
+ const { containerRef, error } = useChart(() => ({
+ series: [{ type: 'bar', data: chartData() }],
+ }))
+
+ return (
+
+ {() => error() ? (
+
+
Failed to render chart: {error()!.message}
+
+ ) : null}
+
containerRef.set(el)} style="height: 300px" />
+
+ )
+}
+```
+
+Common error scenarios:
+- Container element has zero height (ECharts requires a sized container)
+- Invalid option structure passed to `setOption`
+- Network failure when lazy-loading ECharts modules
diff --git a/content/docs/feature/index.mdx b/content/docs/feature/index.mdx
new file mode 100644
index 0000000..bf581d1
--- /dev/null
+++ b/content/docs/feature/index.mdx
@@ -0,0 +1,670 @@
+---
+title: Feature
+description: Schema-driven CRUD primitives with auto-generated hooks for lists, forms, tables, and stores.
+---
+
+`@pyreon/feature` eliminates CRUD boilerplate by deriving an entire feature's data layer from a single schema definition. Define your entity once with `defineFeature`, and get reactive hooks for listing, searching, creating, updating, and deleting records -- all backed by `@pyreon/query` for server state and `@pyreon/store` for client-side cache.
+
+
+
+## Installation
+
+```package-install
+@pyreon/feature
+```
+
+## Quick Start
+
+Define a feature with a schema and API configuration, then use the auto-generated hooks in your components:
+
+```ts
+import { defineFeature } from '@pyreon/feature'
+import { z } from 'zod'
+
+const taskFeature = defineFeature({
+ name: 'task',
+ schema: z.object({
+ id: z.string(),
+ title: z.string().min(1),
+ status: z.enum(['todo', 'in-progress', 'done']),
+ assignee: z.string().optional(),
+ dueDate: z.string().optional(),
+ }),
+ api: {
+ baseUrl: '/api/tasks',
+ },
+})
+```
+
+Use the generated hooks in a component:
+
+```tsx
+import { defineComponent } from '@pyreon/core'
+
+const TaskList = defineComponent(() => {
+ const { items, isLoading } = taskFeature.useList()
+
+ return () => (
+
+ {isLoading() ? (
+
Loading tasks...
+ ) : (
+
+ {items().map(task => (
+
+ {task.title} — {task.status}
+
+ ))}
+
+ )}
+
+ )
+})
+```
+
+Add a creation form with a single hook:
+
+```tsx
+const CreateTask = defineComponent(() => {
+ const { form, handleSubmit, isSubmitting } = taskFeature.useForm({ mode: 'create' })
+
+ return () => (
+
+ )
+})
+```
+
+## `defineFeature` Configuration
+
+The `defineFeature` function accepts a configuration object that drives all auto-generated hooks:
+
+```ts
+const feature = defineFeature({
+ // Required: unique name used as query key prefix and store ID
+ name: 'task',
+
+ // Required: Zod schema describing the entity shape (including `id`)
+ schema: z.object({
+ id: z.string(),
+ title: z.string().min(1),
+ status: z.enum(['todo', 'in-progress', 'done']),
+ assignee: z.string().optional(),
+ }),
+
+ // Required: API configuration
+ api: {
+ baseUrl: '/api/tasks',
+
+ // Optional: custom fetch headers
+ headers: () => ({
+ Authorization: `Bearer ${getToken()}`,
+ }),
+
+ // Optional: override individual endpoints
+ endpoints: {
+ list: (params) => ({ url: '/api/tasks', method: 'GET', params }),
+ byId: (id) => ({ url: `/api/tasks/${id}`, method: 'GET' }),
+ create: (data) => ({ url: '/api/tasks', method: 'POST', body: data }),
+ update: (id, data) => ({ url: `/api/tasks/${id}`, method: 'PATCH', body: data }),
+ delete: (id) => ({ url: `/api/tasks/${id}`, method: 'DELETE' }),
+ search: (query) => ({ url: '/api/tasks/search', method: 'GET', params: { q: query } }),
+ },
+ },
+
+ // Optional: default page size for pagination
+ pageSize: 20,
+})
+```
+
+### Return Value
+
+`defineFeature` returns an object containing all generated hooks and utilities:
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `useList` | `(opts?) => ListResult` | Fetch and display a paginated list of entities |
+| `useById` | `(id) => ByIdResult` | Fetch a single entity by ID |
+| `useSearch` | `(opts?) => SearchResult` | Search entities with a reactive query signal |
+| `useCreate` | `() => CreateResult` | Mutation hook for creating entities |
+| `useUpdate` | `(opts?) => UpdateResult` | Mutation hook with optimistic updates |
+| `useDelete` | `() => DeleteResult` | Mutation hook for deleting entities |
+| `useForm` | `(opts) => FormResult` | Form hook with create/edit modes |
+| `useTable` | `(opts?) => TableResult` | Table hook with schema-inferred columns |
+| `useStore` | `() => StoreResult` | Reactive client-side cache |
+| `schema` | `ZodSchema` | The original schema passed to `defineFeature` |
+| `name` | `string` | The feature name |
+
+## Hooks
+
+### `useList`
+
+Fetches a paginated list of entities. Returns reactive signals for the items, loading state, and pagination controls:
+
+```tsx
+const TaskList = defineComponent(() => {
+ const { items, isLoading, error, page, pageSize, totalPages, nextPage, prevPage } =
+ taskFeature.useList({ pageSize: 10 })
+
+ return () => (
+
+ {isLoading() &&
Loading...
}
+ {error() &&
Error: {error().message}
}
+
+
+ {items().map(task => (
+
{task.title}
+ ))}
+
+
+
+
+ Page {page()} of {totalPages()}
+
+
+
+ )
+})
+```
+
+### `useById`
+
+Fetches a single entity by ID. The ID can be a reactive signal:
+
+```tsx
+const TaskDetail = defineComponent((props: { id: string }) => {
+ const { data, isLoading, error } = taskFeature.useById(props.id)
+
+ return () => (
+
+ )
+})
+```
+
+## `useStore`
+
+`useStore` provides direct access to the feature's reactive client-side cache. It exposes signals for the entity list, selected item, and loading states:
+
+```tsx
+const TaskDashboard = defineComponent(() => {
+ const { items, selected, loading, select, clear } = taskFeature.useStore()
+
+ return () => (
+
+
{items().length} tasks loaded
+
Loading: {loading() ? 'Yes' : 'No'}
+
+ {selected() && (
+
+
Selected: {selected().title}
+
+
+ )}
+
+ )
+})
+```
+
+| Signal / Method | Type | Description |
+|-----------------|------|-------------|
+| `items` | `Signal` | All cached entities |
+| `selected` | `Signal` | Currently selected entity |
+| `loading` | `Signal` | Whether any query is in flight |
+| `select(item)` | `(item: T) => void` | Set the selected entity |
+| `clear()` | `() => void` | Clear the selection |
+
+## Integration with Other Packages
+
+`@pyreon/feature` is a composition layer that builds on top of several Pyreon fundamentals packages. You can use them directly when you need more control.
+
+### With `@pyreon/query`
+
+All data fetching hooks (`useList`, `useById`, `useSearch`, `useCreate`, `useUpdate`, `useDelete`) are thin wrappers around `@pyreon/query`. You can access the underlying query options:
+
+```ts
+import { useQuery } from '@pyreon/query'
+
+// Use the feature's query key factory for custom queries
+const customQuery = useQuery({
+ queryKey: [taskFeature.name, 'custom', { status: 'overdue' }],
+ queryFn: () => fetch('/api/tasks/overdue').then(r => r.json()),
+})
+```
+
+### With `@pyreon/form`
+
+`useForm` wraps `@pyreon/form`'s `useForm` with schema-derived validation and automatic initial values. You can pass any `@pyreon/form` option through:
+
+```ts
+const { form } = taskFeature.useForm({
+ mode: 'create',
+ validateOn: 'blur',
+ debounceMs: 200,
+})
+```
+
+### With `@pyreon/table`
+
+`useTable` wraps `@pyreon/table`'s `useTable` with schema-inferred column definitions. Pass additional TanStack Table options through:
+
+```ts
+const { table } = taskFeature.useTable({
+ enableSorting: true,
+ enableFiltering: true,
+ manualPagination: true,
+})
+```
+
+### With `@pyreon/validation`
+
+Schema validation uses `@pyreon/validation` adapters internally. The schema passed to `defineFeature` is automatically wrapped with the appropriate adapter (Zod, Valibot, or ArkType):
+
+```ts
+import { z } from 'zod'
+
+// Zod schemas work out of the box
+const feature = defineFeature({
+ name: 'task',
+ schema: z.object({ /* ... */ }),
+ api: { baseUrl: '/api/tasks' },
+})
+```
+
+### With `@pyreon/store`
+
+The feature's `useStore` is built on `@pyreon/store`'s `defineStore`. You can compose it with other stores:
+
+```ts
+import { defineStore, signal, computed } from '@pyreon/store'
+
+const useDashboard = defineStore('dashboard', () => {
+ const tasks = taskFeature.useStore()
+ const users = userFeature.useStore()
+
+ const assignedTaskCount = computed(() =>
+ tasks.items().filter(t => t.assigneeId != null).length
+ )
+
+ return { tasks, users, assignedTaskCount }
+})
+```
+
+## Why
+
+A typical CRUD feature in a modern frontend requires list queries, detail queries, search, create/update/delete mutations, forms with validation, table columns, pagination, optimistic updates, and cache management. Written by hand, each feature requires approximately 200 lines of repetitive wiring code.
+
+`defineFeature` replaces that with roughly 10 lines. You declare the schema and the API base URL. The package generates every hook, infers form fields and table columns from the schema, handles optimistic updates, manages pagination signals, and keeps the client-side cache in sync.
+
+This is especially valuable in AI-assisted development workflows. Instead of generating 200 lines of boilerplate that must be reviewed line by line, an LLM produces a single `defineFeature` call. The generated code is declarative, auditable at a glance, and guaranteed to follow consistent patterns across every feature in your application.