Console nav / IA — modules vs operator tools (the org-context resolution)
reference· designer · updated 2026-06-27 · source
Settles the two-overlapping-navs collision now that the module system is live, so effort #963
(module pages + per-module sub-navigation) builds on a decided IA. Sibling to
console-structure-spec.md (the org→module→section→resource model +
the GLOBAL/ORG/MODULE config taxonomy), console-design.md (Aurea Ink matiz,
black top-bar chrome), and console-overview-spec.md (the org landing).
Grid v2: grid-system.md. Hues: color-scope.md (Amber =
console). Rubric: design-principles.md.
Frontend implements off this; Designer reviews the rendered result. This is IA + nav pattern + routing shape + tokens, not pixel layout.
The collision (today). Two navs occupy one flat org namespace:
- Operator top-bar tabs (
packages/ui/src/admin/console/console-nav.ts) — Overview · Builder · Datos & Memoria · Finanzas · Infra · Logs · Configurar, all at/org/<slug>/<segment>.- Modules (
apps/console/src/lib/module-overview-fixtures.ts) — Educación · Finanzas · Certificados · CRM, surfaced as Overview cards, each pointingsegmentinto the same flat namespace.So Finanzas is a tab and a module on the same route; Educación squats on
/datos(overlapping the Datos & Memoria tab); Certificados/CRM fall through the[section]catch-all stub. The fix is to give modules their own namespace and split the nav into two classes.
1. The decision — three nav classes, modules are primary
The org context (L2) nav resolves into three classes, by what they answer. Knowing a thing's class is the structural call:
| Class | Items | Route shape | Source | Role in chrome |
|---|---|---|---|---|
| Org index | Overview | /org/<slug>/overview | fixed | Row-2 first tab, the org home |
| Modules (primary) | Educación · Finanzas · Certificados · CRM · (future) | /org/<slug>/<module>/<section> | generated from enabledModules (#922) | Row-2 left cluster, the protagonists |
| Operator tools (secondary) | Builder · Memoria & IA · Infra 🔒 · Logs · Configurar | /org/<slug>/<tool> | fixed registry | Row-2 right cluster, all folded into one Operador ▾ dropdown |
Modules are the primary org nav — they are the client's business, what the operator works in day-to-day, and they must scale to N by declaration (structure-spec §1.2). Operator tools are cross-cutting utilities — not domains; they recede into a single dropdown.
Naming (recommended, ratify with José): "Datos & Memoria" → "Memoria & IA". The data now lives in the modules (alumnos, pagos…), so "Datos" duplicated the module nav and muddied the section. What remains here is the AI substrate — connectors + the memory layer that powers the org's agents. Renaming removes the confusion (full concept:
console-memoria-spec.md). The nav spec uses the new name throughout; revert to "Datos & Memoria" only if José rejects it.
1.1 Row 2 layout — primary clean, operator tools in ONE dropdown
Decision: the primary row carries only Overview + the enabled modules. All five operator
tools collapse into a single right-aligned Operador ▾ dropdown — including Configurar. This
keeps the module nav pristine and scalable; the ⌘K command pill (Row 1) is the instant fast-path to
any tool, so the dropdown costs no reach.
Row 2 ┌──────────────────────────────────────────────────────────────────────────────┐
│ Overview │ Educación Finanzas Certificados CRM …………hairline…… [ Operador ▾ ] │
└──────────────────────────────────────────────────────────────────────────────┘
org index ── enabled modules (generated, expand left) ── operator tools (one chip)
- Overview first (org index / home), then the enabled modules inline (the expanding
region; module overflow folds into a
Más ▾exactly like the currentconsole-tabs.tsxmechanic). - A full-height hairline separator (
border-l border-console-chrome-edge) then the right-alignedOperador ▾chip.
Why all five (incl. Configurar) and not a persistent gear: one mechanism is calmer than a chip
- a stray gear, and grouping (below) keeps Configurar one obvious click away. Settings is reachable via the dropdown's "Organización" group and ⌘K — a persistent gear would re-introduce a second trailing affordance the fold exists to remove.
The Operador ▾ dropdown — full spec
| Aspect | Spec |
|---|---|
| Trigger | A chip at the row's right end: label Operador + ArrowDown01Icon (14px, 1.5). type-nav-label. Idle text-fg-muted → hover text-fg-primary. Active (any operator tool is the current route) → text-fg-primary and the chrome Amber under-rule sits under the chip (reuse the existing Más ▾ active-state logic in console-tabs.tsx). |
| Contents (grouped) | Two labelled groups, forward-compatible with the L1 lift (§1.3): · Plataforma — Builder · Infra 🔒 · Logs · Organización — Memoria & IA · Configurar Group headers: type-meta text-fg-muted (uppercase eyebrow), non-interactive; a border-t border-border-subtle between groups. Each item: HugeIcon (16px) + label, superadmin items keep the lock glyph. |
| Placement | Radix DropdownMenu, align="end", sideOffset={4}, anchored to the chip (top-right of the chrome). min-w-56. |
| Item active state | the current tool → text-accent-text (Amber, the dropdown's read-only "you are here" cue — a category echo of the chrome accent, not a second action magnet); others text-fg-secondary → data-[highlighted]:bg-surface. Mirrors the existing Sección ▾ item styling. |
| Tokens | content rounded-md border border-border-default bg-elevated p-1 shadow-md z-[var(--z-modal)]; items rounded-sm px-2.5 py-2 font-… via type-nav-label. All .console scope — no raw hex. |
| Motion | popover scales from the trigger (transform-origin), --duration-enter (220ms) --ease-out; exit --duration-exit (160ms). prefers-reduced-motion → opacity-only. No animation on the ⌘K path. |
| Responsive | unchanged from today: <md the whole primary row (Overview + modules) already collapses to Sección ▾; the operator tools join that same dropdown under their groups so there is one menu on mobile. |
Why fold the tools, not interleave them. Modules (4) + Overview + 5 tools = 10 mono-label tabs is a wall, and it worsens as modules grow — the exact anti-pattern the module system exists to avoid. One grouped dropdown makes "modules = the work, tools = utilities" legible (Simplicity, Familiarity) and frees the module region to expand. The grouping (Plataforma / Organización) pre-stages the L1 lift (§1.3): when Builder/Infra/Logs move to the platform shell, the dropdown simply drops the Plataforma group.
1.2 Duplication — resolved explicitly
| Conflict | Resolution |
|---|---|
| Finanzas (tab ⨯ module) | Folds into the module. Remove the standalone Finanzas operator tab from org context. In an org, "Finanzas" = the client's finance operations = the Finanzas module (/org/<slug>/finances/*: pagos, facturas, conciliación — the admin's Pagos & CFDI domain). The operator billing the client (MRR/receivables) is a platform L1 roll-up (deferred, structure-spec §1) plus an org "Billing setup" row under Configurar (org scope). One word, one meaning in org context. |
Datos & Memoria (tab overlapping Educación's /datos) | Renamed to "Memoria & IA"; stays an operator tool — does NOT fold into a module. The data is the modules; what remains is the AI substrate (connectors + memory layer). It is the ADR-007 substrate (cross-module), distinct from any single module's CRUD. The overlap was an accident of Educación squatting on /datos; fixed in §3 (Educación → /education/*). It moves to /org/<slug>/memoria and lives in the Operador ▾ dropdown (Organización group). Full concept + screen: console-memoria-spec.md. |
Certificados / CRM (modules in the [section] catch-all) | Become first-class module namespaces (/org/<slug>/certificates/*, /crm/*); the flat catch-all is retired (§3). |
Net: modules own a /<module>/<section> namespace; operator tools own flat /<tool> segments;
the two never collide because they live at different levels.
1.3 Forward note — L1 vs L2 (deferred, unblocks #963 either way)
Builder and Infra are genuinely platform (L1) surfaces (operator building tedos itself / fleet
health — console-design.md §5), not per-org. In the two-level target (structure-spec §1) they
lift to an L1 platform shell and leave org context entirely; only Memoria & IA · Logs (org
audit) · Configurar (org) remain org-context operator tools (the dropdown's Organización group).
Until that lift lands they all sit in the Operador ▾ dropdown. #963 does not depend on the
lift — it only touches module nav.
2. The module sub-nav (the #963 pattern)
Inside a module (/org/<slug>/<module>/<section>) the module's sections need a secondary nav.
The columns + data joins each section table renders are specced in
console-module-tables-spec.md (names not FK ids, formatted
numbers/money/dates, categorical badges).
2.1 Placement — a content sub-tab strip, not a left rail, not a third chrome row
A horizontal sub-tab strip in the content area, at the top of the module page, below a
compact module header and above the section body. This reuses the Builder sub-tab pattern
(console-design.md §4 Builder: "Sub-tab strip under the primary tab"), so it is already a console
idiom.
┌ black top-bar (Row 1 + Row 2) ──────────────────────────────────────────┐ chrome (unchanged)
└──────────────────────────────────────────────────────────────────────────┘
Educación [ Nuevo alumno ] module header (+ the
──────────────────────────────────────────────────────────────────────── ONE content amber moment)
▸ Alumnos Cursos Inscripciones section sub-tab strip
──────────────────────────────────────────────────────────────────────── ← border-b border-border-subtle
┌ DataTable (ResourceListView) ─────────────────────────────────────────┐ section body (full width)
Not a left rail: module screens are data-dense (wide DataTables) and honor G5 anti-stretch —
they want full width. The left rail stays reserved for Configurar (ConfigureLayout), keeping
that surface's "the one local left-nav" identity intact (console-design.md §4 Configurar). Sub-tabs
≠ config sections.
Not a third top-bar row: a third sticky bar pushes content down and competes with the chrome's two rows. The strip lives in the content, scrolls with it (or sticks just under the top-bar — Frontend's call; default static at content top).
2.2 Active state — NEUTRAL (no second Amber)
The chrome's Amber under-rule already marks the active module in Row 2 (chrome budget). The
module screen's content Amber moment is reserved for its one primary action (e.g. Nuevo alumno / Crear) or a data protagonist. Therefore the section sub-tab active state is neutral —
it consumes no accent (G17: one Amber per screen, and the chrome under-rule + one content moment is
the established two-budget pattern, same as the Overview screen).
Sections are route links, not a local toggle, so style them as link-tabs (not a Segmented
thumb):
| State | Treatment |
|---|---|
| active | bg-elevated text-fg-primary rounded-md · type-nav-label · aria-current="page" |
| inactive | text-fg-muted → hover text-fg-primary + bg-surface |
| focus | focus-visible ring (--shadow-focus) — a11y gate, not polish |
This reads clearly distinct from the chrome's sliding Amber under-rule (a different shape — pill vs under-rule — and a different hue — neutral vs Amber).
2.3 How sections render
Each section is a resource list → ResourceListView (structure-spec §3) over the DataTable
family, with its own 4 states + toolbar (search/filter/columns). The sub-tab navigates between
sections; the body is the generic list view fed by the resource registry. Resource detail/edit/create
render on deeper routes (§3), not in the strip.
Module landing (/org/<slug>/<module> with no section): redirect to the module's default
section (first in sections[]) — e.g. /org/<slug>/education → …/education/alumnos. No empty
module index page.
2.4 Responsive
- ≥sm — sub-tabs inline (modules carry 2–3 sections; they fit). Wrapping is forbidden (no
ragged second line);
overflow-xis clip (G6). - <sm (or if a future module's sections overflow) — collapse the whole strip to a single
Sección ▾dropdown, mirroringConsoleTabs'<mdbehavior. One scroll-free pattern at every size.
3. Routing shape (concrete)
/ org picker (L1 home, no tabs)
/org/<slug>/overview org index / landing [org index]
/org/<slug>/<module> → redirect to default section
/org/<slug>/<module>/<section> resource LIST (the section body)
/org/<slug>/<module>/<section>/<id> resource SHOW
/org/<slug>/<module>/<section>/<id>/edit resource EDIT
/org/<slug>/<module>/<section>/new resource CREATE
/org/<slug>/memoria[/<section>] Memoria & IA [operator tool] (was /datos)
/org/<slug>/builder[/<section>] Builder [operator tool]
/org/<slug>/infra Infra 🔒 [operator tool]
/org/<slug>/logs Logs [operator tool]
/org/<slug>/configurar/* Configurar (org) [operator tool]
3.1 Module + section slugs
Route segments are identifiers → English (language.md), even though UI labels stay Spanish.
The module segment = the module key. Recommended mapping (UI label → route slug):
| Module (label) | key / segment | Sections (label → slug) |
|---|---|---|
| Educación | education | Alumnos → students · Cursos → courses · Inscripciones → enrollments |
| Finanzas | finances | Pagos → payments · Facturas → invoices · Conciliación → reconciliation |
| Certificados | certificates | Emitidos → issued · Plantillas → templates |
| CRM | crm | Contactos → contacts · Leads → leads · Pipeline → pipeline |
The English-slug migration of the existing Spanish stubs (
datos/cursos/finanzas…) is a Frontend/Tech-Lead application oflanguage.md(latest-touched wins). The load-bearing design decision is the module-namespacing (/<module>/<section>), which kills the collision regardless of slug language. If the operator/Tech-Lead prefer Spanish slugs short-term, namespacing alone still resolves it — but new routes should land English.
3.2 What happens to the existing routes + the [section] catch-all
- Operator-tool flat routes stay:
overview,builder,infra,logs,configurarremain at/org/<slug>/<tool>.datos→ renamedmemoria(redirect the old path). finanzasflat route is removed/repurposed — folds into thefinancesmodule namespace (/org/<slug>/finances/*). The old/org/<slug>/finanzaseither redirects to…/financesor is deleted with the standalone tab.- The
[section]catch-all stub is RETIRED. It catches module sections at the wrong (flat) level and is the source of the collision. Replace it with the nested module tree:/org/<slug>/[module]/page.tsx— redirect to default section./org/<slug>/[module]/[section]/page.tsx— the genericResourceListView(data-in-props, fed by the resource registry; structure-spec §5 composition contract).- deeper
[id]/[id]/edit/newroutes per §3 as the views land. Static operator-tool segments take precedence over the[module]dynamic segment (as the static siblings already do over today's[section]).
module-overview-fixtures.ts(Overview cards) updates: Educaciónsegmentdatos→education; Finanzasfinanzas→finances; sectionsegments become module-relative so a card's section link resolves to/org/<slug>/<module>/<section>(e.g. Educación →education+students), andAbrir→ the module's default section. This also de-collides the Overview links.console-nav.ts: drop Finanzas from the operator tab list; the module set is not hardcoded here — it generates fromenabledModules(a new module-nav source, structure-spec §1.2).console-nav.tskeeps only the operator tools (Builder · Memoria & IA · Infra · Logs · Configurar) plus Overview, each carrying agroup: 'plataforma' | 'organizacion'field so the dropdown renders the two groups (§1.1) from data.
3.3 Operator-tool page layouts (target, so Frontend fixes to a spec)
The operator-tool pages need layout fixes (José). Targets — brief, token-level; full per-screen
directions live in console-design.md §4. All reuse the §4 tokens + the §2
neutral content sub-tab strip where a screen has sub-sections (no second Amber).
| Tool | Target layout |
|---|---|
| Builder | Adopt the §2 sub-tab strip for its sub-sections (Estado · Roadmap · Bounties · Mi trabajo · Aportar al kit) — same neutral pill strip as modules, not an ad-hoc tab row (this is the "tabs layout" fix). Body per sub-tab: Estado = efforts board — a CSS-grid of columns (Backlog · En progreso · En revisión · Hecho), each column a vertical stack of effort cards (parent title, sub-issue progress ring, role = neutral/info badge, PR/deploy chips in type-mono); one Amber moment = the single focused/In-Progress effort. Roadmap = a lane/timeline. Cards drag-snap (C2: scale(1.02) lift, dotted-ink ghost, transform-only). Full-width canvas (G5), overflow-x: clip (columns scroll-y within, page never scrolls-x). 4 states; empty = "Sin efforts abiertos". |
| Infra 🔒 | Read-only, no accent. Top: a grid of service health cards (Vercel admin · Vercel api · Fly api · DB · Engine) — each bg-surface rounded-xl card with a StatusDot (success/warning/danger), last deploy, region, latency sparkline. Below: a recent-deploys DataTable (service · type-mono sha · status badge · time · duration). Row → DetailPanel (right slide-in; rollback = the only danger button, inline confirm). "Solo superadmin" pill + lock glyph in the header. |
| Logs | Dense event-log DataTable (Tipo · Sujeto · Proyecto · Fuente · type-mono timestamp) + a left filter rail (event type · subject · source) + search + a range control. Virtualize; selection is instant (no per-row tween — high-frequency surface). Row → DetailPanel (right) with the raw payload as a type-mono JSON block on bg-sunken (copy button). Selected row = bg-accent-soft tint (the one Amber, as an active-row cue). 4 states; empty = "Sin eventos en el rango". |
The left filter rail (Logs) and the ConfigureLayout left rail (Configurar) are the only local
left-navs (console-design.md); module + Builder sub-sections use the top sub-tab strip, never a
rail (§2.1).
4. Tokens · states · motion (Frontend invents nothing)
All utilities resolve through the .console ink scope already on <body>
(packages/ui/src/admin/console/console-theme.css). No raw hex in TSX.
Tokens
| Element | Token / utility |
|---|---|
| Row-2 separator (modules ⟍ tools) | border-l --console-chrome-edge |
Operador ▾ chip | type-nav-label · text-fg-muted → hover text-fg-primary; active (a tool is open) text-fg-primary + the chrome Amber under-rule |
| Chrome active under-rule (the ONE chrome Amber) | bg-accent, 2px, slides via translateX (unchanged) |
| Module header label | type-h2 text-fg-primary |
| Sub-tab strip container | flex gap-1 + border-b border-border-subtle; overflow-x: clip |
| Sub-tab (active) | bg-elevated text-fg-primary rounded-md (R3) · type-nav-label |
| Sub-tab (inactive) | text-fg-muted → hover text-fg-primary + bg-surface |
| Section body | ResourceListView / DataTable on bg-surface, header bg-sunken |
| Primary action (the content Amber moment) | the foundation Button (one per module screen, e.g. Nuevo …) — Amber per the scope |
Type roles: type-nav-label (tabs), type-h2 (module header), type-meta/type-mono (table) —
all existing (admin-ui-conventions.md §Typography). Radii G19. Icons HugeIcons stroke 1.5.
The 4 states (every section list — frontend-guardrails.md H1)
| State | Treatment |
|---|---|
| loading | layout-matched skeleton over the DataTable (animate-pulse on bg-sunken); no spinner |
| empty | EmptyState — honest copy ("Sin alumnos todavía"), never fake rows; seed mark allowed in calm empties |
| error | inline ReadError (danger tone, role="alert"), reachable via ApiResult.!ok (DF4) — never inferred from empty |
| success | the table |
The sub-tab strip itself needs no separate states (it is nav); a module with zero enabled
sections → the module landing shows the EmptyState instead of redirecting.
Motion (console tokens, transform/opacity only — console-design.md §6, C3)
| Interaction | Spec |
|---|---|
| Chrome under-rule | slides translateX 200ms --ease-out (unchanged); width/left never animated |
Operador ▾ / Sección ▾ popover | scales from trigger (transform-origin), --duration-enter (220ms) --ease-out |
| Sub-tab switch | color/bg only (no sliding rule, no thumb) — instant, --duration-press (120ms) --ease-out. Nav between data views must feel immediate (perceived performance). |
| Section list mount | optional opacity 0→1 + translateY(8px→0) --duration-enter --ease-out; reduced-motion → instant |
| Press feedback | scale(0.97) --duration-press on the primary action |
prefers-reduced-motion → kill transforms/loops, keep ≤200ms opacity crossfades. No animation on
keyboard-repeated actions (⌘K instant). Sub-tab navigation is not animated with a sliding
indicator — that would read as a second under-rule competing with the chrome's Amber one.
5. Principle check + open items
Rubric (design-principles.md): Simplicity + Familiarity are
load-bearing — splitting nav into modules (primary, generated) vs operator tools (folded) makes the
mental model legible and stops the wall-of-tabs. Flexibility — the module row + [module]/[section]
tree scale to N modules by declaration (structure-spec §5), zero new nav code per module.
Craft/Delight — one Amber chrome under-rule + one content Amber moment per module screen; neutral
sub-tabs; calm ink.
Named trade-off — Simplicity vs Agency: folding the five operator tools behind Operador ▾
costs one click to reach Builder/Logs/etc. Mitigated by ⌘K (Row 1, instant) and by the forward L1
lift that removes Builder/Infra from org context entirely (§1.3). The win — modules unambiguously
primary, no collision — outweighs the extra click for low-frequency tools.
Open items (for the operator / Tech-Lead — assumed, not decided):
- "Datos & Memoria" → "Memoria & IA" rename (§1, recommended) — needs José's ratify. The whole nav + memoria spec assume the new name; defaulting to it.
- Slug language (§3.1) — English module/section slugs (recommended,
language.md) vs keep Spanish short-term. Namespacing resolves the collision either way. - L1/L2 lift (§1.3) — when Builder/Infra/platform-Logs/Finanzas-rollup move to an L1 platform
shell; until then they sit in the
Operador ▾dropdown (and the dropdown drops its Plataforma group on the lift). enabledModulessource — the module-nav generation reads per-org enablement (#922); fixture today, typed contract when #934/#922 land.- Module landing — redirect-to-default-section (recommended) vs a per-module mini-overview. If a module ever wants its own glance page, the landing route hosts it instead of redirecting.
Next step: Frontend builds the [module]/[section] route tree + the neutral sub-tab strip
(reusing Builder's pattern) + updates console-nav.ts (drop Finanzas) and module-overview-fixtures
(module-relative segments). Designer reviews the rendered result against the 8 principles before
close.