```
The real hook can call any React hooks. `createGScopeHook` wraps it so that in preview, the frame-supplied scope is returned instead.
For discriminated unions, combined scope+provider patterns, and collection branches, see the [full reference](../skills/authoring-runelight-react/REFERENCE.md).
## The Hook Boundary
Inside a `.g.tsx` component body, call only:
- `useGContext(Provider)` — read provider values
- Hooks from `createGScopeHook(useRealHook)` — read scope
Never call `useState`, `useEffect`, `useQuery`, or other React/library hooks directly. Production behavior lives inside the real hook; `createGScopeHook` wraps it.
## Frames
Frames are static object literals attached to the component export.
**Naming:** describe the visual state, not the implementation.
| Component kind | Good names |
|---------------|------------|
| Basic UI | `default`, `disabled`, `danger`, `open`, `selected`, `longLabel` |
| Composite | `empty`, `populated`, `overflowing`, `permissionDenied` |
| Page/feature | `loading`, `errorRetryable`, `ready`, `unauthorized` |
**Rules:**
- Happy-path frame first, then edge states.
- At least two frames (unless the component truly has one stable visual state).
- Required static `description` strings for every frame.
- Static object literals only — no computed keys, no dynamic generation.
- No secrets or customer data.
- No-op functions for callbacks: `increment() {}`.
- JSX-valued props, `children`, render props, icons, actions, and slots are visual inputs. Use them only when they are the real public contract, and make the frame values representative enough to review. A placeholder `` is not acceptable when the slot carries the surface's actual visual content.
- When a JSX-valued input is needed, trace what production passes and use the closest safe equivalent: real pure child components, design-system pieces, or a local static fixture that preserves density, hierarchy, labels, controls, and edge state. If that would require mounting old hookful/runtime UI, descend, extract, or migrate that child surface instead of faking it.
**Coverage:** think about boundary states for every component — happy path, empty, loading, error, overflow, disabled, first-use, permission-denied.
## JSX Branches
If props, scope, or provider context decides whether JSX renders, write that branch so `runelight check` can trace it back. Use direct conditionals, `if` returns, `&&`, `||`, and traceable `map` callbacks. Avoid helper predicates, `switch`, or stored JSX variables.
For the full rules on what counts as inspectable vs. opaque, see [Static Contract](./runelight-static-contract.md).
## Verification And Feedback
```sh
runelight check src/Badge.g.tsx # validate the static contract
runelight preview-targets src/Badge.g.tsx --json # list browser-ready preview paths
runelight capture --path "" # capture one selected rendered state
runelight check src # validate a directory
```
Authoring is a feedback loop, not only a static check:
1. Run `runelight check ` and fix diagnostics.
2. Choose the observation root. Default to the nearest meaningful covered app/screen/parent entry that renders the component, especially when checking layout, spacing, density, theme, container width, or sibling alignment.
3. Use the component's own entry only when no covered parent exists, the component is itself the app/screen entry, or you are debugging its isolated frame contract.
4. Run `runelight containing-frames --json`. Prefer contexts whose `root.coordinate` differs from the target; if every returned root is the target, treat it as target-level coverage rather than broader app/screen context. If no ancestor context is available, run `runelight preview-targets --json`.
5. Choose representative paths from the output: the happy path plus new, changed, or risky edge frames. For child-component work, prefer paths whose `paths` nodes include both the parent state and the target child state.
6. Open the `/runelight?...` paths in the browser, or capture selected targets with `runelight capture --path ""`.
7. Compare the rendered output with the frame `description`, intended props/scope/provider values, branch coverage, parent layout context, and local design language.
8. If the render is wrong, edit the component or frames and observe the same targets again.
9. Finish with `runelight check` and project typecheck or host build when the change can affect ordinary app code.
If the Host or preview route is not wired yet, report that rendered feedback is blocked and follow the setup playbook before claiming preview confidence. If the work becomes subjective visual polish rather than coverage authoring, use the `polish` workflow and its required sync.
| Diagnostic | Fix |
|-----------|-----|
| `missing-frames` | Add `Component.frames = { ... } satisfies GFrames<…>` |
| `non-static-frame-key` | Use literal frame keys |
| `missing-frame-description` | Add a static `description` string to the frame |
| `non-static-frame-description` | Replace the frame `description` with a literal string |
| `thin-wrapper` | Move the real visual TSX into the `.g.tsx` export, or attach frames to the component that owns it |
| `non-runelight-hook` | Wrap with `createGScopeHook`, call only the returned hook |
| `scope-hook-frames-unsupported` | Move `.frames` from scope hook to component export |
| `missing-provider-variant-frames` | Mark frames with `GProviderFrame` for every consumed provider variant |
| `opaque-jsx-control-flow` | Rewrite branches as direct props/scope/context expressions |
| `uncovered-jsx-branch` | Add a frame that makes the branch reachable |
Full diagnostic list: [Static Contract — Diagnostics](./runelight-static-contract.md#diagnostics).