- New untitled-ui demo theme + theming showcase route (web/routes/temas) - Engine: density tokens scale via calc(var(--density-*-scale)); contrast slot defaults to on-solid - Eidos theming audit (THEMING_AUDIT_2026-06-01.md) + two-tier color-model RFC (COLOR_MODEL_RFC.md) - P0: replace phantom foundation tokens in recipes + component CSS — shadows -> semantic scale (subtle/raised/overlay), add --space-7, --border-width-strong -> thick; neutral solid contrast -> step 12 (AA in light + dark) - P1: validate hex color values + per-theme semantic completeness; regression tests for contrast, density, dark output, and TSC multi-part/composition emission Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>active-uix
parent
9324764675
commit
218ccfcc5e
@ -0,0 +1,119 @@
|
||||
<script lang="ts">
|
||||
// Índice de temas. Página plana (sin runtime Eidos) que enlaza a cada
|
||||
// tema. El primero es Untitled UI.
|
||||
const themes = [
|
||||
{
|
||||
slug: 'untitled-ui',
|
||||
name: 'Untitled UI',
|
||||
desc: 'Inter · brand violeta #7F56D9 · grises fríos · sombras en capas',
|
||||
accent: '#7F56D9'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
||||
/>
|
||||
</svelte:head>
|
||||
|
||||
<main class="temas-index">
|
||||
<header>
|
||||
<h1>Temas</h1>
|
||||
<p>Galería de temas para activeUIX. Cada uno retinta los 9 roles canónicos de Eidos.</p>
|
||||
</header>
|
||||
|
||||
<ul>
|
||||
{#each themes as t}
|
||||
<li>
|
||||
<a href="/temas/{t.slug}">
|
||||
<span class="swatch" style="background: {t.accent}"></span>
|
||||
<span class="meta">
|
||||
<strong>{t.name}</strong>
|
||||
<small>{t.desc}</small>
|
||||
</span>
|
||||
<span class="go" aria-hidden="true">→</span>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.temas-index {
|
||||
min-height: 100dvh;
|
||||
max-width: 720px;
|
||||
margin-inline: auto;
|
||||
padding: 80px 24px;
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
color: #101828;
|
||||
background: #f9fafb;
|
||||
}
|
||||
:global(html) {
|
||||
background: #f9fafb;
|
||||
}
|
||||
header h1 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 40px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
header p {
|
||||
margin: 0 0 40px;
|
||||
color: #475467;
|
||||
font-size: 18px;
|
||||
}
|
||||
ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid #eaecf0;
|
||||
box-shadow: 0 1px 2px rgb(16 24 40 / 0.05);
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition: box-shadow 160ms, transform 160ms, border-color 160ms;
|
||||
}
|
||||
a:hover {
|
||||
box-shadow: 0 4px 8px -2px rgb(16 24 40 / 0.1), 0 2px 4px -2px rgb(16 24 40 / 0.06);
|
||||
border-color: #d0d5dd;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.swatch {
|
||||
inline-size: 44px;
|
||||
block-size: 44px;
|
||||
border-radius: 10px;
|
||||
flex: none;
|
||||
box-shadow: inset 0 0 0 1px rgb(16 24 40 / 0.08);
|
||||
}
|
||||
.meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
flex: 1;
|
||||
}
|
||||
.meta strong {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.meta small {
|
||||
color: #667085;
|
||||
font-size: 13px;
|
||||
}
|
||||
.go {
|
||||
color: #7f56d9;
|
||||
font-size: 20px;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,310 @@
|
||||
/**
|
||||
* Untitled UI — tema para activeUIX / Eidos.
|
||||
*
|
||||
* 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 } 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 = {
|
||||
// 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 = {
|
||||
// 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 = {
|
||||
// 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 solo con el modo (el step 9 también se invierte).
|
||||
primary: { scale: 'carbon', slots: { contrast: '1' } },
|
||||
secondary: 'violet',
|
||||
tertiary: 'blue',
|
||||
neutral: 'gray',
|
||||
affirm: 'teal',
|
||||
fulfill: 'green',
|
||||
risk: 'amber',
|
||||
threat: 'red',
|
||||
loss: 'plum'
|
||||
}
|
||||
|
||||
// ── 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 Untitled UI. Se pasa a
|
||||
* `ActiveEidos.create({ config: untitledUiConfig, theme: 'untitled-ui' })`.
|
||||
*/
|
||||
export const untitledUiConfig: EidosConfig = {
|
||||
primitives: base.primitives,
|
||||
recipes: base.recipes,
|
||||
semantics: { color: { roles: ROLES } },
|
||||
themes: {
|
||||
'untitled-ui-light': {
|
||||
color: { scales: LIGHT, ...LIGHT_SEMANTICS },
|
||||
shadow: LIGHT_SHADOW
|
||||
},
|
||||
'untitled-ui-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
|
||||
}
|
||||
|
||||
/** Los 9 roles canónicos, con la escala Untitled UI a la que mapean. */
|
||||
export const THEME_ROLES: readonly ThemeRoleInfo[] = [
|
||||
{ role: 'primary', scale: 'carbon', label: 'Primary', note: 'Carbón · grafito #1F2024' },
|
||||
{ role: 'secondary', scale: 'violet', label: 'Secondary', note: 'Brand UUI · violeta #7F56D9' },
|
||||
{ role: 'tertiary', scale: 'blue', label: 'Tertiary', note: 'Acento · azul #1570EF' },
|
||||
{ role: 'neutral', scale: 'gray', label: 'Neutral', note: 'Gris frío Untitled UI' },
|
||||
{ role: 'affirm', scale: 'teal', label: 'Affirm', note: 'Positivo ligero · teal' },
|
||||
{ role: 'fulfill', scale: 'green', label: 'Fulfill', note: 'Éxito / completado · verde' },
|
||||
{ role: 'risk', scale: 'amber', label: 'Risk', note: 'Aviso · warning #DC6803' },
|
||||
{ role: 'threat', scale: 'red', label: 'Threat', note: 'Error / peligro · #D92D20' },
|
||||
{ role: 'loss', scale: 'plum', label: 'Loss', note: 'Pérdida grave · violeta sombrío' }
|
||||
]
|
||||
|
||||
/** Escalas físicas del theme, en orden de presentación. */
|
||||
export const THEME_SCALES: readonly { name: string; label: string }[] = [
|
||||
{ name: 'carbon', label: 'Carbon' },
|
||||
{ name: 'violet', label: 'Violet · brand' },
|
||||
{ name: 'blue', label: 'Blue' },
|
||||
{ name: 'gray', label: 'Gray' },
|
||||
{ name: 'teal', label: 'Teal' },
|
||||
{ name: 'green', label: 'Success' },
|
||||
{ name: 'amber', label: 'Warning' },
|
||||
{ name: 'red', label: 'Error' },
|
||||
{ name: 'plum', label: 'Plum · grave' }
|
||||
]
|
||||
|
||||
export const COLOR_STEPS = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'] as const
|
||||
@ -0,0 +1,3 @@
|
||||
// SSR activo (default). Sólo desactivamos el prerender estático: esta ruta
|
||||
// es una demo en vivo.
|
||||
export const prerender = false
|
||||
@ -0,0 +1,187 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Shell del tema Untitled UI.
|
||||
*
|
||||
* Arranca su propio `ActiveUix` + `Soma` + `ActiveEidos` (igual que la
|
||||
* docs-site de UIX) pero con el patch de tema `untitled-ui`. Es la
|
||||
* raíz de composición de esta ruta: posee modo (claro/oscuro) y
|
||||
* densidad, que se inyectan a Eidos como fuentes visuales.
|
||||
*
|
||||
* `+layout@.svelte` resetea la herencia al layout raíz (passthrough),
|
||||
* así esta ruta no hereda el chrome de `/uix`.
|
||||
*/
|
||||
import { onDestroy } from 'svelte'
|
||||
import type { Density } from '$libs/density'
|
||||
import type { ThemeEffective } from '$libs/theme'
|
||||
import { createActiveUix, setActiveUix } from '$active-uix'
|
||||
import { Soma } from '$soma/core/soma.svelte'
|
||||
import { siumLangs } from '$sium/langs/langs'
|
||||
import { secsLangs } from '$libs/secs'
|
||||
import { ActiveEidos } from '$uix/eidos'
|
||||
import {
|
||||
accordionSema,
|
||||
checkboxSema,
|
||||
contextMenuSema,
|
||||
dialogSema,
|
||||
dropdownMenuSema,
|
||||
menubarSema,
|
||||
popoverSema,
|
||||
radioGroupSema,
|
||||
selectSema,
|
||||
sliderSema,
|
||||
switchSema,
|
||||
tabsSema,
|
||||
toggleSema,
|
||||
toggleGroupSema,
|
||||
toolbarSema,
|
||||
tooltipSema
|
||||
} from '$uix/sema/components'
|
||||
import { untitledUiConfig } from '../_lib/untitled-ui'
|
||||
import '@/uix/eidos/index.css'
|
||||
import './theme.css'
|
||||
import './untitled-ui.css'
|
||||
|
||||
let { children } = $props()
|
||||
|
||||
// ── Runtime ───────────────────────────────────────────────────────
|
||||
// `events` enciende la capa semántica (sema): sonido + haptic + las
|
||||
// reacciones visuales `data-event-*`. Los packs afinan la cascada por
|
||||
// componente. Cablearlo además resuelve la hidratación de los
|
||||
// componentes con portal (menubar / dropdown / tooltip / popover /
|
||||
// dialog), que acceden a `uix.events` al montar.
|
||||
const uix = createActiveUix({
|
||||
langs: { schema: { sium: siumLangs, secs: secsLangs }, defaultLocale: 'es' as const },
|
||||
events: {
|
||||
sound: true,
|
||||
haptic: true,
|
||||
components: [
|
||||
accordionSema,
|
||||
checkboxSema,
|
||||
contextMenuSema,
|
||||
dialogSema,
|
||||
dropdownMenuSema,
|
||||
menubarSema,
|
||||
popoverSema,
|
||||
radioGroupSema,
|
||||
selectSema,
|
||||
sliderSema,
|
||||
switchSema,
|
||||
tabsSema,
|
||||
toggleSema,
|
||||
toggleGroupSema,
|
||||
toolbarSema,
|
||||
tooltipSema
|
||||
]
|
||||
}
|
||||
})
|
||||
setActiveUix(uix)
|
||||
Soma.create()
|
||||
|
||||
// ── Modo + densidad (fuentes visuales para Eidos) ─────────────────
|
||||
let mode = $state<ThemeEffective>('light')
|
||||
let density = $state<Density>('comfortable')
|
||||
|
||||
const modeListeners = new Set<(v: ThemeEffective) => void>()
|
||||
const densityListeners = new Set<(v: Density) => void>()
|
||||
|
||||
const activeEidos = ActiveEidos.create({
|
||||
applyDom: true,
|
||||
config: untitledUiConfig,
|
||||
theme: 'untitled-ui',
|
||||
modeSource: {
|
||||
get: () => mode,
|
||||
onChange(handler) {
|
||||
modeListeners.add(handler)
|
||||
return () => modeListeners.delete(handler)
|
||||
}
|
||||
},
|
||||
densitySource: {
|
||||
get: () => density,
|
||||
onChange(handler) {
|
||||
densityListeners.add(handler)
|
||||
return () => densityListeners.delete(handler)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
const m = mode
|
||||
for (const l of modeListeners) l(m)
|
||||
})
|
||||
$effect(() => {
|
||||
const d = density
|
||||
for (const l of densityListeners) l(d)
|
||||
})
|
||||
|
||||
const DENSITIES: { id: Density; label: string }[] = [
|
||||
{ id: 'compact', label: 'Compact' },
|
||||
{ id: 'comfortable', label: 'Comfortable' },
|
||||
{ id: 'spacious', label: 'Spacious' }
|
||||
]
|
||||
|
||||
onDestroy(() => {
|
||||
activeEidos.dispose()
|
||||
uix.dispose()
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Roboto+Mono:wght@400;500&display=swap"
|
||||
/>
|
||||
</svelte:head>
|
||||
|
||||
<div data-temas-shell>
|
||||
<header data-temas-topbar>
|
||||
<div data-temas-brand>
|
||||
<span data-temas-brand-mark>U</span>
|
||||
<div data-temas-brand-text>
|
||||
<strong>Untitled UI</strong>
|
||||
<small>Tema · activeUIX / Eidos</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-temas-tools>
|
||||
<div data-temas-segmented role="group" aria-label="Densidad">
|
||||
{#each DENSITIES as d}
|
||||
<button
|
||||
type="button"
|
||||
data-active={density === d.id ? 'true' : 'false'}
|
||||
onclick={() => (density = d.id)}
|
||||
>
|
||||
{d.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
data-temas-mode-btn
|
||||
aria-label="Cambiar modo"
|
||||
aria-pressed={mode === 'dark'}
|
||||
onclick={() => (mode = mode === 'light' ? 'dark' : 'light')}
|
||||
>
|
||||
{#if mode === 'light'}
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
||||
</svg>
|
||||
<span>Light</span>
|
||||
{:else}
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="4" />
|
||||
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41" />
|
||||
</svg>
|
||||
<span>Dark</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main data-temas-canvas>
|
||||
{@render children?.()}
|
||||
</main>
|
||||
</div>
|
||||
@ -0,0 +1,836 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Página de theming — Untitled UI.
|
||||
* Muestra todos los elementos del tema: paletas de color (roles + rampas
|
||||
* de 12 pasos + superficies), tipografía, espaciado, radios, sombras,
|
||||
* tamaños y el aspecto real de los controles con todas sus variantes.
|
||||
*/
|
||||
import { onMount } from 'svelte'
|
||||
import Button from '$uix/eidos/components/button'
|
||||
import Badge from '$uix/eidos/components/badge'
|
||||
import Card from '$uix/eidos/components/card'
|
||||
import Field from '$uix/eidos/components/field'
|
||||
import Select from '$uix/eidos/components/select'
|
||||
import Checkbox from '$uix/eidos/components/checkbox'
|
||||
import RadioGroup from '$uix/eidos/components/radio-group'
|
||||
import Switch from '$uix/eidos/components/switch'
|
||||
import Slider from '$uix/eidos/components/slider'
|
||||
import Progress from '$uix/eidos/components/progress'
|
||||
import Banner from '$uix/eidos/components/banner'
|
||||
import Avatar from '$uix/eidos/components/avatar'
|
||||
import Separator from '$uix/eidos/components/separator'
|
||||
import Tabs from '$uix/eidos/components/tabs'
|
||||
import Accordion from '$uix/eidos/components/accordion'
|
||||
import Toggle from '$uix/eidos/components/toggle'
|
||||
import ToggleGroup from '$uix/eidos/components/toggle-group'
|
||||
import Toolbar from '$uix/eidos/components/toolbar'
|
||||
import Popover from '$uix/eidos/components/popover'
|
||||
import Dialog from '$uix/eidos/components/dialog'
|
||||
import DropdownMenu from '$uix/eidos/components/dropdown-menu'
|
||||
import ContextMenu from '$uix/eidos/components/context-menu'
|
||||
import Menubar from '$uix/eidos/components/menubar'
|
||||
import Tooltip from '$uix/eidos/components/tooltip'
|
||||
import {
|
||||
THEME_ROLES,
|
||||
THEME_SCALES,
|
||||
COLOR_STEPS
|
||||
} from '../_lib/untitled-ui'
|
||||
|
||||
// ── Hex resolución (lee los literales de las escalas en runtime) ──
|
||||
let hex = $state<Record<string, string>>({})
|
||||
|
||||
function readHex() {
|
||||
if (typeof document === 'undefined') return
|
||||
const cs = getComputedStyle(document.documentElement)
|
||||
const out: Record<string, string> = {}
|
||||
for (const sc of THEME_SCALES) {
|
||||
for (const step of COLOR_STEPS) {
|
||||
const v = cs.getPropertyValue(`--scale-${sc.name}-${step}`).trim()
|
||||
if (v) out[`${sc.name}-${step}`] = v
|
||||
}
|
||||
}
|
||||
hex = out
|
||||
}
|
||||
|
||||
function luminance(value: string): number {
|
||||
const m = value.replace('#', '')
|
||||
if (m.length < 6) return 0.5
|
||||
const r = parseInt(m.slice(0, 2), 16) / 255
|
||||
const g = parseInt(m.slice(2, 4), 16) / 255
|
||||
const b = parseInt(m.slice(4, 6), 16) / 255
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b
|
||||
}
|
||||
|
||||
const stepInk = (key: string) =>
|
||||
luminance(hex[key] ?? '#000000') > 0.55 ? '#101828' : '#ffffff'
|
||||
|
||||
onMount(() => {
|
||||
readHex()
|
||||
const obs = new MutationObserver(() => readHex())
|
||||
obs.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['data-mode', 'data-theme']
|
||||
})
|
||||
return () => obs.disconnect()
|
||||
})
|
||||
|
||||
// ── Datos de las secciones de tokens ──────────────────────────────
|
||||
const TYPE_STYLES = [
|
||||
{ key: 'hero', label: 'Hero', sample: 'Diseña lo que ocurre' },
|
||||
{ key: 'h1', label: 'Heading 1', sample: 'The quick brown fox' },
|
||||
{ key: 'h2', label: 'Heading 2', sample: 'The quick brown fox' },
|
||||
{ key: 'h3', label: 'Heading 3', sample: 'The quick brown fox jumps' },
|
||||
{ key: 'h4', label: 'Heading 4', sample: 'The quick brown fox jumps over' },
|
||||
{ key: 'body', label: 'Body', sample: 'The quick brown fox jumps over the lazy dog.' },
|
||||
{ key: 'label', label: 'Label', sample: 'Email address' },
|
||||
{ key: 'caption', label: 'Caption', sample: 'Last updated 3 minutes ago' },
|
||||
{ key: 'code', label: 'Code', sample: 'const theme = "untitled-ui"' }
|
||||
]
|
||||
|
||||
const SPACE = ['0', '0-5', '1', '1-5', '2', '2-5', '3', '3-5', '4', '5', '6', '8', '10', '12', '16']
|
||||
const RADII = ['none', 'sm', 'md', 'lg', 'xl', 'xxl']
|
||||
const SHADOWS = ['1', '2', '3', '4', '5', '6']
|
||||
const SIZES = ['xxs', 'xs', 'sm', 'md', 'lg', 'xl', 'xxl']
|
||||
|
||||
const BTN_VARIANTS = ['solid', 'soft', 'surface', 'outline', 'ghost', 'plain'] as const
|
||||
const BTN_SIZES = ['xs', 'sm', 'md', 'lg', 'xl'] as const
|
||||
const CHIP_VARIANTS = ['soft', 'solid', 'outline', 'ghost'] as const
|
||||
const BADGE_COLORS = [
|
||||
'primary',
|
||||
'neutral',
|
||||
'affirm',
|
||||
'fulfill',
|
||||
'risk',
|
||||
'threat',
|
||||
'loss'
|
||||
] as const
|
||||
|
||||
// ── Estado de los controles interactivos ──────────────────────────
|
||||
let email = $state('olivia@untitledui.com')
|
||||
let fruit = $state<string[]>(['mango'])
|
||||
let agree = $state(true)
|
||||
let notify = $state(false)
|
||||
let plan = $state('pro')
|
||||
let wifi = $state(true)
|
||||
let analytics = $state(false)
|
||||
let volume = $state([64])
|
||||
|
||||
let tab = $state('overview')
|
||||
let tabsPills = $state('account')
|
||||
let faq = $state<string[]>(['what'])
|
||||
|
||||
let bold = $state(true)
|
||||
let fmt = $state<string[]>(['bold'])
|
||||
let align = $state<string[]>(['left'])
|
||||
let toolbarFmt = $state<string[]>(['bold'])
|
||||
|
||||
// ── Estado de los overlays / menús ────────────────────────────────
|
||||
let menuNotifications = $state(true)
|
||||
let menuTheme = $state('system')
|
||||
let menubarValue = $state('')
|
||||
|
||||
</script>
|
||||
|
||||
{#snippet plusIcon()}
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M12 5v14M5 12h14" />
|
||||
</svg>
|
||||
{/snippet}
|
||||
|
||||
{#snippet arrowIcon()}
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M5 12h14M13 6l6 6-6 6" />
|
||||
</svg>
|
||||
{/snippet}
|
||||
|
||||
<!-- ═══ Hero ═══════════════════════════════════════════════════════ -->
|
||||
<section data-temas-hero>
|
||||
<h1>Untitled UI · Carbon</h1>
|
||||
<p>
|
||||
Tema para activeUIX sobre el sistema de theming de Eidos. Primario <strong>carbón</strong>
|
||||
monocromo, tipografía Inter, grises fríos, radios de 8px, sombras en capas y anillos de foco
|
||||
suaves — los 9 roles canónicos retintados, sin inventar variants ni roles nuevos.
|
||||
</p>
|
||||
<div data-temas-hero-meta>
|
||||
<span data-temas-pill><i style="background: var(--color-primary-solid)"></i> Carbon · #1F2024</span>
|
||||
<span data-temas-pill><i style="background: var(--color-content-primary)"></i> Inter</span>
|
||||
<span data-temas-pill><i style="background: var(--color-fulfill-solid)"></i> 9 roles · 5 variants</span>
|
||||
<span data-temas-pill><i style="background: var(--color-border-strong)"></i> Light · Dark · Density</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══ Color ══════════════════════════════════════════════════════ -->
|
||||
<section data-temas-section>
|
||||
<div data-temas-section-head>
|
||||
<h2>Color</h2>
|
||||
<p>Los 9 roles canónicos mapeados a las paletas de Untitled UI. El theme retinta; los roles son canon fijo del framework.</p>
|
||||
</div>
|
||||
|
||||
<div data-temas-stack>
|
||||
<p data-temas-subhead>Roles semánticos</p>
|
||||
<div data-temas-roles>
|
||||
{#each THEME_ROLES as r}
|
||||
<div data-temas-role>
|
||||
<div data-temas-role-solid style="background: var(--color-{r.role}-solid)"></div>
|
||||
<div data-temas-role-body>
|
||||
<strong>{r.label}</strong>
|
||||
<small>{r.note}</small>
|
||||
<div data-temas-role-tints>
|
||||
<i style="background: var(--color-{r.role}-track)"></i>
|
||||
<i style="background: var(--color-{r.role}-element)"></i>
|
||||
<i style="background: var(--color-{r.role}-border)"></i>
|
||||
<i style="background: var(--color-{r.role}-text)"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-temas-stack>
|
||||
<p data-temas-subhead>Escalas físicas · 12 pasos</p>
|
||||
{#each THEME_SCALES as sc}
|
||||
<div data-temas-ramp>
|
||||
<div data-temas-ramp-head>
|
||||
<strong>{sc.label}</strong>
|
||||
<code>{hex[`${sc.name}-9`] ?? `--scale-${sc.name}-9`}</code>
|
||||
</div>
|
||||
<div data-temas-ramp-steps>
|
||||
{#each COLOR_STEPS as step}
|
||||
<div
|
||||
data-temas-step
|
||||
style="background: var(--scale-{sc.name}-{step}); color: {stepInk(`${sc.name}-${step}`)}"
|
||||
title={hex[`${sc.name}-${step}`] ?? ''}
|
||||
>
|
||||
{step}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div data-temas-stack>
|
||||
<p data-temas-subhead>Superficies, contenido y bordes</p>
|
||||
<div data-temas-surfaces>
|
||||
<div data-temas-surface style="background: var(--color-surface-default)">
|
||||
<span>surface</span><code>default</code>
|
||||
</div>
|
||||
<div data-temas-surface style="background: var(--color-surface-raised)">
|
||||
<span>surface</span><code>raised</code>
|
||||
</div>
|
||||
<div data-temas-surface style="background: var(--color-surface-muted)">
|
||||
<span>surface</span><code>muted</code>
|
||||
</div>
|
||||
<div data-temas-surface style="background: var(--color-content-primary); color: var(--color-surface-default)">
|
||||
<span>content</span><code style="color: inherit; opacity: .7">primary</code>
|
||||
</div>
|
||||
<div data-temas-surface style="background: var(--color-content-secondary); color: var(--color-surface-default)">
|
||||
<span>content</span><code style="color: inherit; opacity: .7">secondary</code>
|
||||
</div>
|
||||
<div data-temas-surface style="background: var(--color-surface-raised); border-color: var(--color-border-strong); border-width: 2px">
|
||||
<span>border</span><code>strong</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══ Tipografía ═════════════════════════════════════════════════ -->
|
||||
<section data-temas-section>
|
||||
<div data-temas-section-head>
|
||||
<h2>Tipografía</h2>
|
||||
<p>Inter para display y texto. Named styles del sistema (hero → caption + code).</p>
|
||||
</div>
|
||||
<div data-temas-panel>
|
||||
{#each TYPE_STYLES as t}
|
||||
<div data-temas-type-row>
|
||||
<div data-temas-type-meta>
|
||||
<strong>{t.label}</strong>
|
||||
<code>--style-{t.key}</code>
|
||||
</div>
|
||||
<div
|
||||
data-temas-type-sample
|
||||
style="font-family: var(--style-{t.key}-font-family); font-size: var(--style-{t.key}-font-size); font-weight: var(--style-{t.key}-font-weight); line-height: var(--style-{t.key}-line-height); letter-spacing: var(--style-{t.key}-letter-spacing);"
|
||||
>
|
||||
{t.sample}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══ Espaciado / Radio / Sombra / Tamaño ════════════════════════ -->
|
||||
<section data-temas-section>
|
||||
<div data-temas-section-head>
|
||||
<h2>Espaciado, radio, sombra y tamaño</h2>
|
||||
<p>Primitivas estables del tema. La escala base es de 4px; controles a 8px de radio.</p>
|
||||
</div>
|
||||
|
||||
<div data-temas-fieldset>
|
||||
<div data-temas-panel data-temas-stack>
|
||||
<p data-temas-subhead>Spacing</p>
|
||||
{#each SPACE as k}
|
||||
<div data-temas-space-row>
|
||||
<code>--space-{k}</code>
|
||||
<div data-temas-space-bar style="inline-size: var(--space-{k})"></div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div data-temas-stack>
|
||||
<div data-temas-panel data-temas-stack>
|
||||
<p data-temas-subhead>Radius</p>
|
||||
<div data-temas-tiles>
|
||||
{#each RADII as k}
|
||||
<div data-temas-radius-tile style="border-radius: var(--radius-{k})">{k}</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-temas-panel data-temas-stack>
|
||||
<p data-temas-subhead>Sizes · control-height</p>
|
||||
{#each SIZES as k}
|
||||
<div data-temas-size-row>
|
||||
<div data-temas-size-bar style="block-size: var(--size-{k}-control-height)">{k}</div>
|
||||
<code style="color: var(--color-content-muted); font-family: var(--font-mono); font-size: var(--font-size-xs)">var(--size-{k}-control-height)</code>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-temas-panel data-temas-stack>
|
||||
<p data-temas-subhead>Shadows</p>
|
||||
<div data-temas-tiles>
|
||||
{#each SHADOWS as k}
|
||||
<div data-temas-shadow-tile style="box-shadow: var(--shadow-{k})">shadow-{k}</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══ Botones ════════════════════════════════════════════════════ -->
|
||||
<section data-temas-section>
|
||||
<div data-temas-section-head>
|
||||
<h2>Botones</h2>
|
||||
<p>6 variants · 9 roles · 5 tamaños · estados. El primario es carbón; el texto sobre sólidos de color resuelve a blanco.</p>
|
||||
</div>
|
||||
|
||||
<div data-temas-demo>
|
||||
<div data-temas-demo-block>
|
||||
<p data-temas-subhead>Variants</p>
|
||||
<div data-temas-cluster>
|
||||
{#each BTN_VARIANTS as v}
|
||||
<Button variant={v}>{v}</Button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-temas-demo-block>
|
||||
<p data-temas-subhead>Roles e intents</p>
|
||||
<div data-temas-cluster>
|
||||
<Button color="primary">Primary</Button>
|
||||
<Button color="secondary" variant="surface">Secondary</Button>
|
||||
<Button color="neutral" variant="outline">Neutral</Button>
|
||||
<Button intent="affirm">Affirm</Button>
|
||||
<Button intent="fulfill">Fulfill</Button>
|
||||
<Button intent="risk">Risk</Button>
|
||||
<Button intent="threat">Threat</Button>
|
||||
<Button intent="loss">Loss</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-temas-demo-block>
|
||||
<p data-temas-subhead>Tamaños</p>
|
||||
<div data-temas-cluster>
|
||||
{#each BTN_SIZES as sz}
|
||||
<Button size={sz}>Button {sz}</Button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-temas-demo-block>
|
||||
<p data-temas-subhead>Con icono y estados</p>
|
||||
<div data-temas-cluster>
|
||||
<Button icon={plusIcon}>Nuevo</Button>
|
||||
<Button variant="outline" endIcon={arrowIcon}>Continuar</Button>
|
||||
<Button iconOnly icon={plusIcon} aria-label="Añadir" />
|
||||
<Button loading>Guardando</Button>
|
||||
<Button disabled>Desactivado</Button>
|
||||
<Button intent="threat" variant="soft">Eliminar</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══ Badges ═════════════════════════════════════════════════════ -->
|
||||
<section data-temas-section>
|
||||
<div data-temas-section-head>
|
||||
<h2>Badges</h2>
|
||||
<p>Pills de estado en los 4 chip-variants y la paleta completa de roles.</p>
|
||||
</div>
|
||||
<div data-temas-demo>
|
||||
<div data-temas-demo-block>
|
||||
<p data-temas-subhead>Colores (soft)</p>
|
||||
<div data-temas-cluster>
|
||||
{#each BADGE_COLORS as c}
|
||||
<Badge color={c} dot>{c}</Badge>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div data-temas-demo-block>
|
||||
<p data-temas-subhead>Variants</p>
|
||||
<div data-temas-cluster>
|
||||
{#each CHIP_VARIANTS as v}
|
||||
<Badge color="primary" variant={v}>{v}</Badge>
|
||||
{/each}
|
||||
<Badge color="fulfill" variant="soft">Activo</Badge>
|
||||
<Badge color="risk" variant="soft" removable onRemove={() => {}}>Filtro</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══ Formulario ═════════════════════════════════════════════════ -->
|
||||
<section data-temas-section>
|
||||
<div data-temas-section-head>
|
||||
<h2>Controles de formulario</h2>
|
||||
<p>Field, Select, Checkbox, Radio, Switch y Slider con el aspecto del tema.</p>
|
||||
</div>
|
||||
|
||||
<div data-temas-fieldset>
|
||||
<div data-temas-panel data-temas-stack>
|
||||
<Field name="email">
|
||||
<Field.Label>Email <Field.RequiredIndicator /></Field.Label>
|
||||
<Field.Control>
|
||||
<Field.Input type="email" bind:value={email} placeholder="tú@empresa.com" />
|
||||
</Field.Control>
|
||||
<Field.HelperText>Usaremos esto para iniciar sesión.</Field.HelperText>
|
||||
</Field>
|
||||
|
||||
<Field name="fruit">
|
||||
<Field.Label>Fruta favorita</Field.Label>
|
||||
<Select bind:value={fruit}>
|
||||
<Select.Trigger>
|
||||
<Select.Value placeholder="Elige una fruta" />
|
||||
<Select.Indicator />
|
||||
</Select.Trigger>
|
||||
<Select.Portal>
|
||||
<Select.Content sideOffset={6}>
|
||||
<Select.Viewport>
|
||||
<Select.Item value="mango" label="Mango">Mango<Select.ItemIndicator /></Select.Item>
|
||||
<Select.Item value="lychee" label="Lichi">Lichi<Select.ItemIndicator /></Select.Item>
|
||||
<Select.Item value="guava" label="Guayaba">Guayaba<Select.ItemIndicator /></Select.Item>
|
||||
<Select.Item value="papaya" label="Papaya">Papaya<Select.ItemIndicator /></Select.Item>
|
||||
</Select.Viewport>
|
||||
</Select.Content>
|
||||
</Select.Portal>
|
||||
</Select>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div data-temas-panel data-temas-stack>
|
||||
<p data-temas-subhead>Selección</p>
|
||||
<label data-temas-cluster style="cursor: pointer">
|
||||
<Checkbox bind:checked={agree}><Checkbox.Indicator /></Checkbox>
|
||||
<span>Acepto los términos del servicio</span>
|
||||
</label>
|
||||
<label data-temas-cluster style="cursor: pointer">
|
||||
<Checkbox bind:checked={notify} color="affirm"><Checkbox.Indicator /></Checkbox>
|
||||
<span>Enviarme novedades del producto</span>
|
||||
</label>
|
||||
|
||||
<Separator />
|
||||
|
||||
<RadioGroup bind:value={plan}>
|
||||
<RadioGroup.Label data-temas-subhead>Plan</RadioGroup.Label>
|
||||
<label data-temas-cluster style="cursor: pointer">
|
||||
<RadioGroup.Item value="starter"><RadioGroup.Indicator /></RadioGroup.Item>
|
||||
<span>Starter</span>
|
||||
</label>
|
||||
<label data-temas-cluster style="cursor: pointer">
|
||||
<RadioGroup.Item value="pro"><RadioGroup.Indicator /></RadioGroup.Item>
|
||||
<span>Pro</span>
|
||||
</label>
|
||||
<label data-temas-cluster style="cursor: pointer">
|
||||
<RadioGroup.Item value="enterprise"><RadioGroup.Indicator /></RadioGroup.Item>
|
||||
<span>Enterprise</span>
|
||||
</label>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
<div data-temas-panel data-temas-stack>
|
||||
<p data-temas-subhead>Switch & Slider</p>
|
||||
<label data-temas-cluster style="justify-content: space-between; cursor: pointer">
|
||||
<span>Wi-Fi</span>
|
||||
<Switch bind:checked={wifi}><Switch.Thumb /></Switch>
|
||||
</label>
|
||||
<label data-temas-cluster style="justify-content: space-between; cursor: pointer">
|
||||
<span>Analítica</span>
|
||||
<Switch bind:checked={analytics} intent="affirm"><Switch.Thumb /></Switch>
|
||||
</label>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div data-temas-stack style="gap: var(--space-2)">
|
||||
<span data-temas-subhead style="margin: 0">Volumen · {volume[0]}%</span>
|
||||
<Slider bind:value={volume} min={0} max={100} aria-label="Volumen">
|
||||
<Slider.Range />
|
||||
<Slider.Thumb index={0} />
|
||||
</Slider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══ Feedback ═══════════════════════════════════════════════════ -->
|
||||
<section data-temas-section>
|
||||
<div data-temas-section-head>
|
||||
<h2>Feedback y estado</h2>
|
||||
<p>Banners por intent, progreso, avatares y tarjetas.</p>
|
||||
</div>
|
||||
|
||||
<div data-temas-stack>
|
||||
<Banner intent="primary" variant="soft">Tu prueba gratuita termina en 7 días. Mejora tu plan cuando quieras.</Banner>
|
||||
<Banner intent="fulfill" variant="soft">Cambios guardados correctamente.</Banner>
|
||||
<Banner intent="risk" variant="soft">Tu método de pago caduca pronto.</Banner>
|
||||
<Banner intent="threat" variant="soft">No se pudo procesar el pago. Revisa tus datos.</Banner>
|
||||
</div>
|
||||
|
||||
<div data-temas-fieldset>
|
||||
<div data-temas-panel data-temas-stack>
|
||||
<p data-temas-subhead>Progress</p>
|
||||
<Progress value={64} max={100}>
|
||||
<Progress.Label>Subiendo archivo</Progress.Label>
|
||||
<Progress.ValueText />
|
||||
<Progress.Indicator />
|
||||
</Progress>
|
||||
<div data-temas-cluster>
|
||||
<Progress value={72} max={100} shape="circular"><Progress.Indicator /></Progress>
|
||||
<Progress value={30} max={100} shape="circular"><Progress.Indicator /></Progress>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-temas-panel data-temas-stack>
|
||||
<p data-temas-subhead>Avatares</p>
|
||||
<div data-temas-cluster>
|
||||
<Avatar><Avatar.Fallback>OV</Avatar.Fallback></Avatar>
|
||||
<Avatar color="primary"><Avatar.Fallback>AM</Avatar.Fallback></Avatar>
|
||||
<Avatar color="affirm" variant="soft">
|
||||
<Avatar.Fallback>JV</Avatar.Fallback>
|
||||
<Avatar.Badge color="fulfill" position="bottom-end" dot />
|
||||
</Avatar>
|
||||
<Avatar color="risk" ring="soft"><Avatar.Fallback>SK</Avatar.Fallback></Avatar>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card variant="soft" color="primary">
|
||||
<Card.Header>
|
||||
<Card.Title>Plan Pro</Card.Title>
|
||||
<Card.Description>Todo lo del Starter, más colaboración ilimitada.</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Body>
|
||||
<strong style="font-size: var(--font-size-xl)">$29</strong>
|
||||
<span style="color: var(--color-content-muted)"> / mes</span>
|
||||
</Card.Body>
|
||||
<Card.Footer>
|
||||
<Button block icon={arrowIcon}>Elegir Pro</Button>
|
||||
</Card.Footer>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══ Tabs ═══════════════════════════════════════════════════════ -->
|
||||
<section data-temas-section>
|
||||
<div data-temas-section-head>
|
||||
<h2>Tabs</h2>
|
||||
<p>Variantes line y pills. Indicador animado, foco con anillo carbón, activo en color primario.</p>
|
||||
</div>
|
||||
<div data-temas-fieldset>
|
||||
<div data-temas-panel data-temas-stack>
|
||||
<p data-temas-subhead>Line</p>
|
||||
<Tabs bind:value={tab} variant="line">
|
||||
<Tabs.List aria-label="Secciones de ejemplo">
|
||||
<Tabs.Trigger value="overview">Overview</Tabs.Trigger>
|
||||
<Tabs.Trigger value="activity">Activity</Tabs.Trigger>
|
||||
<Tabs.Trigger value="settings">Settings</Tabs.Trigger>
|
||||
<Tabs.Indicator />
|
||||
</Tabs.List>
|
||||
<Tabs.Content value="overview">Resumen de la cuenta y métricas clave del mes.</Tabs.Content>
|
||||
<Tabs.Content value="activity">Registro de actividad reciente del equipo.</Tabs.Content>
|
||||
<Tabs.Content value="settings">Preferencias de notificación y seguridad.</Tabs.Content>
|
||||
</Tabs>
|
||||
</div>
|
||||
<div data-temas-panel data-temas-stack>
|
||||
<p data-temas-subhead>Pills</p>
|
||||
<Tabs bind:value={tabsPills} variant="pills">
|
||||
<Tabs.List aria-label="Secciones de ejemplo">
|
||||
<Tabs.Trigger value="account">Account</Tabs.Trigger>
|
||||
<Tabs.Trigger value="team">Team</Tabs.Trigger>
|
||||
<Tabs.Trigger value="billing">Billing</Tabs.Trigger>
|
||||
<Tabs.Indicator />
|
||||
</Tabs.List>
|
||||
<Tabs.Content value="account">Datos de perfil y sesión.</Tabs.Content>
|
||||
<Tabs.Content value="team">Miembros e invitaciones.</Tabs.Content>
|
||||
<Tabs.Content value="billing">Plan, método de pago y facturas.</Tabs.Content>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══ Disclosure ═════════════════════════════════════════════════ -->
|
||||
<section data-temas-section>
|
||||
<div data-temas-section-head>
|
||||
<h2>Disclosure</h2>
|
||||
<p>Accordion (outline) — apertura única, chevron y foco coherentes con el tema.</p>
|
||||
</div>
|
||||
<div data-temas-panel>
|
||||
<Accordion bind:value={faq} type="single" variant="outline">
|
||||
<Accordion.Item value="what">
|
||||
<Accordion.Header><Accordion.Trigger>¿Qué es activeUIX?</Accordion.Trigger></Accordion.Header>
|
||||
<Accordion.Content>
|
||||
Un sistema de UI en cuatro capas — morfo, soma, sema y eidos — donde el theming vive en
|
||||
eidos y retinta los roles canónicos.
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
<Accordion.Item value="theme">
|
||||
<Accordion.Header><Accordion.Trigger>¿Cómo se crea un tema?</Accordion.Trigger></Accordion.Header>
|
||||
<Accordion.Content>
|
||||
Con <code>ActiveEidos.create({ config })</code>: se remapean los 9 roles a paletas
|
||||
y se ajustan primitivas estables (tipografía, radio, sombra).
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
<Accordion.Item value="dark">
|
||||
<Accordion.Header><Accordion.Trigger>¿Y el modo oscuro?</Accordion.Trigger></Accordion.Header>
|
||||
<Accordion.Content>
|
||||
El tema declara escalas light y dark; el primario carbón se invierte solo gracias al slot
|
||||
<code>contrast</code>.
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<!-- ═══ Toggles ════════════════════════════════════════════════════ -->
|
||||
<section data-temas-section>
|
||||
<div data-temas-section-head>
|
||||
<h2>Toggles</h2>
|
||||
<p>Toggle suelto y toggle-group (segmentado, single + multiple). Activo en color primario.</p>
|
||||
</div>
|
||||
<div data-temas-demo>
|
||||
<div data-temas-demo-block>
|
||||
<p data-temas-subhead>Toggle</p>
|
||||
<div data-temas-cluster>
|
||||
<Toggle bind:pressed={bold}>Bold</Toggle>
|
||||
<Toggle variant="outline">Outline</Toggle>
|
||||
<Toggle variant="ghost">Ghost</Toggle>
|
||||
<Toggle disabled>Disabled</Toggle>
|
||||
</div>
|
||||
</div>
|
||||
<div data-temas-demo-block>
|
||||
<p data-temas-subhead>Toggle group · segmentado</p>
|
||||
<div data-temas-cluster>
|
||||
<ToggleGroup type="multiple" attached bind:value={fmt} aria-label="Formato de texto">
|
||||
<ToggleGroup.Item value="bold">B</ToggleGroup.Item>
|
||||
<ToggleGroup.Item value="italic">I</ToggleGroup.Item>
|
||||
<ToggleGroup.Item value="underline">U</ToggleGroup.Item>
|
||||
</ToggleGroup>
|
||||
<ToggleGroup type="single" attached bind:value={align} aria-label="Alineación">
|
||||
<ToggleGroup.Item value="left">Izq.</ToggleGroup.Item>
|
||||
<ToggleGroup.Item value="center">Centro</ToggleGroup.Item>
|
||||
<ToggleGroup.Item value="right">Der.</ToggleGroup.Item>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══ Toolbar ════════════════════════════════════════════════════ -->
|
||||
<section data-temas-section>
|
||||
<div data-temas-section-head>
|
||||
<h2>Toolbar</h2>
|
||||
<p>Barra de herramientas con botones, separador y grupo segmentado.</p>
|
||||
</div>
|
||||
<div data-temas-panel>
|
||||
<Toolbar aria-label="Editor">
|
||||
<Toolbar.Button>Deshacer</Toolbar.Button>
|
||||
<Toolbar.Button>Rehacer</Toolbar.Button>
|
||||
<Toolbar.Separator />
|
||||
<Toolbar.Group type="multiple" bind:value={toolbarFmt} aria-label="Formato">
|
||||
<Toolbar.GroupItem value="bold">B</Toolbar.GroupItem>
|
||||
<Toolbar.GroupItem value="italic">I</Toolbar.GroupItem>
|
||||
<Toolbar.GroupItem value="underline">U</Toolbar.GroupItem>
|
||||
</Toolbar.Group>
|
||||
<Toolbar.Separator />
|
||||
<Toolbar.Button>Compartir</Toolbar.Button>
|
||||
</Toolbar>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<!-- ═══ Overlays ═══════════════════════════════════════════════════ -->
|
||||
<section data-temas-section>
|
||||
<div data-temas-section-head>
|
||||
<h2>Overlays</h2>
|
||||
<p>Popover y dialog — paneles portaleados con la superficie, borde y sombra del tema.</p>
|
||||
</div>
|
||||
<div data-temas-panel data-temas-cluster>
|
||||
<Popover>
|
||||
<Popover.Trigger>Abrir popover</Popover.Trigger>
|
||||
<Popover.Portal>
|
||||
<Popover.Content side="bottom" align="start" sideOffset={8}>
|
||||
<Popover.Arrow />
|
||||
<Popover.Title>Dimensiones</Popover.Title>
|
||||
<Popover.Description>Ajusta el ancho del panel lateral.</Popover.Description>
|
||||
<div data-temas-cluster style="margin-block-start: var(--space-3)">
|
||||
<Button size="sm">Aplicar</Button>
|
||||
<Popover.Close>Cerrar</Popover.Close>
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover.Portal>
|
||||
</Popover>
|
||||
|
||||
<Dialog>
|
||||
<Dialog.Trigger>Abrir dialog</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay />
|
||||
<Dialog.Content>
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>¿Eliminar proyecto?</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Esta acción es permanente y no se puede deshacer.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
<Dialog.Footer>
|
||||
<Dialog.Close>Cancelar</Dialog.Close>
|
||||
<Button intent="threat">Eliminar</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══ Menús ══════════════════════════════════════════════════════ -->
|
||||
<section data-temas-section>
|
||||
<div data-temas-section-head>
|
||||
<h2>Menús</h2>
|
||||
<p>Dropdown, menubar, context menu y tooltip — todos portaleados, con la sombra del tema.</p>
|
||||
</div>
|
||||
<div data-temas-panel data-temas-stack>
|
||||
<p data-temas-subhead>Menubar</p>
|
||||
<Menubar bind:value={menubarValue}>
|
||||
<Menubar.Menu value="file">
|
||||
<Menubar.Trigger>Archivo</Menubar.Trigger>
|
||||
<Menubar.Portal>
|
||||
<Menubar.Content>
|
||||
<Menubar.Item>Nuevo documento</Menubar.Item>
|
||||
<Menubar.Item>Abrir…</Menubar.Item>
|
||||
<Menubar.Separator />
|
||||
<Menubar.Item>Guardar</Menubar.Item>
|
||||
</Menubar.Content>
|
||||
</Menubar.Portal>
|
||||
</Menubar.Menu>
|
||||
<Menubar.Menu value="edit">
|
||||
<Menubar.Trigger>Editar</Menubar.Trigger>
|
||||
<Menubar.Portal>
|
||||
<Menubar.Content>
|
||||
<Menubar.Item>Deshacer</Menubar.Item>
|
||||
<Menubar.Item>Rehacer</Menubar.Item>
|
||||
</Menubar.Content>
|
||||
</Menubar.Portal>
|
||||
</Menubar.Menu>
|
||||
<Menubar.Menu value="view">
|
||||
<Menubar.Trigger>Ver</Menubar.Trigger>
|
||||
<Menubar.Portal>
|
||||
<Menubar.Content>
|
||||
<Menubar.CheckboxItem bind:checked={menuNotifications}>
|
||||
Mostrar barra lateral
|
||||
</Menubar.CheckboxItem>
|
||||
</Menubar.Content>
|
||||
</Menubar.Portal>
|
||||
</Menubar.Menu>
|
||||
</Menubar>
|
||||
|
||||
<div data-temas-cluster>
|
||||
<DropdownMenu>
|
||||
<DropdownMenu.Trigger variant="outline">Opciones</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content side="bottom" align="start" sideOffset={6}>
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.GroupHeading>Cuenta</DropdownMenu.GroupHeading>
|
||||
<DropdownMenu.Item>Perfil</DropdownMenu.Item>
|
||||
<DropdownMenu.Item>Facturación</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.CheckboxItem bind:checked={menuNotifications}>
|
||||
Notificaciones
|
||||
</DropdownMenu.CheckboxItem>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.RadioGroup bind:value={menuTheme}>
|
||||
<DropdownMenu.RadioItem value="light">Claro</DropdownMenu.RadioItem>
|
||||
<DropdownMenu.RadioItem value="dark">Oscuro</DropdownMenu.RadioItem>
|
||||
<DropdownMenu.RadioItem value="system">Sistema</DropdownMenu.RadioItem>
|
||||
</DropdownMenu.RadioGroup>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
|
||||
<Tooltip.Group delayDuration={250}>
|
||||
<Tooltip>
|
||||
<Tooltip.Trigger>Pasa el ratón</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content>
|
||||
<Tooltip.Arrow />
|
||||
Tooltip con la superficie y la sombra del tema
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip>
|
||||
</Tooltip.Group>
|
||||
</div>
|
||||
|
||||
<ContextMenu>
|
||||
<ContextMenu.Trigger>
|
||||
<div data-temas-ctx-target>Click derecho aquí</div>
|
||||
</ContextMenu.Trigger>
|
||||
<ContextMenu.Content>
|
||||
<ContextMenu.Item>Copiar</ContextMenu.Item>
|
||||
<ContextMenu.Item>Pegar</ContextMenu.Item>
|
||||
<ContextMenu.Separator />
|
||||
<ContextMenu.Item>Duplicar</ContextMenu.Item>
|
||||
</ContextMenu.Content>
|
||||
</ContextMenu>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══ Semántica ══════════════════════════════════════════════════ -->
|
||||
<section data-temas-section>
|
||||
<div data-temas-section-head>
|
||||
<h2>Semántica (sema)</h2>
|
||||
<p>
|
||||
La capa perceptual está activa: cada interacción emite una señal con familia + intent que se
|
||||
materializa en <strong>sonido</strong> (earcon), <strong>haptic</strong> (Vibration API) y una
|
||||
<strong>reacción visual</strong> <code>data-event-*</code> teñida por el intent del tema.
|
||||
Pulsa los botones — en un navegador con gesto de usuario sonarán.
|
||||
</p>
|
||||
</div>
|
||||
<div data-temas-panel data-temas-stack>
|
||||
<p data-temas-subhead>Intents perceptuales</p>
|
||||
<div data-temas-cluster>
|
||||
<Button intent="affirm">Affirm</Button>
|
||||
<Button intent="fulfill">Fulfill</Button>
|
||||
<Button intent="risk" variant="soft">Risk</Button>
|
||||
<Button intent="threat" variant="soft">Threat</Button>
|
||||
<Button intent="loss" variant="soft">Loss</Button>
|
||||
</div>
|
||||
<p style="color: var(--color-content-muted); font-size: var(--font-size-sm)">
|
||||
El motor (<code>EngineSemantic</code>) se cablea en el layout vía
|
||||
<code>createActiveUix({ events: { sound, haptic, components } })</code> con
|
||||
los packs sema por componente. Sonido y haptic son opt-in; aquí están encendidos.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer data-temas-foot>
|
||||
Tema <code>untitled-ui</code> · construido con <code>ActiveEidos.create({ config })</code> ·
|
||||
primario carbón + 9 roles canónicos retintados sobre Untitled UI.
|
||||
</footer>
|
||||
@ -0,0 +1,2 @@
|
||||
// SSR queda activo (default). El render estático se desactiva en +layout.ts.
|
||||
export const prerender = false
|
||||
@ -0,0 +1,157 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Isla cliente: secciones con componentes que portalean su contenido a
|
||||
* `<body>` (popover, dialog, dropdown, menubar, context-menu, tooltip).
|
||||
*
|
||||
* El SSR de esos portales rompe la hidratación de toda la página (mueven
|
||||
* nodos a `<body>` y desincronizan el cursor de hidratación de Svelte).
|
||||
* Por eso se importan SOLO desde aquí y este componente se carga con
|
||||
* `import()` dinámico tras `onMount` (ver `+page.svelte`): nunca entra en
|
||||
* el SSR ni en la hidratación inicial, y monta limpio en cliente — con el
|
||||
* contexto del layout (ActiveUix / Soma / Eidos / events) disponible.
|
||||
*/
|
||||
import Button from '$uix/eidos/components/button'
|
||||
import Popover from '$uix/eidos/components/popover'
|
||||
import Dialog from '$uix/eidos/components/dialog'
|
||||
import DropdownMenu from '$uix/eidos/components/dropdown-menu'
|
||||
import ContextMenu from '$uix/eidos/components/context-menu'
|
||||
import Menubar from '$uix/eidos/components/menubar'
|
||||
import Tooltip from '$uix/eidos/components/tooltip'
|
||||
|
||||
let menuNotifications = $state(true)
|
||||
let menuTheme = $state('system')
|
||||
let menubarValue = $state<string | undefined>(undefined)
|
||||
</script>
|
||||
|
||||
<!-- ═══ Overlays ═══════════════════════════════════════════════════ -->
|
||||
<section data-temas-section>
|
||||
<div data-temas-section-head>
|
||||
<h2>Overlays</h2>
|
||||
<p>Popover y dialog — paneles portaleados con la superficie, borde y sombra del tema.</p>
|
||||
</div>
|
||||
<div data-temas-panel data-temas-cluster>
|
||||
<Popover>
|
||||
<Popover.Trigger>Abrir popover</Popover.Trigger>
|
||||
<Popover.Portal>
|
||||
<Popover.Content side="bottom" align="start" sideOffset={8}>
|
||||
<Popover.Arrow />
|
||||
<Popover.Title>Dimensiones</Popover.Title>
|
||||
<Popover.Description>Ajusta el ancho del panel lateral.</Popover.Description>
|
||||
<div data-temas-cluster style="margin-block-start: var(--space-3)">
|
||||
<Button size="sm">Aplicar</Button>
|
||||
<Popover.Close>Cerrar</Popover.Close>
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover.Portal>
|
||||
</Popover>
|
||||
|
||||
<Dialog>
|
||||
<Dialog.Trigger>Abrir dialog</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay />
|
||||
<Dialog.Content>
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>¿Eliminar proyecto?</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Esta acción es permanente y no se puede deshacer.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
<Dialog.Footer>
|
||||
<Dialog.Close>Cancelar</Dialog.Close>
|
||||
<Button intent="threat">Eliminar</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══ Menús ══════════════════════════════════════════════════════ -->
|
||||
<section data-temas-section>
|
||||
<div data-temas-section-head>
|
||||
<h2>Menús</h2>
|
||||
<p>Dropdown, menubar, context menu y tooltip — todos portaleados, con la sombra del tema.</p>
|
||||
</div>
|
||||
<div data-temas-panel data-temas-stack>
|
||||
<p data-temas-subhead>Menubar</p>
|
||||
<Menubar bind:value={menubarValue}>
|
||||
<Menubar.Menu value="file">
|
||||
<Menubar.Trigger>Archivo</Menubar.Trigger>
|
||||
<Menubar.Portal>
|
||||
<Menubar.Content>
|
||||
<Menubar.Item>Nuevo documento</Menubar.Item>
|
||||
<Menubar.Item>Abrir…</Menubar.Item>
|
||||
<Menubar.Separator />
|
||||
<Menubar.Item>Guardar</Menubar.Item>
|
||||
</Menubar.Content>
|
||||
</Menubar.Portal>
|
||||
</Menubar.Menu>
|
||||
<Menubar.Menu value="edit">
|
||||
<Menubar.Trigger>Editar</Menubar.Trigger>
|
||||
<Menubar.Portal>
|
||||
<Menubar.Content>
|
||||
<Menubar.Item>Deshacer</Menubar.Item>
|
||||
<Menubar.Item>Rehacer</Menubar.Item>
|
||||
</Menubar.Content>
|
||||
</Menubar.Portal>
|
||||
</Menubar.Menu>
|
||||
<Menubar.Menu value="view">
|
||||
<Menubar.Trigger>Ver</Menubar.Trigger>
|
||||
<Menubar.Portal>
|
||||
<Menubar.Content>
|
||||
<Menubar.CheckboxItem bind:checked={menuNotifications}>
|
||||
Mostrar barra lateral
|
||||
</Menubar.CheckboxItem>
|
||||
</Menubar.Content>
|
||||
</Menubar.Portal>
|
||||
</Menubar.Menu>
|
||||
</Menubar>
|
||||
|
||||
<div data-temas-cluster>
|
||||
<DropdownMenu>
|
||||
<DropdownMenu.Trigger variant="outline">Opciones</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content side="bottom" align="start" sideOffset={6}>
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.GroupHeading>Cuenta</DropdownMenu.GroupHeading>
|
||||
<DropdownMenu.Item>Perfil</DropdownMenu.Item>
|
||||
<DropdownMenu.Item>Facturación</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.CheckboxItem bind:checked={menuNotifications}>
|
||||
Notificaciones
|
||||
</DropdownMenu.CheckboxItem>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.RadioGroup bind:value={menuTheme}>
|
||||
<DropdownMenu.RadioItem value="light">Claro</DropdownMenu.RadioItem>
|
||||
<DropdownMenu.RadioItem value="dark">Oscuro</DropdownMenu.RadioItem>
|
||||
<DropdownMenu.RadioItem value="system">Sistema</DropdownMenu.RadioItem>
|
||||
</DropdownMenu.RadioGroup>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
|
||||
<Tooltip.Group delayDuration={250}>
|
||||
<Tooltip>
|
||||
<Tooltip.Trigger>Pasa el ratón</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content>
|
||||
<Tooltip.Arrow />
|
||||
Tooltip con la superficie y la sombra del tema
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip>
|
||||
</Tooltip.Group>
|
||||
</div>
|
||||
|
||||
<ContextMenu>
|
||||
<ContextMenu.Trigger>
|
||||
<div data-temas-ctx-target>Click derecho aquí</div>
|
||||
</ContextMenu.Trigger>
|
||||
<ContextMenu.Content>
|
||||
<ContextMenu.Item>Copiar</ContextMenu.Item>
|
||||
<ContextMenu.Item>Pegar</ContextMenu.Item>
|
||||
<ContextMenu.Separator />
|
||||
<ContextMenu.Item>Duplicar</ContextMenu.Item>
|
||||
</ContextMenu.Content>
|
||||
</ContextMenu>
|
||||
</div>
|
||||
</section>
|
||||
@ -0,0 +1,479 @@
|
||||
/*
|
||||
* Chrome del shell + bloques de la página de foundations del tema.
|
||||
* Todo se construye con tokens de Eidos (`--color-*`, `--space-*`,
|
||||
* `--radius-*`, `--shadow-*`, `--font-*`) para que el propio chrome
|
||||
* refleje el tema y reaccione al modo claro/oscuro y a la densidad.
|
||||
*/
|
||||
|
||||
[data-temas-shell] {
|
||||
min-block-size: 100dvh;
|
||||
background: var(--color-surface-muted);
|
||||
color: var(--color-content-primary);
|
||||
font-family: var(--font-sans);
|
||||
font-size: var(--font-size-md);
|
||||
line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* ── Topbar ─────────────────────────────────────────────────────── */
|
||||
[data-temas-topbar] {
|
||||
position: sticky;
|
||||
inset-block-start: 0;
|
||||
z-index: var(--z-index-sticky);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-3) var(--space-6);
|
||||
background: color-mix(in srgb, var(--color-surface-raised) 86%, transparent);
|
||||
backdrop-filter: saturate(1.6) blur(10px);
|
||||
border-block-end: 1px solid var(--color-border-subtle);
|
||||
}
|
||||
|
||||
[data-temas-brand] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2-5);
|
||||
}
|
||||
|
||||
[data-temas-brand-mark] {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
inline-size: 36px;
|
||||
block-size: 36px;
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--color-primary-solid);
|
||||
color: var(--color-content-on-solid);
|
||||
font-weight: 700;
|
||||
font-size: var(--font-size-lg);
|
||||
box-shadow: var(--shadow-1);
|
||||
}
|
||||
|
||||
[data-temas-brand-text] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
line-height: 1.2;
|
||||
}
|
||||
[data-temas-brand-text] strong {
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
[data-temas-brand-text] small {
|
||||
color: var(--color-content-muted);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
[data-temas-tools] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
/* segmented control para densidad */
|
||||
[data-temas-segmented] {
|
||||
display: inline-flex;
|
||||
padding: 3px;
|
||||
gap: 2px;
|
||||
background: var(--color-surface-muted);
|
||||
border: 1px solid var(--color-border-subtle);
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
[data-temas-segmented] button {
|
||||
appearance: none;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
padding: var(--space-1) var(--space-2-5);
|
||||
border-radius: var(--radius-md);
|
||||
background: transparent;
|
||||
color: var(--color-content-secondary);
|
||||
font: inherit;
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 500;
|
||||
transition: background 140ms, color 140ms;
|
||||
}
|
||||
[data-temas-segmented] button[data-active='true'] {
|
||||
background: var(--color-surface-raised);
|
||||
color: var(--color-content-primary);
|
||||
box-shadow: var(--shadow-1);
|
||||
}
|
||||
|
||||
[data-temas-mode-btn] {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1-5);
|
||||
padding: var(--space-1-5) var(--space-3);
|
||||
border: 1px solid var(--color-border-default);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-surface-raised);
|
||||
color: var(--color-content-secondary);
|
||||
font: inherit;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 140ms, border-color 140ms;
|
||||
}
|
||||
[data-temas-mode-btn]:hover {
|
||||
background: var(--color-surface-muted);
|
||||
}
|
||||
[data-temas-mode-btn] svg {
|
||||
inline-size: 16px;
|
||||
block-size: 16px;
|
||||
}
|
||||
|
||||
/* ── Canvas ─────────────────────────────────────────────────────── */
|
||||
[data-temas-canvas] {
|
||||
max-inline-size: 1120px;
|
||||
margin-inline: auto;
|
||||
padding: var(--space-10) var(--space-6) var(--space-16);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-16);
|
||||
}
|
||||
|
||||
/* ── Hero ───────────────────────────────────────────────────────── */
|
||||
[data-temas-hero] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
[data-temas-hero] h1 {
|
||||
margin: 0;
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--font-size-xxl);
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.03em;
|
||||
line-height: 1.05;
|
||||
}
|
||||
[data-temas-hero] p {
|
||||
margin: 0;
|
||||
max-inline-size: 60ch;
|
||||
color: var(--color-content-secondary);
|
||||
font-size: var(--font-size-lg);
|
||||
line-height: 1.5;
|
||||
}
|
||||
[data-temas-hero-meta] {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
margin-block-start: var(--space-2);
|
||||
}
|
||||
[data-temas-pill] {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1-5);
|
||||
padding: var(--space-1) var(--space-2-5);
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--color-surface-raised);
|
||||
border: 1px solid var(--color-border-subtle);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 500;
|
||||
color: var(--color-content-secondary);
|
||||
}
|
||||
[data-temas-pill] i {
|
||||
inline-size: 10px;
|
||||
block-size: 10px;
|
||||
border-radius: var(--radius-full);
|
||||
}
|
||||
|
||||
/* ── Section ────────────────────────────────────────────────────── */
|
||||
[data-temas-section] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-5);
|
||||
}
|
||||
[data-temas-section-head] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-1);
|
||||
padding-block-end: var(--space-3);
|
||||
border-block-end: 1px solid var(--color-border-subtle);
|
||||
}
|
||||
[data-temas-section-head] h2 {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
[data-temas-section-head] p {
|
||||
margin: 0;
|
||||
color: var(--color-content-muted);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
[data-temas-subhead] {
|
||||
margin: 0 0 var(--space-1);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--color-content-muted);
|
||||
}
|
||||
|
||||
/* ── Panel / card genérico ──────────────────────────────────────── */
|
||||
[data-temas-panel] {
|
||||
background: var(--color-surface-raised);
|
||||
border: 1px solid var(--color-border-subtle);
|
||||
border-radius: var(--radius-xl);
|
||||
padding: var(--space-6);
|
||||
box-shadow: var(--shadow-1);
|
||||
}
|
||||
|
||||
/* ── Layout helpers ─────────────────────────────────────────────── */
|
||||
[data-temas-stack] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
[data-temas-cluster] {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
[data-temas-cols] {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
/* ── Color: role grid ───────────────────────────────────────────── */
|
||||
[data-temas-roles] {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: var(--space-3);
|
||||
}
|
||||
[data-temas-role] {
|
||||
border: 1px solid var(--color-border-subtle);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: clip;
|
||||
background: var(--color-surface-raised);
|
||||
}
|
||||
[data-temas-role-solid] {
|
||||
block-size: 64px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
padding: var(--space-2);
|
||||
}
|
||||
[data-temas-role-solid] span {
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 600;
|
||||
color: var(--color-content-on-solid);
|
||||
}
|
||||
[data-temas-role-body] {
|
||||
padding: var(--space-2-5) var(--space-3);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
[data-temas-role-body] strong {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
}
|
||||
[data-temas-role-body] small {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-content-muted);
|
||||
}
|
||||
[data-temas-role-tints] {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-block-start: var(--space-1);
|
||||
}
|
||||
[data-temas-role-tints] i {
|
||||
block-size: 14px;
|
||||
flex: 1;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid color-mix(in srgb, var(--color-content-primary) 8%, transparent);
|
||||
}
|
||||
|
||||
/* ── Color: full ramps ──────────────────────────────────────────── */
|
||||
[data-temas-ramp] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
[data-temas-ramp-head] {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
}
|
||||
[data-temas-ramp-head] strong {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
}
|
||||
[data-temas-ramp-head] code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-content-muted);
|
||||
}
|
||||
[data-temas-ramp-steps] {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: clip;
|
||||
border: 1px solid var(--color-border-subtle);
|
||||
}
|
||||
[data-temas-step] {
|
||||
block-size: 56px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
padding-block-end: 4px;
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* ── Surfaces row ───────────────────────────────────────────────── */
|
||||
[data-temas-surfaces] {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: var(--space-3);
|
||||
}
|
||||
[data-temas-surface] {
|
||||
border: 1px solid var(--color-border-subtle);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-3);
|
||||
min-block-size: 76px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
[data-temas-surface] span {
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 600;
|
||||
}
|
||||
[data-temas-surface] code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
color: var(--color-content-muted);
|
||||
}
|
||||
|
||||
/* ── Typography ─────────────────────────────────────────────────── */
|
||||
[data-temas-type-row] {
|
||||
display: grid;
|
||||
grid-template-columns: 120px 1fr;
|
||||
gap: var(--space-4);
|
||||
align-items: baseline;
|
||||
padding-block: var(--space-3);
|
||||
border-block-end: 1px solid var(--color-border-subtle);
|
||||
}
|
||||
[data-temas-type-row]:last-child {
|
||||
border-block-end: 0;
|
||||
}
|
||||
[data-temas-type-meta] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
[data-temas-type-meta] strong {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
}
|
||||
[data-temas-type-meta] code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-content-muted);
|
||||
}
|
||||
[data-temas-type-sample] {
|
||||
min-inline-size: 0;
|
||||
color: var(--color-content-primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ── Spacing scale ──────────────────────────────────────────────── */
|
||||
[data-temas-space-row] {
|
||||
display: grid;
|
||||
grid-template-columns: 6rem 1fr;
|
||||
gap: var(--space-3);
|
||||
align-items: center;
|
||||
}
|
||||
[data-temas-space-row] code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-content-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
[data-temas-space-bar] {
|
||||
block-size: 16px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-primary-solid);
|
||||
}
|
||||
|
||||
/* ── Radius / shadow / size tiles ───────────────────────────────── */
|
||||
[data-temas-tiles] {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
gap: var(--space-4);
|
||||
}
|
||||
[data-temas-radius-tile] {
|
||||
block-size: 84px;
|
||||
background: color-mix(in srgb, var(--color-primary-solid) 12%, var(--color-surface-raised));
|
||||
border: 1.5px solid var(--color-primary-solid);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
padding: var(--space-2);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 600;
|
||||
color: var(--color-content-secondary);
|
||||
}
|
||||
[data-temas-shadow-tile] {
|
||||
block-size: 84px;
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--color-surface-raised);
|
||||
border: 1px solid var(--color-border-subtle);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
padding: var(--space-2);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 600;
|
||||
color: var(--color-content-secondary);
|
||||
}
|
||||
[data-temas-size-row] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
[data-temas-size-bar] {
|
||||
border-radius: var(--radius-md);
|
||||
background: color-mix(in srgb, var(--color-primary-solid) 16%, var(--color-surface-raised));
|
||||
border: 1px solid color-mix(in srgb, var(--color-primary-solid) 36%, transparent);
|
||||
min-inline-size: 120px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-inline: var(--space-3);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 600;
|
||||
color: var(--color-content-secondary);
|
||||
}
|
||||
|
||||
/* ── Component showcase ─────────────────────────────────────────── */
|
||||
[data-temas-demo] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-5);
|
||||
}
|
||||
[data-temas-demo-block] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
[data-temas-fieldset] {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: var(--space-5);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
[data-temas-foot] {
|
||||
text-align: center;
|
||||
color: var(--color-content-muted);
|
||||
font-size: var(--font-size-sm);
|
||||
padding-block-start: var(--space-6);
|
||||
border-block-start: 1px solid var(--color-border-subtle);
|
||||
}
|
||||
[data-temas-foot] code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-content-secondary);
|
||||
}
|
||||
@ -0,0 +1,238 @@
|
||||
/*
|
||||
* Untitled UI — capa de pulido de componentes.
|
||||
*
|
||||
* Los recipes de Eidos son genéricos; esta capa los lleva a la anatomía
|
||||
* exacta de Untitled UI (inputs con sombra + borde gris-300 + anillo de
|
||||
* foco lavanda de 4px, botones con sombra y anillo real, switch/checkbox/
|
||||
* tabs/select coherentes, cards y badges con su contorno).
|
||||
*
|
||||
* Está scopeada a `[data-theme^='untitled-ui']` — atributo que ActiveEidos
|
||||
* proyecta en `<html>`, así que:
|
||||
* - solo aplica cuando el tema Untitled UI está activo (no contamina),
|
||||
* - tiene mayor especificidad que el recipe base (gana sin `!important`),
|
||||
* - alcanza también el contenido portaleado (select / menús / tooltip),
|
||||
* que cuelga de `<body>` bajo el mismo `<html>`.
|
||||
*/
|
||||
|
||||
[data-theme^='untitled-ui'] {
|
||||
/* Anillo de foco lavanda de Untitled UI: usa el brand violeta (= rol
|
||||
* secundario aquí), NO el color del control. Es la firma de UUI — un
|
||||
* halo suave de 4px del brand, no un anillo del color del propio control. */
|
||||
--uui-ring: 0 0 0 4px color-mix(in srgb, var(--color-secondary-solid) 24%, transparent);
|
||||
--uui-ring-error: 0 0 0 4px color-mix(in srgb, var(--color-threat-solid) 24%, transparent);
|
||||
/* Color de borde del control enfocado (brand violeta). */
|
||||
--uui-focus-border: var(--color-secondary-border);
|
||||
/* Sombra base de controles (Untitled UI shadow-xs). */
|
||||
--uui-control-shadow: 0 1px 2px 0 rgb(16 24 40 / 0.05);
|
||||
}
|
||||
|
||||
[data-theme^='untitled-ui'][data-mode='dark'] {
|
||||
--uui-ring: 0 0 0 4px color-mix(in srgb, var(--color-secondary-solid) 40%, transparent);
|
||||
--uui-ring-error: 0 0 0 4px color-mix(in srgb, var(--color-threat-solid) 40%, transparent);
|
||||
--uui-control-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.35);
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════════
|
||||
* Text inputs (Field)
|
||||
* ════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
[data-theme^='untitled-ui'] [data-field-control] {
|
||||
border-color: var(--color-border-default);
|
||||
background: var(--color-surface-default);
|
||||
box-shadow: var(--uui-control-shadow);
|
||||
}
|
||||
|
||||
[data-theme^='untitled-ui'] [data-field]:not([data-disabled]):not([data-readonly]) [data-field-control]:hover {
|
||||
border-color: var(--color-border-strong);
|
||||
}
|
||||
|
||||
[data-theme^='untitled-ui'] [data-field-control]:focus-within {
|
||||
border-color: var(--uui-focus-border);
|
||||
box-shadow: var(--uui-control-shadow), var(--uui-ring);
|
||||
}
|
||||
|
||||
[data-theme^='untitled-ui'] [data-field][data-invalid] [data-field-control] {
|
||||
border-color: var(--color-threat-border);
|
||||
}
|
||||
|
||||
[data-theme^='untitled-ui'] [data-field][data-invalid] [data-field-control]:focus-within {
|
||||
box-shadow: var(--uui-control-shadow), var(--uui-ring-error);
|
||||
}
|
||||
|
||||
[data-theme^='untitled-ui'] [data-field-label] {
|
||||
font-weight: 500;
|
||||
color: var(--color-content-secondary);
|
||||
}
|
||||
|
||||
[data-theme^='untitled-ui'] [data-field-input]::placeholder {
|
||||
color: var(--color-content-muted);
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════════
|
||||
* Select (trigger igual que un input, panel pulido)
|
||||
* ════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
[data-theme^='untitled-ui'] [data-select-trigger] {
|
||||
border-color: var(--color-border-default);
|
||||
background: var(--color-surface-default);
|
||||
box-shadow: var(--uui-control-shadow);
|
||||
}
|
||||
|
||||
[data-theme^='untitled-ui'] [data-select-trigger]:hover:not(:disabled):not([data-disabled]) {
|
||||
border-color: var(--color-border-strong);
|
||||
background: var(--color-surface-default);
|
||||
}
|
||||
|
||||
[data-theme^='untitled-ui'] [data-select-trigger]:focus-visible,
|
||||
[data-theme^='untitled-ui'] [data-select-trigger][data-state='open'] {
|
||||
border-color: var(--uui-focus-border);
|
||||
box-shadow: var(--uui-control-shadow), var(--uui-ring);
|
||||
}
|
||||
|
||||
[data-theme^='untitled-ui'] [data-select-content] {
|
||||
border-color: var(--color-border-subtle);
|
||||
box-shadow:
|
||||
0 12px 16px -4px rgb(16 24 40 / 0.08),
|
||||
0 4px 6px -2px rgb(16 24 40 / 0.03);
|
||||
}
|
||||
|
||||
[data-theme^='untitled-ui'][data-mode='dark'] [data-select-content] {
|
||||
box-shadow:
|
||||
0 12px 16px -4px rgb(0 0 0 / 0.5),
|
||||
0 4px 6px -2px rgb(0 0 0 / 0.3);
|
||||
}
|
||||
|
||||
[data-theme^='untitled-ui'] [data-select-item] {
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════════
|
||||
* Buttons — sombra sutil + anillo real (Untitled UI usa halo, no outline)
|
||||
* ════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
[data-theme^='untitled-ui'] [data-button]:not([data-variant='ghost']):not([data-variant='plain']) {
|
||||
box-shadow: var(--uui-control-shadow);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
[data-theme^='untitled-ui'] [data-button][data-variant='outline'],
|
||||
[data-theme^='untitled-ui'] [data-button][data-variant='surface'] {
|
||||
border-color: var(--color-border-default);
|
||||
color: var(--color-content-secondary);
|
||||
}
|
||||
|
||||
[data-theme^='untitled-ui'] [data-button][data-variant='outline']:hover:not([data-disabled]),
|
||||
[data-theme^='untitled-ui'] [data-button][data-variant='surface']:hover:not([data-disabled]) {
|
||||
background: var(--color-surface-muted);
|
||||
border-color: var(--color-border-strong);
|
||||
}
|
||||
|
||||
[data-theme^='untitled-ui'] [data-button]:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: var(--uui-control-shadow), var(--uui-ring);
|
||||
}
|
||||
|
||||
/* (El texto sobre sólidos de color lo resuelve ahora el engine: el slot
|
||||
* `contrast` por defecto mapea a `--color-content-on-solid`. Ver el fix en
|
||||
* render-css.ts + THEMING.md. Antes había aquí un workaround forzando blanco.) */
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════════
|
||||
* Switch — pista off gris claro + anillo de foco
|
||||
* ════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
[data-theme^='untitled-ui'] [data-switch]:not([data-state='checked']):not([data-disabled]) {
|
||||
background: var(--color-surface-muted);
|
||||
border-color: var(--color-border-default);
|
||||
}
|
||||
|
||||
[data-theme^='untitled-ui'] [data-switch]:focus-visible {
|
||||
box-shadow: var(--uui-ring);
|
||||
}
|
||||
|
||||
[data-theme^='untitled-ui'] [data-switch-thumb] {
|
||||
box-shadow:
|
||||
0 1px 3px rgb(16 24 40 / 0.1),
|
||||
0 1px 2px rgb(16 24 40 / 0.06);
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════════
|
||||
* Checkbox / Radio — anillo de foco coherente
|
||||
* ════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
[data-theme^='untitled-ui'] [data-checkbox]:focus-visible,
|
||||
[data-theme^='untitled-ui'] [data-checkbox]:has(:focus-visible),
|
||||
[data-theme^='untitled-ui'] [data-radio-group-item]:focus-visible,
|
||||
[data-theme^='untitled-ui'] [data-radio-group-item]:has(:focus-visible) {
|
||||
outline: none;
|
||||
box-shadow: var(--uui-ring);
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════════
|
||||
* Tabs — foco coherente + indicador brand
|
||||
* ════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
[data-theme^='untitled-ui'] [data-tabs-trigger]:focus-visible {
|
||||
box-shadow: var(--uui-ring);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
[data-theme^='untitled-ui'] [data-tabs][data-variant='line'] [data-tabs-trigger][data-state='active'] {
|
||||
color: var(--color-primary-text);
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════════
|
||||
* Accordion — foco
|
||||
* ════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
[data-theme^='untitled-ui'] [data-accordion-trigger]:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: var(--uui-ring);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════════
|
||||
* Menús portaleados (dropdown-menu / menubar / tooltip)
|
||||
* ════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
[data-theme^='untitled-ui'] :is([data-dropdown-menu-content], [data-menubar-content], [data-context-menu-content]) {
|
||||
border-color: var(--color-border-subtle);
|
||||
box-shadow:
|
||||
0 12px 16px -4px rgb(16 24 40 / 0.08),
|
||||
0 4px 6px -2px rgb(16 24 40 / 0.03);
|
||||
}
|
||||
|
||||
[data-theme^='untitled-ui'][data-mode='dark'] :is([data-dropdown-menu-content], [data-menubar-content], [data-context-menu-content]) {
|
||||
box-shadow:
|
||||
0 12px 16px -4px rgb(0 0 0 / 0.5),
|
||||
0 4px 6px -2px rgb(0 0 0 / 0.3);
|
||||
}
|
||||
|
||||
[data-theme^='untitled-ui'] :is([data-dropdown-menu-item], [data-menubar-item]):focus-visible,
|
||||
[data-theme^='untitled-ui'] :is([data-dropdown-menu-item], [data-menubar-item])[data-highlighted] {
|
||||
background: var(--color-surface-muted);
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════════
|
||||
* Card — borde gris-200 + sombra de Untitled UI
|
||||
* ════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
[data-theme^='untitled-ui'] [data-card] {
|
||||
border: 1px solid var(--color-border-subtle);
|
||||
box-shadow:
|
||||
0 1px 3px rgb(16 24 40 / 0.1),
|
||||
0 1px 2px -1px rgb(16 24 40 / 0.06);
|
||||
}
|
||||
|
||||
[data-theme^='untitled-ui'][data-mode='dark'] [data-card] {
|
||||
box-shadow:
|
||||
0 1px 3px rgb(0 0 0 / 0.4),
|
||||
0 1px 2px -1px rgb(0 0 0 / 0.4);
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════════
|
||||
* Badge — borde sutil del mismo tinte (firma de Untitled UI)
|
||||
* ════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
[data-theme^='untitled-ui'] [data-badge][data-variant='soft'] {
|
||||
border-color: color-mix(in srgb, var(--_badge-solid) 24%, transparent);
|
||||
}
|
||||
Loading…
Reference in new issue