Framework contract
Component Spec
componentManager + componentInspectorThe current source of truth for how DOM Studio discovers, decorates, documents, and exposes components to Studio.
Purpose
One contract, multiple consumers
A component should be discoverable from its folder, then decorated by inspecting the Vue component. The docs site, sidebar, generated pages, Studio palette, and future component database should all be able to use the same shape.
The names below document what exists now. They are intentionally precise so that future refactors from __doc, __studio, and _edit can be made against a known contract.
Adding a component
Put an El*.vue file under components, forms, or visual. It should appear in the generated docs and Studio automatically unless hidden.
Auto-documenter
Take the docs engine with the components
Optional when adopting DOM Studio
When someone copies, vendors, or builds on the components, they can also bring the auto-documenter. It turns local Vue component metadata into living docs inside their own app, so the component library does not need a separate Storybook-style setup to stay inspectable.
| Discovery | Finds local El*.vue files in configured component folders with Vite glob imports. |
| Inspection | Reads __doc, __studio, Vue prop definitions, slots, and events from the component object. |
| Generated page | Renders a playground, props table, slots, and events when no bespoke Index.vue exists. |
| Navigation | Adds the component to the sidebar unless __doc.hidden or __doc.nav.hidden is set. |
| Studio | Makes the component available to visual editing unless __doc.studio.hidden is set. |
// 1. Add or copy a component into a discovered folder.
src/pages/forms/my-input/DomMyInput.vue
// 2. Give the component lightweight docs metadata.
defineOptions({
__doc: {
name: 'My input',
tag: '<DomMyInput>',
description: 'A labelled input for project-specific values.',
studio: { group: 'Project forms' },
},
});
// 3. Export it when the app needs package-style imports.
export { default as DomMyInput } from '../../forms/my-input/DomMyInput.vue';
// 4. The docs route is created automatically if there is no Index.vue.
/forms/my-inputProgressive docs
Start plain, then refine
The happy path
The core workflow is deliberately light. Focus on making the component first. Once it lands in a discovered folder, it shows up in the docs automatically. Later, when the generated page reveals what needs better naming, examples, or inspector controls, add only the metadata or custom page you actually need.
| Make the component | Build a normal Vue SFC in a discovered folder. No docs ceremony is required for the first pass. |
| It appears in docs | The route, sidebar item, generated playground, and props reference are inferred from the folder, export name, and defineProps. |
| Tweak metadata | Add __doc when the default label, description, nav order, Studio group, slots, or events need product language. |
| Tune the inspector | Add _edit on props when the generated playground should use a richer editor or constrained options. |
| Override the page | Add Index.vue beside the component only when the generated docs are no longer enough. |
Generated inspector
Status pill
AutoString / Text input
defaultInferred from defineProps. Add _edit.options later to turn this into a select-style control.
String / Text input
defaultSeeded from the prop default and bound to the live component preview.
<!-- src/pages/components/status-pill/DomStatusPill.vue -->
<script setup>
const props = defineProps({
tone: { type: String, default: 'neutral' },
label: { type: String, default: 'Draft' },
});
</script>
<template>
<span class="rounded-full border border-border px-2 py-1 text-xs">
{{ label }}
</span>
</template>
<!-- Result: /components/status-pill appears automatically.
The generated page uses the folder/export name, prop definitions, and playground. --><!-- Tweak the generated docs without writing a page. -->
<script setup>
defineOptions({
__doc: {
name: 'Status pill',
tag: '<DomStatusPill>',
description: 'A compact status label for workflow state.',
order: 40,
studio: {
group: 'Feedback',
icon: 'Circle',
},
},
});
const props = defineProps({
tone: {
type: String,
default: 'neutral',
_edit: {
options: ['neutral', 'success', 'warning', 'danger'],
description: 'Visual state shown by the pill.',
},
},
label: { type: String, default: 'Draft' },
});
</script>
<!-- Override the whole docs page only when needed:
src/pages/components/status-pill/Index.vue -->Adoption
What a consuming app chooses
| Step | Why it matters |
|---|---|
| Copy the engine | Bring componentManager.js, componentInspector.js, GeneratedComponentPage.vue, and the docs helpers used by that page into the app. |
| Point discovery at the app | Change the import.meta.glob patterns to match the app component folders that should be documented. |
| Add component metadata | Use defineOptions({ __doc }) for name, tag, description, slots, events, nav, and Studio hints. |
| Choose the page style | Omit Index.vue for generated docs, or add Index.vue when the component needs authored examples and deeper guidance. |
| Keep examples nearby | Store examples beside the component so authored pages and future generated examples can stay close to the source. |
Discovery
Component row
The component row is intentionally plain. It is the database-like record of what exists and where it lives. It does not decide labels, props, visibility, icons, or Studio behaviour.
// Discovered by componentManager.js from:
// src/pages/components/*/El*.vue
// src/pages/forms/*/El*.vue
// src/pages/visual/*/El*.vue
{
"id": "forms/text-input",
"path": "../forms/text-input/DomTextInput.vue",
"section": "forms",
"slug": "text-input",
"exportName": "DomTextInput",
"route": "/forms/text-input",
"component": "DomTextInput"
}Inspection
Decorated component row
The decorated row is created by inspecting the loaded Vue component. This is the shape consumers usually want when rendering docs, navigation, or Studio UI.
// Returned by inspecting the discovered row and Vue component.
{
"id": "forms/text-input",
"path": "../forms/text-input/DomTextInput.vue",
"section": "forms",
"slug": "text-input",
"exportName": "DomTextInput",
"route": "/forms/text-input",
"component": "DomTextInput",
"doc": {
"name": "Text input",
"tag": "<DomTextInput>",
"description": "A labelled single-line text field."
},
"studio": {
"group": "Forms",
"hidden": false
},
"label": "Text input",
"badge": null,
"icon": "M5 7h14M12 7v10M8 17h8",
"order": 100,
"hidden": false,
"navHidden": false,
"studioHidden": false,
"props": {
"modelValue": { "type": "String", "default": "" },
"label": { "type": "String", "default": "" }
}
}Metadata
Current __doc contract
| Field | Current meaning |
|---|---|
| name | Human label used by docs and navigation. Falls back to a label from the folder slug. |
| tag | Display tag shown at the top of docs, such as <DomTextInput>. |
| description | Short documentation summary. Used by generated docs. |
| icon | SVG path data. This is the preferred component icon location. |
| order | Numeric sort order inside the inferred group. Defaults to 100. |
| badge | General badge. nav.badge is more specific for sidebar usage. |
| hidden | Hide the component from generated docs/navigation/studio discovery. |
| nav | Navigation-specific decoration: badge, hidden, icon. |
| studio | Studio-specific decoration: group, icon, hidden, defaults, accepts, hints. |
| slots | Array of slot docs: name, payload, description. |
| events | Array of event docs: name, payload, description, and optional details for exact payload shapes or examples. |
<script setup>
defineOptions({
__doc: {
name: 'Text input',
tag: '<DomTextInput>',
description: 'A labelled single-line text field.',
icon: 'M5 7h14M12 7v10M8 17h8',
order: 100,
badge: 'New',
nav: {
badge: 'New',
hidden: false,
icon: 'M5 7h14M12 7v10M8 17h8',
},
studio: {
group: 'Forms',
icon: 'T',
hidden: false,
},
slots: [
{ name: 'prefix', description: 'Rendered before the input.' },
],
events: [
{ name: 'update:modelValue', payload: 'string', description: 'Fired when text changes.' },
],
},
});
</script>Inspector
Current prop _edit contract
_edit lives on Vue prop definitions. Studio's inspector reads it to choose the editor component and pass editor-specific props. This is not required for ordinary app usage.
| component | Name of the inspector editor component. Example: DomTextInput, DomJsonInput, DomJsonListInput. |
| description | Helper copy shown by the inspector field. |
| options | Option values passed to the default editor when useful. |
| props | Props passed to the editor component. Use this for compact, rows, schema, addLabel, and similar editor-specific props. |
| type comments | Use JSDoc @typedef and @type comments near the component props when array/object shapes need richer documentation. |
| label | Optional label override when the editor uses a different visible label. |
const props = defineProps({
options: {
type: Array,
default: () => [],
_edit: {
component: 'DomJsonListInput',
description: 'Options shown in the picker.',
props: {
compact: true,
addLabel: '+ Add option',
schema: [
{ key: 'label', label: 'Label', default: (index) => `Option ${index + 1}` },
{ key: 'value', label: 'Value', default: (index) => `option-${index + 1}` },
],
},
},
},
});Studio
Studio renderer spec
The Studio renderer spec is a saved UI tree. It references components by name and stores props and children. It is not the same thing as the component row, but it depends on the component registry being able to resolve names back to components.
// Studio renderer spec. This is different from the component database row.
{
"id": "email-field",
"label": "Email field",
"component": "DomTextInput",
"props": {
"label": "Email address",
"placeholder": "you@example.com"
},
"children": []
}Responsibilities
Where logic belongs today
| Part | Responsibility |
|---|---|
| Component manager | Discovers component files and derives path facts only: section, slug, exportName, route, id, and loader/component. |
| Component inspector | Decorates a discovered row by reading the loaded Vue component: __doc, __studio, props, labels, icons, hidden flags, and ordering. |
| Docs routes | If a component folder has an Index.vue, that page is used. If it has no Index.vue, GeneratedComponentPage.vue creates a reference page from inspected metadata. |
| Sidebar | Uses discovered and inspected rows, but owns display ordering and fallback icons as presentation concerns. |
| Studio | Uses discovered and inspected rows to build the palette. It also infers defaults from props and local sample data. |
Convention
Folder shape
A custom docs page is optional. If there is no Index.vue, the generated docs page uses component metadata and runtime props. Examples are still recommended because they show intended composition.
src/pages/forms/my-input/
ElMyInput.vue
Index.vue
examples/
Basic.vue