references/components.md
# Components Reference
All components are auto-imported when `@websideproject/nuxt-protokit` is registered in `nuxt.config.ts`.
---
## `<ProtoTool>` — Main Wrapper
The top-level container for a prototype. Handles `isReady` lifecycle (shows skeleton while IndexedDB loads) and renders the full tool UI from schema + prototype data.
```vue
<template>
<ProtoTool :schema="schema" :prototype="proto" />
</template>
<script setup>
const proto = usePrototype(schema)
</script>
```
**Props:**
- `:schema` — `PrototypeSchema`
- `:prototype` — return value of `usePrototype(schema)`
When `schema.layout` is defined, renders `<ProtoDashboard>`. Otherwise auto-renders form + results.
---
## `<ProtoDashboard>` — Dashboard with Layout/Tabs
Renders a full dashboard driven by `schema.layout`. Handles tabs, rows, and all `LayoutItem` types.
```vue
<ProtoDashboard :schema="schema" :prototype="proto" />
```
Usually used indirectly via `<ProtoTool>`. Use directly if you need to embed the dashboard inside a custom wrapper.
---
## `<ProtoForm>` — Field Renderer
Renders all fields from `schema.fields` (or a specific `SectionDef`) as form inputs.
```vue
<ProtoForm :schema="schema" :state="proto.state" />
<!-- Render specific section only -->
<ProtoForm :schema="schema" :state="proto.state" :sectionIndex="0" />
```
---
## `<ProtoSection>` — Field Group Container
Renders a labelled group of fields. Used inside custom layouts.
```vue
<ProtoSection title="Revenue" icon="i-heroicons-currency-dollar">
<ProtoForm :schema="schema" :state="proto.state" :sectionIndex="0" />
</ProtoSection>
```
---
## `<ProtoCard>` / `<ProtoDetailCard>` — Stat Cards
```vue
<ProtoCard :def="schema.cards[0]" :ctx="proto.computeContext.value" />
```
---
## `<ProtoCrudModal>` — Collection Editor (Modal)
Full CRUD modal for a collection. Triggered by an "Add" button + edit/delete actions per row.
```vue
<ProtoCrudModal
:schema="schema"
:collection-key="'competitors'"
:collection="proto.collections.competitors"
/>
```
**Props:**
- `:schema` — parent `PrototypeSchema`
- `:collection-key` — key in `schema.collections`
- `:collection` — `UseProtoListReturn` (from `proto.collections[key]`)
Renders in `'modal'` editMode (default). Switch to `'inline'` in `CollectionSchema.editMode` for inline editing.
---
## `<ProtoCrudTable>` — Table View
Renders collection items as a sortable table. Uses `schema.collections[key].tableColumns`.
```vue
<ProtoCrudTable
:schema="schema"
:collection-key="'items'"
:collection="proto.collections.items"
/>
```
---
## `<ProtoCrudList>` — List View
Renders collection items as a searchable list. Uses `schema.collections[key].listDisplay`.
```vue
<ProtoCrudList
:schema="schema"
:collection-key="'items'"
:collection="proto.collections.items"
/>
```
---
## `<ProtoCalendar>` / `<ProtoCalendarView>` — Calendar
Day / week / infinite month calendar (from the Nuxt UI calendar template): drag to create, move and
resize, inline event form, quick natural-language events. Needs `@nuxt/ui` >= 4.5.
```vue
<!-- Y.js-backed, persists itself -->
<ProtoCalendar doc-key="my-calendar" class="h-screen" />
<!-- Bring your own data: events in, changes out -->
<ProtoCalendarView
v-model:view="view"
:events="events"
:calendars="[{ id: 'work', name: 'Work', color: 'primary' }]"
sidebar
@create="onCreate"
@update="onUpdate"
@remove="onRemove"
@event-click="onClick"
/>
```
- `CalendarViewEvent` (`#protokit/calendar`): `{ id, title, start, end, allDay?, calendarId?, color?, description?, location?, editable? }` — floating local `YYYY-MM-DDTHH:mm:ss`, `end` EXCLUSIVE
- Flags: `editable` (move/resize), `creatable` (draw new), `popover` (inline form), `droppable` (`@external-drop`), `sidebar`, `toolbar`, `shortcuts`, `weekStartsOn`, `views`, `loading`
- A plain `view="month"` pins the view; use `v-model:view` when offering several
- `ProtoCalendar` passes every view prop/listener through; converts stored ⇄ view with `fromStoredEvent` / `toStoredPatch`
## `<VizCollectionCalendar>` — Collection on a Calendar
Renders collection items as calendar events; dragging one writes the new date/time back via `onUpdate`.
```vue
<VizCollectionCalendar
:items="proto.collections.events.items.value"
:config="{
dateField: 'date',
titleField: 'title',
timeField: 'startTime',
endTimeField: 'endTime',
statusColorMap: { scheduled: 'blue', done: 'green' },
defaultColor: 'neutral',
}"
:on-update="(idx, val) => proto.collections.events.update(idx, val)"
/>
```
**`CollectionCalendarConfig` props:**
- `dateField` — required, primary date field key
- `titleField` — required, field key for event title
- `idField?` — defaults to `_id`
- `endDateField?` — end date for multi-day events
- `timeField?` — HH:mm time field
- `endTimeField?` — HH:mm end time
- `allDayField?` — boolean field; when absent, events default to all-day
- `statusColorMap?` — `Record<statusValue, CalendarColor>`
- `defaultColor?` — fallback color (default: `'neutral'`)
Or use via `DashboardLayout`:
```ts
layout: {
rows: [{ cols: 1, items: [{
type: 'collection', collectionKey: 'events', view: 'calendar',
calendarConfig: { dateField: 'date', titleField: 'title' }
}]}]
}
```
---
## `<ProtoViz>` — Visualization Dispatcher
Renders any visualization type by dispatching to the correct viz component.
```vue
<ProtoViz :def="schema.visualizations[0]" :ctx="proto.computeContext.value" />
```
Usually rendered automatically by `<ProtoTool>` / `<ProtoDashboard>`. Use directly when building custom layouts.
---
## `<ProtoActionBar>` — Action Buttons
Renders the `schema.actions` array as buttons (copy-text, export-markdown, reset).
```vue
<ProtoActionBar :schema="schema" :ctx="proto.computeContext.value" />
```
---
## `<ProtoDebugPanel>` — Development Tool
Shows raw Y.js state for debugging. Dev-only, renders nothing in production.
```vue
<ProtoDebugPanel :prototype="proto" />
```
---
## `<ProtoCorruptionModal>` — Corruption Recovery
Monitors for IndexedDB corruption and presents a recovery UI when detected. Add once per page or in the root layout.
```vue
<ProtoCorruptionModal />
```
No props needed — it uses `useProtoCorruption()` internally.
---
## Field Input Components (internal)
These render automatically inside `<ProtoForm>` based on field type. They can also be used standalone:
| Component | Field type |
|-----------|------------|
| `<ProtoFieldText>` | `text` |
| `<ProtoFieldTextarea>` | `textarea` |
| `<ProtoFieldNumber>` | `number` |
| `<ProtoFieldSelect>` | `select` |
| `<ProtoFieldSegmented>` | `segmented` |
| `<ProtoFieldToggle>` | `toggle` |
| `<ProtoFieldRange>` | `range` |
| `<ProtoFieldRating>` | `rating` |
| `<ProtoFieldColor>` | `color` |
| `<ProtoFieldDate>` | `date` |
| `<ProtoFieldTags>` | `tags` |
| `<ProtoFieldLinkedResponses>` | `linked-responses` |
references/composables.md
# Composables Reference
## `usePrototype` — High-Level Orchestrator
The primary composable. Combines doc, fields (Y.Map), collections (Y.Arrays), derived, permissions.
```ts
const proto = usePrototype(schema, options?)
```
### Options
```ts
{
docKey?: string // Override doc key (default: schema.key)
existingDoc?: Y.Doc // Share an existing Y.js doc
disableSync?: boolean // Force local-only, ignore global serverSync config
namespace?: string // Prefix for IndexedDB isolation (see below)
encryption?: {
password: string // AES-GCM encryption for IndexedDB updates
}
}
```
### Return Value
```ts
{
state: Record<string, Ref> // one Ref per schema.fields key
collections: Record<string, UseProtoListReturn> // one per schema.collections key
derived: ComputedRef<Record<string, any>> // computed derived values
computeContext: ComputedRef<ComputeContext> // { fields, derived, connections, collections }
reset: () => void // reset fields + all collections to defaults
isReady: Ref<boolean> // false until IndexedDB loaded
doc: Y.Doc // underlying Y.js document
fieldPermissions: Record<string, FieldPermissionsResolved>
collectionPermissions: Record<string, CollectionPermissionsResolved>
}
```
### Usage Patterns
```vue
<script setup>
// Basic
const proto = usePrototype(schema)
// Namespace isolation (multi-tenant)
const { user } = useAuth()
const proto = usePrototype(schema, { namespace: user.value.tenantId })
// Per-user isolation
const proto = usePrototype(schema, {
namespace: computed(() => `${tenantId.value}:${userId.value}`)
})
// Encryption
const proto = usePrototype(schema, { encryption: { password: userPassphrase } })
// Public demo tool (never syncs to server)
const proto = usePrototype(schema, { disableSync: true })
// Read state
const revenue = proto.state.revenue // Ref<number>
const items = proto.collections.competitors.items // Ref<any[]>
const burn = proto.derived.value.monthlyBurn
// Mutate state directly
proto.state.revenue.value = 50000
// Collections CRUD
proto.collections.team.add({ name: 'Alice', role: 'Engineer' })
proto.collections.team.update(0, { name: 'Alice', role: 'Lead' })
proto.collections.team.remove(0)
</script>
```
---
## `useProtoCollection` — Standalone Collection
For collections that don't need a parent prototype. Has its own doc.
```ts
const coll = useProtoCollection(collectionSchema, options?)
```
### Options
```ts
{
docKey?: string // Override doc key (default: 'collection-<schema.key>')
existingDoc?: Y.Doc // Share an existing Y.js doc
disableSync?: boolean
}
```
### Return Value
```ts
{
doc: Y.Doc
items: Ref<any[]>
add: (item: any) => void
update: (index: number, item: any) => void
remove: (index: number) => void
move: (from: number, to: number) => void
count: ComputedRef<number>
search: Ref<string> // bind to search input
filtered: ComputedRef<any[]> // items filtered by search
sortKey: Ref<string>
sortOrder: Ref<'asc' | 'desc'>
sorted: ComputedRef<any[]> // filtered + sorted
reset: () => void
isReady: Ref<boolean>
permissions: CollectionPermissionsResolved
destroy: () => void // clean up IndexedDB listener
}
```
---
## `useProtoDoc` — Low-Level Y.js Document
For advanced use. Usually prefer `usePrototype` or `useProtoCollection`.
```ts
const { doc, isReady, destroy } = useProtoDoc(key, options?)
```
### Options
```ts
{
disableSync?: boolean
encryption?: { password: string }
}
```
The doc is persisted to IndexedDB under `key` and synced via BroadcastChannel between tabs. If `serverSync` is enabled in module config, it also syncs to `/api/yjs/<key>` (or custom baseUrl).
---
## `useProtoCalendar` — Calendar Event Store
```ts
const { events, addEvent, updateEvent, removeEvent, moveEvent, isReady, doc } = useProtoCalendar({
docKey: 'my-calendar', // or existingDoc: doc to share a Y.Doc with other tools
})
// events: Ref<CalendarEvent[]> in the STORED shape — startAt/endAt, all-day endAt = last day (inclusive)
```
`<ProtoCalendar>` wraps this for you. To drive `<ProtoCalendarView>` from it yourself, convert with
`fromStoredEvent` / `toStoredPatch` from `#protokit/calendar` (the view's events are [start, end)).
---
## `useProtoPermissions`
Resolves field/collection permission guards reactively.
```ts
const { resolveFieldPermissions, resolveCollectionPermissions } = useProtoPermissions()
```
Usually accessed via `proto.fieldPermissions` and `proto.collectionPermissions` — not called directly.
---
## `useProtoCorruption` — Corruption Detection
```ts
const { isCorrupted, clearCorruption } = useProtoCorruption()
```
Used internally by `<ProtoCorruptionModal>`. In most cases use the component directly rather than this composable.
---
## `useProtoKitConfig` — Runtime Config Access
```ts
const { serverSync } = useProtoKitConfig()
// { enabled: boolean, baseUrl: string }
```
---
## Namespace Isolation
The `namespace` option creates fully isolated IndexedDB stores and BroadcastChannels. The resulting doc key is `<namespace>:<docKey>`.
```ts
// Org-shared: all users in the org see the same data
usePrototype(schema, { namespace: orgId })
// Private per user in org
usePrototype(schema, { namespace: `${orgId}:${userId}` })
// Reactive namespace (re-mounts when user switches org)
const ns = computed(() => user.value?.orgId)
usePrototype(schema, { namespace: ns })
```
When `namespace` changes, the composable unmounts the old doc and mounts a fresh one — no data bleeds between namespaces.
---
## Permission Types
```ts
interface FieldPermissionsResolved {
visible: ComputedRef<boolean>
editable: ComputedRef<boolean>
}
interface CollectionPermissionsResolved {
canAdd: ComputedRef<boolean>
canEdit: ComputedRef<boolean>
canDelete: ComputedRef<boolean>
canReorder: ComputedRef<boolean>
}
```
All flags default to `true` when no permissions are defined on the schema.
references/extensions.md
# Extensions Reference
Register custom field types and visualization types to extend nuxt-protokit beyond its built-ins.
## `defineProtokitExtension`
```ts
import { defineProtokitExtension } from '@websideproject/nuxt-protokit'
const myExtension = defineProtokitExtension({
namespace: 'myapp', // required, must be unique — not 'proto', 'protokit', 'nuxt', or 'vue'
fields?: {
'field-name': {
component: MyFieldComponent, // Vue component or async import
},
},
vizTypes?: {
'viz-name': {
component: MyVizComponent,
},
},
})
```
The `namespace` + `name` form the full type string: `'myapp:field-name'` or `'myapp:viz-name'`.
---
## Custom Field Type
### 1. Define the component
```vue
<!-- components/MyRatingStars.vue -->
<script setup>
const props = defineProps<{
modelValue: number
fieldDef: any // the FieldDef from schema.fields
disabled?: boolean
}>()
const emit = defineEmits<{ 'update:modelValue': [value: number] }>()
</script>
<template>
<div class="flex gap-1">
<button v-for="n in 10" :key="n" @click="emit('update:modelValue', n)"
:class="n <= props.modelValue ? 'text-yellow-400' : 'text-gray-200'">
★
</button>
</div>
</template>
```
### 2. Register the extension
```ts
// plugins/protokit-extensions.ts (Nuxt plugin)
import { defineProtokitExtension } from '@websideproject/nuxt-protokit'
import MyRatingStars from '~/components/MyRatingStars.vue'
export const extension = defineProtokitExtension({
namespace: 'myapp',
fields: {
'rating-stars': {
component: MyRatingStars,
// Async import also supported:
// component: () => import('~/components/MyRatingStars.vue'),
},
},
})
```
### 3. Register with the module
```ts
// nuxt.config.ts — via hook
export default defineNuxtConfig({
hooks: {
'protokit:register-extension': ({ addComponentsDir, addImportsDir }) => {
// For extensions defined in a separate Nuxt module
}
}
})
```
Or provide the extension at the app level via `useProtoExtensionRegistry()`:
```ts
// composables/useExtensions.ts
import { useProtoExtensionRegistry } from '@websideproject/nuxt-protokit'
const { register } = useProtoExtensionRegistry()
register(extension)
```
### 4. Use in schema
```ts
fields: {
satisfaction: {
type: 'myapp:rating-stars', // 'namespace:field-name'
label: 'Customer Satisfaction',
default: 0,
props: { maxStars: 10 }, // passed as component props
},
}
```
---
## Custom Viz Type
### 1. Define the component
```vue
<!-- components/MyHeatmap.vue -->
<script setup>
const props = defineProps<{
def: any // the VizDef from schema.visualizations
ctx: any // ComputeContext
}>()
// Access config via props.def.config
// Access data via props.ctx.fields, props.ctx.collections, etc.
</script>
```
### 2. Register
```ts
defineProtokitExtension({
namespace: 'myapp',
vizTypes: {
heatmap: { component: MyHeatmap },
},
})
```
### 3. Use in schema
```ts
visualizations: [
{
type: 'myapp:heatmap', // 'namespace:viz-name'
title: 'Activity Heatmap',
showWhen: ctx => ctx.collections.events.length > 0,
config: { colorScale: 'green', bucketSize: 'week' }, // any shape
},
]
```
---
## Reserved Namespaces
These namespaces are reserved and will throw an error in development if used:
- `proto`
- `protokit`
- `nuxt`
- `vue`
Use a unique namespace based on your package or app name.
---
## Async Components
Extensions support async (lazy-loaded) components:
```ts
fields: {
'rich-editor': {
component: () => import('~/components/RichEditor.vue'),
},
}
```
nuxt-protokit wraps these with `defineAsyncComponent` automatically.
references/headless.md
# Headless Mode
Use nuxt-protokit's Y.js persistence layer — IndexedDB, tab sync, server sync, migrations — **without any UI components**.
---
## `defineHeadlessSchema`
A minimal schema definition that only requires storage-relevant fields. UI-only properties (`title`, `shortTitle`, `description`, `icon`, `results`, `visualizations`, `layout`, etc.) are optional or omitted.
```ts
import { defineHeadlessSchema } from '@websideproject/nuxt-protokit'
const settingsSchema = defineHeadlessSchema({
key: 'user-settings', // required — Y.js doc key and IndexedDB store
fields: { // required
theme: { type: 'select', label: 'Theme', default: 'system',
options: ['system', 'light', 'dark'] },
language: { type: 'select', label: 'Language', default: 'en',
options: ['en', 'de', 'fr'] },
sidebar: { type: 'toggle', label: 'Sidebar', default: true },
},
collections?: { ... }, // optional
derived?: { ... }, // optional
version?: number,
migrations?: { ... },
})
```
Returns a full `PrototypeSchema` with empty strings for UI fields — ready to pass directly to `usePrototype`.
---
## Usage with `usePrototype`
```vue
<script setup>
import { settingsSchema } from '~/schemas/settings'
// Local-only (no server sync for settings)
const { state, isReady } = usePrototype(settingsSchema, { disableSync: true })
// Reactive refs — automatically persisted to IndexedDB
const theme = state.theme // Ref<'system' | 'light' | 'dark'>
const language = state.language // Ref<string>
const sidebar = state.sidebar // Ref<boolean>
// Mutate directly
function setTheme(t: string) { theme.value = t }
</script>
```
No `<ProtoTool>` or any Proto component needed. The composable handles all persistence.
---
## Headless Collections
```ts
const notesSchema = defineHeadlessSchema({
key: 'user-notes',
fields: {},
collections: {
notes: defineCollection({
key: 'notes',
title: 'Notes',
itemLabel: item => item.title,
fields: {
title: { type: 'text', label: 'Title', default: '' },
content: { type: 'textarea', label: 'Content', default: '' },
pinned: { type: 'toggle', label: 'Pinned', default: false },
},
defaults: { title: '', content: '', pinned: false },
}),
},
})
const { collections, isReady } = usePrototype(notesSchema)
const notes = collections.notes // UseProtoListReturn
// CRUD
notes.add({ title: 'Hello', content: 'World', pinned: false })
notes.update(0, { ...notes.items.value[0], pinned: true })
notes.remove(0)
// Access items
const allNotes = notes.items // Ref<any[]>
const pinnedNotes = computed(() => allNotes.value.filter(n => n.pinned))
```
---
## Standalone Collection
`useProtoCollection` is even simpler for a single collection without a parent prototype:
```ts
import { defineCollection } from '@websideproject/nuxt-protokit'
const taskSchema = defineCollection({
key: 'tasks',
title: 'Tasks',
itemLabel: item => item.name,
fields: {
name: { type: 'text', label: 'Task', default: '' },
done: { type: 'toggle', label: 'Done', default: false },
due: { type: 'date', label: 'Due', default: '' },
},
defaults: { name: '', done: false, due: '' },
searchable: true,
sortBy: 'due',
})
const tasks = useProtoCollection(taskSchema, { disableSync: true })
// In template or other composables
tasks.items.value // all tasks
tasks.sorted.value // sorted by 'due'
tasks.search.value = 'bug' // filter by search
tasks.filtered.value // filtered results
```
---
## Use Cases
| Use Case | Pattern |
|----------|---------|
| User preferences / settings | `defineHeadlessSchema` + `disableSync: true` |
| Per-user bookmarks, notes | `defineHeadlessSchema` + `namespace: userId` |
| Shared team data (no UI) | `defineHeadlessSchema` + `namespace: teamId` + server sync |
| Background data caching | `useProtoDoc` directly for raw Y.Map/Y.Array access |
| Custom UI over persistent storage | `usePrototype(headlessSchema)` — read/write `state.*` refs |
---
## Schema Migrations in Headless Mode
Migrations work identically to UI mode:
```ts
const schema = defineHeadlessSchema({
key: 'settings',
fields: {
theme: { type: 'select', label: 'Theme', default: 'system', options: [...] },
// v2: added 'accentColor'
accentColor: { type: 'color', label: 'Accent', default: '#3B82F6' },
},
version: 2,
migrations: {
2: data => ({ ...data, accentColor: data.accentColor ?? '#3B82F6' }),
},
})
```
references/module-authoring.md
# Module Authoring with nuxt-protokit
How to ship nuxt-protokit extensions — custom field types and custom viz types — inside a reusable Nuxt module.
---
## How It Works
nuxt-protokit exposes the `protokit:register-extension` hook. During module setup it calls this hook synchronously, passing `addComponentsDir` and `addImportsDir`. Your module hooks into it to register the directories containing your custom Vue components and composables. They become globally available (auto-imported) in the host app alongside protokit's built-ins.
At runtime, the host app calls `useProtoExtensionRegistry().registerExtension(ext)` (in a plugin) to wire the custom types to their components.
---
## Two Approaches
### Approach A: Module provides components only (simpler)
The module exposes components and composables via the hook. The host app calls `defineProtokitExtension` and `registerExtension` in its own plugin.
**Best for:** distributing components as a package; the host controls the namespace.
### Approach B: Module registers the extension itself (batteries-included)
The module registers components via the hook AND ships a Nuxt plugin that calls `registerExtension` automatically.
**Best for:** fully self-contained feature modules with a fixed namespace.
---
## Directory Structure
```
modules/my-charts/
├── module.ts # defineNuxtModule — hooks into protokit:register-extension
└── runtime/
├── plugin.ts # Nuxt plugin — calls registerExtension at runtime
├── components/
│ ├── MyChartField.vue # Custom field component
│ └── MyHeatmapViz.vue # Custom viz component
└── composables/
└── useMyChartHelpers.ts # Optional composables auto-imported by host
```
---
## `module.ts` — Approach B (batteries-included)
```ts
// module.ts
import { defineNuxtModule, createResolver, addPlugin } from '@nuxt/kit'
import type { Nuxt } from '@nuxt/schema'
export default defineNuxtModule({
meta: {
name: '@myorg/module-charts',
configKey: 'moduleCharts',
},
setup(_options, nuxt: Nuxt) {
const resolver = createResolver(import.meta.url)
// 1. Register components + composables via the protokit hook
nuxt.hook('protokit:register-extension' as any, ({ addComponentsDir, addImportsDir }: any) => {
addComponentsDir(resolver.resolve('./runtime/components'))
addImportsDir(resolver.resolve('./runtime/composables'))
})
// 2. Add a plugin that registers the extension at runtime
addPlugin(resolver.resolve('./runtime/plugin'))
},
})
```
```ts
// runtime/plugin.ts
import { defineNuxtPlugin } from '#app'
import { defineProtokitExtension, useProtoExtensionRegistry } from '@websideproject/nuxt-protokit'
import MyChartField from './components/MyChartField.vue'
import MyHeatmapViz from './components/MyHeatmapViz.vue'
export default defineNuxtPlugin(() => {
const ext = defineProtokitExtension({
namespace: 'charts', // unique namespace — not 'proto', 'protokit', 'nuxt', 'vue'
fields: {
'line-sparkline': { component: MyChartField },
},
vizTypes: {
heatmap: { component: MyHeatmapViz },
},
})
const { registerExtension } = useProtoExtensionRegistry()
registerExtension(ext)
})
```
---
## TypeScript: Augmenting the Hook
Avoid `as any` casts:
```ts
// types/nuxt.d.ts (in your module package)
declare module '@nuxt/schema' {
interface NuxtHooks {
'protokit:register-extension': (ctx: {
addComponentsDir: (dir: string) => void
addImportsDir: (dir: string) => void
}) => void | Promise<void>
}
}
```
---
## Custom Field Component
Receives `modelValue`/`update:modelValue` for two-way binding, plus any keys from `fieldDef.props`.
```vue
<!-- runtime/components/MyChartField.vue -->
<script setup lang="ts">
const props = defineProps<{
modelValue: number[] // matches fieldDef.default type
fieldDef?: any // the full FieldDef from schema.fields (optional)
disabled?: boolean
// additional keys come from fieldDef.props:
min?: number
max?: number
color?: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: number[]]
}>()
</script>
<template>
<div class="...">
<!-- your field UI -->
<button @click="emit('update:modelValue', [...props.modelValue, 0])">
Add point
</button>
</div>
</template>
```
**Usage in schema:**
```ts
fields: {
sparklineData: {
type: 'charts:line-sparkline', // 'namespace:field-name'
label: 'Trend',
default: [],
props: { min: 0, max: 100, color: '#3B82F6' },
},
}
```
---
## Custom Viz Component
Receives `config` (from `VizDef.config`) and `ctx` (the `ComputeContext`).
```vue
<!-- runtime/components/MyHeatmapViz.vue -->
<script setup lang="ts">
import type { ComputeContext } from '@websideproject/nuxt-protokit'
const props = defineProps<{
config: Record<string, any> // VizDef.config, any shape you define
ctx: ComputeContext // { fields, derived, connections, collections }
}>()
// config.data is a function that receives ComputeContext
const cells = computed(() => {
const raw = typeof props.config.data === 'function'
? props.config.data(props.ctx)
: []
return raw as Array<{ label: string; value: number }>
})
</script>
<template>
<div class="grid grid-cols-7 gap-1">
<div v-for="cell in cells" :key="cell.label"
:style="{ opacity: cell.value / 100 }"
class="h-4 bg-blue-500 rounded" />
</div>
</template>
```
**Usage in schema:**
```ts
visualizations: [
{
type: 'charts:heatmap', // 'namespace:viz-name'
title: 'Activity Heatmap',
showWhen: ctx => ctx.collections.events.length > 0,
config: {
data: ctx => ctx.collections.events.map(e => ({
label: e.date,
value: e.count,
})),
colorScheme: 'blue',
},
},
]
```
---
## Async Components
For large components, use async imports to keep the initial bundle small:
```ts
defineProtokitExtension({
namespace: 'charts',
fields: {
'rich-chart': {
component: () => import('./components/RichChart.vue'),
},
},
})
// nuxt-protokit wraps these with defineAsyncComponent automatically
```
---
## `protokit:register-extension` Hook Context
```ts
{
addComponentsDir: (dir: string) => void
// Registers a components dir with global: true — components are auto-imported
// everywhere in the host app, no explicit import needed
addImportsDir: (dir: string) => void
// Registers a composables dir — exports auto-imported in <script setup>
}
```
Both functions call the underlying `@nuxt/kit` helpers with the correct options. `addComponentsDir` uses `global: true`, meaning components don't need to be prefixed with the namespace in templates (they just use their actual Vue file name).
---
## `useProtoExtensionRegistry` — Extension Discovery
In the host app or custom pages, you can query registered extensions:
```ts
const { getFieldComponent, getVizComponent } = useProtoExtensionRegistry()
const FieldComp = getFieldComponent('charts:line-sparkline') // Component | undefined
const VizComp = getVizComponent('charts:heatmap') // Component | undefined
```
---
## Reserved Namespaces
These will throw an error in development if used:
```
proto protokit nuxt vue
```
Use a unique, package-based namespace like your org name or module name.
---
## Module Checklist
1. Hook into `'protokit:register-extension'` to expose components + composables
2. Add a Nuxt plugin that calls `defineProtokitExtension` + `registerExtension`
3. Custom field components: accept `modelValue`/`update:modelValue` + `props.*` keys
4. Custom viz components: accept `config` and `ctx: ComputeContext`
5. Use async `() => import(...)` for large components
6. Namespace must be unique and not reserved
7. Use `resolver.resolve()` (never relative paths) in module setup
references/offline-first.md
# Offline-First Architecture
## How It Works
Every prototype uses **Y.js CRDT documents** stored in IndexedDB:
- Each `usePrototype(schema)` call opens (or reuses) a Y.js doc keyed by `schema.key`
- Top-level fields are stored in a `Y.Map`; collections are stored in `Y.Array`s
- On mount, data is loaded from IndexedDB — `isReady` becomes `true` when loaded
- Changes sync automatically via **BroadcastChannel** to other tabs in the same origin
- If `serverSync` is enabled (default), changes also sync to `/api/yjs/<key>`
---
## Multi-Tab Sync
Automatic — no configuration needed. Opening the same prototype in multiple tabs keeps them in sync in real time via BroadcastChannel.
---
## Namespace Isolation
The `namespace` option creates **fully isolated** IndexedDB stores and BroadcastChannels. Resulting key: `<namespace>:<docKey>`.
```ts
// All users in a tenant share the same data
usePrototype(schema, { namespace: tenantId })
// Per-user private data within a tenant
usePrototype(schema, { namespace: `${tenantId}:${userId}` })
// Reactive — re-mounts when user switches tenant
const ns = computed(() => currentUser.value?.tenantId)
usePrototype(schema, { namespace: ns })
```
When `namespace` changes, the composable destroys the old doc and mounts a fresh one. **No data bleeds between namespaces.**
---
## Server Sync
Configured in `nuxt.config.ts`:
```ts
protokit: {
serverSync: true, // default — syncs to /api/yjs/<key>
// serverSync: false // local-only
// serverSync: { enabled: true, baseUrl: '/api/yjs' }
}
```
Per-prototype override:
```ts
// Force local-only for a specific prototype (public demo, scratch pad)
usePrototype(schema, { disableSync: true })
```
The server endpoint must implement the Y.js sync protocol. The `@websideproject/module-yjs-sync` module provides this for Nuxt.
---
## Encryption
Encrypts all IndexedDB updates with **AES-GCM**. Keys are derived via PBKDF2 from the provided password.
```ts
// Encrypt with a user's passphrase
const proto = usePrototype(schema, {
encryption: { password: userPassphrase }
})
// Or via useProtoDoc directly
const { doc } = useProtoDoc('my-key', {
encryption: { password: passphrase }
})
```
**Important notes:**
- Encryption is per-doc. Different docs can use different passwords.
- Server sync works with encrypted docs — encrypted bytes are synced, not plaintext.
- If the password changes, the old IndexedDB data cannot be decrypted. Implement a migration flow to re-encrypt.
- Use `useEncryptedIdb()` for direct encrypted storage access outside of protokit.
---
## Corruption Recovery
IndexedDB data can occasionally become corrupted (browser crashes, storage quota exceeded, partial writes). nuxt-protokit detects this automatically.
### Setup
Add `<ProtoCorruptionModal />` once in your app layout or on the page:
```vue
<!-- layouts/default.vue or pages/my-tool.vue -->
<template>
<div>
<slot />
<ProtoCorruptionModal />
</div>
</template>
```
When corruption is detected, the modal presents the user with options to:
1. Clear the corrupted doc and start fresh
2. Attempt recovery from the last clean snapshot
### Manual detection
```ts
const { isCorrupted, clearCorruption } = useProtoCorruption()
if (isCorrupted.value) {
// handle in custom UI
await clearCorruption()
}
```
---
## Document Size & GC
Y.js documents grow over time as change history accumulates. Tips:
- Avoid storing large binary blobs (images, files) in Y.js. Use separate storage (NuxtHub blob, etc.) and store only URLs.
- Y.js has built-in garbage collection for deleted content. It runs automatically.
- Use `maxItems` in `CollectionSchema` to cap collection size.
- For very large datasets (10k+ items), consider storing aggregates in Y.js and full data in a separate DB.
---
## `isReady` Pattern
Always check `isReady` before relying on state values. `<ProtoTool>` handles this automatically with a loading skeleton. For custom UIs:
```vue
<template>
<div v-if="!proto.isReady.value">Loading...</div>
<div v-else>{{ proto.state.revenue.value }}</div>
</template>
```
references/schema.md
# Schema Reference
## Helper Utilities
```ts
import { definePrototype, defineCollection, defineHeadlessSchema } from '@websideproject/nuxt-protokit'
// Type-safe schema helper (pass-through, for TS inference)
const schema = definePrototype({ key, title, shortTitle, description, icon, fields, ... })
// Type-safe collection helper
const myCollection = defineCollection({ key, title, fields, defaults, itemLabel, ... })
// Headless schema (no UI metadata required)
const headless = defineHeadlessSchema({ key, fields, collections?, derived? })
```
---
## PrototypeSchema
```ts
interface PrototypeSchema {
key: string // Unique ID — used as Y.js doc key and IndexedDB store
title: string
shortTitle: string
description: string
icon: string // Iconify icon, e.g. 'i-heroicons-calculator'
tags?: string[]
// Input state (persisted in Y.Map)
fields: Record<string, FieldDef>
sections?: SectionDef[] // Visual grouping of fields
defaultCols?: 1 | 2 | 3 | 4
// CRUD collections (persisted in Y.Arrays)
collections?: Record<string, CollectionSchema>
// Computed values (NOT stored in Y.js)
derived?: Record<string, DerivedDef>
// Display
results?: ResultSectionDef[]
visualizations?: VizDef[]
cards?: CardDef[]
actions?: ProtoAction[]
// Dashboard layout
layout?: DashboardLayout
// Cross-tool data sharing
produces?: Record<string, 'number' | 'string' | 'boolean' | 'string[]' | 'object' | 'object[]'>
consumes?: Record<string, string>
// Schema migration
version?: number
migrations?: SchemaMigrations // Record<number, (data) => data>
}
```
---
## Field Types (`FieldDef`)
All fields share: `label`, `default`, `help?`, `hint?`, `showWhen?`, `permissions?`
### Simple Fields
```ts
// number — numeric input
{ type: 'number', label: 'Revenue', default: 0,
min?: number, max?: number, step?: number,
leading?: '$', trailing?: '/mo',
format?: 'money' | 'percent' | 'number' | 'date' | ((v) => string) }
// text — single-line input
{ type: 'text', label: 'Name', default: '', placeholder?: string }
// textarea — multi-line
{ type: 'textarea', label: 'Notes', default: '' }
// select — dropdown
{ type: 'select', label: 'Plan', default: 'starter',
options: ['starter', 'pro'] | [{ label: 'Starter', value: 'starter', icon?: string }] }
// segmented — segmented control (like radio but compact)
{ type: 'segmented', label: 'View', default: 'week',
options: ['day', 'week', 'month'] }
// toggle — boolean switch
{ type: 'toggle', label: 'Enabled', default: false }
// range — slider
{ type: 'range', label: 'Growth %', default: 10, min: 0, max: 100, step: 1,
rangeLabels?: { min: 'Low', max: 'High' },
format?: 'percent' }
// rating — star rating
{ type: 'rating', label: 'Confidence', default: 3, min: 1, max: 5 }
// color — color picker
{ type: 'color', label: 'Brand Color', default: '#3B82F6' }
// date — date picker
{ type: 'date', label: 'Launch Date', default: '' }
// tags — tag input (array of strings)
{ type: 'tags', label: 'Keywords', default: [] }
```
### Linked Responses (cross-collection)
```ts
{
type: 'linked-responses',
label: 'Team Answers',
default: [], // Array<{ sourceId: string, answer: string }>
sourceCollection: 'team', // key in schema.collections
sourceLabel: (item) => item.name,
sourceBadge?: (item) => item.role,
answerPlaceholder?: string,
answerRows?: number,
}
```
### Custom Field
```ts
{
type: 'myns:myfield', // namespaced: 'namespace:name'
label: 'Custom',
default: null,
props?: Record<string, any>,
showWhen?: (values) => boolean,
}
```
### Field Permissions
```ts
permissions?: {
visible?: (ctx: PermissionContext) => boolean
editable?: (ctx: PermissionContext) => boolean
}
// ⚠️ Frontend only — Y.js data is always present regardless
```
---
## CollectionSchema
```ts
interface CollectionSchema {
key: string
title: string
icon?: string
itemLabel: (item: Record<string, any>) => string // Display name for each item
fields: Record<string, FieldDef>
defaults: Record<string, any> // Default values for new items
maxItems?: number
sortBy?: string // Default sort field key
sortOrder?: 'asc' | 'desc'
searchable?: boolean
searchFields?: string[]
editMode?: 'inline' | 'modal' // How items are edited
modalSize?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl'
// Table view config
tableColumns?: Array<{
key: string
label: string
sortable?: boolean
format?: 'money' | 'percent' | 'number' | 'date' | ((v) => string)
}>
// List view config
listDisplay?: {
primaryField: string
secondaryField?: string
badgeField?: string
avatarField?: string
}
// Status badge per item
statusField?: {
key: string
states: Record<string, { label: string, color: 'neutral'|'info'|'success'|'warning'|'error', icon?: string }>
}
// Per-item action buttons in the row
itemActions?: CollectionItemAction[]
// Expandable panels per item
itemPanels?: CollectionItemPanel[]
// Preset data packs
presets?: PresetPack[]
onboardingPresets?: boolean // Show preset picker on first load
// Validation
validate?: (item) => true | false | string | Record<string, string>
// CRUD guards (frontend only)
permissions?: CollectionPermissions
// Schema migration
version?: number
migrations?: SchemaMigrations
}
```
### Item Actions
```ts
// Open a URL
{ type: 'open-url', id: 'view', icon: 'i-heroicons-arrow-top-right-on-square',
url: (item, collections) => item.websiteUrl }
// Set a status field
{ type: 'status-set', id: 'mark-done', icon: 'i-heroicons-check',
toStatus: 'done', showWhen: (item) => item.status !== 'done' }
// Custom action (emits event handled in component)
{ type: 'custom', id: 'analyze', icon: 'i-heroicons-sparkles', label: 'Analyze',
showWhen: (item) => !!item.url }
```
### Item Panels
```ts
// Time series history panel
{ type: 'time-series', id: 'history', label: 'Metrics History',
historyKey: 'metricsHistory',
metrics: [{ key: 'mrr', label: 'MRR' }, { key: 'users', label: 'Users' }] }
// Text template panel
{ type: 'text-template', id: 'prompt', label: 'AI Prompt',
sections: [{ id: 'overview', label: 'Overview',
template: (item, ctx) => `Product: ${item.name}\nRevenue: ${item.mrr}` }] }
// URL input panel
{ type: 'url-input', id: 'website', label: 'Website', field: 'url',
onConfirm: (url, item) => ({ url, domain: new URL(url).hostname }) }
```
---
## DerivedDef & ComputeContext
```ts
interface DerivedDef {
deps?: string[] // field keys this depends on (optimization hint)
compute: (ctx: ComputeContext) => any
format?: 'money' | 'percent' | 'number' | 'date' | ((v) => string)
}
interface ComputeContext {
fields: Record<string, any> // current field values
derived: Record<string, any> // other derived values (chained)
connections: Record<string, any> // values from connected prototypes
collections: Record<string, any[]> // collection item arrays
}
// Example
derived: {
monthlyBurn: {
deps: ['salaries', 'infrastructure', 'marketing'],
compute: ctx => ctx.fields.salaries + ctx.fields.infrastructure + ctx.fields.marketing,
format: 'money',
},
runway: {
deps: ['cash', 'monthlyBurn'], // can depend on other derived values
compute: ctx => ctx.fields.cash / ctx.derived.monthlyBurn,
},
}
```
---
## SectionDef (field grouping)
```ts
interface SectionDef {
title: string
icon?: string
description?: string
fields: string[] // keys into schema.fields
cols?: 1 | 2 | 3 | 4
collapsible?: boolean
defaultOpen?: boolean
}
```
---
## DashboardLayout
```ts
interface DashboardLayout {
rows?: LayoutRow[] // flat layout
tabs?: DashboardTabDef[] // tabbed layout
}
interface LayoutRow {
cols: 1 | 2 | 3 | 4
gap?: number
items: LayoutItem[]
}
// LayoutItem union
type LayoutItem =
| { type: 'form', sectionIndex?: number, span?: number }
| { type: 'stats', resultIndex: number, span?: number }
| { type: 'viz', vizIndex: number, span?: number }
| { type: 'card', cardIndex: number, span?: number }
| { type: 'collection', collectionKey: string, view: 'list' | 'table', span?: number }
| { type: 'collection', collectionKey: string, view: 'calendar',
calendarConfig: CollectionCalendarConfig, span?: number }
| { type: 'section', title: string, items: LayoutItem[], span?: number }
| { type: 'tabs', tabs: InlineTabDef[], span?: number }
```
---
## ProtoAction
```ts
// Copy text to clipboard
{ type: 'copy-text', id: 'copy', label: 'Copy Report',
text: (ctx, collections) => `Revenue: ${ctx.fields.revenue}` }
// Export markdown file
{ type: 'export-markdown', id: 'export', label: 'Export',
content: (ctx) => `# Report\n\nRevenue: ${ctx.fields.revenue}`,
filename: 'report.md' }
// Reset all fields to defaults
{ type: 'reset', id: 'reset', label: 'Reset' }
```
---
## Schema Migrations
```ts
// Increment version when stored data shape changes
version: 2,
migrations: {
// key = target version; runs when stored version < target
2: (data) => ({ ...data, newField: data.oldField ?? 0 }),
},
```
For manual use: `import { runMigrations } from '#protokit/utils/runMigrations'` — `runMigrations(data, storedVersion, currentVersion, migrations)`.
references/visualizations.md
# Visualizations Reference
Add visualizations to `schema.visualizations: VizDef[]`. Each item references a zero-based index in `layout`.
```ts
visualizations: [
{ type: 'bar-chart', config: { ... } }, // index 0
{ type: 'stat-cards', config: { ... } }, // index 1
],
layout: {
rows: [{ cols: 2, items: [
{ type: 'viz', vizIndex: 0 },
{ type: 'viz', vizIndex: 1 },
]}]
}
```
All `config` callbacks receive `ctx: ComputeContext` (`{ fields, derived, connections, collections }`).
---
## Built-In Viz Types
### `progress` — Progress Bar
```ts
{
type: 'progress',
title?: 'Runway',
config: {
type: 'progress',
value: ctx => ctx.derived.runwayMonths,
max?: 24,
label?: ctx => `${ctx.derived.runwayMonths} months`,
thresholds?: [
{ value: 6, color: 'error', label: 'Critical' },
{ value: 12, color: 'warning', label: 'Low' },
{ value: 24, color: 'success', label: 'Healthy' },
],
},
}
```
### `benchmark` — Benchmark Bar
Positions a value against a median reference with min/max range.
```ts
{
type: 'benchmark',
config: {
type: 'benchmark',
value: ctx => ctx.fields.cac,
median: 150,
min?: 50,
max?: 500,
label?: 'CAC vs Industry',
unit?: '$',
},
}
```
### `bar-chart`
```ts
{
type: 'bar-chart',
config: {
type: 'bar-chart',
data: ctx => [
{ label: 'Jan', value: ctx.fields.jan, color?: '#3B82F6' },
{ label: 'Feb', value: ctx.fields.feb },
],
maxValue?: number,
unit?: '$',
},
}
```
### `line-chart`
```ts
{
type: 'line-chart',
config: {
type: 'line-chart',
data: ctx => ctx.collections.metrics.map(m => ({ label: m.month, value: m.mrr })),
color?: '#3B82F6',
showAxes?: true,
unit?: '$',
emptyText?: 'No data yet',
},
}
```
### `area-chart` — Multi-Series
```ts
{
type: 'area-chart',
config: {
type: 'area-chart',
series: ctx => [
{ label: 'Revenue', color: '#10B981',
data: ctx.collections.months.map(m => ({ label: m.name, value: m.revenue })) },
{ label: 'Costs', color: '#F43F5E',
data: ctx.collections.months.map(m => ({ label: m.name, value: m.costs })) },
],
showAxes?: true,
unit?: '$',
},
}
```
### `pie-chart` / `donut-chart`
```ts
{
type: 'pie-chart', // or 'donut-chart'
config: {
type: 'pie-chart',
data: ctx => ctx.collections.channels.map(c => ({ label: c.name, value: c.revenue })),
unit?: '$',
showLegend?: true,
emptyText?: 'No channels yet',
},
}
```
### `funnel-chart`
```ts
{
type: 'funnel-chart',
config: {
type: 'funnel-chart',
data: ctx => [
{ label: 'Visitors', value: ctx.fields.visitors },
{ label: 'Signups', value: ctx.fields.signups },
{ label: 'Paid', value: ctx.fields.paid },
],
unit?: '',
showConversion?: true, // show conversion % between stages
},
}
```
### `stat-cards`
```ts
{
type: 'stat-cards',
config: {
type: 'stat-cards',
cols?: 3, // 1 | 2 | 3 | 4
cards: ctx => [
{
label: 'MRR',
value: ctx.derived.mrr,
subLabel?: 'Monthly Recurring Revenue',
trend?: 12.5, // percentage change
trendLabel?: 'vs last month',
unit?: '$',
icon?: 'i-heroicons-currency-dollar',
color?: 'emerald', // 'primary' | 'emerald' | 'rose' | 'amber' | 'violet' | 'sky' | 'neutral'
sparkline?: [120, 135, 148, 162, 180], // mini chart
},
],
},
}
```
### `comparison-table`
```ts
{
type: 'comparison-table',
config: {
type: 'comparison-table',
columns: [
{ key: 'name', label: 'Plan' },
{ key: 'price', label: 'Price' },
{ key: 'users', label: 'Users' },
],
rows: ctx => ctx.collections.plans.map(p => ({
name: p.name, price: p.price, users: p.maxUsers
})),
},
}
```
### `feature-matrix`
```ts
{
type: 'feature-matrix',
config: {
type: 'feature-matrix',
features: ['SSO', 'API Access', 'Custom Domain', 'SLA'],
entities: ctx => ctx.collections.competitors.map(c => ({
name: c.name,
coverage: { SSO: c.hasSso, 'API Access': 'partial', 'Custom Domain': false, SLA: c.hasSla }
// value: true | false | 'partial'
})),
},
}
```
### `timeline`
```ts
{
type: 'timeline',
config: {
type: 'timeline',
items: ctx => ctx.collections.milestones.map(m => ({
title: m.name,
description?: m.notes,
date?: m.targetDate,
status?: m.done ? 'done' : m.current ? 'current' : 'upcoming',
icon?: 'i-heroicons-flag',
})),
},
}
```
---
## `showWhen` on VizDef
All viz types support `showWhen` to conditionally render:
```ts
{
type: 'bar-chart',
showWhen: ctx => ctx.collections.months.length > 0,
config: { ... }
}
```
---
## Custom Viz Types
Custom viz must be registered via `defineProtokitExtension`. See `references/extensions.md`.
```ts
{
type: 'myns:heatmap', // 'namespace:name'
title?: 'Activity Heatmap',
showWhen?: ctx => true,
config?: { /* passed as props to your component */ }
}
```
---
## `CollectionCalendarConfig` (for collection calendar views)
Used in `LayoutItem` with `view: 'calendar'`:
```ts
{
dateField: 'scheduledDate', // required
titleField: 'name', // required
idField?: '_id',
endDateField?: 'endDate',
timeField?: 'startTime', // HH:mm
endTimeField?: 'endTime', // HH:mm
allDayField?: 'isAllDay', // boolean field; absent = all-day
statusColorMap?: { scheduled: 'blue', done: 'green', cancelled: 'red' },
defaultColor?: 'neutral',
}
```
SKILL.md
---
name: nuxt-protokit
description: Use when building schema-driven prototypes with nuxt-protokit (@websideproject/nuxt-protokit) - provides definePrototype, usePrototype, ProtoTool, collections, derived values, and visualizations for rapid SaaS tool prototyping.
license: MIT
---
# nuxt-protokit
Schema-driven rapid prototyping for Nuxt 4. Turn a TypeScript schema into a persistent, interactive SaaS tool in minutes — backed by Y.js CRDT + IndexedDB, auto-synced across tabs, with optional server sync.
## When to Use
- Defining a `PrototypeSchema` with fields, collections, derived values, or visualizations
- Writing `usePrototype()` or `useProtoCollection()` composables
- Using `<ProtoTool>`, `<ProtoDashboard>`, `<ProtoCrudModal>`, `<ProtoCalendar>`, `<ProtoViz>` components
- Configuring offline persistence, encryption, namespace isolation, or corruption recovery
- Creating custom field/viz extensions with `defineProtokitExtension`
- Using headless mode (`defineHeadlessSchema`) without UI components
## Quick Start
```ts
// schema.ts
import { definePrototype } from '@websideproject/nuxt-protokit'
export const schema = definePrototype({
key: 'my-tool',
title: 'My Tool',
shortTitle: 'My Tool',
description: 'Description',
icon: 'i-heroicons-calculator',
fields: {
revenue: { type: 'number', label: 'Revenue', default: 0, format: 'money' },
growth: { type: 'range', label: 'Growth %', default: 10, min: 0, max: 100 },
plan: { type: 'select', label: 'Plan', default: 'starter',
options: ['starter', 'pro', 'enterprise'] },
},
derived: {
projectedRevenue: {
deps: ['revenue', 'growth'],
compute: ctx => ctx.fields.revenue * (1 + ctx.fields.growth / 100),
},
},
})
```
```vue
<!-- pages/my-tool.vue -->
<script setup>
import { schema } from '~/schema'
const proto = usePrototype(schema)
</script>
<template>
<ProtoTool :schema="schema" :prototype="proto" />
</template>
```
## Available Guidance
| File | Topics |
|------|--------|
| **[references/schema.md](references/schema.md)** | PrototypeSchema, all field types, CollectionSchema, DerivedDef, DashboardLayout, ProtoAction |
| **[references/composables.md](references/composables.md)** | usePrototype, useProtoCollection, useProtoDoc, namespace, encryption options |
| **[references/components.md](references/components.md)** | ProtoTool, ProtoDashboard, ProtoCrudModal, ProtoCalendar, ProtoViz, ProtoActionBar |
| **[references/visualizations.md](references/visualizations.md)** | All 12 viz types with config interfaces and examples |
| **[references/offline-first.md](references/offline-first.md)** | Y.js + IndexedDB, multi-tab sync, encryption, corruption recovery, server sync |
| **[references/extensions.md](references/extensions.md)** | defineProtokitExtension, custom field/viz type registration |
| **[references/headless.md](references/headless.md)** | defineHeadlessSchema, programmatic usage without UI components |
| **[references/module-authoring.md](references/module-authoring.md)** | Building a Nuxt module that ships custom fields/viz: protokit:register-extension hook, plugin pattern |
## Progressive Loading
Load only what you need:
- Defining fields, collections, or layout? → [references/schema.md](references/schema.md)
- Writing `usePrototype` or composable options? → [references/composables.md](references/composables.md)
- Using `<ProtoTool>`, `<ProtoCrudModal>`, `<ProtoCalendar>`? → [references/components.md](references/components.md)
- Configuring `visualizations: []` in schema? → [references/visualizations.md](references/visualizations.md)
- Encryption, multi-tenant, corruption? → [references/offline-first.md](references/offline-first.md)
- Custom field or viz type? → [references/extensions.md](references/extensions.md)
- No UI, programmatic storage? → [references/headless.md](references/headless.md)
- Building a Nuxt module that ships extensions? → [references/module-authoring.md](references/module-authoring.md)
**DO NOT read all files at once.** Load based on current task.
## Related Skills
- **`nuxt`** — Nuxt 4 routing, server routes, middleware
- **`nuxt-ui`** — @nuxt/ui components for surrounding page layout
- **`vue`** — Vue 3 composables, reactivity patterns
## Module Config (`nuxt.config.ts`)
```ts
export default defineNuxtConfig({
modules: ['@websideproject/nuxt-protokit'],
protokit: {
serverSync: true, // default — syncs to /api/yjs
// serverSync: false // local-only, no HTTP
// serverSync: { enabled: true, baseUrl: '/api/yjs' }
}
})
```
_Token efficiency: Main skill ~400 tokens. Each reference file ~800–1400 tokens._