This is the abridged developer documentation for Cobalt Design System # Cobalt Design System > La estructura invisible que hace que todo funcione. Web Components framework-agnostic, tokens y guías que unifican todos los productos Prolibu. Tokens Sistema `--co-*`: colores, tipografía, spacing, radius y sombras. Una sola fuente de verdad, tema claro y oscuro out-of-the-box. [Ver tokens →](/theming/tokens/) 53 Componentes Web Components `co-*` listos para producción. API auto-documentada desde el Custom Elements Manifest. [Ver componentes →](/componentes/co-button/) Theming Cuatro colores de marca, cuatro compuestos químicos del cobalto. Personalizá vía CSS variables, modo oscuro nativo. [Ver dark mode →](/theming/dark-mode/) Instalación `@prolibu-suite/cobalt-core` en npm — o ` ``` El builder se encarga internamente de: * Crear su propio store **scoped por instancia** (podés tener N builders en la misma página). * Registrar los Web Components Cobalt (`co-input`, `co-form`, etc.) en `onMounted`. * Sincronizar el schema bidireccionalmente con `v-model`. * Atajos de teclado: `⌘Z` / `⌘⇧Z` undo / redo, `Esc` colapsa el field editor inline. ## Props [Sección titulada «Props»](#props) | Prop | Tipo | Default | Descripción | | -------------------- | --------------------------------------- | ----------------------- | ------------------------------------------------------------------------- | | `modelValue` | `Record` | `{}` | Schema editado (paged-shape preferido). v-model bidireccional. | | `tabs` | `('designer' \| 'preview' \| 'json')[]` | todas | Qué tabs exponer. Ejemplo: `['designer']` para esconder Preview y JSON. | | `hideBrand` | `boolean` | `false` | Ocultar la marca “CF” del topbar (cuando el host tiene su propio chrome). | | `brandLabel` | `string` | `'Cobalt Form Builder'` | Texto del brand. | | `skipCobaltRegister` | `boolean` | `false` | No auto-registrar los Web Components Cobalt (si el host ya lo hizo). | ## Slots [Sección titulada «Slots»](#slots) Cuatro slots para personalizar el chrome: | Slot | Default | Scope | | --------- | ----------------- | ----------- | | `brand` | Marca CF + label | — | | `actions` | Botones Undo/Redo | `{ store }` | | `before` | (vacío) | — | | `after` | (vacío) | `{ store }` | ```vue ``` ## Eventos granulares [Sección titulada «Eventos granulares»](#eventos-granulares) Además del `update:modelValue` de v-model, el builder emite eventos específicos para cada mutación. Útil para analytics, autosave, audit log. ```vue ``` | Evento | Payload | Cuándo se emite | | ------------------- | ---------------------------------- | ------------------------------------------------ | | `field-added` | `{ pageIdx, fieldName, kind }` | Click en toolbox o “Add field below” | | `field-removed` | `{ pageIdx, fieldName }` | Delete en field card | | `field-renamed` | `{ pageIdx, oldName, newName }` | Rename en properties panel | | `field-updated` | `{ pageIdx, fieldName, patch }` | Cualquier prop del field cambia | | `field-duplicated` | `{ pageIdx, sourceName, newName }` | Duplicate field | | `field-reordered` | `{ pageIdx, order }` | Drag-and-drop reorder | | `page-added` | `{ pageIdx }` | Add page | | `page-removed` | `{ pageIdx }` | Delete page | | `page-duplicated` | `{ sourceIdx, newIdx }` | Duplicate page | | `page-updated` | `{ pageIdx, patch }` | Page title / description / visibleIf cambia | | `theme-changed` | `{ theme }` | Cualquier setting de Theme cambia | | `selection-changed` | `{ selection }` | Cambio de scope (form / page / field) | | `schema-replaced` | — | JSON Editor → Apply, o `replaceAll()` desde host | | `undo` / `redo` | — | Triggerado por teclado o botón | | `event` | `SchemaEvent` (union) | Cualquier evento de los anteriores | Cada evento individual ES también enviado como `event` (genérico). Útil para logging exhaustivo. ## Uso avanzado: store directo [Sección titulada «Uso avanzado: store directo»](#uso-avanzado-store-directo) Si necesitás acceso al store fuera del componente (por ejemplo, persistencia compleja, integración con tu propio sistema de undo): ```ts import { createSchemaStore } from '@prolibu-suite/cobalt-form-builder'; const store = createSchemaStore({ initialSchema: existingSchemaFromBackend, onEvent: (event) => { auditLog.push(event); }, }); // Mutar: store.addField('text'); store.setTheme({ accent: 'verde' }); // Leer (reactive — usable en `computed`, `watch`): console.log(store.exported); console.log(store.canUndo); ``` ## Limitaciones / FAQ [Sección titulada «Limitaciones / FAQ»](#limitaciones--faq) **¿Puedo tener varios builders en la misma página?** Sí. Cada `` crea su propio store scoped con provide/inject. No hay singleton global. **¿Funciona en SSR?** La parte que mounts el componente no — los Web Components Cobalt requieren ventana real. `v-if` el componente detrás de un mount guard si tu app es SSR. **¿Por qué Pinia ya no es peer dep?** El store es composable plano con `reactive()`. Eliminamos la dep externa para que el builder sea más liviano de instalar. **¿Cómo hago un botón “Save”?** Usá el slot `actions` con acceso al store: ``. O escuchá `update:modelValue` y guardás con debounce. **¿Custom CSS para forms embebidos en landing pages?** Theme → Advanced tab expone un textarea con CSS scoped al form. Ver [Theme docs](/formularios/theme/). # CoFormRenderer > El componente Vue que renderiza un formulario completo desde su controller. `CoFormRenderer` toma el objeto que devuelve `useForm()` y renderiza todos los campos automáticamente. Para casos típicos no necesitas escribir un solo `` a mano. ## Uso básico [Sección titulada «Uso básico»](#uso-básico) ```vue ``` ## Props [Sección titulada «Props»](#props) | Prop | Tipo | Default | Descripción | | ----------------- | --------------------------- | -------- | ------------------------------------------------------------------------ | | `form` | `UseFormReturn` | — | Objeto retornado por `useForm()`. **Requerido**. | | `layout` | `'grid' \| 'stack'` | `'grid'` | Layout de los campos | | `disabled` | `boolean` | `false` | Deshabilita todos los campos | | `readOnly` | `boolean` | `false` | Modo lectura (todos los campos) | | `excludeFields` | `string[]` | `[]` | Nombres de campos a NO renderizar (siguen validando) | | `fieldComponents` | `Record` | — | Override de renderers (ver [Custom fields](/formularios/custom-fields/)) | | `refResolver` | `RefResolver` | — | Callback para campos `ref` (ver [Ref fields](/formularios/ref-fields/)) | ## Eventos [Sección titulada «Eventos»](#eventos) | Evento | Payload | Cuándo | | --------- | -------------------------------------- | ----------------------- | | `submit` | `{ values: Record }` | Submit exitoso (válido) | | `invalid` | `{ errors: Record }` | Submit con errores | | `change` | `{ name: string; value: any }` | Cada cambio de campo | ```vue ``` ## Layout [Sección titulada «Layout»](#layout) ### Grid (default) [Sección titulada «Grid (default)»](#grid-default) CSS Grid con 12 columnas. Cada campo ocupa por defecto 6 columnas (la mitad). Si el field schema tiene `full: true`, ocupa las 12. ```js const schema = { firstName: { type: 'String' }, // 6 cols lastName: { type: 'String' }, // 6 cols (al lado) bio: { type: 'String', full: true }, // 12 cols (línea completa) }; ``` ### Stack [Sección titulada «Stack»](#stack) Una columna, un campo por fila. Útil para forms angostos (drawers, modales). ```vue ``` ## Slots [Sección titulada «Slots»](#slots) `CoFormRenderer` expone slots para personalizar zonas específicas. ### `header` y `footer` [Sección titulada «header y footer»](#header-y-footer) ```vue ``` Si no defines `#footer`, se renderiza un botón “Submit” por defecto. ### `field:` — override de un campo específico [Sección titulada «field:\ — override de un campo específico»](#fieldname--override-de-un-campo-específico) Para cambiar cómo se renderiza UN campo sin tocar los demás: ```vue ``` El slot recibe: | Variable | Tipo | Descripción | | ---------- | --------------------- | -------------------------------------------- | | `field` | `FieldDescriptor` | Metadata del campo (name, label, kind, etc.) | | `value` | `any` | Valor actual | | `setValue` | `(v) => void` | Setter | | `error` | `string \| undefined` | Primer error (si touched) | | `errors` | `string[]` | Todos los errores | | `touched` | `boolean` | Si el campo fue tocado | ## Excluir campos sin sacarlos del schema [Sección titulada «Excluir campos sin sacarlos del schema»](#excluir-campos-sin-sacarlos-del-schema) A veces quieres validar un campo (porque la API lo requiere) pero no mostrarlo: ```vue ``` Los campos excluidos: * **NO se renderizan** en la UI * **SÍ participan** de validación * **SÍ están** en `form.values.value` (con su default) Diferente a poner `hidden: true` en el schema: ese marca a nivel schema; `excludeFields` es por instancia de renderer. ## Pasar disabled / readOnly globalmente [Sección titulada «Pasar disabled / readOnly globalmente»](#pasar-disabled--readonly-globalmente) ```vue ``` Tiene precedencia sobre `disabled`/`readOnly` individuales en el schema. ## Ejemplo completo [Sección titulada «Ejemplo completo»](#ejemplo-completo) ```vue ``` # Campos custom > Cómo agregar tus propios componentes de campo (firma, color picker, file uploader...). Cobalt cubre los casos comunes (text, number, select, boolean, date, ref, textarea). Para casos específicos del dominio — firma digital, color picker, file uploader, JSON editor, mapa — necesitas un **campo custom**. Hay dos formas de hacerlo, según el caso. ## Opción 1: `uiCom` + componente custom (Vue) [Sección titulada «Opción 1: uiCom + componente custom (Vue)»](#opción-1-uicom--componente-custom-vue) Declara el componente en el schema con `uiCom`: ```js const schema = { signature: { type: 'String', uiCom: 'signature', // string libre que identifica tu componente label: 'Firma', required: true, }, accentColor: { type: 'String', uiCom: 'color-picker', default: '#02a270', label: 'Color de marca', }, }; ``` Crea el componente custom: SignatureField.vue ```vue ``` Y pásalo a `CoFormRenderer` vía `fieldComponents`: ```vue ``` `CoFormRenderer` ahora usa tu componente cada vez que encuentra un campo con `uiCom: 'signature'`. ## Opción 2: Slot `field:` (caso único) [Sección titulada «Opción 2: Slot field:\ (caso único)»](#opción-2-slot-fieldname-caso-único) Si solo necesitas customizar UN campo en UN form (no reutilizable), usa el slot directamente: ```vue ``` No necesitas `uiCom` en el schema porque el slot machea por **nombre del campo**, no por kind. ## Cuál usar [Sección titulada «Cuál usar»](#cuál-usar) | Caso | Opción recomendada | | ------------------------------------------------------------------------- | --------------------------- | | Componente reutilizable en muchos forms | `uiCom` + `fieldComponents` | | Override puntual de UN campo en UN form | Slot `field:` | | Mismo tipo de campo en muchos schemas | `uiCom` + `fieldComponents` | | Diferentes overrides en cada form (ej. password con eye toggle solo aquí) | Slot | ## Props que recibe un campo custom [Sección titulada «Props que recibe un campo custom»](#props-que-recibe-un-campo-custom) ```ts interface FieldRendererProps { field: FieldDescriptor; // metadata del campo desde el schema value: any; // valor actual setValue: (v: any) => void; // setter touch: () => void; // marca como touched error: string | undefined; // primer error (si touched) errors: string[]; // todos los errores touched: boolean; disabled: boolean; // disabled global o del campo readOnly: boolean; } ``` `FieldDescriptor` te da acceso a TODOS los atributos del schema: ```ts interface FieldDescriptor { name: string; kind: FieldKind; label: string; placeholder: string; required: boolean; disabled: boolean; hidden: boolean; fullWidth: boolean; helperText: string; originalAttrs: Record; // ← TU `uiCom`, `description`, etc. ajvProperty: Record; } ``` Si pusiste `uiCom: 'signature'` en el schema, lo encuentras en `field.originalAttrs.uiCom`. ## Ejemplo: file uploader [Sección titulada «Ejemplo: file uploader»](#ejemplo-file-uploader) FileUploadField.vue ```vue ``` Schema: ```js { attachments: { type: 'String', // o 'Mixed', solo importa que AJV lo valide uiCom: 'file-upload', multiple: true, label: 'Adjuntos', }, } ``` Y en el form: ```vue ``` ## Custom kinds (override del FieldKind built-in) [Sección titulada «Custom kinds (override del FieldKind built-in)»](#custom-kinds-override-del-fieldkind-built-in) También puedes reemplazar los kinds estándar pasando la misma key como en `FieldKind`: ```js const customFields = { 'text': MyFancyTextField, // reemplaza TODOS los text fields 'date': DatePickerWithPresets, }; ``` Esto es útil si quieres un look-and-feel propio diferente al de Cobalt en TODA tu app. Mantén la firma de props Para que tu componente custom funcione con `CoFormRenderer`, debe aceptar **al menos** `field`, `value`, `setValue`. Si esperas otra forma de input (`v-model`, `@input`, etc.), envuélvelo en un wrapper que adapte. ## Custom fields en Web Component [Sección titulada «Custom fields en Web Component»](#custom-fields-en-web-component) Para `` (Web Component), los custom fields se pasan vía slots `field:`: ```html
``` Es más manual que la versión Vue: tú escuchas el evento del componente custom y propagas el valor al form via `setValue()`. # Ejemplos > Casos reales y completos — signup, settings, multi-step, edición inline. Cuatro ejemplos end-to-end con **el form real renderizado**. Cada demo es un `` funcional — escribe, mira cómo cambian los valores y errores en el panel de la derecha, y dale submit para ver el payload final. *** ## 1. Signup (registro de usuario) [Sección titulada «1. Signup (registro de usuario)»](#1-signup-registro-de-usuario) Form típico con validación, errores traducidos, password con patrón, y submit. #### Demo en vivo — interactúa con el form **Valores** — ``` {} ``` **Errores** ``` {} ``` ### Código [Sección titulada «Código»](#código) ```vue ``` *** ## 2. Edición con datos prefetched [Sección titulada «2. Edición con datos prefetched»](#2-edición-con-datos-prefetched) El form abre con datos existentes (simulando un edit). `email` es read-only, `role` viene de un enum, `bio` ocupa todo el ancho. #### Demo — perfil prellenado **Valores** — ``` {} ``` **Errores** ``` {} ``` ### Código [Sección titulada «Código»](#código-1) ```vue ``` Lo interesante: * `form.isDirty` solo es `true` cuando el usuario cambia algo respecto al estado prefetched. * “Cancelar” llama `reset()` para volver al estado original. * “Guardar” se deshabilita si no hay cambios. *** ## 3. Form de settings (tipos variados) [Sección titulada «3. Form de settings (tipos variados)»](#3-form-de-settings-tipos-variados) Mezcla de todos los tipos: string con patrón, number con rango, boolean, enum, date. #### Demo — todos los tipos de campo **Valores** — ``` {} ``` **Errores** ``` {} ``` Probá: * Dejar el slug con mayúsculas → ves el error del pattern * Bajar los asientos a `0` → error de `min` * Cambiar el plan → se actualiza al instante en el panel de valores *** ## 4. Multi-step (wizard) [Sección titulada «4. Multi-step (wizard)»](#4-multi-step-wizard) Wizard de 3 pasos con validación por paso. Usa `useForm` directamente para tener control total del renderizado entre pasos. #### Demo — paso 1 del wizard (datos personales) **Valores** — ``` {} ``` **Errores** ``` {} ``` #### Demo — paso 2 (datos de la empresa) **Valores** — ``` {} ``` **Errores** ``` {} ``` ### Código [Sección titulada «Código»](#código-2) ```vue ``` Patterns clave: * **Un solo `useForm`** para todo el wizard. No hagas un form por paso. * **Validación por paso** vía `currentStepValid` que solo mira los campos del paso actual. * **Touched solo on-demand** — solo marcamos touched al intentar avanzar. *** ## Tips de producción [Sección titulada «Tips de producción»](#tips-de-producción) * **Loading durante submit**: usa `form.isSubmitting.value` para mostrar spinner en el botón. El controller la setea automáticamente durante `onSubmit`. * **Toasts post-submit**: emitir desde el componente o desde `onSubmit`. No tires `await` sin try/catch o perderás errores silenciosamente. * **Reset entre instancias**: si tu drawer se reutiliza, observa el `open` y llama `form.reset(newInitialValues)` al abrir. * **Dirty check al cerrar**: si `form.isDirty.value` es `true` cuando el usuario cierra el drawer, pregunta antes de descartar cambios. # Instalación > Cómo instalar y configurar los paquetes de formularios según tu stack. ## Vue 3 (más común) [Sección titulada «Vue 3 (más común)»](#vue-3-más-común) 1. **Instala los paquetes**: * pnpm ```bash pnpm add @prolibu-suite/cobalt-form-vue @prolibu-suite/cobalt-core @prolibu-suite/cobalt-tokens ``` * npm ```bash npm install @prolibu-suite/cobalt-form-vue @prolibu-suite/cobalt-core @prolibu-suite/cobalt-tokens ``` * yarn ```bash yarn add @prolibu-suite/cobalt-form-vue @prolibu-suite/cobalt-core @prolibu-suite/cobalt-tokens ``` `cobalt-form-vue` arrastra `cobalt-form-core` y AJV automáticamente. 2. **Carga los Web Components Cobalt** (`co-input`, `co-select`, etc.) en el entry de tu app: main.js ```js import { defineCustomElements } from '@prolibu-suite/cobalt-core/loader'; defineCustomElements(window); ``` 3. **Configura Vue para reconocer custom elements**: vite.config.js ```js import vue from '@vitejs/plugin-vue'; export default { plugins: [vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('co-') } } })] }; ``` 4. **Importa los tokens y los estilos del form** en tu CSS global: ```css @import '@prolibu-suite/cobalt-tokens/css'; @import '@prolibu-suite/cobalt-tokens/css/dark'; @import '@prolibu-suite/cobalt-form-vue/dist/style.css'; ``` 5. **Úsalo en cualquier componente**: ```vue ``` ## Vanilla / cualquier framework (Web Component) [Sección titulada «Vanilla / cualquier framework (Web Component)»](#vanilla--cualquier-framework-web-component) 1. **Instala el paquete del Web Component**: ```bash pnpm add @prolibu-suite/cobalt-form @prolibu-suite/cobalt-core @prolibu-suite/cobalt-tokens ``` 2. **Registra ``** y los componentes base de Cobalt: ```js import { defineCustomElements as defineCoreElements } from '@prolibu-suite/cobalt-core/loader'; import { defineCustomElements as defineFormElements } from '@prolibu-suite/cobalt-form/loader'; defineCoreElements(window); defineFormElements(window); ``` 3. **Usa el tag en HTML**: ```html ``` ## Solo el core (Svelte, Solid, React, Node) [Sección titulada «Solo el core (Svelte, Solid, React, Node)»](#solo-el-core-svelte-solid-react-node) 1. **Instala**: ```bash pnpm add @prolibu-suite/cobalt-form-core ``` 2. **Crea un controller**: ```js import { createForm } from '@prolibu-suite/cobalt-form-core'; const form = createForm({ schema: { modelSchema: yourSchema, locale: 'es' }, initialValues: { email: '' }, }); form.subscribe((state) => { console.log(state.values, state.errors, state.isValid); }); form.setValue('email', 'foo@bar.com'); await form.submit(); ``` 3. **Construye tu UI** consumiendo `form.state` y `form.fields`. ## Sin build · vía CDN [Sección titulada «Sin build · vía CDN»](#sin-build--vía-cdn) Para prototipos rápidos o cualquier HTML vanilla, podés cargar `` directamente desde el CDN de Prolibu — sin npm, sin bundler. Copy-paste: ```html ``` ### Recursos CDN [Sección titulada «Recursos CDN»](#recursos-cdn) | Paquete | URL CDN | | -------------------------------------------------- | -------------------------------------------------------------------------------- | | `cobalt-tokens` (CSS) | `https://dev10.prolibu.com/ui/static/cobalt/tokens/tokens.css` | | Dark mode | `https://dev10.prolibu.com/ui/static/cobalt/tokens/tokens-dark.css` | | `cobalt-core` (Web Components base) | `https://dev10.prolibu.com/ui/static/cobalt/cobalt/cobalt.esm.js` | | `cobalt-form` (``) | `https://dev10.prolibu.com/ui/static/cobalt-form/cobalt-form/cobalt-form.esm.js` | | `cobalt-form-core` (controller framework-agnostic) | `https://dev10.prolibu.com/ui/static/cobalt-form-core/index.js` | ### Demos en vivo [Sección titulada «Demos en vivo»](#demos-en-vivo) 5 ejemplos consumiendo `` desde CDN, de menos invasivo a totalmente custom: * [Nivel 0 · Default](https://dev10.prolibu.com/ui/static/cobalt-form-demo/01-default.html) — `` stock con AJV + ref fields * [Nivel 1 · Tokens](https://dev10.prolibu.com/ui/static/cobalt-form-demo/02-tokens.html) — theming con CSS variables * [Nivel 2 · CSS local](https://dev10.prolibu.com/ui/static/cobalt-form-demo/03-css.html) — override de layout sin tocar la lib * [Nivel 3 · Slot](https://dev10.prolibu.com/ui/static/cobalt-form-demo/04-slot.html) — reemplazar un campo por HTML custom * [Nivel 4 · `form-core`](https://dev10.prolibu.com/ui/static/cobalt-form-demo/05-custom.html) — renderer propio sin `` ## Verifica que funciona [Sección titulada «Verifica que funciona»](#verifica-que-funciona) Importa el form, renderízalo y abre devtools. Si ves: * Los `co-input` renderizados con label correcto * Errores en tiempo real al escribir (rojos debajo del input) * En consola, `form.values`, `form.errors`, `form.isValid` actualizándose …entonces todo está conectado. Conflicto con Quasar / Vuetify Si tu app usa Quasar o Vuetify, asegúrate de que `compilerOptions.isCustomElement` en `vite.config.js` no choque con sus prefijos. `co-*` es exclusivo de Cobalt, así que normalmente no hay problema. # Introducción > Sistema de formularios de Cobalt — schema-driven, headless, framework-agnostic. El sistema de formularios de Cobalt está pensado para que **escribas un schema, no un formulario**. Defines qué campos hay, qué tipo son y cómo se validan — la UI se renderiza sola, validada con AJV y traducida al locale del usuario. ## Cuatro paquetes en capas [Sección titulada «Cuatro paquetes en capas»](#cuatro-paquetes-en-capas) @prolibu-suite/cobalt-form-core El **controlador headless**. Maneja state, validación con AJV, errores, dirty/touched, expression evaluator, scoring, theme resolver. Sin dependencia de framework. Úsalo si: estás en Svelte, Solid, vanilla JS, o quieres control total. @prolibu-suite/cobalt-form-vue **Adaptador Vue 3** con `useForm()` y ``. Refs reactivos, hooks, field components listos. Úsalo si: tu app es Vue (suite-v2, suite, productos internos). @prolibu-suite/cobalt-form **Web Components `` y ``**. Cero dependencias en runtime. Schema + theme vía atributo o prop. Úsalo si: trabajas con HTML plano, embebes en otro framework, o necesitas CDN-friendly. @prolibu-suite/cobalt-form-builder **Builder visual Vue 3**. Editor drag-and-drop que produce schemas. Usable como app standalone o componente embebible. Úsalo si: querés exponer a usuarios admin la creación visual de forms. ## Filosofía [Sección titulada «Filosofía»](#filosofía) * **Schema-first**: la fuente de verdad es un schema Mongoose-style. La UI se deriva, no se cablea. * **Validación AJV**: el mismo schema que valida en backend valida en frontend. Sin doble código. * **i18n built-in**: errores traducidos a `es`, `en`, `pt-BR` vía `ajv-i18n`. * **Headless core**: form-core no sabe de DOM. form-vue y form lo envuelven en su capa de renderizado. * **Field composition**: cada campo es un componente Cobalt (`co-input`, `co-select`, etc.). Puedes reemplazar cualquiera. * **Conditional logic**: `visibleIf` / `enableIf` / `requiredIf` con expressions simples (`{field} == value`, `&&`, `||`, `contains`). * **Multi-page wizard**: pages array + stepper navegable + validación por página. * **Theme system scoped**: accent, dark mode, background image, custom CSS — todo aplicado al form scope sin tocar tokens globales. ## Diagrama de capas [Sección titulada «Diagrama de capas»](#diagrama-de-capas) ```plaintext ┌─────────────────────────────────────────────────────────────┐ │ Tu app (suite-v2 / vanilla / React / iframe embed) │ └────┬──────────────────┬──────────────────┬──────────────────┘ │ │ │ ┌────▼─────────────┐ ┌──▼──────────────┐ ┌─▼───────────────────┐ │ cobalt-form- │ │ cobalt-form-vue │ │ cobalt-form │ │ builder │ │ (useForm, │ │ (, │ │ () │ │ Builder>) │ │ │ │ Stencil WC │ └────┬─────────────┘ └────┬────────────┘ └─────┬───────────────┘ │ │ │ └────────┬───────────┴────────────────────┘ ▼ ┌────────────────────────────┐ │ cobalt-form-core │ │ FormController + AJV + │ │ expression evaluator + │ │ theme resolver + scoring │ └────────────┬───────────────┘ ▼ ┌────────────────────────────┐ │ @prolibu-suite/cobalt-core│ │ co-input, co-select, etc. │ └────────────────────────────┘ ``` ## Schema mínimo [Sección titulada «Schema mínimo»](#schema-mínimo) ```js const schema = { email: { type: 'String', required: true, format: 'email', label: 'Email', }, password: { type: 'String', required: true, minLength: 8, label: 'Contraseña', }, }; ``` Eso es todo. Con ese objeto tienes un formulario completo con validación, errores traducidos, y UI Cobalt. ## Próximos pasos [Sección titulada «Próximos pasos»](#próximos-pasos) * [Instalación](/formularios/instalacion/) — Configurar los paquetes en tu proyecto. * [Schema](/formularios/schema/) — Referencia completa del formato de schema. * [Validación](/formularios/validacion/) — Cómo funciona AJV y la i18n de errores. * [Web Component](/formularios/web-component/) — Render runtime con `` y ``. * [Builder](/formularios/builder/) — Editor visual `` (Vue 3) para generar schemas. * [Theme](/formularios/theme/) — Sistema de theming scoped (accent, dark mode, custom CSS). * [Wizard](/formularios/wizard/) — Multi-page, conditional logic, quiz con scoring. * [Ejemplos](/formularios/ejemplos/) — Casos reales: signup, settings, multi-step. # Campos de referencia > Campos que apuntan a otros modelos backend (Company, User, Product...) con búsqueda async y paginación. Los **ref fields** son el tipo de campo más usado en aplicaciones de Prolibu: apuntan a entidades de otro modelo del backend. Se renderizan como `` (un autocomplete con búsqueda, paginación y prefetch). ## Definición en el schema [Sección titulada «Definición en el schema»](#definición-en-el-schema) ```js const dealSchema = { company: { type: 'ObjectId', ref: 'Company', // nombre del modelo backend displayName: 'name', // qué campo mostrar en la UI label: 'Empresa', required: true, }, owner: { type: 'ObjectId', ref: 'User', displayName: 'fullName', label: 'Responsable', }, }; ``` | Atributo | Tipo | Descripción | | ------------- | ------------------------ | ------------------------------------------------------------------------------ | | `type` | `'ObjectId' \| 'String'` | El tipo del ID. ObjectId es el típico de Mongo. | | `ref` | `string` | Nombre del modelo. Se pasa al `refResolver` para que sepa qué endpoint llamar. | | `displayName` | `string` | Campo del documento referenciado que se muestra (default: `'name'`). | | `multiple` | `boolean` | Si `true`, el campo guarda un array de IDs. | ## El `refResolver` [Sección titulada «El refResolver»](#el-refresolver) Es **la función que tu app define** para resolver qué opciones mostrar. Recibe la query del usuario y retorna las opciones a renderizar. ```ts type RefResolver = (params: { model: string; // el ref del schema query?: string; // texto que escribió el usuario page?: number; // página solicitada (1-based) pageSize?: number; // default 20 ids?: string[]; // si está presente, debes resolver POR estos IDs }) => Promise<{ options: Array<{ value: string; // el _id label: string; // el displayName resuelto meta?: any; // info opcional adicional }>; total?: number; hasMore?: boolean; }>; ``` ## Implementación típica [Sección titulada «Implementación típica»](#implementación-típica) ```js const refResolver = async ({ model, query, page = 1, pageSize = 20, ids }) => { // Caso 1: hydration — el form abre con un _id existente y necesita traer su label if (ids?.length) { const res = await api.get(`/${model.toLowerCase()}/byIds`, { params: { ids: ids.join(',') }, }); return { options: res.data.map((doc) => ({ value: doc._id, label: doc.name, // o el displayName meta: doc, })), }; } // Caso 2: búsqueda o paginación const res = await api.get(`/${model.toLowerCase()}`, { params: { search: query, page, pageSize }, }); return { options: res.data.items.map((doc) => ({ value: doc._id, label: doc.name, meta: doc, })), total: res.data.total, hasMore: page * pageSize < res.data.total, }; }; ``` ## Conectarlo en Vue [Sección titulada «Conectarlo en Vue»](#conectarlo-en-vue) ```vue ``` ## Conectarlo en Web Component [Sección titulada «Conectarlo en Web Component»](#conectarlo-en-web-component) ```js document.querySelector('co-form').refResolver = refResolver; ``` ## Ciclo de vida: hydration [Sección titulada «Ciclo de vida: hydration»](#ciclo-de-vida-hydration) Si abres un form con un valor `_id` ya seteado en `initialValues`: ```js const form = useForm(() => ({ schema: { modelSchema: dealSchema, locale: 'es' }, initialValues: { company: '64a7f12e9d3b88a01c1234ef', // ID, no objeto }, })); ``` `` automáticamente: 1. Detecta que el value es un string (no un objeto con label). 2. Llama a `refResolver({ model: 'Company', ids: ['64a7f12...'] })`. 3. Espera la respuesta y renderiza el label resuelto en el input. No tienes que hacer nada — solo asegúrate de que tu resolver maneje el parámetro `ids`. ## Multiple refs [Sección titulada «Multiple refs»](#multiple-refs) ```js const dealSchema = { tags: { type: 'ObjectId', ref: 'Tag', multiple: true, label: 'Etiquetas', }, }; ``` El valor entonces es `string[]`. La UI muestra chips removibles. ```js // initialValues { tags: ['id1', 'id2', 'id3'] } // El refResolver recibe ids = ['id1', 'id2', 'id3'] ``` ## Búsqueda con debounce [Sección titulada «Búsqueda con debounce»](#búsqueda-con-debounce) `` ya hace debounce internamente (300ms por default). Tu `refResolver` solo se llama una vez que el usuario para de escribir, así no saturas la API. Si necesitas más control: ```vue ``` ```js const cache = new Map(); const inflight = new Map(); const resolveWithCache = async (params) => { const key = JSON.stringify(params); if (cache.has(key)) return cache.get(key); if (inflight.has(key)) return inflight.get(key); const promise = realResolver(params).then((result) => { cache.set(key, result); inflight.delete(key); return result; }); inflight.set(key, promise); return promise; }; ``` ## Validación [Sección titulada «Validación»](#validación) Por defecto, un ref field con `required: true` valida que el valor no sea null/undefined. La validación de “el ID realmente existe en backend” es responsabilidad del backend al hacer submit — no se hace round-trip al validar. Si necesitas validación adicional (ej. “este Company debe estar activo”), hazla en el submit: ```js const onSubmit = async (values) => { const company = await api.get(`/company/${values.company}`); if (!company.active) { form.setError('company', ['Esta empresa está desactivada']); return; } await api.createDeal(values); }; ``` ## Pattern: resolver global compartido [Sección titulada «Pattern: resolver global compartido»](#pattern-resolver-global-compartido) En una app con muchos forms, conviene tener UN solo refResolver para toda la app: composables/useGlobalRefResolver.js ```js import { useAuth } from '@/composables/useAuth'; export function useGlobalRefResolver() { const { token } = useAuth(); return async ({ model, query, page, pageSize, ids }) => { const headers = { Authorization: `Bearer ${token.value}` }; const baseUrl = `/api/${model.toLowerCase()}`; if (ids?.length) { const { data } = await fetch(`${baseUrl}/byIds?ids=${ids.join(',')}`, { headers }) .then((r) => r.json()); return { options: data.map(toOption) }; } const params = new URLSearchParams({ q: query || '', page, pageSize }); const { data, total } = await fetch(`${baseUrl}?${params}`, { headers }) .then((r) => r.json()); return { options: data.map(toOption), total, hasMore: page * pageSize < total, }; }; } function toOption(doc) { return { value: doc._id, label: doc.name || doc.fullName || doc.title || doc._id, meta: doc, }; } ``` Y en cualquier componente: ```vue ``` # Schema > Referencia completa del formato de schema Mongoose-style con extensiones Prolibu. El schema es un **objeto plano** donde cada clave es el nombre del campo y el valor describe cómo renderizarlo y validarlo. La forma sigue convenciones de Mongoose y se traduce internamente a JSON Schema para AJV. ```js const schema = { fieldName: { type: 'String', // requerido required: true, // validación label: 'Nombre', // UI hint placeholder: 'Tu nombre', minLength: 2, }, // ...más campos }; ``` ## Tipos de campo soportados [Sección titulada «Tipos de campo soportados»](#tipos-de-campo-soportados) | `type` | JS / TS | Renderiza como | | ------------ | -------------------- | --------------------------------- | | `'String'` | `string` | `` (text por defecto) | | `'Number'` | `number` | `` | | `'Boolean'` | `boolean` | `` | | `'Date'` | `Date \| string ISO` | `` | | `'ObjectId'` | `string` | `` (requiere `ref`) | | `'Mixed'` | `any` | `` (text) | ## Atributos de validación (estándar JSON Schema) [Sección titulada «Atributos de validación (estándar JSON Schema)»](#atributos-de-validación-estándar-json-schema) | Atributo | Aplica a | Descripción | | ----------------- | --------------- | ------------------------------------------------------- | | `required` | todos | El campo debe tener valor | | `min` | Number | Valor numérico mínimo | | `max` | Number | Valor numérico máximo | | `minLength` | String | Longitud mínima | | `maxLength` | String | Longitud máxima | | `enum` | String | Valores permitidos. Renderiza como `` | | `match` o `regex` | String | RegExp pattern | | `format` | String | `email`, `date-time`, `uri`, `uuid`, etc. (ajv-formats) | | `default` | todos | Valor inicial. Puede ser función `() => value` | | `multiple` | String/ObjectId | Hace que el campo sea array | ## Atributos UI (extensiones Prolibu) [Sección titulada «Atributos UI (extensiones Prolibu)»](#atributos-ui-extensiones-prolibu) | Atributo | Tipo | Descripción | | ------------- | ------- | --------------------------------------------------------------------------- | | `label` | string | Etiqueta visible. Si falta, se deriva del `name` (camelCase → “Camel Case”) | | `placeholder` | string | Placeholder del input | | `eg` | string | Ejemplo en helper text (`e.g. foo@bar.com`) | | `description` | string | Texto auxiliar bajo el campo | | `helperText` | string | Alias de `description` | | `readOnly` | boolean | El usuario no puede editar | | `disabled` | boolean | El campo está deshabilitado | | `hidden` | boolean | El campo no se renderiza pero sí participa de validación | | `full` | boolean | El campo ocupa el ancho completo en layout grid | | `uiCom` | string | Override del componente: `'TextArea'`, `'HtmlEditor'`, custom | ## Atributos para campos de referencia [Sección titulada «Atributos para campos de referencia»](#atributos-para-campos-de-referencia) | Atributo | Tipo | Descripción | | ------------- | ------ | -------------------------------------------------------------- | | `ref` | string | Modelo backend al que apunta (ej. `'Company'`, `'User'`) | | `displayName` | string | Campo del modelo que se muestra al usuario (default: `'name'`) | Ver [Campos de referencia](/formularios/ref-fields/) para el detalle. ## Ejemplo completo [Sección titulada «Ejemplo completo»](#ejemplo-completo) ```js const userSchema = { // String simple firstName: { type: 'String', required: true, minLength: 2, maxLength: 50, label: 'Nombre', placeholder: 'Tu nombre', }, // Email con formato validado email: { type: 'String', required: true, format: 'email', label: 'Email', eg: 'usuario@empresa.com', }, // Number con rango age: { type: 'Number', min: 18, max: 120, default: 25, label: 'Edad', }, // Boolean → checkbox newsletter: { type: 'Boolean', default: true, label: 'Suscribirme al newsletter', }, // Enum → select country: { type: 'String', enum: ['Colombia', 'México', 'Argentina', 'España'], default: 'Colombia', required: true, label: 'País', }, // Reference field company: { type: 'ObjectId', ref: 'Company', displayName: 'name', label: 'Empresa', required: true, }, // Textarea (override vía uiCom) bio: { type: 'String', uiCom: 'TextArea', maxLength: 500, label: 'Biografía', description: 'Cuéntanos sobre ti (máx. 500 caracteres)', full: true, }, // Date birthDate: { type: 'Date', label: 'Fecha de nacimiento', }, // Password (con pattern) password: { type: 'String', required: true, minLength: 8, match: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/, label: 'Contraseña', description: 'Mínimo 8 caracteres, una mayúscula y un número', }, }; ``` ## Defaults dinámicos [Sección titulada «Defaults dinámicos»](#defaults-dinámicos) `default` acepta una función que se invoca una sola vez al construir el form: ```js const schema = { createdAt: { type: 'Date', default: () => new Date(), }, slug: { type: 'String', default: () => crypto.randomUUID(), }, }; ``` ## Inferencia automática del label [Sección titulada «Inferencia automática del label»](#inferencia-automática-del-label) Si no defines `label`, se deriva del nombre del campo aplicando case conversion: | `name` | `label` inferido | | --------------- | ---------------- | | `firstName` | `First Name` | | `email_address` | `Email Address` | | `phoneNumber` | `Phone Number` | | `URL` | `URL` | ## Conversión interna a JSON Schema [Sección titulada «Conversión interna a JSON Schema»](#conversión-interna-a-json-schema) `form-core` traduce tu schema Mongoose a JSON Schema antes de pasarlo a AJV. Si necesitas inspeccionarlo: ```js import { mongooseToJsonSchema } from '@prolibu-suite/cobalt-form-core'; const jsonSchema = mongooseToJsonSchema(userSchema); console.log(jsonSchema); // { type: 'object', properties: {...}, required: [...] } ``` Esto es útil para sincronizar con OpenAPI o tooling externo de validación. # Theme > Sistema de theming scoped para Cobalt Form — accent, dark mode, background, custom CSS. El **theme system** permite personalizar la apariencia del form sin tocar los tokens globales de Cobalt. Vive en el bloque `theme` del schema (sibling de `pages`), se aplica como CSS custom properties + data attributes **scoped al form**. ```jsonc { "title": "Mi Form", "theme": { "colorScheme": "dark", "accent": "custom", "accentHex": "#7C3AED", "borderRadius": "rounded", "density": "comfortable", "fontFamily": "Inter, system-ui, sans-serif", "background": { "color": "#0F1420", "imageUrl": "https://example.com/bg.jpg", "imageSize": "cover", "imageAttachment": "fixed", "opacity": 100 }, "header": { "show": true, "backgroundColor": null, "height": "180px" }, "customCss": "[data-co-field=\"email\"] co-input { letter-spacing: 0.02em }" }, "pages": [/* … */] } ``` ## Cuatro tabs en el builder [Sección titulada «Cuatro tabs en el builder»](#cuatro-tabs-en-el-builder) El editor visual expone el theme en 4 sub-tabs cuando seleccionás el scope **Form**: | Tab | Settings | | -------------- | ----------------------------------------------------------------------------------------------------------------------------- | | **General** | Color scheme (Light/Dark) · Question appearance (Panels/Flat) · Density (Comfortable/Compact) · Border radius (Rounded/Sharp) | | **Header** | Show header toggle · Background color · Height (CSS length) | | **Background** | Background color · Image URL · Image size (auto/contain/cover) · Image attachment (scroll/fixed) · Opacity slider | | **Appearance** | Accent presets (Azul / Verde / Amarillo / Rosado) o custom hex · Font family | | **Advanced** | Custom CSS textarea (escape hatch, sanitizado) | ## Aplicación scoped [Sección titulada «Aplicación scoped»](#aplicación-scoped) El renderer convierte el theme en: 1. **CSS custom properties** sobre el wrapper del form (`--co-color-primary-azul`, `--co-form-header-bg`, etc.). Cascadeánan a hijos. 2. **Data attributes** (`data-theme="dark"`, `data-appearance="flat"`, `data-density="compact"`, `data-radius="sharp"`, `data-has-bg`, `data-hide-header`). CSS los matchea con selectors específicos. 3. **Inline styles** para el background-image y background-color del host. 4. **Inyección de `` (would break out of the injected style element) `url(http://...)` SÍ está permitido (custom fonts, decorative images son casos legítimos). ### Class hooks targeteables [Sección titulada «Class hooks targeteables»](#class-hooks-targeteables) Cada field se renderiza con atributos estables que el customCss puede targetear: ```html
``` ```css /* Por nombre específico de field */ [data-co-field="email"] co-input { /* ... */ } /* Por tipo de field */ [data-co-field-kind="select"] { /* todos los selects */ } [data-co-field-kind="textarea"] co-input { min-height: 200px } ``` Estos atributos son **API pública** — no se rompen entre versiones del DS. ### Otros class hooks útiles [Sección titulada «Otros class hooks útiles»](#otros-class-hooks-útiles) | Selector | Qué matchea | | ------------------------------ | ----------------------------- | | `.co-form-wizard` | Container del wizard | | `.co-form-wizard__title-block` | Card del form title | | `.co-form-wizard__page-card` | Card de la page activa | | `.co-form-wizard__page-head` | Header dentro de la page card | | `.co-form-wizard__page-title` | h2 del page title | | `.co-form-wizard__page-pill` | Pill “PAGE 1” | | `.co-form-wizard__nav` | Footer con Anterior/Siguiente | | `.co-form-wizard__title` | h1 del form | | `.co-form-wizard__title-desc` | Description del form | ## Accent [Sección titulada «Accent»](#accent) Cuatro presets que mapean al primary palette de Cobalt: | Preset | Hex | | ---------- | --------- | | `azul` | `#2563EB` | | `verde` | `#02A270` | | `amarillo` | `#FDBF00` | | `rosado` | `#F32A73` | O `accent: "custom"` + `accentHex: "#xxxxxx"` para color libre. El accent **ripple-ea**: todos los azules tintados (borders selected, focus rings, pills, drop targets) usan `color-mix(in srgb, var(--co-color-primary-azul) X%, transparent)` para que cambien al unísono. ## Question appearance [Sección titulada «Question appearance»](#question-appearance) | Modo | Comportamiento | | ------------------ | --------------------------------------------------------------------------------------- | | `panels` (default) | Cada page renderiza como card con border + shadow + header propio | | `flat` | Strip total del chrome — title + page sin border ni shadow, fields directos sobre el bg | ## Density [Sección titulada «Density»](#density) | Modo | Cambio | | ------------- | -------------------------------------------------------- | | `comfortable` | Gap default (16px entre cards, padding 20-24px) | | `compact` | Gap reducido (8-12px), paddings más chicos en header/nav | # useForm (Vue) > El composable principal para manejar formularios en Vue 3. `useForm()` es el punto de entrada en Vue. Crea un `FormController` reactivo y expone refs computados con todo el state. ## Firma [Sección titulada «Firma»](#firma) ```ts function useForm( options: MaybeRefOrGetter ): UseFormReturn; interface FormControllerOptions { schema: { modelSchema: Record; locale?: 'es' | 'en' | 'pt-BR'; }; initialValues?: Record; validateOn?: 'change' | 'blur' | 'submit'; onSubmit?: (values: Record) => Promise | void; } ``` El argumento puede ser un objeto literal, un ref o un getter. Si pasas un getter, el form se reconstruye cuando cambien las dependencias reactivas. ## Lo que retorna [Sección titulada «Lo que retorna»](#lo-que-retorna) ```ts interface UseFormReturn { // Refs computados reactivos values: ComputedRef>; errors: ComputedRef>; touched: ComputedRef>; isValid: ComputedRef; isDirty: ComputedRef; isSubmitting: ComputedRef; fields: ComputedRef; // Acciones (sin .value, son métodos) setValue(name: string, value: any): void; setValues(partial: Record): void; touch(name: string): void; touchAll(): void; validate(): boolean; submit(): Promise; reset(values?: Record): void; // Bajo nivel controller: Ref; } ``` ## Uso básico [Sección titulada «Uso básico»](#uso-básico) ```vue ``` Con eso ya tienes: * Render automático de campos según el schema * Validación en vivo con AJV * Mensajes traducidos a español * Submit habilitado solo si el form es válido ## Acceder al state reactivo [Sección titulada «Acceder al state reactivo»](#acceder-al-state-reactivo) ```vue ``` ## Acciones programáticas [Sección titulada «Acciones programáticas»](#acciones-programáticas) ```js // Setear un valor form.setValue('email', 'foo@bar.com'); // Setear varios (merge, no replace) form.setValues({ email: 'foo@bar.com', age: 30 }); // Marcar como touched (muestra errores) form.touch('email'); form.touchAll(); // Forzar validación const isOk = form.validate(); // Submit (corre validate + onSubmit) await form.submit(); // Reset a initialValues + defaults del schema form.reset(); // Reset a valores específicos form.reset({ email: 'nuevo@ejemplo.com' }); ``` ## Schema reactivo [Sección titulada «Schema reactivo»](#schema-reactivo) Si el schema depende de props o state (ej. cambia según el rol del usuario), pasa un **getter**: ```vue ``` Cuando `props.role` cambie, `useForm` reconstruye el controller con el nuevo schema. Los valores existentes se preservan en la medida que sean compatibles con el nuevo schema. ## Initial values [Sección titulada «Initial values»](#initial-values) ```js const form = useForm(() => ({ schema: { modelSchema: userSchema, locale: 'es' }, initialValues: { email: 'usuario@empresa.com', role: 'admin', newsletter: true, }, })); ``` `initialValues` se mergea con los `default` del schema. Los valores explícitos siempre ganan sobre los defaults. `isDirty` se calcula contra esta combinación (initialValues + defaults). Si haces `setValue` y luego vuelves al valor inicial, `isDirty` regresa a `false`. ## Submit [Sección titulada «Submit»](#submit) ```vue ``` ### Flujo interno del `submit()` [Sección titulada «Flujo interno del submit()»](#flujo-interno-del-submit) 1. `touchAll()` — marca todos los campos como touched (para mostrar todos los errores) 2. `validate()` — re-valida con AJV 3. Si **inválido**: detiene el submit y deja `isSubmitting = false` 4. Si **válido**: setea `isSubmitting = true`, ejecuta `onSubmit(values)`, espera la promise 5. Al finalizar (éxito o error): `isSubmitting = false` ## Subscripción cruda al state [Sección titulada «Subscripción cruda al state»](#subscripción-cruda-al-state) Si necesitas un listener (ej. para logging, telemetría), usa `form.controller.value.subscribe`: ```js const unsubscribe = form.controller.value.subscribe((state) => { console.log('form state:', state); }); onUnmounted(unsubscribe); ``` # Validación > Cómo funciona AJV, cuándo se valida, locales de error y validación custom. La validación es **síncrona, schema-driven, traducida** — funciona out-of-the-box sin que escribas un solo validador. ## Motor: AJV 8 [Sección titulada «Motor: AJV 8»](#motor-ajv-8) [AJV](https://ajv.js.org/) es el validador de JSON Schema más rápido y estricto en npm. Cobalt lo configura con: * **`ajv-formats`** para `email`, `date`, `uri`, `uuid`, `regex`, etc. * **`ajv-i18n`** para traducir mensajes de error al locale del usuario. * **`allErrors: true`** para capturar TODOS los errores de un campo, no solo el primero. ## Cuándo se valida [Sección titulada «Cuándo se valida»](#cuándo-se-valida) Por defecto, la validación corre: 1. **En la construcción del form** — para que el state inicial refleje si el form es válido o no (útil para deshabilitar el submit desde el inicio). 2. **En cada `setValue()`** — validación en vivo mientras el usuario escribe. 3. **En `submit()`** — re-valida antes de emitir el evento. Si hay errores, marca todos los campos como `touched` para que el usuario vea todos los mensajes a la vez. Puedes cambiar este comportamiento con la opción `validateOn`: ```js const form = createForm({ schema: { modelSchema, locale: 'es' }, validateOn: 'blur', // 'change' | 'blur' | 'submit' }); ``` | Valor | Comportamiento | | ---------------------- | ----------------------------------------------------------------------- | | `'change'` *(default)* | Valida en cada `setValue`. UI en vivo. | | `'blur'` | Valida solo cuando el campo pierde foco. Menos ruido para forms largos. | | `'submit'` | Solo valida en `submit()`. Modo “old school”. | ## Errores [Sección titulada «Errores»](#errores) El state expone `errors` como `Record`: ```ts { email: ['debe coincidir con el formato "email"'], password: [ 'debe NO tener menos que 8 caracteres', 'debe coincidir con el patrón "^(?=...)"', ], } ``` Un campo puede tener varios errores (por eso es array). La UI por defecto muestra solo el primero, pero puedes acceder a todos. ### Errores solo en campos “touched” [Sección titulada «Errores solo en campos “touched”»](#errores-solo-en-campos-touched) Para evitar mostrar errores antes de que el usuario interactúe, la UI por defecto **solo muestra errores de campos `touched`**. Un campo se marca touched cuando: * El usuario lo modifica (`setValue` lo marca touched) * El campo pierde foco * El form intenta hacer submit (`touchAll()`) Si quieres mostrar todos los errores sin esperar, llama `form.touchAll()` al inicio. ## Locales [Sección titulada «Locales»](#locales) `ajv-i18n` viene con traducciones para 30+ idiomas. Cobalt configura por defecto `es`, `en`, `pt-BR`: ```js const form = createForm({ schema: { modelSchema: userSchema, locale: 'es', // 'es' | 'en' | 'pt-BR' }, }); ``` ### Ejemplos de mensajes traducidos [Sección titulada «Ejemplos de mensajes traducidos»](#ejemplos-de-mensajes-traducidos) | Validación | `en` | `es` | `pt-BR` | | ----------------- | --------------------------------------- | ------------------------------------- | -------------------------------------- | | `required` | should have required property ’…‘ | debe tener la propiedad requerida ’…‘ | deve ter a propriedade obrigatória ’…’ | | `format: 'email'` | should match format “email” | debe coincidir con el formato “email” | deve corresponder ao formato “email” | | `minLength: 8` | should NOT have fewer than 8 characters | debe NO tener menos que 8 caracteres | deve NÃO ter menos que 8 caracteres | ## Estado de validez [Sección titulada «Estado de validez»](#estado-de-validez) ```js form.state.isValid // true si NO hay errores en ningún campo form.state.isDirty // true si values !== initialValues form.state.isSubmitting // true durante el async submit form.state.touched // Record ``` Útil para deshabilitar el botón submit: ```vue ``` ## Validación de campos individuales [Sección titulada «Validación de campos individuales»](#validación-de-campos-individuales) ```js form.validate(); // valida todo form.validate(['email']); // valida solo email (si tu builder lo soporta) form.touch('email'); // marca touched (re-valida en validateOn change) form.touchAll(); // marca todos touched ``` ## Custom validators [Sección titulada «Custom validators»](#custom-validators) El form-core trabaja con cualquier schema que cumpla la interfaz `FormSchemaLike`: ```ts interface FormSchemaLike { fields: string[]; properties: Record; validator?: { validate(data): boolean; errors: AjvError[] }; validateField?(name, value, data): string[]; // ... } ``` Para casos complejos (validación async contra backend, reglas cross-field), registra un builder custom: ```js import { configureFormSchemaBuilder, FormSchemaWeb } from '@prolibu-suite/cobalt-form-core'; configureFormSchemaBuilder(async (modelSchema, locale) => { // FormSchemaWeb es el builder completo de Prolibu (de @skemify/cases) // Soporta validación async, reglas custom, hooks de transformación return new FormSchemaWeb({ modelSchema, locale }); }); ``` A partir de ese momento, todos los `createForm()` usan ese builder. ### Validación cross-field (manual) [Sección titulada «Validación cross-field (manual)»](#validación-cross-field-manual) Para reglas como “password debe coincidir con confirmPassword”, lo más simple es validar manualmente antes de submit: ```js const onSubmit = async (e) => { if (form.values.value.password !== form.values.value.confirmPassword) { form.setError('confirmPassword', ['Las contraseñas no coinciden']); return; } await api.signup(form.values.value); }; ``` ## Acceso bajo del API [Sección titulada «Acceso bajo del API»](#acceso-bajo-del-api) Para casos avanzados puedes invocar AJV directamente: ```js import { createInlineValidator } from '@prolibu-suite/cobalt-form-core'; const schema = createInlineValidator(modelSchema, 'es'); const isValid = schema.validator.validate(data); console.log(schema.validator.errors); // → AJV error objects (con keyword, schemaPath, instancePath, message, params) ``` Útil para validar fuera de un form (ej. en una API route). # (Web Component) > Usa formularios Cobalt en HTML plano, React, Svelte o cualquier stack. `` es el Web Component oficial. Mismo motor que el composable Vue, pero accesible desde cualquier framework o desde HTML vanilla. ## Setup mínimo [Sección titulada «Setup mínimo»](#setup-mínimo) ```html ``` ## Atributos [Sección titulada «Atributos»](#atributos) | Atributo | Tipo | Default | Descripción | | ---------------- | ------------------------- | -------- | ----------------------------------------------------- | | `schema` | JSON string | — | Schema del form. **Requerido**. | | `initial-values` | JSON string | — | Valores iniciales (mergeados con defaults del schema) | | `locale` | `'es' \| 'en' \| 'pt-BR'` | `'en'` | Idioma de mensajes de error | | `layout` | `'grid' \| 'stack'` | `'grid'` | Layout de campos | | `exclude-fields` | string CSV | — | Lista separada por comas de campos a ocultar | | `disabled` | boolean | `false` | Deshabilita todos los campos | | `read-only` | boolean | `false` | Modo lectura | Schemas grandes vía property, no attribute Los atributos HTML tienen límites prácticos (\~64KB en algunos navegadores). Para schemas grandes, asígnalos como **property** vía JavaScript: ```js const form = document.querySelector('co-form'); form.schemaInstance = bigSchemaObject; // objeto JS, no string ``` `schemaInstance` acepta el objeto crudo (sin parseo JSON), lo que también permite incluir funciones como `default: () => Date.now()`. ## Properties (solo vía JS) [Sección titulada «Properties (solo vía JS)»](#properties-solo-vía-js) | Property | Tipo | Descripción | | ---------------- | ---------------- | ---------------------------------------------- | | `schemaInstance` | `FormSchemaLike` | Schema pre-construido (alternativa a `schema`) | | `refResolver` | `RefResolver` | Función para resolver opciones de campos `ref` | ```js const form = document.querySelector('co-form'); form.refResolver = async ({ model, query, page, pageSize = 20, ids }) => { const params = new URLSearchParams({ q: query, page, pageSize }); if (ids) params.set('ids', ids.join(',')); const res = await fetch(`/api/${model.toLowerCase()}?${params}`); const { data, total } = await res.json(); return { options: data.map((item) => ({ value: item._id, label: item.name, })), total, hasMore: page * pageSize < total, }; }; ``` ## Métodos [Sección titulada «Métodos»](#métodos) Todos los métodos son `async`: ```js const form = document.querySelector('co-form'); await form.setValue('email', 'foo@bar.com'); const email = await form.getValue('email'); await form.touch('password'); ``` ## Eventos [Sección titulada «Eventos»](#eventos) ```js form.addEventListener('coSubmit', (e) => { console.log('values:', e.detail.values); }); form.addEventListener('coChange', (e) => { console.log(`${e.detail.name} →`, e.detail.value); }); form.addEventListener('coValidate', (e) => { console.log('isValid:', e.detail.isValid); console.log('errors:', e.detail.errors); }); ``` | Evento | `detail` | | ------------ | -------------------------------------------------------- | | `coSubmit` | `{ values: Record }` | | `coChange` | `{ name: string; value: any }` | | `coValidate` | `{ isValid: boolean; errors: Record }` | ## Slots [Sección titulada «Slots»](#slots) | Slot | Contenido | | -------------- | ----------------------------------------- | | `header` | Aparece arriba de los campos | | `footer` | Aparece abajo (por defecto: botón submit) | | `field:` | Override de un campo específico | ```html

Nuevo usuario

``` ## Ejemplo completo (vanilla HTML) [Sección titulada «Ejemplo completo (vanilla HTML)»](#ejemplo-completo-vanilla-html) ```html ``` ## Integración con React [Sección titulada «Integración con React»](#integración-con-react) CoFormReact.jsx ```jsx import { useEffect, useRef } from 'react'; export function CoFormReact({ schema, initialValues, locale = 'es', onSubmit }) { const ref = useRef(null); useEffect(() => { const el = ref.current; el.schemaInstance = schema; if (initialValues) el.initialValues = JSON.stringify(initialValues); const handler = (e) => onSubmit?.(e.detail.values); el.addEventListener('coSubmit', handler); return () => el.removeEventListener('coSubmit', handler); }, [schema, initialValues, onSubmit]); return ; } ``` Y en TypeScript necesitas declarar el tag: types.d.ts ```ts declare module 'react' { namespace JSX { interface IntrinsicElements { 'co-form': React.DetailedHTMLProps< React.HTMLAttributes & { schema?: string; locale?: string; layout?: string; }, HTMLElement >; } } } ``` ## Diferencias con el composable Vue [Sección titulada «Diferencias con el composable Vue»](#diferencias-con-el-composable-vue) | Aspecto | `useForm` + `CoFormRenderer` | `` | | --------------------- | ---------------------------- | ------------------------------------- | | Bundle size adicional | + form-vue (\~5KB) | + cobalt-form (\~8KB) | | Reactivity nativa | Sí (refs Vue) | Vía eventos DOM | | Slots / overrides | Sí, `field:` | Sí, `field:` | | State accesible | Refs computados | `form.getValue()` (async) | | SSR | Limitado | No (custom elements son cliente-only) | | Mejor para… | Apps Vue | HTML, otros frameworks, CDN | # Wizard > Multi-page forms con stepper, conditional logic, y quiz scoring via co-form-wizard. `` envuelve `` para soportar **multi-page forms** con stepper navegable, validación por página, conditional logic a nivel page, y scoring opcional para modo quiz. ## Schema paged [Sección titulada «Schema paged»](#schema-paged) Cuando un schema tiene `pages: [...]`, el preview enruta automáticamente al wizard (en lugar del `` flat). ```jsonc { "title": "Registro DevConf 2026", "description": "Únete a la conferencia", "mode": "form", "pages": [ { "name": "personal", "title": "Datos personales", "description": "Solo lo esencial", "fields": { "full_name": { "type": "String", "required": true, "label": "Nombre" }, "email": { "type": "String", "required": true, "format": "email" } } }, { "name": "preferences", "title": "Preferencias", "visibleIf": "{full_name} != ''", "fields": { "tracks": { "type": "String", "enum": ["AI/ML", "Frontend"], "multiple": true } } } ] } ``` ## Características [Sección titulada «Características»](#características) ### Stepper navegable [Sección titulada «Stepper navegable»](#stepper-navegable) Cada page aparece como un step en el header. Click en un step: * **Hacia atrás** → salta directo (ya visitaste esa page). * **Hacia adelante** → valida la page actual y avanza una a la vez (no salta sin validar intermedias). Los steps marcan `is-active` y `is-done` con colores del accent. ### `visibleIf` a nivel page [Sección titulada «visibleIf a nivel page»](#visibleif-a-nivel-page) Una page entera se oculta si su `visibleIf` evalúa a falso contra los valores acumulados: ```jsonc { "name": "advanced", "title": "Configuración avanzada", "visibleIf": "{role} == 'admin'", "fields": {/* ... */} } ``` Sintaxis igual que en fields — operadores `==`, `!=`, `>`, `<`, `>=`, `<=`, `contains`, `&&`, `||`. ### Validación por página [Sección titulada «Validación por página»](#validación-por-página) `Siguiente` solo avanza si la page actual valida (touched + isValid). Si hay errores, los muestra y queda en la page. AJV se ejecuta scoped a los fields de la page. ### Page header smart [Sección titulada «Page header smart»](#page-header-smart) El page header (`PAGE N` pill + título + description) se muestra **solo cuando aporta info**: * **Multi-page** → siempre se muestra (orientación al usuario). * **Single-page con título default “Page N” y sin description** → se oculta entero (el form title ya da contexto). * **Single-page con título custom o description** → muestra solo lo customizado. Override explícito por page: ```jsonc { "name": "p1", "title": "Page 1", "hideTitle": true, // ocultar el h2 "hideDescription": true // ocultar el párrafo } ``` ## Quiz mode [Sección titulada «Quiz mode»](#quiz-mode) `mode: "quiz"` activa el sistema de scoring. Cada field puede declarar `quiz: { correctAnswer, points }`: ```jsonc { "mode": "quiz", "pages": [{ "name": "p1", "title": "Vue 3 quiz", "fields": { "q1": { "type": "String", "enum": ["ref()", "reactive()", "computed()"], "label": "¿Cuál crea un proxy reactivo?", "quiz": { "correctAnswer": "reactive()", "points": 10 } }, "q2": { "type": "Boolean", "label": "Los refs auto-unwrappean en template", "quiz": { "correctAnswer": true, "points": 5 } } } }], "scoring": { "showScore": true, "passingScore": 70, "feedbackRules": [ { "expression": "{_score} >= 80", "message": "Excelente!" }, { "expression": "{_score} < 50", "message": "Repasar la doc" } ] } } ``` Al hacer Submit, el wizard: 1. Calcula el score sumando puntos de respuestas correctas vs total. 2. Compara con `passingScore` → `passed: boolean`. 3. Evalúa cada `feedbackRules.expression` (con `{_score}` disponible como % calculado). 4. Renderiza un panel de resultados con score, pass/fail badge y feedback messages. 5. Emite `coSubmit` event con `{ values, score: ScoreResult }`. ### Comparación de respuestas [Sección titulada «Comparación de respuestas»](#comparación-de-respuestas) * **Escalares**: loose equality (`==`), así `"5" == 5` es `true`. * **Arrays** (multi-select): order-insensitive — `["a","b"]` matchea `["b","a"]`. ## Estructura visual [Sección titulada «Estructura visual»](#estructura-visual) ```plaintext ┌─ co-form-wizard (data-theme, data-density, ...) ─────────┐ │ │ │ ┌─ Title block card ────────────────────────────────┐ │ │ │ Form title (h1) │ │ │ │ Form description │ │ │ └───────────────────────────────────────────────────┘ │ │ │ │ Stepper: ① Page 1 ② Page 2 ③ Page 3 │ │ │ │ ┌─ Page card (active) ──────────────────────────────┐ │ │ │ ┌─ Page head ─────────────────────────────────┐ │ │ │ │ │ PAGE 2 pill │ │ │ │ │ │ Page title (h2) │ │ │ │ │ │ Page description │ │ │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ │ │ │ │ con los fields de esta page │ │ │ │ │ │ │ │ ┌─ Nav ────────────────────────────────────────┐ │ │ │ │ │ [Anterior] [Siguiente] │ │ │ │ │ └─────────────────────────────────────────────┘ │ │ │ └───────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────┘ ``` ## Embebido directo [Sección titulada «Embebido directo»](#embebido-directo) ```html ``` ## Props [Sección titulada «Props»](#props) | Prop | Tipo | Default | Descripción | | ---------------- | ---------------------- | -------- | ------------------------------------------------------- | | `schema` | `string` (JSON) | required | Paged schema con `pages[]`. | | `theme` | `string` (JSON) | — | Theme spec — ver [Theme](/formularios/theme/). | | `initial-values` | `string` (JSON) | `'{}'` | Pre-fill global, distribuye automáticamente por página. | | `locale` | `'en' \| 'es' \| 'pt'` | `'es'` | Idioma de errores AJV. | | `layout` | `'grid' \| 'stack'` | `'grid'` | Layout pasado al `` interno. | | `columns` | `1 \| 2 \| 3 \| 4` | `1` | Columnas del grid. | ## Eventos [Sección titulada «Eventos»](#eventos) | Event | Payload | Cuándo | | -------------- | -------------------- | ---------------------------------------------------------- | | `coSubmit` | `{ values, score? }` | Al completar el último step. `score` solo en `mode: quiz`. | | `coPageChange` | `{ index, page }` | Cambio de page activa (manual o via stepper). | # Iconos > Cómo usar Phosphor Icons a través de y . Cobalt utiliza [Phosphor Icons](https://phosphoricons.com/) como librería oficial. Los iconos se renderizan vía `` con el atributo `name` en kebab-case. ## Uso básico [Sección titulada «Uso básico»](#uso-básico) ## Pesos (weights) [Sección titulada «Pesos (weights)»](#pesos-weights) Phosphor soporta 6 pesos: `thin`, `light`, `regular` (default), `bold`, `fill`, `duotone`. ## Iconos en botones [Sección titulada «Iconos en botones»](#iconos-en-botones) Usa `` para acciones icon-only o las props `icon-left` / `icon-right` en ``. ## Iconos comunes en Prolibu [Sección titulada «Iconos comunes en Prolibu»](#iconos-comunes-en-prolibu) | Nombre | Uso | | ----------------------------- | ----------------- | | `magnifying-glass` | Búsqueda | | `bell` | Notificaciones | | `user`, `users` | Usuario / equipo | | `house` | Inicio | | `gear` | Configuración | | `sign-out` | Cerrar sesión | | `plus`, `x`, `check` | Acciones básicas | | `pencil-simple`, `trash` | Editar / eliminar | | `funnel`, `dots-three` | Filtros / menú | | `envelope`, `phone` | Comunicación | | `kanban`, `squares-four` | Vistas | | `calendar`, `timer`, `folder` | Tiempo / archivos | Encuentra el catálogo completo en [phosphoricons.com](https://phosphoricons.com/). # Instalación > Cómo instalar Cobalt en un proyecto Vue, React o vanilla. ## Requisitos [Sección titulada «Requisitos»](#requisitos) * Node.js ≥ 20 * Un bundler moderno (Vite, webpack 5+) o ESM nativo ## Instalación [Sección titulada «Instalación»](#instalación) 1. **Añade los paquetes**: * pnpm ```bash pnpm add @prolibu-suite/cobalt-core @prolibu-suite/cobalt-tokens ``` * npm ```bash npm install @prolibu-suite/cobalt-core @prolibu-suite/cobalt-tokens ``` * yarn ```bash yarn add @prolibu-suite/cobalt-core @prolibu-suite/cobalt-tokens ``` 2. **Importa los tokens** (en tu CSS global): ```css @import '@prolibu-suite/cobalt-tokens/css'; @import '@prolibu-suite/cobalt-tokens/css/dark'; ``` 3. **Registra los Web Components** (una sola vez en tu entry point): * Vue 3 / Vite main.js ```js import { defineCustomElements } from '@prolibu-suite/cobalt-core/loader'; defineCustomElements(window); ``` Y en `vite.config.js`: ```js export default { plugins: [vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('co-') } } })] }; ``` * React main.jsx ```jsx import { defineCustomElements } from '@prolibu-suite/cobalt-core/loader'; defineCustomElements(window); ``` Para tipado completo, prefiere los wrappers oficiales: `@prolibu-suite/cobalt-react`. * HTML ```html ``` 4. **Úsalos como cualquier elemento HTML**: ```html ``` ## Sin build · vía CDN [Sección titulada «Sin build · vía CDN»](#sin-build--vía-cdn) Para prototipos rápidos, demos o HTML vanilla, podés cargar Cobalt directamente desde el CDN de Prolibu sin instalar nada ni correr un bundler. Copy-paste en cualquier `.html`: ```html ``` ### Paquetes vía CDN [Sección titulada «Paquetes vía CDN»](#paquetes-vía-cdn) | Recurso | URL | | ------------------------------------ | -------------------------------------------------------------------------------- | | Design tokens (CSS) | `https://dev10.prolibu.com/ui/static/cobalt/tokens/tokens.css` | | Dark mode tokens | `https://dev10.prolibu.com/ui/static/cobalt/tokens/tokens-dark.css` | | Web Components | `https://dev10.prolibu.com/ui/static/cobalt/cobalt/cobalt.esm.js` | | `` schema-driven | `https://dev10.prolibu.com/ui/static/cobalt-form/cobalt-form/cobalt-form.esm.js` | | Form controller (framework-agnostic) | `https://dev10.prolibu.com/ui/static/cobalt-form-core/index.js` | ### Demos en vivo [Sección titulada «Demos en vivo»](#demos-en-vivo) 5 ejemplos consumiendo Cobalt desde CDN, de menos invasivo a totalmente custom: * [Nivel 0 · Default](https://dev10.prolibu.com/ui/static/cobalt-form-demo/01-default.html) — `` stock con AJV y ref fields * [Nivel 1 · Tokens](https://dev10.prolibu.com/ui/static/cobalt-form-demo/02-tokens.html) — theming con CSS variables * [Nivel 2 · CSS local](https://dev10.prolibu.com/ui/static/cobalt-form-demo/03-css.html) — override de layout sin tocar la lib * [Nivel 3 · Slot](https://dev10.prolibu.com/ui/static/cobalt-form-demo/04-slot.html) — reemplazar un campo por HTML custom * [Nivel 4 · `form-core`](https://dev10.prolibu.com/ui/static/cobalt-form-demo/05-custom.html) — renderer propio sin `` ## Docs para LLMs [Sección titulada «Docs para LLMs»](#docs-para-llms) La documentación se publica también en formato optimizado para asistentes de código (Cursor, Claude, ChatGPT, Windsurf…) vía [llms.txt](https://llmstxt.org/). Pegá el link como contexto: * [`llms.txt`](https://dev10.prolibu.com/ui/static/cobalt-docs/llms.txt) — índice * [`llms-small.txt`](https://dev10.prolibu.com/ui/static/cobalt-docs/llms-small.txt) — versión compacta * [`llms-full.txt`](https://dev10.prolibu.com/ui/static/cobalt-docs/llms-full.txt) — documentación completa inline ## Verificación [Sección titulada «Verificación»](#verificación) Si todo funciona, deberías ver: # Introducción > Cobalt es la estructura invisible que hace que todo funcione — el design system oficial de Prolibu. **Cobalt** es el design system oficial de Prolibu. La estructura invisible que une todos los productos — Ventas, Operaciones, Soporte — bajo un mismo lenguaje visual y técnico. Web Components estándar construidos con [Stencil](https://stenciljs.com/). Funcionan idénticamente en Vue, React, Svelte o HTML plano. **Una sola implementación. Múltiples consumidores.** ## ¿Por qué Cobalt? [Sección titulada «¿Por qué Cobalt?»](#por-qué-cobalt) Cobalt es el **elemento #27** de la tabla periódica. El mineral responsable del azul más profundo que existe en la naturaleza. Durante siglos dio color a la porcelana china, a los vitrales de las catedrales góticas y a las obras de Vermeer. **Es un azul que no se desvanece — perdura.** Hoy, el cobalto es esencial en la tecnología que mueve al mundo: baterías de litio-ion, aleaciones de motores jet, superaleaciones resistentes al calor. Es el elemento que está dentro de las cosas que funcionan. > Por eso elegimos su nombre. Cobalt es lo que está debajo: invisible, estable, esencial. ## Cobalt es Prolibu [Sección titulada «Cobalt es Prolibu»](#cobalt-es-prolibu) La metáfora no es decorativa — es operacional: | Cobalt (mineral) | Prolibu (plataforma) | | --------------------------------------- | ---------------------------------------------------- | | Se extrae de lo profundo | Se conecta a lo más profundo de la operación | | Es estructura invisible | Es la infraestructura que une Ventas + Ops + Soporte | | No se oxida, no se desvanece | Plataforma estable, precio predecible, sin sorpresas | | Esencial para baterías | Esencial para que la empresa funcione | | Un solo elemento → múltiples compuestos | Una sola plataforma → múltiples módulos | ## Un elemento. Cuatro colores de marca. [Sección titulada «Un elemento. Cuatro colores de marca.»](#un-elemento-cuatro-colores-de-marca) El cobalto produce los 4 colores de Prolibu según su compuesto químico: | Color | Compuesto | Hex | Uso | | ------------ | --------- | --------- | -------------------------------- | | **Azul** | CoAl₂O₄ | `#2563EB` | Acciones primarias, links, brand | | **Verde** | CoCO₃ | `#02A270` | Success, estados activos | | **Rosado** | Co(OH)₂ | `#F32A73` | Acentos, badges | | **Amarillo** | CoC₂O₄ | `#FDBF00` | Warnings, highlights | Cuatro pigmentos del mismo elemento. Misma química, distinto compuesto. Como los módulos de Prolibu — distintos en función, idénticos en raíz. ## Qué incluye [Sección titulada «Qué incluye»](#qué-incluye) | Paquete | Contenido | | ------------------------------ | -------------------------------------------------- | | `@prolibu-suite/cobalt-core` | 53 Web Components (`co-*`) + loader perezoso | | `@prolibu-suite/cobalt-tokens` | Tokens en CSS, SCSS y JS — fuente única de verdad | | `@prolibu-suite/cobalt-vue` | Wrappers Vue 3 con tipos completos | | `@prolibu-suite/cobalt-react` | Wrappers React 17/18/19 con tipos completos | | `@prolibu-suite/cobalt-form` | `` schema-driven, validación AJV incluida | ## Web Components, framework-agnostic [Sección titulada «Web Components, framework-agnostic»](#web-components-framework-agnostic) * **Una sola implementación** — el mismo `` funciona en Vue 3 (suite-v2), Vue 2 (suite legacy), React o HTML plano. * **Shadow DOM** — los estilos del componente no contaminan tu app y tu CSS no contamina los componentes. * **API nativa del navegador** — props vía atributos, eventos vía `CustomEvent`, slots para composición. * **Tree-shaking** — el loader carga cada componente bajo demanda; pagas solo por lo que usas. ## Convivencia con Quasar [Sección titulada «Convivencia con Quasar»](#convivencia-con-quasar) En `prolibu-front-v2/projects/suite-v2/` Cobalt convive con Quasar 2. La regla es simple: **preferí `co-*` sobre `q-*` cuando exista un equivalente**. Quasar queda como fallback para casos donde Cobalt aún no tiene componente (tablas complejas, uploader, editor WYSIWYG). Ver el mapeo completo Quasar ↔ Cobalt en el [CLAUDE.md del repo de suite-v2](https://github.com/prolibu/prolibu-front-v2/blob/main/CLAUDE.md). ## Próximos pasos [Sección titulada «Próximos pasos»](#próximos-pasos) * [Instalación →](/instalacion/) — añade Cobalt a tu proyecto (npm o CDN). * [Tokens →](/theming/tokens/) — entiende cómo se construye el sistema de diseño. * [Iconos →](/iconos/) — usá Phosphor a través de ``. # Colores > Paleta de colores Cobalt — base, primary, status, opacity y escalas. ## Colores base [Sección titulada «Colores base»](#colores-base) Negros, blancos y off-white. Punto de partida de la paleta neutra. `--co-color-base-negro` #1f1c1b `--co-color-base-blanco` #ffffff `--co-color-base-off-white` #f7f6f0 ## Primary [Sección titulada «Primary»](#primary) Los cuatro colores de marca Prolibu. **Verde** es el principal, **Azul** se usa para acciones interactivas. `--co-color-primary-verde` #02a270 `--co-color-primary-amarillo` #fdbf00 `--co-color-primary-azul` #2563eb `--co-color-primary-rosado` #f32a73 ## Escalas — Azul [Sección titulada «Escalas — Azul»](#escalas--azul) `--co-color-azul-100` #eff4ff `--co-color-azul-200` #dbe6fe `--co-color-azul-300` #bfd3fe `--co-color-azul-400` #93b4fd `--co-color-azul-500` #6090fa `--co-color-azul-600` #3b76f6 `--co-color-azul-700` #2563eb `--co-color-azul-800` #1d58d8 `--co-color-azul-900` #1e408a ## Escalas — Verde [Sección titulada «Escalas — Verde»](#escalas--verde) `--co-color-verde-100` #ecfdf5 `--co-color-verde-200` #d0fbe5 `--co-color-verde-300` #a5f5d0 `--co-color-verde-400` #6beab8 `--co-color-verde-500` #31d69a `--co-color-verde-600` #0cbd82 `--co-color-verde-700` #02a270 `--co-color-verde-800` #027a58 `--co-color-verde-900` #046147 ## Escalas — Amarillo [Sección titulada «Escalas — Amarillo»](#escalas--amarillo) `--co-color-amarillo-100` #ffffea `--co-color-amarillo-200` #fffbc5 `--co-color-amarillo-300` #fff885 `--co-color-amarillo-400` #ffe01b `--co-color-amarillo-500` #fdbf00 `--co-color-amarillo-600` #e29500 `--co-color-amarillo-700` #bb6902 `--co-color-amarillo-800` #985108 `--co-color-amarillo-900` #7c420b ## Escalas — Rosado [Sección titulada «Escalas — Rosado»](#escalas--rosado) `--co-color-rosado-100` #fef1f6 `--co-color-rosado-200` #fde6f0 `--co-color-rosado-300` #fdcde2 `--co-color-rosado-400` #fda4c8 `--co-color-rosado-500` #fb6ba3 `--co-color-rosado-600` #f32a73 `--co-color-rosado-700` #e41e5c `--co-color-rosado-800` #c61044 `--co-color-rosado-900` #a41039 ## Grises [Sección titulada «Grises»](#grises) `--co-color-gray-100` #f6f6f6 `--co-color-gray-200` #e3e3e3 `--co-color-gray-300` #c4c4c4 `--co-color-gray-400` #a0a0a0 `--co-color-gray-500` #888888 `--co-color-gray-600` #585858 `--co-color-gray-700` #1f1c1b `--co-color-gray-800` #0a0a0a ## Neutrales [Sección titulada «Neutrales»](#neutrales) `--co-color-neutral-100` #e6e5df `--co-color-neutral-200` #eeede7 `--co-color-neutral-300` #f3f2ec ## Secundarios [Sección titulada «Secundarios»](#secundarios) `--co-color-secondary-rosado-100` #d74ba0 `--co-color-secondary-rosado-50` #ffc8eb `--co-color-secondary-rosado-25` #ffe6f5 `--co-color-secondary-rojo-100` #eb1e1e `--co-color-secondary-rojo-50` #ff878c `--co-color-secondary-rojo-25` #ffd7dc `--co-color-secondary-naranja-100` #f0821e `--co-color-secondary-naranja-50` #ffbe82 `--co-color-secondary-naranja-25` #ffe6d2 `--co-color-secondary-amarillo-100` #fad223 `--co-color-secondary-amarillo-50` #ffe67d `--co-color-secondary-amarillo-25` #fffac3 `--co-color-secondary-verde-100` #afe664 `--co-color-secondary-verde-50` #d7ffb4 `--co-color-secondary-verde-25` #ebffc8 `--co-color-secondary-oliva-100` #aab43c `--co-color-secondary-oliva-50` #d2dc96 `--co-color-secondary-oliva-25` #f0f0d2 `--co-color-secondary-turquesa-100` #00afaf `--co-color-secondary-turquesa-50` #3ce1c8 `--co-color-secondary-turquesa-25` #c8f0f0 `--co-color-secondary-azul-claro-100` #00bee6 `--co-color-secondary-azul-claro-50` #6ee6f0 `--co-color-secondary-azul-claro-25` #befaff `--co-color-secondary-azul-electrico-100` #000aff `--co-color-secondary-azul-electrico-50` #8095ff `--co-color-secondary-azul-electrico-25` #cce0ff `--co-color-secondary-violeta-100` #5f4bd7 `--co-color-secondary-violeta-50` #b9b4ff `--co-color-secondary-violeta-25` #e1e1ff `--co-color-secondary-marron-100` #af7355 `--co-color-secondary-marron-50` #d7b9a5 `--co-color-secondary-marron-25` #f0e1d7 `--co-color-secondary-guinda-100` #811137 `--co-color-secondary-guinda-50` #c0889c `--co-color-secondary-guinda-25` #e6cfd7 ## Status [Sección titulada «Status»](#status) Colores semánticos para estados de UI (success, error, warning, progress, etc.). `--co-color-status-success` #0a7724 `--co-color-status-bg-success` #ebf9ec `--co-color-status-progress` #2d6cc5 `--co-color-status-bg-progress` #eef3fa `--co-color-status-submitted` #7556ca `--co-color-status-bg-submitted` #f4f1fb `--co-color-status-review` #85571f `--co-color-status-bg-review` #fcf5db `--co-color-status-pending` #b45103 `--co-color-status-bg-pending` #f9f1eb `--co-color-status-error` #aa2e1b `--co-color-status-bg-error` #ffefef ## Opacity [Sección titulada «Opacity»](#opacity) Capas semitransparentes sobre negro y blanco — útiles para hover, overlays y bordes sutiles. `--co-color-opacity-negro-4` rgba(31, 28, 27, 0.04) `--co-color-opacity-negro-8` rgba(31, 28, 27, 0.08) `--co-color-opacity-negro-16` rgba(31, 28, 27, 0.16) `--co-color-opacity-negro-25` rgba(31, 28, 27, 0.25) `--co-color-opacity-negro-50` rgba(31, 28, 27, 0.50) `--co-color-opacity-negro-80` rgba(31, 28, 27, 0.80) `--co-color-opacity-blanco-4` rgba(255, 255, 255, 0.04) `--co-color-opacity-blanco-8` rgba(255, 255, 255, 0.08) `--co-color-opacity-blanco-16` rgba(255, 255, 255, 0.16) `--co-color-opacity-blanco-25` rgba(255, 255, 255, 0.25) `--co-color-opacity-blanco-50` rgba(255, 255, 255, 0.50) `--co-color-opacity-blanco-80` rgba(255, 255, 255, 0.80) # Modo oscuro > Cómo se activa el tema oscuro y cómo personalizarlo. Cobalt soporta tema oscuro nativo. Toda la capa semántica (`--co-semantic-*`) cambia automáticamente cuando el documento tiene `[data-theme="dark"]`. ## Activación [Sección titulada «Activación»](#activación) ```css @import '@prolibu-suite/cobalt-tokens/css'; /* modo claro (root) */ @import '@prolibu-suite/cobalt-tokens/css/dark'; /* overrides scoped a [data-theme="dark"] */ ``` Luego en tu app, cambia el atributo del ``: ```js document.documentElement.setAttribute('data-theme', 'dark'); ``` ## Detectar preferencia del sistema [Sección titulada «Detectar preferencia del sistema»](#detectar-preferencia-del-sistema) ```js const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; document.documentElement.setAttribute('data-theme', prefersDark ? 'dark' : 'light'); ``` ## Qué cambia y qué no [Sección titulada «Qué cambia y qué no»](#qué-cambia-y-qué-no) | Capa | Cambia en dark | Notas | | ------------------------------------ | -------------- | ----------------------------------- | | Base (`--co-color-azul-700`, etc.) | ❌ | Son constantes | | Primary (`--co-color-primary-azul`) | ❌ | Son alias estables | | Status (`--co-color-status-success`) | ❌ | Mismo color en ambos modos | | **Semantic (`--co-semantic-*`)** | ✅ | Es la capa que define la apariencia | Por eso la **regla práctica**: en componentes y app, consume siempre `--co-semantic-*` para que el tema oscuro funcione gratis. # Espaciado y radios > Spacing, border-radius y shadows. ## Spacing [Sección titulada «Spacing»](#spacing) Escala basada en múltiplos de 2px y 4px. Úsalos para `gap`, `padding`, `margin` y posicionamiento. | Token | Valor | Preview | | ------------------- | -------- | ------- | | `--co-spacing-xxs` | `2px` | | | `--co-spacing-xs` | `4px` | | | `--co-spacing-sm` | `6px` | | | `--co-spacing-sm2` | `8px` | | | `--co-spacing-md` | `12px` | | | `--co-spacing-lg` | `16px` | | | `--co-spacing-xl` | `20px` | | | `--co-spacing-2xl` | `24px` | | | `--co-spacing-3xl` | `32px` | | | `--co-spacing-4xl` | `40px` | | | `--co-spacing-5xl` | `48px` | | | `--co-spacing-6xl` | `56px` | | | `--co-spacing-7xl` | `64px` | | | `--co-spacing-8xl` | `72px` | | | `--co-spacing-9xl` | `80px` | | | `--co-spacing-10xl` | `88px` | | | `--co-spacing-11xl` | `96px` | | | `--co-spacing-12xl` | `104px` | | | `--co-spacing-13xl` | `112px` | | | `--co-spacing-14xl` | `120px` | | | `--co-spacing-full` | `9999px` | | ## Border radius [Sección titulada «Border radius»](#border-radius) Sistema de esquinas redondeadas escalable. Usa `--co-border-radius-full` para chips/avatares circulares. | Token | Valor | Preview | | ------------------------- | -------- | ------- | | `--co-border-radius-xxs` | `4px` | | | `--co-border-radius-xs` | `8px` | | | `--co-border-radius-sm` | `12px` | | | `--co-border-radius-lg` | `16px` | | | `--co-border-radius-xl` | `24px` | | | `--co-border-radius-2xl` | `32px` | | | `--co-border-radius-3xl` | `40px` | | | `--co-border-radius-4xl` | `48px` | | | `--co-border-radius-5xl` | `56px` | | | `--co-border-radius-6xl` | `64px` | | | `--co-border-radius-full` | `9999px` | | ## Sombras [Sección titulada «Sombras»](#sombras) Una sola sombra elevada estándar. Filosofía Cobalt: minimal-shadow, más peso visual en bordes y contraste que en sombras flotantes. | Token | Valor | Preview | | ---------------- | -------------------------------------------------------------------------------- | ------- | | `--co-shadow-lg` | `0 10px 15px -3px rgba(26, 26, 26, 0.05), 0 4px 6px -4px rgba(26, 26, 26, 0.05)` | | # Tipografía > Familias, tamaños, pesos y line-heights del sistema Cobalt. ## Familias [Sección titulada «Familias»](#familias) | Token | Familia | | -------------------------- | ------------------------------------- | | `--co-font-family-primary` | **Geist** — interfaz, texto general | | `--co-font-family-mono` | **Geist Mono** — código, valores, IDs | [Geist](https://vercel.com/font) es una sans-serif geométrica de Vercel, optimizada para UIs densas y data tables. ## Tokens tipográficos [Sección titulada «Tokens tipográficos»](#tokens-tipográficos) | Token | Valor | | ----------------------------------- | ------------------------------------------------------------------------ | | `--co-font-family-primary` | `'Geist', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif` | | `--co-font-family-mono` | `'Geist Mono', 'SFMono-Regular', Consolas, 'Liberation Mono', monospace` | | `--co-font-size-10` | `10px` | | `--co-font-size-12` | `12px` | | `--co-font-size-14` | `14px` | | `--co-font-size-16` | `16px` | | `--co-font-size-18` | `18px` | | `--co-font-size-20` | `20px` | | `--co-font-size-22` | `22px` | | `--co-font-size-24` | `24px` | | `--co-font-size-26` | `26px` | | `--co-font-size-30` | `30px` | | `--co-font-size-32` | `32px` | | `--co-font-size-36` | `36px` | | `--co-font-size-40` | `40px` | | `--co-font-size-44` | `44px` | | `--co-font-size-48` | `48px` | | `--co-font-size-50` | `50px` | | `--co-font-size-56` | `56px` | | `--co-font-size-64` | `64px` | | `--co-font-size-72` | `72px` | | `--co-font-size-80` | `80px` | | `--co-font-size-96` | `96px` | | `--co-font-size-104` | `104px` | | `--co-font-size-112` | `112px` | | `--co-font-size-128` | `128px` | | `--co-font-size-144` | `144px` | | `--co-font-size-160` | `160px` | | `--co-font-size-180` | `180px` | | `--co-font-size-200` | `200px` | | `--co-font-size-220` | `220px` | | `--co-font-weight-regular` | `400` | | `--co-font-weight-medium` | `500` | | `--co-font-weight-semibold` | `600` | | `--co-font-weight-bold` | `700` | | `--co-font-line-height-none` | `1` | | `--co-font-line-height-tight` | `1.2` | | `--co-font-line-height-snug` | `1.3` | | `--co-font-line-height-normal` | `1.5` | | `--co-font-line-height-relaxed` | `1.6` | | `--co-font-letter-spacing-body` | `-0.03em` | | `--co-font-letter-spacing-overline` | `0.08em` | # Tokens > Sistema de tokens de Cobalt — capas, formatos y filosofía. Los **tokens** son la fuente única de verdad del sistema visual. Cada decisión de diseño (color, espacio, radio, sombra, tipografía) se expresa como un token y se consume desde CSS, SCSS o JS. ## Arquitectura en capas [Sección titulada «Arquitectura en capas»](#arquitectura-en-capas) Cobalt organiza los tokens en tres capas, de menor a mayor nivel de abstracción: 1. **Base** — primitivas crudas: `--co-color-azul-700: #2563eb`. No tienen significado de uso. 2. **Primary / Status** — alias semánticos del primer nivel: `--co-color-primary-azul`, `--co-color-status-success`. 3. **Semantic** — pensados por contexto de uso: `--co-semantic-surface-primary`, `--co-semantic-text-default`. **Cambian automáticamente en modo oscuro**. > **Regla de oro**: en código de aplicación usa tokens semánticos. En componentes de bajo nivel puedes usar base. Nunca hardcodees hex. ## Formatos disponibles [Sección titulada «Formatos disponibles»](#formatos-disponibles) | Import | Uso | | --------------------------------------- | ----------------------------------- | | `@prolibu-suite/cobalt-tokens/css` | Variables CSS modo claro | | `@prolibu-suite/cobalt-tokens/css/dark` | Override para `[data-theme="dark"]` | | `@prolibu-suite/cobalt-tokens/scss` | Mixins SCSS | | `@prolibu-suite/cobalt-tokens` | Constantes JS/TS | ## Build pipeline [Sección titulada «Build pipeline»](#build-pipeline) Los tokens se generan con [Style Dictionary v5](https://styledictionary.com/) desde los JSON fuente en `packages/tokens/src/`. El comando `pnpm tokens` reconstruye todos los formatos y los sincroniza con `@prolibu-suite/cobalt-core` para que los Web Components los puedan consumir.