Plugin Architecture — Boundaries & Ownership
Defines what belongs to the host plugin (ui/) vs what comes from the lib (@middag-io/react).
This guide uses MIDDAG Account (WordPress) as reference, but the same boundaries apply to any Inertia host.
Plugin Structure (scaffolded by create-middag-ui)
ui/src/
├── app/ # PLUGIN — App bootstrap & registration
│ ├── register-{product}.ts # Selective block/shell/layout registration
│ └── page-resolver.tsx # Contract: prefix + direct page resolution
├── adapters/ # PLUGIN — Inertia mock adapters (dev only)
├── mock/ # PLUGIN — Dev mock data & navigation
│ ├── navigation.ts # Sidebar structure for dev server
│ └── data.ts # Synthetic props for direct pages
├── components/
│ └── {product}/ # PLUGIN — Product-specific components
├── lib/ # PLUGIN — Utilities & hooks
├── pages/ # PLUGIN — Page components
│ └── mock-contracts/ # Static PageContract for dev mock
├── contracts.ts # PLUGIN — Type re-exports from lib
├── entry-wp.tsx # PLUGIN — WP production entry (or entry-{host}.tsx)
├── main.tsx # PLUGIN — Dev server entry
├── app.tsx # PLUGIN — Dev mock app (BrowserRouter + routes)
├── tailwind.css # PLUGIN — Tailwind v4 @theme config
├── theme.css # PLUGIN — Host isolation + token overrides
└── theme-*.css # PLUGIN — Optional theme variantsBoundaries
PLUGIN (host developers own this)
Everything in ui/src/ that is specific to the product.
| Area | What | Rule |
|---|---|---|
app/ | Bootstrap & registration | Register only blocks/shells the plugin uses |
components/{product}/ | Product-specific components | Custom UI for the product |
lib/ | Utils & hooks | Shared utilities for the plugin |
pages/ | Direct pages | Pages using usePage() from Inertia |
mock/ | Dev data & navigation | Mock data files, not inline in app.tsx |
*.css | Themes & Tailwind | Tokens, host isolation, theme variants |
entry-*.tsx | Host entry | Boot Inertia in production |
main.tsx / app.tsx | Dev entry | Mock app with BrowserRouter |
LIB (comes from @middag-io/react — don't copy)
| What | How to use |
|---|---|
| Shells | registerShell("product", ProductShell) |
| Layouts | registerLayout("stack", StackLayout) |
| Blocks | registerBlock("dense_table", DenseTableBlock) |
| ContractPage | <ContractPage contract={...} /> |
| Registries | import { registerBlock } from "@middag-io/react" |
| Providers | I18nProvider, AuthProvider, FlashProvider, etc. |
| Theme tokens | import "@middag-io/react/style.css" |
| ReUI components | import { Button } from "@middag-io/react/reui/button" |
| Mock utilities | import { MockPageProvider } from "@middag-io/react/mock" |
Rules
- Never copy lib components — import from
@middag-io/reactor@middag-io/react/reui/* - Product-specific components →
components/{product}/(notblocks/) - Mock data →
mock/data.ts(not inline in app.tsx) - Navigation →
mock/navigation.ts(not inline in app.tsx) - CSS isolation →
theme.csshandles#middag-app { all: initial }for WP - Selective registration →
register-{product}.ts(notregisterDefaults()in production for bundle size)
Data flow
Host (PHP/Moodle) → Inertia props → entry-{host}.tsx → page-resolver → ContractPage or DirectPage- Contract pages: Host sends
{ contract: PageContract }→ContractPagerenders - Direct pages: Host sends custom props → React component consumes via
usePage() - Dev mode:
app.tsxprovides mock data viaMockPageProvider
Scaffold template
When create-middag-ui scaffolds a new project, it should generate:
src/
├── mock/
│ ├── navigation.ts # buildNavigation() — sidebar for dev
│ └── data.ts # sharedProps + mock page props
├── pages/
│ └── mock-contracts/ # Static PageContract examples
├── app.tsx # Slim: imports from mock/*, defines routes only
├── main.tsx # Dev entry
└── entry-wp.tsx # Host entry (or entry-moodle.tsx)app.tsx should contain only route definitions and wrappers — no inline data or navigation.