You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/web/routes/temas/_lib/grafito.ts

325 lines
14 KiB

/**
* Grafito — tema para activeUIX / Eidos (paleta derivada de Untitled UI, variada).
*
* Reproduce el lenguaje visual de https://www.untitledui.com con el
* sistema de theming canónico de Eidos: NO inventa variants ni roles
* nuevos (eso es canon fijo del framework, ver `eidos/THEMING.md §19`),
* solo **retinta** los 9 roles canónicos y ajusta primitivas estables
* (tipografía, radio, sombra) que un theme/app puede mover.
*
* Anclas del design system de Untitled UI:
* - Tipografía : Inter (display + texto), mono para código.
* - Brand 600 : #7F56D9 → role `primary` (escala `brand`).
* - Gray 900 : #101828 → role `neutral`/`secondary` (escala `gray`).
* - Error 600 : #D92D20 → role `threat` (escala `red`).
* - Warning 600 : #DC6803 → role `risk` (escala `amber`).
* - Success 600 : #079455 → role `fulfill` (escala `green`).
* - Teal 600 : #0E9384 → role `affirm` (escala `teal`).
* - Plum 900 : #7A3AAD → role `loss` (escala `plum`, violeta-grave).
* - Radio : 8px en controles (`md`), 12px paneles.
* - Sombras : sistema en capas de Untitled UI (xs … 2xl).
*
* Las escalas físicas se mapean del ramp 25→900 de Untitled UI al modelo
* funcional de 12 pasos de Eidos (Radix-style): paso 1-2 fondos, 3-5
* fondos de componente, 6-8 bordes, 9 sólido (= el `-600` de Untitled UI),
* 10 sólido-hover, 11 texto, 12 texto de alto contraste.
*
* Se construye como **config standalone** (no como patch sobre el theme
* base): conserva las primitivas y recipes del base pero deja únicamente
* los themes `untitled-ui-{light,dark}` y los 9 roles re-mapeados a las
* escalas de Untitled UI. (Un patch dejaría los themes base, cuyo set de
* escalas no coincide con los roles, y el validador de Eidos lo
* rechazaría.)
*/
import {
createThemeBaseEidosConfig,
THEME_BASE_LIGHT_COLOR_SCALES,
THEME_BASE_DARK_COLOR_SCALES
} from '$uix/eidos/lib/themes/base'
import type { ColorScale, ColorRoleMap, EidosConfig } from '$uix/eidos/lib/config-types'
const s = (
c1: string,
c2: string,
c3: string,
c4: string,
c5: string,
c6: string,
c7: string,
c8: string,
c9: string,
c10: string,
c11: string,
c12: string
): ColorScale => ({
'1': c1,
'2': c2,
'3': c3,
'4': c4,
'5': c5,
'6': c6,
'7': c7,
'8': c8,
'9': c9,
'10': c10,
'11': c11,
'12': c12
})
// ── Escalas — modo claro ──────────────────────────────────────────────
// prettier-ignore
const LIGHT = {
// Hereda la paleta base del framework (31 escalas Radix). Grafito sobreescribe
// abajo las escalas de marca (carbon, gray, violet, blue + intents UUI-tuned) y
// hereda las demás → ~32 escalas en la paleta del tema.
...THEME_BASE_LIGHT_COLOR_SCALES,
// Carbon — grafito casi negro. Primario monocromo (Vercel / Linear style).
// 9 = sólido oscuro del botón; con slots.contrast='1' el texto (step 1)
// queda casi blanco. En dark la escala se invierte y todo encaja solo.
carbon: s('#fafafa','#f5f5f6','#ececee','#e3e3e6','#d8d9dd','#c7c8cd','#a1a2a9','#717179','#1f2024','#101114','#34353b','#08090b'),
// Untitled UI Gray (neutral frío) — 900 = #101828
gray: s('#fcfcfd','#f9fafb','#f2f4f7','#eaecf0','#e4e7ec','#d0d5dd','#c3cad3','#98a2b3','#667085','#475467','#344054','#101828'),
// Untitled UI Brand (violeta #7F56D9) — el primario REAL de Untitled UI,
// aquí como secundario (jerarquía de apoyo).
violet: s('#fcfaff','#f9f5ff','#f4ebff','#e9d7fe','#d6bbfb','#c3a5f7','#b692f6','#9e77ed','#7f56d9','#6941c6','#5b34b5','#42307d'),
// Untitled UI Blue — 600 = #1570EF (terciario).
blue: s('#f5faff','#eff8ff','#d1e9ff','#b2ddff','#84caff','#6cbbfd','#53b1fd','#2e90fa','#1570ef','#175cd3','#1849a9','#194185'),
// Untitled UI Teal — 600 = #0E9384
teal: s('#f6fefc','#f0fdf9','#ccfbef','#99f6e0','#5fe9d0','#47d9c0','#2ed3b7','#15b79e','#0e9384','#0c7e72','#107569','#134e48'),
// Untitled UI Success (verde) — 600 = #079455
green: s('#f6fef9','#ecfdf3','#dcfae6','#abefc6','#75e0a7','#5bd295','#47cd89','#17b26a','#079455','#067647','#085d3a','#074d31'),
// Untitled UI Warning (ámbar) — 600 = #DC6803
amber: s('#fffcf5','#fffaeb','#fef0c7','#fedf89','#fec84b','#fdb937','#fdb022','#f79009','#dc6803','#b54708','#93370d','#7a2e0e'),
// Untitled UI Error (rojo) — 600 = #D92D20
red: s('#fffbfa','#fef3f2','#fee4e2','#fecdca','#fda29b','#fb897f','#f97066','#f04438','#d92d20','#b42318','#912018','#7a271a'),
// Loss — violeta-grave (plum). Sólido más oscuro y magenta que el violeta
// de marca (secondary), para que la pérdida se lea sombría y posterior.
plum: s('#fdfbfe','#faf5fd','#f3e9fb','#eaddf7','#dfccf1','#d0b4e8','#bd99dc','#a577cd','#7a3aad','#6f349f','#652f90','#321a4a')
} satisfies Record<string, ColorScale>
// ── Escalas — modo oscuro ─────────────────────────────────────────────
// Ramps oscuros perceptualmente coherentes con el mismo hue: 1 = fondo
// más oscuro, 12 = texto más claro, 9 = sólido (= `-600`, igual que claro),
// 10 = `-500` (más claro para hover sobre oscuro).
// prettier-ignore
const DARK = {
...THEME_BASE_DARK_COLOR_SCALES,
// ── Escalas de marca Grafito (dark) ──
// Carbon invertido: 1 = casi negro, 9 = botón claro, contrast(1) = oscuro.
carbon: s('#0b0c0e','#131418','#1b1c20','#232428','#2c2d32','#3a3b41','#565760','#7c7d86','#f4f4f6','#ffffff','#cdced4','#fbfbfc'),
gray: s('#0c111d','#101828','#1d2939','#243144','#2c3a4f','#344054','#475467','#5d6b7e','#667085','#98a2b3','#ced5de','#f5f6f8'),
violet: s('#1a132b','#20183a','#2e1f55','#382569','#432d7d','#503690','#5e40a8','#6e4bc4','#7f56d9','#9e77ed','#c9b5f8','#ece3ff'),
blue: s('#0d1a2e','#11213a','#0f2f5e','#0b3b7a','#0e4894','#1559b0','#1f6fd6','#2a86ec','#1570ef','#2e90fa','#84caff','#d1e9ff'),
teal: s('#0b1b19','#0f2422','#103832','#0e463e','#11554b','#166458','#1a7568','#178573','#0e9384','#15b79e','#5fe9d0','#c9f7ec'),
green: s('#0c1a12','#0f2417','#0f3a23','#0e4a2c','#155b36','#1d6c41','#218358','#06934f','#079455','#17b26a','#75e0a7','#cdf5dd'),
amber: s('#1c1408','#241b09','#3a2a08','#4a3607','#5c4408','#70530c','#8a6410','#b07908','#dc6803','#f79009','#fec84b','#fee9c0'),
red: s('#1f1314','#281618','#41181c','#551a1e','#6b1f22','#842529','#a12d2e','#c13438','#d92d20','#f04438','#fda29b','#fee0de'),
plum: s('#160f1f','#1d142b','#2a1a40','#341e50','#3e255f','#4a2f72','#5a3a8a','#6d47a6','#8348c0','#9560d0','#c2a8e6','#ece1fb')
} satisfies Record<string, ColorScale>
// ── Mapeo role → escala (los 9 roles canónicos de Eidos) ──────────────
const ROLES: ColorRoleMap = {
// Solo la JERARQUÍA se mapea explícitamente (decisión de marca). Carbon
// monocromo: `slots.contrast: '1'` hace que el texto sobre el sólido sea el
// step 1 — casi blanco en claro, casi negro en oscuro — invirtiéndose con el
// modo (el step 9 también se invierte).
primary: { scale: 'carbon', slots: { contrast: '1' } },
secondary: 'violet',
tertiary: 'blue'
// Los 6 intents (neutral + affirm/fulfill/risk/threat/loss) NO se cablean:
// auto-derivan de la paleta por la convención del libro
// (gray/teal/green/amber/red/plum — todas presentes arriba), identidad = step 9.
// Un tema puede overridear un intent mapeándolo explícitamente.
}
// ── Superficies / contenido / bordes / focus ──────────────────────────
const LIGHT_SEMANTICS = {
surface: {
default: '#ffffff',
raised: '#ffffff',
overlay: 'var(--primitive-neutral-2)',
muted: 'var(--primitive-neutral-3)',
backdrop: 'rgb(16 24 40 / 0.45)'
},
content: {
primary: 'var(--primitive-neutral-12)',
secondary: 'var(--primitive-neutral-11)',
muted: 'var(--primitive-neutral-10)',
disabled: 'var(--primitive-neutral-8)',
onSolid: '#ffffff'
},
border: {
subtle: 'var(--primitive-neutral-4)',
default: 'var(--primitive-neutral-6)',
strong: 'var(--primitive-neutral-8)'
},
focus: {
ring: 'color-mix(in srgb, var(--primitive-secondary-9) 24%, transparent)',
ringError: 'color-mix(in srgb, var(--primitive-threat-9) 22%, transparent)'
}
} as const
const DARK_SEMANTICS = {
surface: {
default: 'var(--primitive-neutral-1)',
raised: 'var(--primitive-neutral-3)',
overlay: 'var(--primitive-neutral-4)',
muted: 'var(--primitive-neutral-2)',
backdrop: 'rgb(0 0 0 / 0.6)'
},
content: {
primary: 'var(--primitive-neutral-12)',
secondary: 'var(--primitive-neutral-11)',
muted: 'var(--primitive-neutral-10)',
disabled: 'var(--primitive-neutral-8)',
onSolid: '#ffffff'
},
border: {
subtle: 'var(--primitive-neutral-4)',
default: 'var(--primitive-neutral-6)',
strong: 'var(--primitive-neutral-8)'
},
focus: {
ring: 'color-mix(in srgb, var(--primitive-secondary-9) 40%, transparent)',
ringError: 'color-mix(in srgb, var(--primitive-threat-9) 40%, transparent)'
}
} as const
// ── Sombras de Untitled UI (xs → 2xl) ─────────────────────────────────
const LIGHT_SHADOW = {
none: 'none',
subtle: 'var(--shadow-1)',
raised: 'var(--shadow-3)',
overlay: 'var(--shadow-5)',
'1': '0 1px 2px 0 rgb(16 24 40 / 0.05)',
'2': '0 1px 3px 0 rgb(16 24 40 / 0.1), 0 1px 2px -1px rgb(16 24 40 / 0.1)',
'3': '0 4px 8px -2px rgb(16 24 40 / 0.1), 0 2px 4px -2px rgb(16 24 40 / 0.06)',
'4': '0 12px 16px -4px rgb(16 24 40 / 0.08), 0 4px 6px -2px rgb(16 24 40 / 0.03)',
'5': '0 20px 24px -4px rgb(16 24 40 / 0.08), 0 8px 8px -4px rgb(16 24 40 / 0.03)',
'6': '0 24px 48px -12px rgb(16 24 40 / 0.18)'
} as const
const DARK_SHADOW = {
none: 'none',
subtle: 'var(--shadow-1)',
raised: 'var(--shadow-3)',
overlay: 'var(--shadow-5)',
'1': '0 1px 2px 0 rgb(0 0 0 / 0.3)',
'2': '0 1px 3px 0 rgb(0 0 0 / 0.4), 0 1px 2px -1px rgb(0 0 0 / 0.4)',
'3': '0 4px 8px -2px rgb(0 0 0 / 0.45), 0 2px 4px -2px rgb(0 0 0 / 0.3)',
'4': '0 12px 16px -4px rgb(0 0 0 / 0.5), 0 4px 6px -2px rgb(0 0 0 / 0.3)',
'5': '0 20px 24px -4px rgb(0 0 0 / 0.55), 0 8px 8px -4px rgb(0 0 0 / 0.3)',
'6': '0 24px 48px -12px rgb(0 0 0 / 0.65)'
} as const
const INTER_FALLBACKS = [
'-apple-system',
'BlinkMacSystemFont',
'Segoe UI',
'Roboto',
'Helvetica Neue',
'Arial',
'sans-serif'
]
// Base con primitivas + recipes del sistema, con las primitivas estables
// que el tema Untitled UI ajusta (tipografía Inter, radio 8px, focus ring).
const base = createThemeBaseEidosConfig({
primitives: {
// Untitled UI = Inter en todo (display + texto), mono para código.
typography: {
families: {
primary: { family: 'Inter', fallbacks: INTER_FALLBACKS, source: 'remote' },
secondary: { family: 'Inter', fallbacks: INTER_FALLBACKS, source: 'remote' },
display: { family: 'Inter', fallbacks: INTER_FALLBACKS, source: 'remote' },
mono: {
family: 'Roboto Mono',
fallbacks: ['ui-monospace', 'SFMono-Regular', 'Menlo', 'Consolas', 'monospace'],
source: 'remote'
}
},
// Tracking ceñido en titulares grandes — firma de Untitled UI.
styles: {
hero: { letterSpacing: '-0.02em' },
h1: { letterSpacing: '-0.02em' },
h2: { letterSpacing: '-0.02em' },
h3: { letterSpacing: '-0.01em' }
}
},
// Radio: controles a 8px (md), paneles a 12px (lg).
radius: {
none: '0px',
sm: '6px',
md: '8px',
lg: '12px',
xl: '16px',
xxl: '20px',
full: '9999px'
},
// Halo de focus suave (Untitled UI usa un anillo amplio, sin hueco).
focusRing: { offset: '1px', width: '3px' }
}
})
/**
* Config completo del tema Grafito. Se pasa a
* `ActiveEidos.create({ config: grafitoConfig, theme: 'grafito' })`.
*/
export const grafitoConfig: EidosConfig = {
primitives: base.primitives,
recipes: base.recipes,
semantics: { color: { roles: ROLES } },
themes: {
'grafito-light': {
color: { scales: LIGHT, ...LIGHT_SEMANTICS },
shadow: LIGHT_SHADOW
},
'grafito-dark': {
color: { scales: DARK, ...DARK_SEMANTICS },
shadow: DARK_SHADOW
}
}
}
// ── Metadatos para la página de documentación del theme ───────────────
export interface ThemeRoleInfo {
readonly role: string
readonly scale: string
readonly label: string
readonly note: string
}
/** Roles de JERARQUÍA — alias explícito a una escala de la paleta (marca). Los
* intents NO van aquí: auto-derivan de la paleta por convención (ver THEME_INTENTS). */
export const THEME_HIERARCHY: readonly ThemeRoleInfo[] = [
{ role: 'primary', scale: 'carbon', label: 'Primary', note: 'Carbón · grafito #1F2024' },
{ role: 'secondary', scale: 'violet', label: 'Secondary', note: 'Violeta de marca #7F56D9' },
{ role: 'tertiary', scale: 'blue', label: 'Tertiary', note: 'Azul acento #1570EF' }
]
/**
* La paleta COMPLETA del tema (~32 escalas), derivada de las claves de `LIGHT`
* para no desincronizarse. Etiquetada por NOMBRE DE COLOR — nunca por intent
* (los intents son una capa semántica encima; ver THEME_INTENTS).
*/
export const THEME_SCALES: readonly { name: string; label: string }[] = Object.keys(LIGHT)
.sort()
.map((name) => ({ name, label: name }))
/**
* Los 6 intents y la escala de la paleta de la que **auto-derivan** (convención
* del libro). El tema NO los cablea — el engine los rellena por nombre,
* identidad = step 9. Esto son solo metadatos para documentar el mapeo en el demo.
*/
export const THEME_INTENTS: readonly { role: string; label: string; scale: string; note: string }[] = [
{ role: 'neutral', label: 'Neutral', scale: 'gray', note: 'Sin valencia · gris de trabajo' },
{ role: 'affirm', label: 'Affirm', scale: 'teal', note: 'Positivo ligero · teal' },
{ role: 'fulfill', label: 'Fulfill', scale: 'green', note: 'Éxito / completado · verde' },
{ role: 'risk', label: 'Risk', scale: 'amber', note: 'Aviso · ámbar' },
{ role: 'threat', label: 'Threat', scale: 'red', note: 'Error / peligro · rojo' },
{ role: 'loss', label: 'Loss', scale: 'plum', note: 'Pérdida grave · violeta sombrío' }
]
export const COLOR_STEPS = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'] as const

Powered by TurnKey Linux.