Console structure — IA + reusable-component design spec
reference· designer · updated 2026-06-27 · source
The big-picture design spec for how the operator console is STRUCTURED as a configuration
surface, built thin over @tedos/core (#934, providers +
resources + common views) + @tedos/ui and the runtime per-tenant config from #922. Sibling to the
focused console-overview-spec.md (the Overview screen) and the parent
brief console-design.md (Aurea Ink matiz, chrome, screens). Grid v2:
grid-system.md. Hues: color-scope.md (Amber = console).
Frontend implements off this; Designer reviews the rendered result against the 8 principles
(design-principles.md). This spec is IA + reusable components, not
pixel layout — per-screen pixels live in Paper + the per-screen specs.
The thesis. The console is a configuration surface first. Almost every screen is "read a list / look at a record / configure X." So the console is built from a small kit of generic, token-driven views + sections, fed by declarations (modules, resources, config descriptors) — not bespoke screens. Adding a module or a config area is declaration + reuse, not new UI.
1. IA / levels — the navigational model end to end
The console has two context levels (mirroring the admin's two-level shell), because some surfaces have no single org and some are scoped to one. The split is the mental model.
L1 · PLATFORM context (no org) L2 · ORG context /org/<slug>/…
──────────────────────────────── ─────────────────────────────────
Org picker / Overview (module panels) /org/<slug>/overview
God-view (health) /god-view Module → section → resource:
Proyectos (all orgs) /proyectos list /org/<slug>/<module> (resource list)
Builder (Plan Engine) /builder show /org/<slug>/<module>/<id>
Infra 🔒 /infra edit /org/<slug>/<module>/<id>/edit
Logs (system/audit) /logs create /org/<slug>/<module>/new
Finanzas (roll-up) /finanzas Org Configurar /org/<slug>/configurar/*
Platform Configurar /configurar/* ├ Módulos (enable/disable)
├ Infra tokens / secrets ├ Marca / white-label
├ Módulos (registry/catalog) ├ General (org profile/domain)
├ Feature-flag defaults ├ <module> config ?module=<key>
├ Operator account ├ Conectores / secrets (org)
└ API keys └ Feature-flag overrides (this org)
Decided vs assumed (plan-mode): the two-level model + the 3-tier config taxonomy (§1.1) are this spec's recommendation. The currently shipped console collapses everything under
/org/<slug>/…(overview, builder, datos, finanzas, infra, logs, configurar) as an MVP simplification. Open decision for the operator: lift the cross-org / platform-global surfaces (God-view, Infra, platform secrets, module registry, platform flag defaults, account, API keys) to L1. Until then they live in L2 with platform data; the config taxonomy below holds either way — it's about what config is, not which URL it sits at.
1.1 The config taxonomy — GLOBAL vs ORG vs MODULE (the heart of "configuration surface")
Three scopes. Knowing which scope a setting belongs to is the single most important structural call,
and it maps 1:1 to @tedos/core's TenantConfig (#922).
| Scope | Owns | Examples | Backed by | Lives at |
|---|---|---|---|---|
| Platform (global) | operator-superadmin, cross-org, no tenant | infra tokens · deploy/infra secrets · the module registry (the catalog of buildable modules) · platform feature-flag defaults · operator account · API keys | operator config store (NOT TenantConfig) | platform Configurar / Infra |
| Org (per-tenant) | one client org | which modules are enabled · branding / white-label tokens · org profile + domain · org connectors/secrets · per-org flag overrides | TenantConfig.enabledModules + brandingTokens + authOrgId (#922) | /org/<slug>/configurar/* |
| Module (per-tenant, per-module) | one module within one org | module-specific settings (Educación: default cert template · Finanzas: CFDI 4.0 + reconciliation rules · CRM: pipeline stages) | per-module config in TenantConfig (module-keyed) | /org/<slug>/configurar?module=<key> |
Rule of thumb to place a setting: "would two different orgs ever want it different?" No → Platform. Yes, but it's org-wide → Org. Yes, and it only matters when a module is on → Module. Infra tokens are platform-global (one fleet, not per-client) — the canonical example.
1.2 The module system
A Module is a domain bundle (Educación / Finanzas / Certificados / CRM / …) in an extensible
registry. An org enables modules (TenantConfig.enabledModules); the console nav, the Overview
panels, and the per-module routes are all generated from the enabled set — never hardcoded per
module.
A ModuleDefinition (the declaration that makes a module exist) carries:
| Field | Drives |
|---|---|
key, label, icon | nav, Overview card identity, Configurar entry |
resources: Resource[] | the @tedos/core resource registry → ListView/ShowView/Edit/Create per entity |
sections | the segments under /org/<slug>/… (Alumnos, Cursos, …) |
summary (mapper) | the Overview card's hero + secondaries (console-overview-spec.md) |
config (descriptor) | the module's SettingsView (the "configure this module" form) |
enabledBy | gate — render only when key ∈ TenantConfig.enabledModules |
Adding a module = adding a ModuleDefinition + fixtures, not screens. The generic views render
it. This is the composition contract (§5).
2. Reusable SECTIONS — the "configure X" pattern
One pattern, reused for org settings · module config · feature flags · secrets · branding. A
config screen is a SettingsView (§3) = a stack of SettingsSections, each section a list of
rows. The repeated pattern already lives, un-extracted, in configure-screen.tsx — formalize it.
A SettingsSection = heading (type-h1/type-h2) + helper (type-body, fg-muted) + body
(rounded-lg border border-border-default bg-elevated, rows divide-y divide-border-subtle).
Row types (one grammar; pick per field):
| Row | Use | Anatomy | Control |
|---|---|---|---|
| ToggleRow | a boolean setting | label + helper, trailing Switch | Switch (44px hit, locked variant for "Siempre activo") |
| ConfigField | a text/number/select value | label above, error below, gap-2 | Input / Select / Textarea |
| SecretField | a credential / token / API key | label + masked value + meta (created/last-used) | reveal · copy · rotate · revoke (revoke = danger, lives in DangerZone semantics) |
| FlagMatrix row | a flag × scope override | flag label + helper · default badge · per-scope Switch columns | DataTable of Switch |
| DangerZone | destructive actions | bordered danger-toned group at the section foot | danger outline buttons; confirm inline, never modal-first |
Section grammar rules:
- One accent moment per config screen (G17): the left-nav active item (Amber-soft + accent-text). Primary save button stays ink (neutral) — don't add a second magnet.
- Status/category never the accent: flag default =
info/mutedbadge; secret state = neutral. - Forms: label above, error below,
gap-2(frontend-guardrails.md). Low-frequency surface → no entrance animation beyond theSwitchthumbtranslateX; instant feedback. - A config screen IS a
ConfigDescriptorrendered generically (§5) — the sameSettingsViewrenders org settings, a module's config, flags, and branding. Different data, one view.
3. Reusable VIEWS — map to @tedos/core
Refine-style "common views," fed by the DataProvider (keyed by Resource.name) + the resource
registry. Core-shared = driven purely by Resource + DataProvider, so admin / console / client
apps render the same. Console-specific = operator-domain surfaces.
| View | Purpose | Core (shared) or Console | Notes |
|---|---|---|---|
| ResourceListView | table over a resource (list) | Core | DataProvider.getList + 4 states; renders the EntityTable/DataTable; toolbar (search/filter/columns); routing via href factory |
| ResourceShowView | single record detail (show) | Core | DataProvider.getOne; fields from resource meta; may host a DetailPanel or full page |
| ResourceEditView / CreateView | resource form (edit/create) | Core | DataProvider.update/create; built from ConfigFields; ApiResult → inline error |
| SettingsView / ConfigView | the "configure X" pattern (§2) | Core (white-label) | renders a ConfigDescriptor of SettingsSections; writes via DataProvider.update on a config "resource" |
| ModuleSummaryView | module glance card on Overview | Console | per console-overview-spec.md (hero + secondaries) |
| OrgPickerView | org picker grid (L1 front door) | Console | cross-org; the one calm, accent-free picker |
| GodView / HealthMatrixView | cross-org health | Console | status dots only, never the accent |
| LogsView | event/audit table + DetailPanel | Console | virtualize; instant selection; JSON payload in bg-sunken |
| BuilderBoardView | Plan-Engine efforts board | Console | columns + drag-snap (C2) |
| InfraView | deploy-health grid + recent deploys | Console | read-only, no accent |
The core views are headless-first: they own data-fetch (via DataProvider), the 4 states, and
the resource→action→id wiring (via RouterProvider.parse); they render through the foundation
presentational primitives (§4). The console skin re-skins those primitives via the .console scope —
no fork. (Whether a core view ships its own thin presentation or only data+slots is a Tech-Lead code
call; the design contract is: data-in, 4 states, presentation = tokenized foundation components.)
4. Reusable COMPONENTS — the inventory
Each: purpose · home · note. All are token + type-* driven (white-label safe; a .console /
client scope re-skins with zero TSX change). [E] = exists, [N] = new to build, [X] = extract
from current apps/console/src/components/* (AP1 debt — presentational, must move to a package).
Chrome — @tedos/ui/admin/console (console skin)
| Component | Purpose | Status |
|---|---|---|
ConsoleShell | black two-row top-bar + single scroll area; owns scroll (G6 clip) | [E] |
ConsoleTopbar / ConsoleSwitchers | Row 1 — brand + Org/Env switchers (ghost chips) | [E] |
ConsoleTabs | Row 2 — primary tabs, sliding Amber under-rule + Más ▾ overflow | [E] |
ConsoleCommandPill / SearchCommand | ⌘K quick-nav (level-scoped) | [E] |
ConsoleAccount | operator avatar/menu | [E] |
ConfigureLayout | the ONE local left sub-nav (config areas); active = Amber-soft | [E] |
DetailPanel | right slide-in panel (Logs/Proyectos/Infra/Finanzas) — translateX, interruptible | [E] |
StatusDot / HealthMatrix | status grid (dots only, never accent) | [E] |
Configuration kit — @tedos/ui foundation (neutral, white-label; the config-surface primitives)
| Component | Purpose | Status |
|---|---|---|
SettingsSection | titled section (heading + helper + bordered body) | [X] from configure-screen |
ToggleRow | label + helper + trailing Switch row | [X] |
ConfigField | label-above / error-below form field | [X] (the Field helper) |
SecretField | masked credential + reveal/copy/rotate/revoke | [N] (ad-hoc in ApiKeysSection today) |
FlagMatrix | flag × scope override table of toggles | [X] from FeatureFlagsSection |
DangerZone | grouped destructive actions, inline confirm | [N] |
EnabledModulesList | org-config "which modules" — toggle rows over the registry | [N] |
BrandingTokenEditor | white-label token overrides (org branding) → TenantConfig.brandingTokens | [N] (advanced; flag for later) |
Data + metric primitives — foundation (reused; skin via scope)
| Component | Purpose | Status |
|---|---|---|
DataTable (+ toolbar/columns/pagination/bulk) | the EntityTable for every resource list | [E] |
StatTile / KpiTile | metric tile (god-view, module secondaries, Finanzas) | [E]/[X] |
ModuleCard (ModuleSummaryView body) | Overview card (hero + secondaries) | [X] from overview/page.tsx |
EmptyState · ReadError · SectionLabel · PageHeader · Button · Input · Switch · Badge | the 4-state + form + chip kit | [E] |
Home rule (ADR-008 A5 / color-scope): generic config + data primitives that any surface configures (incl. white-label client apps) → foundation
@tedos/ui(no ink, no Cian, no Amber). Console-domain chrome + operator surfaces →@tedos/ui/admin/consoleskin. Resource view scaffolds →@tedos/core. Never bake Amber into the foundation — it arrives via the.consolescope. Add the subpath export + an inventory row in the same PR that adds a component.
5. Composition contract — how a console screen is assembled
The whole point: declaration + reuse, never bespoke screens. A screen is a thin app shell that wires core providers to a pure, data-in-props view.
app route (thin) core providers pure view (data-in-props)
──────────────── ───────────── ─────────────────────────
/org/[org]/[module]/page.tsx
await params (org, module) ┐
resolve Resource by module key ├─► DataProvider.getList({resource}) ─► ApiResult<T>
build href factory orgHref(slug) │ │
pass data + state + hrefs ────────┘ ▼
ResourceListView
(EntityTable + 4 states)
Hard rules (ADR-008 A1–A4, BD):
- Apps are thin. Route file + data seam (
DataProvidercall) + a thin shell that composes a view. No presentation, no fetching inside the view. - Reads return
ApiResult(DF4) → the view branches!ok→ error, else empty-vs-success. This is what makes the 4 states reachable — never infer error from empty. - Routing is app-owned. Views take
href/ an href factory (orgHref(slug, segment)), never hardcode/org/[slug]/…. TheRouterProvider.parse()infers resource/action/id from the URL. - Modules + resources are declared, gated by
enabledModules. Nav, Overview, and routes generate from the enabled set. A disabled module renders nothing — no dead links.
Adding a new module (e.g. "Inventario"):
- Write a
ModuleDefinition(key, label, icon,resources[],sections,summary,config). - Register it; add
'inventory'to an org'senabledModules(#922). - Done — nav tab, Overview card, list/show/edit/create routes, and a Configurar form all render via the generic views. Zero new screens.
Adding a new config area (e.g. "Webhooks"):
- Write a
ConfigDescriptor(groups ofToggleRow/ConfigField/SecretField+ aDangerZone). - Add it to the relevant scope's
ConfigureLayoutgroups (platform / org / module). SettingsViewrenders it; writes go throughDataProvider.update. Zero new form code.
This is the leverage: config is data (a ConfigDescriptor), a module is a declaration
(ModuleDefinition), and one set of views renders all of it.
6. Tokens · states · motion (so Frontend invents nothing)
Tokens — the .console ink scope (already shipped, console-theme.css)
Use the semantic utilities; the .console scope on <body> re-skins every primitive to ink. No
raw hex in TSX. Inventory (resolved values verified AA-on-dark — a build gate, not polish):
- Surfaces:
bg-canvas(#14120f) ·bg-surface·bg-elevated(cards/Row 2) ·bg-sunken(table header, JSON blocks). - Borders:
border-border-subtle/-default/-strong·border-input(form controls). - Text:
text-fg-primary(#f5f4ef) ·-secondary·-muted. - Accent (Amber — the ONE moment per screen):
bg-accent·text-accent-text(#f0b95a, AA ≥4.5:1 on ink — use for text/labels) ·bg-accent-soft(active-row tint) ·border-accent-border. - Status (categories, NEVER the accent):
success·warning·danger·info·ai(+-soft).--color-ai(violet) only on AI/memory surfaces, never sharing a group with Amber. - Chrome:
--console-bar(top-bar) ·--console-bar-hover(ghost chip) ·--console-chrome-edge. - Charts:
--console-chart-1(= accent, highlighted series) …-5(warm neutrals). - Radius (G19):
rounded-xl(14, cards) ·-lg(10, sections) ·-md(8, controls) ·-pill. - Type:
type-*composite classes only (G20) —type-metric/-smfor figures,type-monofor IDs,type-meta/type-nav-labelfor overlines. Never a raw font/size tuple. - Icons: HugeIcons only, stroke 1.5, pergamino/muted on ink.
The 4 states — every view, every screen (frontend-guardrails.md H1)
| State | Treatment |
|---|---|
| loading | skeletal, layout-matched (animate-pulse on bg-sunken); no spinner |
| empty | EmptyState — honest copy, never fake data; seed mark allowed in calm empties |
| error | inline ReadError (danger tone, role="alert"); reachable via ApiResult.!ok |
| success | the data |
Config screens additionally: a config area with no fields → empty; a write failure → inline error on the section (keep the input values).
Motion — reuse the shipped tokens, transform/opacity only (console-design.md §6, C3)
| Named token | Value | Use |
|---|---|---|
--ease-out | cubic-bezier(0.23, 1, 0.32, 1) | UI enter / hover / under-rule slide |
--ease-spring | cubic-bezier(0.32, 0.72, 0, 1) | command pill, playful chrome |
--duration-press | 120ms | press feedback scale(0.97) |
--duration-enter | 220ms | DetailPanel translateX, popovers (origin-aware) |
--duration-exit | 160ms | exits (faster than enter) |
--duration-count | 300ms | KPI count-up on mount |
--stagger-band | 60ms | per-band widget stagger on mount |
Rules: tab under-rule slides (translateX, never width/left); DetailPanel slides
(interruptible CSS transition, never right/width); press = scale(0.97); popovers scale from
trigger (transform-origin), modals from center. prefers-reduced-motion → kill transforms/loops,
keep ≤200ms opacity crossfades. No animation on keyboard-repeated actions (⌘K toggle is instant).
Low-frequency config screens: no entrance motion beyond the Switch thumb — instant feedback.
7. Principle check + open items
Rubric (design-principles.md): Simplicity + Flexibility are
the load-bearing pair — the generic-view kit trades per-screen bespoke polish for a system that
scales to N modules by declaration. Familiarity: the two-level platform/org split + ink chrome
encode "which mode am I in." Craft/Delight: one Amber moment per screen, calm ink, no clutter.
Named trade-off: Simplicity vs Flexibility — a fully generic SettingsView/ListView is less
hand-tuned than a bespoke screen; mitigated by the ConfigDescriptor/ModuleDefinition escape
hatches (per-field control, per-module summary mapper) and the per-screen Paper specs for the few
hero surfaces (Overview, God-view).
Open items (assumed, not decided — for the operator / Tech Lead):
- L1/L2 routing split — lift platform-global surfaces (Infra, secrets, module registry, account, API keys, God-view) to a platform level, or keep the MVP single org-contextual level? (§1)
- Core view seam — do
@tedos/coreviews ship thin presentation or data + slots only? Design contract (data-in, 4 states, tokenized foundation rendering) holds either way (§3). @tedos/sharedcontracts — module summary, config descriptor, connectors, and logs have no typed contract yet (fixtures today); lift to@tedos/sharedwhen the APIs land (#934/#922).- AP1 extraction —
apps/console/src/components/*presentational screens must move into@tedos/ui/admin/console(skin) /@tedos/ui(foundation config kit) per ADR-008 A2.
Next step: Paper anchor artboards for the configuration kit (a SettingsSection with all row
types + DangerZone + SecretField) and the L1 platform shell, so Frontend builds the kit once and
every config area reuses it. Designer reviews the rendered result before close.