Tu Empresa Digital OS — App Surface Design System
:::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. :::
historical· designer · updated 2026-06-17 · source
The App-surface visual + interaction spec — the
apps/desktopmatiz 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 inbrand.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 token | Purpose | Value |
|---|---|---|
--color-bg-canvas | App window / sidebar background | TBD (Aurea matiz) |
--color-bg-surface | Cards, panels, topbar | TBD |
--color-bg-elevated | Hover surfaces, popovers, command palette | TBD |
--color-bg-sunken | Inset wells (code blocks, inputs) | TBD |
--color-border-subtle / -default / -strong | Borders by emphasis | TBD |
--color-fg-primary / -secondary / -muted | Text by emphasis | TBD |
--color-fg-onAccent | Text on accent fills | TBD |
--color-accent (+ -hover / -soft / -border) | Primary interactive — derives from Azafrán | TBD |
--color-success / -warning / -danger / -info | Semantic states — pull from the Aurea scene | TBD |
--color-ai | Claude / AI provenance marker only | TBD |
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
| Token | Size / Line | Use |
|---|---|---|
text-display | 32px / 1.15 / 700 / -0.5px | Top-level screen title (Login, Lock) |
text-h1 | 22px / 1.25 / 600 / -0.3px | Screen heading inside shell |
text-h2 | 17px / 1.35 / 600 / -0.2px | Section heading |
text-h3 | 12px / 1.2 / 600 / 0.08em / UPPERCASE | Eyebrow / sidebar section label |
text-body | 14px / 1.55 / 400 | Default body, form labels |
text-body-sm | 13px / 1.5 / 400 | Sidebar items, card bodies, dense lists |
text-caption | 12px / 1.4 / 500 | Captions, status pills, meta |
text-micro | 11px / 1.3 / 500 / 0.02em | Keyboard hint chips, badges |
text-mono-sm | 12px / 1.6 / var(--font-mono) | Code blocks, file paths, diffs |
text-mono-xs | 11px / 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:
| Token | Value | Use |
|---|---|---|
--space-inset-card | 20px (5) | Standard card padding |
--space-inset-card-lg | 24px (6) | Hero card padding |
--space-inset-screen | 32px (8) | Outer screen horizontal padding |
--space-stack-section | 32px (8) | Gap between major sections inside a screen |
--space-stack-block | 16px (4) | Gap between blocks inside a section |
--space-row | 8px (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
| Token | Value | Use |
|---|---|---|
--radius-xs | 4px | Badges, keyboard chips, code |
--radius-sm | 6px | Inputs, small buttons, nav items |
--radius-md | 8px | Buttons, cards (default) |
--radius-lg | 10px | App cards, larger panels |
--radius-xl | 14px | Modals, command palette |
--radius-pill | 9999px | Status 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.
| Token | Value | Use |
|---|---|---|
--shadow-none | none | Cards on bg-canvas (use border + bg-surface instead) |
--shadow-sm | 0 1px 2px rgba(0, 0, 0, 0.20) | Resting buttons (subtle lift) |
--shadow-md | 0 4px 12px rgba(0, 0, 0, 0.28), 0 1px 2px rgba(0, 0, 0, 0.24) | Popovers, dropdowns |
--shadow-lg | 0 16px 40px rgba(0, 0, 0, 0.40), 0 2px 6px rgba(0, 0, 0, 0.30) | Command palette, modals |
--shadow-focus | 0 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
⌘Kpalette 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, transitiontransform 120ms ease-out. - Modals scale-in from
0.97, never from0. 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: 0over 260ms ease-out, exittranslateY(-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.
| Property | Spec |
|---|---|
| Default weight | regular (1.5px stroke equivalent) |
| Emphasis weight | bold (only for filled states: active nav, primary button icons) |
| Default size | 16px |
| Sidebar nav | 18px regular |
| Topbar / inline body | 16px regular |
| Status dots | 8px (custom, not Phosphor — use a styled <span>) |
| Large empty-state | 32px regular, fg-muted |
| Button icons | 14px (sm), 16px (md), 18px (lg) |
Sidebar icon picks (canonical):
| Item | Phosphor icon |
|---|---|
| Home | House |
| Apps | SquaresFour |
| Assistant | ChatCircle |
| Connections | PlugsConnected |
| Settings | Gear |
| Command Palette trigger | Command |
| User chip | User |
| Add (gallery) | Plus |
| Search | MagnifyingGlass |
| Status — connected | (no icon, use StatusDot) |
| Status — error | WarningCircle |
| AI / Claude | Sparkle (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:
| State | Spec |
|---|---|
| Default | Resting visual per token |
| Hover | bg-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 / Pressed | transform: scale(0.97) for buttons. Inputs get border-strong. transition: transform 120ms var(--ease-out). |
| Focus-visible | 2px accent ring with 2px canvas-color offset (use --shadow-focus). Only on :focus-visible, never :focus. |
| Selected / Active route | Sidebar active item: accent-soft bg + accent-border + fg-primary. App card selected: accent-border only, no fill. |
| Disabled | opacity: 0.5, cursor: not-allowed, pointer-events allowed (so tooltips still fire on hover explaining why). No visual hover/active changes. |
| Loading | Inline spinner replaces leading icon (or appears before label). aria-busy="true". Pointer events suppressed. Width preserved (never reflow). |
| Empty | EmptyState primitive: 32px Phosphor icon (fg-muted), 14px heading (fg-primary), 13px description (fg-secondary), optional primary Button. Vertical center, max-width 320px. |
| Error | Inline 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 tonavigator.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)andIntl.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 — exceptAppCard.accentwhich reads fromAppConfig.coloras documented. <button>,<input>,<select>raw HTML. Always primitives.transition: allanywhere. Specify properties.scale(0)entry animation. Start fromscale(0.95)or higher with opacity.ease-inon UI. Never. Useease-outor custom curves.- Animation on
⌘Kor 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-mutedonly 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-blurexcept 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)
Button(this brief)Input,Textarea,FormFieldBadge,StatusDot,Spinner,SeparatorCard,Tooltip,EmptyStateSidebar,TopBar,Shell(layout)Modal,Toast,CommandPaletteAppCard,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.