@archeblack/ui · Guía
Cómo crear un producto
Un producto nuevo de la familia llega a funcionar con Arche en seis pasos: se declara su acento en el repositorio de Arche, se instala el paquete en un proyecto SvelteKit, se conecta el layout y se revisa contra las reglas antes de lanzar. Toda la guía usa example, el producto de ejemplo, y su código se siguió tal cual en un proyecto nuevo, con el nombre de un producto propio.
- Pasos
- 6
- Archivos del ejemplo
- 4
- Comandos
- 7
Paso 1 Qué es un producto
Un producto es una aplicación de la familia que usa @archeblack/. El vacío y la luz son de todos: fondo, capas, bordes, niveles de texto, estados, tipografía, espaciado, radios, brillo y movimiento son los mismos en cada producto. Lo único propio es el acento.
El alcance del acento: marca e interacción
El acento no se usa directo: lo leen cinco roles. Cuatro lo siguen y uno no. El botón primario es blanco también dentro de un producto, porque el blanco es la marca de Arche. Los mismos componentes, sin cambiar una línea, en Arche y en el producto de ejemplo:
Arche
Acento blancoEl link del texto usa color-link: cómo se declara el acento.
Example
Acento #e98950El link del texto usa color-link: cómo se declara el acento.
Los cinco roles
--arche-color-brandEl color de marca: el sigilo en su versión de color, en el favicon y los íconos de app. En la cabecera la marca va en blanco.--arche-color-linkLinks dentro del texto y botones de tipo link.--arche-color-selectedCasillas, pestañas, switches y paginación marcados. Su fondo suave, selected-soft, es el tinte del acento: la fila elegida de una tabla.--arche-color-focusAnillo de foco de todos los controles.--arche-color-quoteLa regla de las citas en la prosa (Prose). Es decorativa, pero el build la mide como el foco: 3:1 contra el fondo y las tres capas.--arche-color-primaryAcción principal. Blanca siempre, también en los productos.
El detalle de cada rol, con su contraste, está en Fundamentos · Acento y roles.
Paso 2 Declarar el producto
El acento se declara en el repositorio de Arche (Arche-DS), no en el producto: así pasa por la normalización y el chequeo de contraste antes de llegar a una pantalla, y el paquete publica el CSS de todos los productos.
El archivo del producto
Va en src/. name es el nombre del archivo, en minúsculas, con números y guiones, y empieza con una letra: es el valor de data-product y el nombre del CSS. accent es la semilla, cualquier color CSS opaco.
{ "name": "example", "accent": "#d4763c"}Después, en Arche:
npm run tokens # normaliza el acento y genera products/example.cssnpm run tokens:check # recetas, salidas y contraste en orden: sale con código 0Qué hace la normalización
La semilla es un punto de partida. El script la lleva a un acento que funciona sobre el vacío, en este orden:
- Lee la semilla como color CSS (hex,
rgb(),oklch()…) y la pasa a OKLCH. Tiene que ser opaca. - Si su croma es menor que 0,03, la semilla es un gris: el acento pasa a ser el blanco, como en Arche.
- Lleva la luminosidad al rango entre 0,72 y 0,84. El piso es el que pasa AA como texto en todos los tonos sobre el fondo más exigente, press sobre
surface-overlay. - Limita el croma a 0,2 y conserva el tono: el acento sigue siendo el color que eligió el producto.
- Si el color no entra en sRGB, baja el croma hasta que entre y elige el hex más cercano que siga dentro de las reglas.
- Compara su tono con los cuatro estados y avisa si queda a menos de 28° de alguno.
#d4763c #e98950 example, leído de resolved.json.
En la semilla la luminosidad quedaba debajo del piso y sube a 0,7219; el tono se mueve 0,1° al redondear a hex.El aviso de choque
Un acento cuyo tono queda a menos de 28° de un estado se permite, pero el build avisa: un link o una selección podrían leerse como un error o una advertencia. El aviso no detiene el build. Lo que el producto no negocia es que cada estado lleve ícono y texto.
Qué genera
La primera vez, npm run tokens escribe el CSS del producto y actualiza resolved.json con su acento, sus choques y su tabla de contraste. Esta es la salida para example, con los productos que hay hoy:
Aviso: Producto «example»: el tono del acento #e98950 (49.88°) está a 25.88° de danger (mínimo 28°). Un link o una selección podrían leerse como un estado.Aviso: Producto «hermes»: el tono del acento #e98950 (49.88°) está a 25.88° de danger (mínimo 28°). Un link o una selección podrían leerse como un estado.Escrito src/lib/styles/products/example.cssEscrito src/lib/tokens/resolved.jsonTokens generados. 1040 pares de contraste verificados, 0 fallas.El CSS generado, src/, solo redefine la familia del acento bajo [data-product="example"]. Los roles se recalculan solos en ese elemento, porque tokens.css los declara para :root y para cada [data-product]. No se edita a mano.
/* Archivo generado: no editar. Fuente: src/lib/tokens/tokens.json (npm run tokens). *//* Producto «example»: acento #d4763c normalizado a #e98950. Aviso: el tono del acento está a 25.88° de danger. */[data-product='example'] { --arche-color-accent: #e98950; --arche-color-accent-hover: #ee9d6e; --arche-color-accent-soft: #2b211f; --arche-color-accent-border: #674231; --arche-color-accent-tint: rgb(233 137 80 / 0.07);}Publicar la versión
El CSS del producto llega a los proyectos con una versión nueva de @archeblack/ en npm. Un producto nuevo sube el último número: no cambia nada de lo que ya existe. npm publish corre antes check, lint, los tests y tokens:check, y no publica si alguno falla.
# En Arche-DS, con el producto declarado y el árbol de git limpionpm version patch # sube la versión (un producto nuevo no rompe nada), con commit y tagnpm publish # corre los chequeos y publica @archeblack/ui en npmPaso 3 Instalar y conectar
El producto es un proyecto SvelteKit con Svelte 5.33 o superior y Node 22 o superior. Instala el paquete, carga sus estilos en el layout raíz y marca el documento con el producto.
La dependencia
npx sv create example --template minimal --types ts --no-add-ons --install npmcd examplenpm install @archeblack/uiEl package.json del producto queda con esta dependencia. El ^ fija la versión menor mientras Arche esté en 0.x: npm update trae los arreglos y los productos nuevos, y un cambio que rompe algo sube la versión menor y se instala a propósito, con @latest.
"@archeblack/ui": "^0.1.0"npm update @archeblack/ui # la última versión compatible con el rango de package.jsonnpm install @archeblack/ui@latest # la última publicada, aunque cambie la versión menor# En Arche-DS: empaqueta la versión local en archeblack-ui-0.1.0.tgz, sin publicarlanpm pack# En el producto, con las dos carpetas una al lado de la otranpm install ../Arche-DS/archeblack-ui-0.1.0.tgzLos estilos, en orden
Van al principio de src/, antes que cualquier otro import. El CSS de cada componente llega solo con el componente.
import '@archeblack/ui/fonts.css';import '@archeblack/ui/tokens.css';import '@archeblack/ui/base.css';import '@archeblack/ui/products/example.css';import './layout.css';fonts.css, opcional: Archivo, Piazzolla e IBM Plex Mono autoalojadas. Si el producto ya carga las fuentes, se omite.tokens.css: todas las variables--arche-*en:rooty los roles, que se recalculan en cada elemento condata-product.base.css: el documento (fondo, texto, margen delbody, selección, foco, el link para saltar al contenido y movimiento reducido), en la capaarche.base.products/: la familia del acento bajoexample.css [data-product="example"]. Le gana atokens.csspor especificidad, no por orden:tokens.cssdeclara el acento con:where(:root, [data-product]), que no pesa nada, y el selector de atributo del producto pesa más. Que vaya después es una convención: se lee en el orden en que se aplica.layout.css, los estilos propios del producto: al final y sin capa, así ganan donde el producto lo decide.
El producto en <html>, desde app.html
data-product va en <html> y se escribe en src/, no en el layout. El layout solo pinta dentro de <body>: ponerlo desde ahí exigiría JavaScript después de hidratar, y hasta entonces la página se vería con el acento blanco. Además, Dialog, Sheet, Popover, Menu, Combobox y DatePicker se abren en un portal al final de <body>, fuera de la raíz de la app: solo <html> los alcanza a todos.
<!doctype html><html lang="es" data-product="example"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="color-scheme" content="dark" /> <meta name="text-scale" content="scale" /> %sveltekit.head% </head> <body data-sveltekit-preload-data="hover"> <div style="display: contents">%sveltekit.body%</div> </body></html>Con Tailwind v4
El núcleo no depende de Tailwind; theme.css es un @theme opcional que apunta las utilidades a los tokens (text-muted, bg-surface, border-subtle, text-link…). El complemento de Tailwind de sv crea src/ y lo importa en el layout.
npx sv add tailwindcss="plugins:none" --install npmsrc/ pasa a cargar Arche. En el layout, los cuatro imports de Arche se van y layout.css queda como el único import de estilos: theme.css tiene que pasar por Tailwind, por eso va ahí y no en el layout. Los estilos propios del producto siguen debajo de estos imports.
@import 'tailwindcss';@import '@archeblack/ui/fonts.css';@import '@archeblack/ui/tokens.css';@import '@archeblack/ui/theme.css';@import '@archeblack/ui/base.css';@import '@archeblack/ui/products/example.css';En src/ se suma el orden de capas, justo antes de %sveltekit.head%; el resto queda como arriba:
<style> @layer properties, theme, base, arche, components, utilities;</style>%sveltekit.head%El orden de capas @layer properties, theme, base, arche, components, utilities; deja a Arche después del reset de Tailwind (base) y antes de las utilidades: un class="px-6" sobre un Button gana, y el reset no le quita el fondo. El orden lo fija la primera vez que aparece cada capa, y SvelteKit puede enlazar el CSS de un componente antes que layout.css; por eso la declaración va en app.html, antes que cualquier hoja. Sin ella, en un build de producción, el reset de Tailwind le borró el fondo y el borde al botón secundario. properties es la capa que Tailwind emite primero.
Paso 4 Usar los componentes
Los componentes, los íconos y las fechas salen del mismo paquete: el producto no suma Bits UI, Tabler ni @internationalized/ a sus dependencias.
Tres puntos de entrada
@archeblack/ trae los componentes y la función toast. @archeblack/ reexporta Tabler, siempre a través de <Icon> o de las props de ícono de cada componente. @archeblack/ reexporta @internationalized/, para el valor de DatePicker.
import { AppShell, Button, Field, Input, toast } from '@archeblack/ui';import { IconSend } from '@archeblack/ui/icons';import { today, getLocalTimeZone } from '@archeblack/ui/date';Una vez, en el layout raíz
- Un link para saltar al contenido, el primer elemento de la página:
<a class="arche-skip-link" href="#content">Ir al contenido</.a> base.csslo oculta hasta que recibe el foco, y lleva al<main id="content">del AppShell. - Un TooltipProvider que envuelve toda la app: comparte el retardo entre tooltips vecinos.
- Un Toaster: monta las regiones vivas desde el principio, y cualquier página llama a
toast(). - Un AppShell con
product="Example": la marca es el nombre del producto, solo y en blanco, en la voz del logotipo (un producto con sigilo pasamarky muestra su logotipo). En pantallas de menos de 48 rem la navegación pasa a un panel.
El ejemplo completo
Sobre el proyecto que crea npx sv create en el paso 3, con el src/ de ese paso, estos 4 archivos, copiados tal cual, dan una app de dos páginas con el acento de example. Así se ve su cabecera, en un marco con data-product="example", y la misma app sin producto, en Arche:
src/. Los estilos en orden, el favicon de la plantilla, el link para saltar al contenido, un TooltipProvider, un Toaster y el AppShell con el nombre del producto.
<script lang="ts"> import '@archeblack/ui/fonts.css'; import '@archeblack/ui/tokens.css'; import '@archeblack/ui/base.css'; import '@archeblack/ui/products/example.css'; import './layout.css'; import { page } from '$app/state'; import { resolve } from '$app/paths'; import favicon from '$lib/assets/favicon.svg'; import { AppShell, AppShellLink, Toaster, TooltipProvider } from '@archeblack/ui'; let { children } = $props(); const links = [ { href: resolve('/'), label: 'Mensajes' }, { href: resolve('/ajustes'), label: 'Ajustes' } ];</script><svelte:head> <link rel="icon" href={favicon} /></svelte:head><!-- Lo primero de la página: salta al <main id="content"> del AppShell. --><a class="arche-skip-link" href="#content">Ir al contenido</a><TooltipProvider> <AppShell product="Example" homeHref={resolve('/')} sticky> {#snippet navigation()} {#each links as link (link.href)} <AppShellLink href={link.href} current={page.url.pathname === link.href}> {link.label} </AppShellLink> {/each} {/snippet} {@render children()} </AppShell> <Toaster /></TooltipProvider>src/. Los estilos propios: la maquetación de las páginas, con clases propias y solo tokens.
/* Estilos propios de Example. Van después de Arche y solo usan clases propias. */.page { display: grid; gap: var(--arche-spacing-6); max-width: 40rem; padding: var(--arche-spacing-8) var(--arche-spacing-6);}.page h1 { margin: 0; font: var(--arche-typography-heading); font-stretch: var(--arche-typography-heading-font-width); letter-spacing: var(--arche-typography-heading-letter-spacing); text-transform: var(--arche-typography-heading-text-transform); color: var(--arche-color-text-strong);}.page p { margin: 0; color: var(--arche-color-text-muted);}.page__form { display: grid; gap: var(--arche-spacing-4);}.page__actions { display: flex; gap: var(--arche-spacing-2);}src/. Un formulario con Field, Input, Button, Tooltip y un toast al enviar.
<script lang="ts"> import { Button, Field, Input, Tooltip, toast } from '@archeblack/ui'; import { IconSend, IconX } from '@archeblack/ui/icons'; let to = $state(''); let error = $state(''); function send(event: SubmitEvent) { event.preventDefault(); if (!to.includes('@')) { error = 'Escribe un correo, por ejemplo ana@ejemplo.com.'; return; } error = ''; toast({ title: 'Mensaje enviado', description: to, variant: 'success' }); to = ''; }</script><svelte:head> <title>Mensajes · Example</title></svelte:head><div class="page"> <h1>Mensajes</h1> <p>Escribe a quién le llega el aviso. Example lo envía al instante.</p> <form class="page__form" novalidate onsubmit={send}> <Field label="Destinatario" hint="Un correo por envío." {error}> <Input type="email" autocomplete="email" bind:value={to} /> </Field> <div class="page__actions"> <Button type="submit" variant="primary" iconStart={IconSend}>Enviar</Button> <Tooltip content="Vaciar el campo"> {#snippet trigger(props)} <Button {...props} variant="ghost" icon={IconX} aria-label="Vaciar el campo" onclick={() => (to = '')} /> {/snippet} </Tooltip> </div> </form></div>src/. La segunda página de la navegación, con un Switch.
<script lang="ts"> import { Switch } from '@archeblack/ui'; let digest = $state(true);</script><svelte:head> <title>Ajustes · Example</title></svelte:head><div class="page"> <h1>Ajustes</h1> <Switch bind:checked={digest} label="Resumen diario" description="Un correo a las 9:00 con los mensajes del día anterior." /></div>npm run dev la levanta; npm run check y npm run build pasan sin errores. El build muestra un aviso de tiempos, [PLUGIN_TIMINGS]: Vite avisa cuando los plugins se llevan la mayor parte del tiempo del build. No es un error ni cambia el resultado. Parte de ese tiempo viene de @archeblack/, un barril que reexporta todo Tabler: para encontrar los íconos que se importan, Vite compila los más de 6.000 componentes del paquete, y el build tarda unas tres veces más que con los íconos importados de a uno. El resultado solo incluye los íconos importados.
Paso 5 Reglas que no se rompen
Los componentes ya cumplen las reglas de Arche. Estas son las que dependen del código del producto.
- Pintar con el acento directo:
color: var(--arche-color-accent)o, con Tailwind,text-accent. Usar el rol que corresponde:--arche-color-brand(la marca),--arche-color-link,--arche-color-selectedo--arche-color-focus. Si mañana el alcance del acento cambia, los roles cambian solos. - Pintar el botón principal con el acento del producto.
<Button variant="primary">, que es blanco en todos los productos, como la marca de la cabecera. El acento toca los links, la selección, el foco, las pestañas, los switches, la regla de las citas y el color de marca (el favicon y los íconos de app). - Un estado solo con color: un punto rojo, un borde verde, un número en naranja. Ícono y texto siempre: Alert, Badge con texto o un Toast con su variante. Con un acento que choca con un estado, esto es lo único que los distingue.
- Colores propios para texto o fondos (un gris a mano, un fondo de marca) sin medirlos. Los niveles de texto y las superficies de los fundamentos, que ya pasan AA.
tokens:checkmide los tokens, no los colores que el producto invente. - Encerrar un campo en una caja con borde o relleno. Field con Input tal cual: solo la línea inferior, que llega a 3:1 y se enciende con el foco.
- Un reset sin capa sobre elementos:
button { background: none }oa { color: inherit }pisan aButtony aCardconhref. Resets en una capa anterior a Arche, declarada enapp.html(<style>@layer reset, arche;</), o reglas que dejen fuera las clasesstyle> arche-*. Una clase propia sin capa sobre un componente sí vale, cuando es a propósito.
Qué mide tokens:check
npm run tokens:check corre en Arche para todos los productos y falla si algún par queda por debajo del mínimo. Hoy son 1040 pares y 0 fallas. Mide:
- Todo nivel de texto, cada estado y el acento como texto (
accent,accent-hover,link): 4,5:1 como mínimo contra el fondo, las tres superficies, hover y press compuestos sobre cada una y los fondos suaves. - El texto sobre el acento, sobre el primario y sobre la selección: 4,5:1.
- Foco, selección y los bordes de los controles (
border-controly su hover): 3:1 contra el fondo y las tres superficies. - Que cada
$valuedetokens.jsoncoincida con su receta y que los CSS generados,resolved.jsony los CSS de producto estén al día (sin archivos de productos que ya no existen).
- Link, el peor fondo
- Foco, el peor fondo
- Selección, el peor fondo
Los números son del producto example. Todo lo que el
producto pinte fuera de los tokens queda fuera del chequeo.
Paso 6 Antes de lanzar
Una pasada completa antes de mostrar el producto. Las casillas no se guardan: sirven para recorrer la lista en una sesión.
-
src/, conlib/ tokens/ products/ example.json npm run tokensynpm run tokens:checken verde. Si hay aviso de choque, está leído y decidido. -
node_modules/existe.@archeblack/ ui/ dist/ styles/ products/ example.css -
data-product="example"ylang="es"ensrc/.app.html -
fonts.css,tokens.css,base.cssy el CSS del producto; los estilos propios al final. Con Tailwind, el orden de capas enapp.html. -
El primer elemento de la página:
<a class="arche-skip-link" href="#content">, que lleva al<main id="content">delAppShell. Con Tab, es lo primero que aparece. -
Una sola vez cada uno, en
src/, con el nombre del producto.routes/ +layout.svelte -
Una búsqueda de
--arche-color-accenty detext-accentensrc/no encuentra nada. -
Errores, avisos y confirmaciones con
Alert,Badge,ToastoFieldcon error. -
Todo color propio de texto pasa 4,5:1 y todo límite de control, 3:1.
-
Los campos van con
FieldeInput,Textarea,Select,ComboboxoDatePicker, sin cajas. -
Los resets van en una capa o dejan fuera las clases
arche-*. -
Tab llega a todo y el foco se ve en el color del acento. Las animaciones propias respetan
prefers-reduced-motion. -
En el proyecto del producto.