Ir al contenido

@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/ui. 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 blanco

El link del texto usa color-link: cómo se declara el acento.

Example

Acento #e98950

El link del texto usa color-link: cómo se declara el acento.

Los cinco roles

  • --arche-color-brand Arche #ffffff example #e98950 El 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-link Arche #ffffff example #e98950 Links dentro del texto y botones de tipo link.
  • --arche-color-selected Arche #ffffff example #e98950 Casillas, 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-focus Arche #ffffff example #e98950 Anillo de foco de todos los controles.
  • --arche-color-quote Arche #ffffff example #e98950 La 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-primary Arche #ffffff example #ffffff · no sigue al acento Acció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.

Con el nombre de tu producto

Toda la guía usa example, el producto de ejemplo que ya está declarado en Arche: el código, las figuras, la salida de los comandos y el CSS generado son reales. Para un producto propio, se reemplaza example por su nombre en todo el código, y con el nombre cambian tres cosas: el archivo del producto, que pasa a ser src/lib/tokens/products/<nombre>.json; el CSS que genera npm run tokens y que importa el layout, products/<nombre>.css; y el valor de data-product en app.html. El nombre visible, Example, también se reemplaza en todo el código: es la prop product del AppShell y aparece en los títulos, en el texto de las páginas y en los comentarios.

El archivo del producto

Va en src/lib/tokens/products/example.json. 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.

json
{	"name": "example",	"accent": "#d4763c"}

Después, en Arche:

bash
npm run tokens        # normaliza el acento y genera products/example.cssnpm run tokens:check  # recetas, salidas y contraste en orden: sale con código 0

Qué 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:

  1. Lee la semilla como color CSS (hex, rgb(), oklch()…) y la pasa a OKLCH. Tiene que ser opaca.
  2. Si su croma es menor que 0,03, la semilla es un gris: el acento pasa a ser el blanco, como en Arche.
  3. 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.
  4. Limita el croma a 0,2 y conserva el tono: el acento sigue siendo el color que eligió el producto.
  5. 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.
  6. Compara su tono con los cuatro estados y avisa si queda a menos de 28° de alguno.
Semilla #d4763c L 0,6606 · C 0,1378 · H 49,98°
Acento #e98950 L 0,7219 · C 0,1378 · H 49,88°
El producto 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.

Aviso del build para example

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.

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:

text
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/lib/styles/products/example.css, 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.

css
/* 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/ui 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.

bash
# 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 npm

Paso 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

bash
npx sv create example --template minimal --types ts --no-add-ons --install npmcd examplenpm install @archeblack/ui

El 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.

json
"@archeblack/ui": "^0.1.0"
bash
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

Probar antes de publicar

Para ver un producto con cambios de Arche que todavía no se publicaron, npm pack arma el paquete local y el producto lo instala desde el archivo. Después de publicar, se vuelve a npm install @archeblack/ui.
bash
# 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.tgz

Los estilos, en orden

Van al principio de src/routes/+layout.svelte, antes que cualquier otro import. El CSS de cada componente llega solo con el componente.

svelte
import '@archeblack/ui/fonts.css';import '@archeblack/ui/tokens.css';import '@archeblack/ui/base.css';import '@archeblack/ui/products/example.css';import './layout.css';
  1. fonts.css, opcional: Archivo, Piazzolla e IBM Plex Mono autoalojadas. Si el producto ya carga las fuentes, se omite.
  2. tokens.css: todas las variables --arche-* en :root y los roles, que se recalculan en cada elemento con data-product.
  3. base.css: el documento (fondo, texto, margen del body, selección, foco, el link para saltar al contenido y movimiento reducido), en la capa arche.base.
  4. products/example.css: la familia del acento bajo [data-product="example"]. Le gana a tokens.css por especificidad, no por orden: tokens.css declara 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.
  5. 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/app.html, 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.

html
<!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/routes/layout.css y lo importa en el layout.

bash
npx sv add tailwindcss="plugins:none" --install npm

src/routes/layout.css 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.

css
@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/app.html se suma el orden de capas, justo antes de %sveltekit.head%; el resto queda como arriba:

html
<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/date a sus dependencias.

Tres puntos de entrada

@archeblack/ui trae los componentes y la función toast. @archeblack/ui/icons reexporta Tabler, siempre a través de <Icon> o de las props de ícono de cada componente. @archeblack/ui/date reexporta @internationalized/date, para el valor de DatePicker.

ts
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.css lo 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 pasa mark y 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/app.html 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:

data-product="example": el nombre en blanco y la selección con el acento
Sin producto: la marca de Arche, en blanco

src/routes/+layout.svelte. 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.

svelte
<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>

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/ui/icons, 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-selected o --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:check mide 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 } o a { color: inherit } pisan a Button y a Card con href. Resets en una capa anterior a Arche, declarada en app.html (<style>@layer reset, arche;</style>), o reglas que dejen fuera las clases 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-control y su hover): 3:1 contra el fondo y las tres superficies.
  • Que cada $value de tokens.json coincida con su receta y que los CSS generados, resolved.json y los CSS de producto estén al día (sin archivos de productos que ya no existen).
Link, el peor fondo (press sobre surface-overlay)
4,97:1 · mínimo 4,50:1
Foco, el peor fondo (surface-overlay)
6,68:1 · mínimo 3,00:1
Selección, el peor fondo (surface-overlay)
6,68:1 · mínimo 3,00:1

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/lib/tokens/products/example.json, con npm run tokens y npm run tokens:check en verde. Si hay aviso de choque, está leído y decidido.

  • node_modules/@archeblack/ui/dist/styles/products/example.css existe.

  • data-product="example" y lang="es" en src/app.html.

  • fonts.css, tokens.css, base.css y el CSS del producto; los estilos propios al final. Con Tailwind, el orden de capas en app.html.

  • El primer elemento de la página: <a class="arche-skip-link" href="#content">, que lleva al <main id="content"> del AppShell. Con Tab, es lo primero que aparece.

  • Una sola vez cada uno, en src/routes/+layout.svelte, con el nombre del producto.

  • Una búsqueda de --arche-color-accent y de text-accent en src/ no encuentra nada.

  • Errores, avisos y confirmaciones con Alert, Badge, Toast o Field con error.

  • Todo color propio de texto pasa 4,5:1 y todo límite de control, 3:1.

  • Los campos van con Field e Input, Textarea, Select, Combobox o DatePicker, 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.

El producto de ejemplo está en src/lib/tokens/products/example.json. El código de la guía vive en src/routes/products/snippets.ts y un test lo revisa contra el paquete.