AdminSection
One section of the admin panel, as a plugin declares it. A section IS a Milpa Component — or a whole VIEW of them. It either names a component the panel already knows (the dashboard primitives — `metric-card`, `data-table`, `dashboard-grid`… — rendered with `$props`), or it brings its own: a `$definition` plus the `$renderer` that paints it, which the panel registers under `$component` before composing; or it brings a `$view` ({@see DeclaredView}) — a tree of components with their definitions, renderers, props and signal seeds, which the panel registers and compiles the same way (greenhouse decisions/0211). Either way the panel never learns the plugin's name — it learns the section's, and that is enough to list it, route it, render it and attribute it: the header of every section says «declared by <Plugin>», read from the catalogue, never from the section. What the panel hands the component: the `ComponentContext` it mounts with carries the section's component id (`milpa-admin-section-<id>`), the `principal` the gate authenticated (the actor's id, or null when nobody is signed in — the same fact the topbar shows), the `locale` the page answers in, the panel's `route` (its mount point) and, in `meta`, the host facts a guest may need without widening any contract: the gate in effect, the active section's id and the request's query ({@see \Milpa\Admin\View\AdminShell::META_GATE}, `META_SECTION`, `META_QUERY`). For the narrow shape the props are `$props` plus the request's query under `query`; a view's props are its own, per component. One prop name is RESERVED: `query`. The shell hands every active section the request's query params under `props['query']` (greenhouse decisions/0205), so a section that declared its own `query` would see it silently replaced on every request — the constructor refuses it instead. The sidebar lists sections by `$group` — one heading per distinct value, `admin` (the panel's own) first, then `app` (the default), then `agent`, then any other name in alphabetical order — and, within a group, by `$order` (lower first; ties break by `$id`, alphabetically). The panel OPENS on the first section in that same (`order`, `id`) order across every group, which is why the convention matters: the panel's own sections take 10..40 (Plugins 10, Routes 20, Settings 25, Stack 30, Dev tools 40), and a guest picks an order AFTER those — greenhouse decisions/0210 names 60 for the Desktop's Agent section — unless it means to be the page the panel opens on. A guest at order 10 named `agent` would tie with Plugins, win the tie by id, and become the front page. What a section may not do: bring its own `$definition` under a name the panel registers itself (a dashboard primitive, `admin-sidebar`, `admin-section-header`) — naming one is fine, redefining one would repaint every section that names it, so the panel refuses the section and says so.
AdminSection::__construct()
public function __construct(string $id, string $title, string $component = '', array $props = [], int $order = 0, string $group = self::GROUP_APP, ?Milpa\Live\Contracts\Component\ComponentDefinitionInterface $definition = null, ?Milpa\Live\Contracts\Rendering\ComponentRendererInterface $renderer = null, string $icon = '', ?Milpa\Admin\Section\DeclaredView $view = null, string $parent = ''):Parameters
| Name | Type | Description |
|---|---|---|
| $id | string | the section's identity and URL segment (`^[a-z][a-z0-9-]{0,40}$`) |
| $title | string | a catalog key the panel knows, or the literal title |
| $component | string | the Milpa component name that renders the section — empty, and only empty, when the section brings a `$view` instead |
| $props | array<string, mixed> | the props the component mounts with — never `query`, which is reserved: the shell fills it with the request's query params. Empty with a `$view`, whose props are per component |
| $order | int | sidebar position within the group, and the panel's front page across groups (lower first; ties break by id). The panel's own are 10..40 — a guest picks an order after those |
| $group | string | the sidebar group: {@see self::GROUP_ADMIN} (the panel's own), {@see self::GROUP_APP} (default), {@see self::GROUP_AGENT}, or any other name — one heading per distinct value, admin → app → agent → others alphabetically; the heading comes from the catalog when it knows the group, else the raw value uppercased |
| $definition | (ComponentDefinitionInterface | null) | the component, when the section brings its own |
| $renderer | (ComponentRendererInterface | null) | the renderer for that component (required with it) |
| $icon | string | a glyph for the sidebar item, optional — painted before the label |
| $view | (DeclaredView | null) | the whole tree the section declares instead of one component (greenhouse decisions/0211); with it, `$component`, `$props`, `$definition` and `$renderer` stay empty — the view carries them |
| $parent | string | the id of the section this one belongs UNDER, optional. A child stays out of the main navigation and is reached through its parent's gear; it routes at `{route}/s/{id}` like any other, keeps the same middleware, and can never be the panel's front page. One level only: a child of a child is refused, because a gear that opens a gear is a menu nobody asked for. A parent id nobody declared leaves the section a ROOT rather than hiding it — uninstalling one plugin must not make another's section unreachable (greenhouse decisions/0268) |
AdminSection::ofView()
public static function ofView(string $id, string $title, Milpa\Admin\Section\DeclaredView $view, int $order = 0, string $group = self::GROUP_APP, string $icon = '', string $parent = ''): selfA section that declares a whole {@see DeclaredView} — the shape a plugin brings its own UI in (greenhouse decisions/0211). The same as the constructor with `view:`, said in one line.
Parameters
| Name | Type | Description |
|---|---|---|
| $id | string | |
| $title | string | a catalog key the panel knows, or the literal title |
| $view | Milpa\Admin\Section\DeclaredView | |
| $order | int | sidebar position within the group; the panel's own take 10..40 |
| $group | string | {@see self::GROUP_ADMIN}, {@see self::GROUP_APP}, {@see self::GROUP_AGENT}, or any name |
| $icon | string | a glyph for the sidebar item, optional |
| $parent | string | the id of the section this one belongs under — out of the main navigation, behind that section's gear, same URL and same gate. A factory that could not say this would make nesting a privilege of the constructor, and every guest that brings a view uses the factory (greenhouse decisions/0268) |
AdminSection::isCustom()
public function isCustom(): boolTrue when the section brings its own component instead of naming a registered one.
AdminSection::hasView()
public function hasView(): boolTrue when the section declares a whole view instead of one component.