Skip to main content

Tu Empresa Digital OS — App Surface Design System

historical · designer · updated 2026-06-17 · source

The App-surface visual + interaction spec — the apps/desktop matiz of the Aurea core (brand.md). Non-color rules here are canonical; color tokens are being re-derived from Aurea (§2). If you can't justify a value from this doc or the Aurea core, don't ship it.


1. Tone & Personality

⚠️ Scope update (José, 2026-06-05). This doc is the App-surface spec — the desktop OS (apps/desktop) matiz of the Aurea core (brand.md §5). The old cool zinc/indigo palette is retired; §2 color tokens are being re-derived from the warm Aurea core and are marked TBD until the Designer lands the app matiz. Everything that is not color — type scale, spacing, radius, shadow, motion, z-index, iconography, component states, anti-patterns — remains valid and surface-agnostic.

Tu Empresa Digital OS is a productive desktop tool, not a marketing site. Its register is editorial restraint, technical confidence, zero ornament — surfaces stay quiet so data and tools can be loud, dense without being cramped, polished without being decorated. It carries the Aurea core (warm, "Botánica Restrained") as its app matiz, tuned for ease and usability; warmth comes from color, typography, and motion together. If a user notices the chrome before the content, we failed.

Anti-vibes: no purple gradients, no glassmorphism, no "AI sparkle" iconography, no nested cards-in-cards-in-cards, no rainbow accent salad.


2. Color Tokens

Retired (2026-06-05): the cool zinc/indigo palette (--color-bg-canvas: #0A0A0B, --color-accent: #6366F1, the dark + light hex tables) is gone. The App surface derives its palette from the Aurea core (packages/ui/src/styles/brand-tokens.css, warm "Botánica Restrained") with a user-focused matiz. Concrete values are TBD — Designer (the matiz token architecture is the open deliverable in brand.md §5).

The semantic role names survive — only their values change. Reference role, never a hue name, in product code (bg-canvas, fg-primary, accent — never zinc / indigo, nor a raw warm hue):

Role tokenPurposeValue
--color-bg-canvasApp window / sidebar backgroundTBD (Aurea matiz)
--color-bg-surfaceCards, panels, topbarTBD
--color-bg-elevatedHover surfaces, popovers, command paletteTBD
--color-bg-sunkenInset wells (code blocks, inputs)TBD
--color-border-subtle / -default / -strongBorders by emphasisTBD
--color-fg-primary / -secondary / -mutedText by emphasisTBD
--color-fg-onAccentText on accent fillsTBD
--color-accent (+ -hover / -soft / -border)Primary interactive — derives from AzafránTBD
--color-success / -warning / -danger / -infoSemantic states — pull from the Aurea sceneTBD
--color-aiClaude / AI provenance marker onlyTBD

Accent discipline carries over from the core: one Azafrán moment per composition (brand.md §6).

Light vs. dark is now a matiz decision for the Designer, not a fixed "dark-default" — re-decide it against the warm Aurea core, with ease and readability prioritized.

Contrast floor stays WCAG AA regardless of matiz; verify ratios once the Aurea-derived values land.


3. Typography

Font stack

--font-sans:
ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Inter', 'SF Pro Text', 'Segoe UI Variable',
'Segoe UI', system-ui, sans-serif;

--font-mono: ui-monospace, 'JetBrains Mono', 'SF Mono', 'Cascadia Code', Menlo, Consolas, monospace;

System stack first (zero load, native rendering, ships with the OS). "Inter" is a soft second-choice for Linux where system defaults are inconsistent. No web fonts — desktop app, offline-first, latency-sensitive.

Scale

TokenSize / LineUse
text-display32px / 1.15 / 700 / -0.5pxTop-level screen title (Login, Lock)
text-h122px / 1.25 / 600 / -0.3pxScreen heading inside shell
text-h217px / 1.35 / 600 / -0.2pxSection heading
text-h312px / 1.2 / 600 / 0.08em / UPPERCASEEyebrow / sidebar section label
text-body14px / 1.55 / 400Default body, form labels
text-body-sm13px / 1.5 / 400Sidebar items, card bodies, dense lists
text-caption12px / 1.4 / 500Captions, status pills, meta
text-micro11px / 1.3 / 500 / 0.02emKeyboard hint chips, badges
text-mono-sm12px / 1.6 / var(--font-mono)Code blocks, file paths, diffs
text-mono-xs11px / 1.5 / var(--font-mono)Inline code, IDs

Weights used: 400 (regular), 500 (medium), 600 (semibold), 700 (bold display only). Never 300 / italic in UI chrome.

Numeric rendering: font-variant-numeric: tabular-nums on any stat, counter, token-usage figure, timestamp. Tabular nums prevent column jitter when values update — the kind of unseen detail that compounds.


4. Spacing Scale

Tailwind default 4-step (0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24, 32 × 4px). Two TeDOS-specific named tokens:

TokenValueUse
--space-inset-card20px (5)Standard card padding
--space-inset-card-lg24px (6)Hero card padding
--space-inset-screen32px (8)Outer screen horizontal padding
--space-stack-section32px (8)Gap between major sections inside a screen
--space-stack-block16px (4)Gap between blocks inside a section
--space-row8px (2)Gap inside a horizontal row (icon + label)

Density philosophy: Compact desktop. Sidebar items are 32px tall, not 40. Inputs 32px (sm) / 36px (md) / 40px (lg). Topbar 44px. Never sub-28px touch targets in primary flows — but secondary chrome (pill statuses, keyboard hints) can go 22px because they're not tap-critical.


5. Radius Scale

TokenValueUse
--radius-xs4pxBadges, keyboard chips, code
--radius-sm6pxInputs, small buttons, nav items
--radius-md8pxButtons, cards (default)
--radius-lg10pxApp cards, larger panels
--radius-xl14pxModals, command palette
--radius-pill9999pxStatus pills, avatar

No border-radius: 0 and no radius-full on rectangles. The shell never renders square corners — even the window itself inherits OS-level rounding.


6. Shadow Scale

Dark UIs lean on borders + elevation tone, not shadows. Use shadows sparingly — only when an element must clearly float above content.

TokenValueUse
--shadow-nonenoneCards on bg-canvas (use border + bg-surface instead)
--shadow-sm0 1px 2px rgba(0, 0, 0, 0.20)Resting buttons (subtle lift)
--shadow-md0 4px 12px rgba(0, 0, 0, 0.28), 0 1px 2px rgba(0, 0, 0, 0.24)Popovers, dropdowns
--shadow-lg0 16px 40px rgba(0, 0, 0, 0.40), 0 2px 6px rgba(0, 0, 0, 0.30)Command palette, modals
--shadow-focus0 0 0 2px var(--color-bg-canvas), 0 0 0 4px var(--color-accent)Focus ring (offset via 2-layer outline pattern)

No drop-shadows on hover — use a 1px border color shift instead. Shadows that pulse on hover feel cheap.


7. Motion Tokens

Per Emil — UI animations are short, decisive, ease-out by default, never ease-in.

--ease-out: cubic-bezier(0.23, 1, 0.32, 1); /* default UI easing */
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1); /* on-screen movement */
--ease-drawer: cubic-bezier(0.32, 0.72, 0, 1); /* iOS drawer feel */

--duration-instant: 0ms; /* keyboard-initiated, ⌘K, hotkey navigation */
--duration-press: 120ms; /* button press feedback */
--duration-hover: 140ms; /* hover color shifts */
--duration-popover: 180ms; /* tooltips, small popovers */
--duration-dropdown: 200ms; /* selects, menus */
--duration-modal: 240ms; /* modals, drawers */
--duration-toast: 260ms; /* toast enter; exit is 200ms */

Hard rules:

  • Never animate ⌘K palette open/close — it's used hundreds of times/day (Raycast rule). It mounts instantly.
  • Never animate sidebar nav transitions. Instant.
  • Buttons get transform: scale(0.97) on :active, transition transform 120ms ease-out.
  • Modals scale-in from 0.97, never from 0. Origin: center.
  • Popovers / command palette scale-in from 0.96, origin: trigger (use Radix transform-origin pattern when we adopt Radix; until then, center is acceptable for the palette since it's a quasi-modal).
  • Toasts: enter from translateY(8px) + opacity: 0 over 260ms ease-out, exit translateY(-4px) + opacity 0 over 200ms ease-out.
  • All animation respects @media (prefers-reduced-motion: reduce) — opacity remains, transforms are nulled.

8. Z-index Layers

Use a closed scale. No z-index: 9999.

--z-base: 0;
--z-sticky: 10; /* sticky section headers in scroll views */
--z-topbar: 20; /* topbar within shell */
--z-dropdown: 30; /* select menus, autocomplete */
--z-popover: 40; /* tooltips, popovers */
--z-modal-backdrop: 50;
--z-modal: 51;
--z-palette: 60; /* ⌘K */
--z-toast: 70;
--z-titlebar: 80; /* Tauri custom titlebar — always on top of in-app overlays */

9. Iconography

Library: Phosphor (@phosphor-icons/react), already in package.json.

PropertySpec
Default weightregular (1.5px stroke equivalent)
Emphasis weightbold (only for filled states: active nav, primary button icons)
Default size16px
Sidebar nav18px regular
Topbar / inline body16px regular
Status dots8px (custom, not Phosphor — use a styled <span>)
Large empty-state32px regular, fg-muted
Button icons14px (sm), 16px (md), 18px (lg)

Sidebar icon picks (canonical):

ItemPhosphor icon
HomeHouse
AppsSquaresFour
AssistantChatCircle
ConnectionsPlugsConnected
SettingsGear
Command Palette triggerCommand
User chipUser
Add (gallery)Plus
SearchMagnifyingGlass
Status — connected(no icon, use StatusDot)
Status — errorWarningCircle
AI / ClaudeSparkle (used only for the AI author chip — never as decoration elsewhere)

Decision (Designer): All emoji placeholders in plan.html are replaced by Phosphor. No emoji in UI chrome — only in user-generated content (chat messages).


10. Component States

Every interactive element must explicitly handle:

StateSpec
DefaultResting visual per token
Hoverbg-elevated for surfaces; accent-hover for accent fills; border-strong for inputs. @media (hover: hover) and (pointer: fine) always. Transition background-color 140ms ease, border-color 140ms ease, color 140ms ease.
Active / Pressedtransform: scale(0.97) for buttons. Inputs get border-strong. transition: transform 120ms var(--ease-out).
Focus-visible2px accent ring with 2px canvas-color offset (use --shadow-focus). Only on :focus-visible, never :focus.
Selected / Active routeSidebar active item: accent-soft bg + accent-border + fg-primary. App card selected: accent-border only, no fill.
Disabledopacity: 0.5, cursor: not-allowed, pointer-events allowed (so tooltips still fire on hover explaining why). No visual hover/active changes.
LoadingInline spinner replaces leading icon (or appears before label). aria-busy="true". Pointer events suppressed. Width preserved (never reflow).
EmptyEmptyState primitive: 32px Phosphor icon (fg-muted), 14px heading (fg-primary), 13px description (fg-secondary), optional primary Button. Vertical center, max-width 320px.
ErrorInline error: 13px text, --color-danger border on input, error text below. Page error: ErrorBoundary card with WarningCircle icon, retry Button. Never red-on-red — always red border + red text on bg-surface.

11. Bilingual Handling Pattern

No i18next yet. Use a typed constants pattern + a useT() hook.

// src/i18n/strings.ts
export const STRINGS = {
'es-MX': {
nav: {
home: 'Inicio',
apps: 'Apps',
assistant: 'Asistente',
connections: 'Conexiones',
settings: 'Ajustes',
},
home: { greeting: 'Buen día', emptyApps: 'Aún no tienes apps instaladas' },
// ...
},
'en-US': {
nav: {
home: 'Home',
apps: 'Apps',
assistant: 'Assistant',
connections: 'Connections',
settings: 'Settings',
},
home: { greeting: 'Good morning', emptyApps: 'No apps installed yet' },
},
} as const

export type Locale = keyof typeof STRINGS
// src/i18n/useT.ts
export function useT(): (key: string) => string {
const { locale } = useShell()
return (key) => get(STRINGS[locale], key) ?? key
}

Rules:

  • Locale lives on ShellContext, defaults to navigator.language.startsWith("es") ? es-MX : en-US.
  • Components pull strings from useT(). No inline literals in feature views.
  • A view is single-language. Never mix <h1>Bienvenido</h1><p>Welcome back</p>.
  • Reserve 25% horizontal padding budget for Spanish overflow on sidebar labels, buttons, app card titles.
  • Dates / numbers use Intl.DateTimeFormat(locale) and Intl.NumberFormat(locale).
  • Mono / technical strings (filenames, model IDs, error codes) stay in English regardless of locale.

Decision (Designer): Spanish-first master copy. José's primary market is MX/LATAM B2B. English translations come second. This affects copy lengths in the screen spec — design to the longer string, then verify the shorter language doesn't look sparse.


12. Anti-Patterns (do not ship)

From impeccable + emil-design-eng (plus the retired minimalist-ui pack, absorbed here — #200):

  • Purple/blue gradients anywhere. Accent is a flat color.
  • Nested cards — a card inside a card inside a card. Use spacing + a single separator instead.
  • Decorative gradients on hero / empty states. Editorial type does the work.
  • AI sparkle iconography scattered across the UI. Sparkle icon appears once (Claude author chip).
  • Inline color hex in TSX. style={{ color: "#c97a2c" }} is forbidden — reference a role token — except AppCard.accent which reads from AppConfig.color as documented.
  • <button>, <input>, <select> raw HTML. Always primitives.
  • transition: all anywhere. Specify properties.
  • scale(0) entry animation. Start from scale(0.95) or higher with opacity.
  • ease-in on UI. Never. Use ease-out or custom curves.
  • Animation on ⌘K or keyboard nav. They mount instantly.
  • Drop shadows on hover (cheap-looking). Use border color shift.
  • Low contrast placeholder text below 4.5:1. Use fg-muted only on ≥14px.
  • Mixed languages in one component.
  • Rainbow color salad — limit to one accent + status colors. Phosphor icons are monochrome (current color), not multicolor.
  • Excessive borders on borders on borders. One border per surface boundary.
  • Animated spinners on idempotent buttons (the loading spinner appears only after >200ms of latency — instant responses don't show one).
  • Skeuomorphic / glassmorphic backdrops (no backdrop-blur except on the command palette overlay).
  • Toasts in the corner with bounce springs. Toasts slide up from the bottom-left, 260ms ease-out, no bounce.

13. Component Inventory (Phase 2 build order)

  1. Button (this brief)
  2. Input, Textarea, FormField
  3. Badge, StatusDot, Spinner, Separator
  4. Card, Tooltip, EmptyState
  5. Sidebar, TopBar, Shell (layout)
  6. Modal, Toast, CommandPalette
  7. AppCard, ConnectionCard, MessageBubble

Each primitive must be < 150 LOC, accept ref forwarding, be TypeScript strict, and support both light/dark via tokens (no theme branching in component code).


14. Dashboard Primitives (Phase 3 — spec only, not yet coded)

Introduced by the Client Dashboard (screens.md §4). Code lands in Phase 4.

MetricCard

Generic large-number card. Slots: eyebrow label (text-h3, fg-muted), value (text-h1, tabular-nums, fg-primary), secondary value (text-caption, fg-secondary), optional inline UsageBar, optional footer caption. Padding --space-inset-card, bg-surface, border-default, radius-lg. No shadow at rest.

UsageBar

Thin horizontal progress rail. Height 6px, radius-pill. Rail bg-elevated. Fill accent by default. State-driven color override: warning (≥80%), danger (≥95%). Transition width 240ms ease-out only — color does not animate (instant semantic shift reads clearly). Always aria-valuenow / -min / -max.

TimelineItem

One row in the read-only activity feed. Three columns: StatusDot (8px, color per event semantic), time (caption, tabular-nums, fg-muted, fixed width ~6ch), description (body-sm, fg-secondary; source name fg-primary medium). No hover state — it's read-only. No avatars (we are not a social feed).

PlanBadge

Plan-identity chip. Variants: starter, pro, enterprise. Style: accent-soft bg, accent-border, text fg-primary 12px medium. Pro/Enterprise get a leading 8px accent dot; Starter gets a fg-muted dot. Optional trailing "renueva 12 jun" caption. Lives in greeting line and Settings § Plan.

QuickActionChip

Larger affordance than Badge, smaller than Button(lg). 36px height, radius-md, icon 16px + label 13px medium. bg-elevated resting → bg-surface + border-strong on hover. Used in the dashboard quick-actions row. Same press feedback as Button. Internally just a Button(variant="subtle", size="md") with icon prop and no border — we may inline this rather than create a new primitive once we prototype it.

AppCard — dashboard variant (not new, extension)

Existing AppCard gains a variant prop: "gallery" | "dashboard". Dashboard variant: timestamp line ("Última vez: hace 2 h", caption, fg-muted, tabular-nums) below description, and a pin toggle button in the top-right corner (Pushpin Phosphor icon, 14px, ghost; filled state when pinned, accent color). No requiredConnections shown — clutter on dashboard.

"Add app" tile

Trailing slot in the dashboard app grid. Dashed border-default, transparent bg, Plus 24px centered, label "Agregar app". Acts as a Button(ghost) shaped like an AppCard — aria-label clear since it's icon-led. Not a separate primitive; documented here so anyone implementing the grid renders it consistently.