Saltar al contenido principal

Console structure — IA + reusable-component design spec

:::note Contenido en inglés Esta página del wiki se sincroniza desde la base de conocimiento en inglés y todavía no está traducida. :::

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).

ScopeOwnsExamplesBacked byLives at
Platform (global)operator-superadmin, cross-org, no tenantinfra tokens · deploy/infra secrets · the module registry (the catalog of buildable modules) · platform feature-flag defaults · operator account · API keysoperator config store (NOT TenantConfig)platform Configurar / Infra
Org (per-tenant)one client orgwhich modules are enabled · branding / white-label tokens · org profile + domain · org connectors/secrets · per-org flag overridesTenantConfig.enabledModules + brandingTokens + authOrgId (#922)/org/<slug>/configurar/*
Module (per-tenant, per-module)one module within one orgmodule-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:

FieldDrives
key, label, iconnav, Overview card identity, Configurar entry
resources: Resource[]the @tedos/core resource registry → ListView/ShowView/Edit/Create per entity
sectionsthe 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)
enabledBygate — 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):

RowUseAnatomyControl
ToggleRowa boolean settinglabel + helper, trailing SwitchSwitch (44px hit, locked variant for "Siempre activo")
ConfigFielda text/number/select valuelabel above, error below, gap-2Input / Select / Textarea
SecretFielda credential / token / API keylabel + masked value + meta (created/last-used)reveal · copy · rotate · revoke (revoke = danger, lives in DangerZone semantics)
FlagMatrix rowa flag × scope overrideflag label + helper · default badge · per-scope Switch columnsDataTable of Switch
DangerZonedestructive actionsbordered danger-toned group at the section footdanger 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/muted badge; secret state = neutral.
  • Forms: label above, error below, gap-2 (frontend-guardrails.md). Low-frequency surface → no entrance animation beyond the Switch thumb translateX; instant feedback.
  • A config screen IS a ConfigDescriptor rendered generically (§5) — the same SettingsView renders 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.

ViewPurposeCore (shared) or ConsoleNotes
ResourceListViewtable over a resource (list)CoreDataProvider.getList + 4 states; renders the EntityTable/DataTable; toolbar (search/filter/columns); routing via href factory
ResourceShowViewsingle record detail (show)CoreDataProvider.getOne; fields from resource meta; may host a DetailPanel or full page
ResourceEditView / CreateViewresource form (edit/create)CoreDataProvider.update/create; built from ConfigFields; ApiResult → inline error
SettingsView / ConfigViewthe "configure X" pattern (§2)Core (white-label)renders a ConfigDescriptor of SettingsSections; writes via DataProvider.update on a config "resource"
ModuleSummaryViewmodule glance card on OverviewConsoleper console-overview-spec.md (hero + secondaries)
OrgPickerVieworg picker grid (L1 front door)Consolecross-org; the one calm, accent-free picker
GodView / HealthMatrixViewcross-org healthConsolestatus dots only, never the accent
LogsViewevent/audit table + DetailPanelConsolevirtualize; instant selection; JSON payload in bg-sunken
BuilderBoardViewPlan-Engine efforts boardConsolecolumns + drag-snap (C2)
InfraViewdeploy-health grid + recent deploysConsoleread-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)

ComponentPurposeStatus
ConsoleShellblack two-row top-bar + single scroll area; owns scroll (G6 clip)[E]
ConsoleTopbar / ConsoleSwitchersRow 1 — brand + Org/Env switchers (ghost chips)[E]
ConsoleTabsRow 2 — primary tabs, sliding Amber under-rule + Más ▾ overflow[E]
ConsoleCommandPill / SearchCommand⌘K quick-nav (level-scoped)[E]
ConsoleAccountoperator avatar/menu[E]
ConfigureLayoutthe ONE local left sub-nav (config areas); active = Amber-soft[E]
DetailPanelright slide-in panel (Logs/Proyectos/Infra/Finanzas) — translateX, interruptible[E]
StatusDot / HealthMatrixstatus grid (dots only, never accent)[E]

Configuration kit — @tedos/ui foundation (neutral, white-label; the config-surface primitives)

ComponentPurposeStatus
SettingsSectiontitled section (heading + helper + bordered body)[X] from configure-screen
ToggleRowlabel + helper + trailing Switch row[X]
ConfigFieldlabel-above / error-below form field[X] (the Field helper)
SecretFieldmasked credential + reveal/copy/rotate/revoke[N] (ad-hoc in ApiKeysSection today)
FlagMatrixflag × scope override table of toggles[X] from FeatureFlagsSection
DangerZonegrouped destructive actions, inline confirm[N]
EnabledModulesListorg-config "which modules" — toggle rows over the registry[N]
BrandingTokenEditorwhite-label token overrides (org branding) → TenantConfig.brandingTokens[N] (advanced; flag for later)

Data + metric primitives — foundation (reused; skin via scope)

ComponentPurposeStatus
DataTable (+ toolbar/columns/pagination/bulk)the EntityTable for every resource list[E]
StatTile / KpiTilemetric 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 · Badgethe 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/console skin. Resource view scaffolds → @tedos/core. Never bake Amber into the foundation — it arrives via the .console scope. 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):

  1. Apps are thin. Route file + data seam (DataProvider call) + a thin shell that composes a view. No presentation, no fetching inside the view.
  2. 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.
  3. Routing is app-owned. Views take href / an href factory (orgHref(slug, segment)), never hardcode /org/[slug]/…. The RouterProvider.parse() infers resource/action/id from the URL.
  4. 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"):

  1. Write a ModuleDefinition (key, label, icon, resources[], sections, summary, config).
  2. Register it; add 'inventory' to an org's enabledModules (#922).
  3. 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"):

  1. Write a ConfigDescriptor (groups of ToggleRow/ConfigField/SecretField + a DangerZone).
  2. Add it to the relevant scope's ConfigureLayout groups (platform / org / module).
  3. SettingsView renders it; writes go through DataProvider.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/-sm for figures, type-mono for IDs, type-meta/type-nav-label for 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)

StateTreatment
loadingskeletal, layout-matched (animate-pulse on bg-sunken); no spinner
emptyEmptyState — honest copy, never fake data; seed mark allowed in calm empties
errorinline ReadError (danger tone, role="alert"); reachable via ApiResult.!ok
successthe 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 tokenValueUse
--ease-outcubic-bezier(0.23, 1, 0.32, 1)UI enter / hover / under-rule slide
--ease-springcubic-bezier(0.32, 0.72, 0, 1)command pill, playful chrome
--duration-press120mspress feedback scale(0.97)
--duration-enter220msDetailPanel translateX, popovers (origin-aware)
--duration-exit160msexits (faster than enter)
--duration-count300msKPI count-up on mount
--stagger-band60msper-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):

  1. 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)
  2. Core view seam — do @tedos/core views ship thin presentation or data + slots only? Design contract (data-in, 4 states, tokenized foundation rendering) holds either way (§3).
  3. @tedos/shared contracts — module summary, config descriptor, connectors, and logs have no typed contract yet (fixtures today); lift to @tedos/shared when the APIs land (#934/#922).
  4. AP1 extractionapps/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.