# .g Protocol
The `.g` protocol is a source-level model for UI states. It lets a component declare the visual states that protocol consumers can render, inspect, and verify.
Runelight is the product. `.g` is the protocol. `.g.tsx` and `.g.vue` are file formats that expose that protocol from different component systems.
## Protocol Model
The protocol answers five questions:
1. **Which files participate?** Participating files use a `.g.*` extension, such as `.g.tsx` or `.g.vue`.
2. **Which components are renderable?** Each indexed component receives a stable coordinate such as `src/Badge.g.tsx#default` or `src/UserCard.g.vue#default`.
3. **Which visual states exist?** Frames are static, named states such as `ready`, `loading`, `empty`, `error`, or `disabled`.
4. **Where can state be substituted?** React uses explicit scope and provider seams. Vue uses template-visible frame scope.
5. **What can be checked statically?** Consumers should be able to enumerate frames and reason about reachable visual branches without executing opaque application state.
These pieces are additive. A `.g` file should remain an ordinary source file for its host framework, with static frame data attached in the format that framework can naturally express.
## File Formats
Different frameworks expose the same protocol through different source shapes.
| Protocol concept | React/TSX | Vue SFC |
| --- | --- | --- |
| Participating file | `.g.tsx` | `.g.vue` |
| Component entry | Exported React component | The SFC default component |
| Frame declaration | `Component.frames = { ... }` | `export default { ... }` |
| Main substitution surface | Props, scope hooks, providers | Props and template scope |
| Branch analysis surface | JSX expressions | SFC template directives |
### React `.g.tsx`
A `.g.tsx` file is ordinary TSX with static visual-state data attached to exported components:
```tsx
import type { GFrames } from "@runelight/react/runtime"
export default function Badge(props: { tone: "neutral" | "warning"; label: string }) {
return {props.label}
}
Badge.frames = {
neutral: { description: "Neutral badge", props: { tone: "neutral", label: "Ready" } },
warning: { description: "Warning badge", props: { tone: "warning", label: "Needs review" } },
} satisfies GFrames<{ tone: "neutral" | "warning"; label: string }>
```
The component remains a real React component. Production code renders the same TSX. Frames are static data for rendering, inspection, and verification.
### Vue `.g.vue`
A `.g.vue` file is an ordinary Vue SFC plus one Runelight custom block. The SFC has one protocol component entry: the default component at `path/to/File.g.vue#default`.
```vue
Loading {{ props.userId }}
export default {
ready: {
description: "Ready state with loaded user details",
props: { userId: "user_42" },
scope: {
status: "ready",
user: { name: "Ada Lovelace" },
},
},
loading: {
description: "Loading state while the remote user is unavailable",
props: { userId: "user_1" },
scope: { status: "loading" },
},
}
```
`` contains a single statically enumerable `export default { ... }` object. Top-level keys are frame names.
During protocol preview, Runelight treats the Vue template as the primary render surface. It keeps the SFC template and styles, reads the selected frame, and injects the frame's `props` and `scope` into a synthetic preview setup. Production-only script state does not need to be intercepted if the template values needed for the visual state are declared by the frame.
## Frames
A frame describes one meaningful visual state. Frame names should describe what appears on screen: `ready`, `loading`, `empty`, `error`, `disabled`, `overflowing`, `admin`, or `anonymous`.
Simple React components often need only props:
```tsx
Badge.frames = {
neutral: { description: "Neutral badge", props: { tone: "neutral", label: "Ready" } },
warning: { description: "Warning badge", props: { tone: "warning", label: "Needs review" } },
} satisfies GFrames
```
Vue frames use the same static object shape inside ``:
```vue
export default {
ready: {
description: "Ready state with loaded user details",
props: { userId: "user_42" },
scope: { status: "ready", user: { name: "Ada Lovelace" } },
},
loading: {
description: "Loading state while the remote user is unavailable",
props: { userId: "user_1" },
scope: { status: "loading" },
},
}
```
Supported frame fields:
| Field | Meaning |
| --- | --- |
| `description` | Required static string for agents and tooling to understand what the frame is meant to show. |
| `props` | Values passed as component props. In Vue preview they are also exposed through `props` and direct prop-key variables. |
| `scope` | State supplied at a protocol seam. React scope hooks read this value; Vue preview exposes it as template-visible scope for the selected frame. |
| `providers` | Runelight provider/injection seam values. React entries are G providers; Vue entries are native injection keys, and Vue preview calls `provide(injectionKey, value)` before rendering the frame. |
Frame data should be static and inspectable: object literals with statically enumerable keys. Protocol consumers should not need to execute application code to discover the frame list.
## Composition
Frames are authored on components, but components also render each other. In composition, Runelight keeps the framework's ordinary data-flow boundaries intact:
- props passed by a parent render are the child's props;
- provider values from an ancestor environment are the child's provider values;
- scope remains local to the component seam whose frame selected it.
A parent frame may use its own `scope` to compute props for a child. That is valid: the child receives normal external props. A parent may also render a provider environment around a child. The child reads that external provider value before any child-local frame provider mock.
Child frames remain useful for isolated preview and, where the framework runtime supports it, explicit child frame overrides. They do not automatically replace child props or provider values that the parent actually produced.
Preview callers that need synthetic exploration can use `inputOverride=:` to explicitly overlay another frame's `props`, `scope`, and `providers` onto a rendered coordinate. That is a preview input override, not evidence that the state is reachable from the selected parent frame.
For the target contract and edge cases, see [Composable Frame Inputs](./runelight-composable-inputs.md).
## State Substitution
The `.g` protocol keeps preview substitution at explicit source-level boundaries. The boundary is framework-specific, but the goal is the same: render a declared visual state without pretending to run the whole application.
### React Seams
React components use explicit seams:
- `createGScopeHook` wraps a production hook. In production it calls the real hook; under protocol rendering it returns the frame-supplied scope.
- `createGProvider` creates a provider whose value can be supplied by frames during protocol rendering. Provider variants can describe finite environment axes such as role, theme, locale, or auth state.
- `useGContext` reads provider values inside a `.g.tsx` component.
The component itself does not branch on the renderer.
### Vue Template Scope
Vue already separates template shape from script setup. `.g.vue` uses that split as the protocol boundary.
Template-visible values that define a visual state should come from one of these sources:
- props declared by the frame;
- `scope` declared by the frame;
- local helpers, imports, and static literals that the preview transform can preserve safely.
Opaque script expressions are not automatically a problem. A formatter such as `formatDate(user.createdAt)` can remain an ordinary helper when it only formats displayed text. It becomes part of the static contract only when an opaque value controls render shape, visibility, iteration, or component selection, for example through `v-if`, `v-else-if`, `v-for`, `v-show`, or dynamic `:is`.
This keeps the Vue contract template-first: analyze what the template needs to render the branch, then require frames to supply those template-visible values.
### Vue Provide/Inject
Vue context uses native `provide` / `inject`. Runelight adds a typed key helper only when the injection should appear as a finite preview axis:
```ts
// auth.ts
import { defineGInjectionKey } from "@runelight/vue/runtime"
export const authKey = defineGInjectionKey<{ role: "admin" | "viewer" }>({
variants: ["admin", "viewer"] as const,
})
```
Production code stays ordinary Vue:
```vue
```
Frames import the same key and use the Runelight `providers` field:
```vue
import { authKey } from "./auth"
import type { GVueFrames, GVueProviderFrame } from "@runelight/vue/runtime"
export default {
admin: {
description: "Admin auth context showing admin tools",
props: {},
providers: [[authKey, { role: "admin" }]],
} satisfies GVueProviderFrame,
viewer: {
description: "Viewer auth context without admin tools",
props: {},
providers: [[authKey, { role: "viewer" }]],
} satisfies GVueProviderFrame,
} satisfies GVueFrames, never, [typeof authKey]>
```
## Static Check
`runelight check` verifies that the frame model can represent the component's reachable visual branches. If render structure depends on props, scope, or provider context, at least one frame should make each branch reachable.
The check is intentionally narrow. It does not prove every possible state combination. It prevents reachable visual branches from escaping the declared frame set.
React diagnostics inspect JSX branches, scope seams, and provider variants. Vue diagnostics inspect SFC frame enumeration, previewability, declared injection-key variants, and template directive reachability. Vue branch analysis is template-first: `v-if`, `v-else-if`, `v-else`, `v-show`, `v-for`, and dynamic component `:is` checks are derived from template expressions over frame `props`, `scope`, and injected `providers` values, not from arbitrary `