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.
325 lines
14 KiB
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
|