- Docs: https://orchidui.vercel.app
- Storybook: https://orchidui.vercel.app/storybook/
npm install
npm run dev # Storybook dev server
npm run build:json # Regenerate docs + llms.txt
npm run mcp # Start MCP server (port 3333)Each component has a .stories.js file. This file is the single source of truth for component metadata, examples, and AI documentation.
export default {
component: MyComponent,
tags: ['autodocs'], // required for Storybook autodocs — do not remove
// ── AI/docs metadata ────────────────────────────────────────────────────
kind: 'leaf', // 'leaf' (standalone) | 'composite' (composes other components)
description: 'One sentence describing what this component does.',
keywords: ['tag1', 'tag2'], // used for search in MCP and llms.txt
use_for: [ // when should a developer reach for this component?
'use case one',
'use case two',
],
understand_with: ['OtherComponent'], // related components worth reading alongside this one
}
descriptionandkeywordshere replacecomponent-meta.js. If omitted, the build falls back tocomponent-meta.js.
Storybook reads JSDoc comments from defineProps and defineEmits in the .vue file and displays them as descriptions in the autodocs controls panel. Write docs there — not in argTypes.
<script setup>
const props = defineProps({
/** Whether the button is in a loading state. */
isLoading: {
type: Boolean,
default: false
},
/**
* Visual style variant.
* Use 'primary' for the main action, 'secondary' for supporting actions.
*/
variant: {
type: String,
default: 'primary'
}
})
const emit = defineEmits({
/** Fired when the button is clicked and not disabled or loading. */
click: []
})
</script>For slots, use a <!-- @slot --> comment directly above the <slot> tag:
<template>
<!-- @slot Default content inside the button. -->
<slot />
<!-- @slot Icon displayed to the left of the label. -->
<slot name="left-icon" />
</template>Rules:
- Write one short sentence for simple props; use a full block comment for props with non-obvious behaviour
- Do not duplicate these descriptions in
argTypes— if both exist, theargTypesvalue wins and the JSDoc is silently ignored
Every component should have one Playground export that uses Storybook args controls. This is what powers the interactive controls panel on the autodocs page.
export const Playground = {
argTypes: {
// Direct component props — omit `description`, Storybook reads it from JSDoc in the .vue file
isDisabled: { control: 'boolean' },
size: {
control: 'select',
options: ['small', 'medium', 'large'],
},
// Story-only controls (not component props) — add description manually
showSlotContent: {
control: 'boolean',
description: 'Toggle the default slot content on/off',
},
},
args: {
isDisabled: false,
size: 'medium',
showSlotContent: true,
},
render: (args) => ({
components: { MyComponent },
setup() {
return { args }
},
template: `
<div class="p-6">
<MyComponent :is-disabled="args.isDisabled" :size="args.size">
<template v-if="args.showSlotContent">Slot content</template>
</MyComponent>
</div>
`
})
}Rules:
Playgroundmust haveargTypesand nocodeproperty — the build pipeline skips it automatically- Descriptions for direct component props are omitted from
argTypes— Storybook reads the JSDoc fromdefinePropsin the.vuefile - Descriptions for story-only controls (args that don't match a component prop) must be written manually
tags: ['autodocs']on the default export is required — this is what enables the autodocs page
Each named export is one example. Extract the Vue code to a separate file in examples/ and import it with ?raw:
MyComponent/
MyComponent.vue
MyComponent.stories.js
examples/
Default.vue
WithSlots.vue
import DefaultExample from './examples/Default.vue'
import DefaultRaw from './examples/Default.vue?raw'
export const Default = {
description: 'One sentence describing this specific example.',
highlights: [ // optional — key points for AI context
'prop-name — what it does',
'#slot-name — when to use it',
],
code: DefaultRaw, // shown in docs + used by build:json
render: () => ({
components: { DefaultExample },
template: `<div class="p-6"><DefaultExample /></div>`
})
}Rules:
codemust beSomeNameRaw(a?rawimport) — the build pipeline detects this pattern- The
examples/*.vuefile should useimport { ... } from '@orchidui/core'(not the internal alias) - Keep the render template minimal — just wrap the example component in a
<div class="p-6">
// OcDataTable.stories.js
import { DataTable } from '@/orchidui-core'
import BasicExample from './examples/Basic.vue'
import BasicRaw from './examples/Basic.vue?raw'
export default {
component: DataTable,
tags: ['autodocs'],
kind: 'composite',
description: 'Data table with filtering, pagination, search, and bulk actions.',
keywords: ['table', 'data', 'filter', 'pagination', 'crud'],
use_for: ['resource list', 'CRUD table', 'paginated data display'],
understand_with: ['FormBuilder', 'Modal', 'Chip'],
}
export const Basic = {
description: 'Minimal DataTable with search, tabs, and pagination.',
highlights: ['options.tableOptions — headers + fields', 'update:filter event to trigger data fetch'],
code: BasicRaw,
render: () => ({
components: { BasicExample },
template: `<div class="p-6"><BasicExample /></div>`
})
}Running npm run build:json generates:
| Output | Description |
|---|---|
public/json/orchid-core.json |
Component index with description, tags, kind |
public/json/components/<Name>.detail.json |
Props, events, slots, example list |
public/json/components/<Name>.<id>.json |
Individual example with full code |
public/raw/docs/components/<Name>.md |
Full component doc (markdown) |
public/raw/docs/examples/<Name>-<id>.md |
Individual example (markdown) |
public/llms.txt |
AI discovery index |
public/llms-full.txt |
All-in-one AI reference |