@clara.care/form
v2.0.0
Published
Clara medical form: React component, <clara-form> web component and CDN script, with live filling from a Clara consultation, validation and FHIR R4 QuestionnaireResponse export.
Maintainers
Readme
@clara.care/form
Formulario médico de Clara a partir de una plantilla de Clara: componente React,
web component <clara-form> y script de CDN. Se rellena en vivo desde una
consulta de Clara, valida, y exporta a JSON y a FHIR R4 QuestionnaireResponse.
Los valores del formulario y las frases de la consulta son PHI: el paquete no los escribe en la consola ni los envía a ningún sitio salvo a tus callbacks.
- Instalación · React · Web component · CDN
- Formulario + widget · Guardar y enviar
- Modo en vivo · Validación · FHIR y JSON
- Tema · Accesibilidad · CSP · Ejemplos · Desarrollo
Instalación
npm
npm install @clara.care/form react react-domreact y react-dom (>= 18) son dependencias peer, también para el web component
importado desde npm. El odontograma va incluido en el paquete. ESM y CommonJS, con
tipos TypeScript.
| Import | Contenido |
| --- | --- |
| @clara.care/form | ClaraForm, useClaraForm, toFhirQuestionnaireResponse, toLabeledJSON, initClaraForm, VERSION, tipos |
| @clara.care/form/element | registra <clara-form> (efecto secundario) y exporta ClaraFormElement, defineClaraFormElement |
| @clara.care/form/iife | dist/clara-form.iife.js, el mismo script que el CDN (incluye React) |
// React
import { ClaraForm } from '@clara.care/form';
<ClaraForm templateId="TEMPLATE_ID" apiKey="PUBLISHABLE_KEY" lang="es" onSubmit={(values, { fhir }) => save(fhir)} />;// Web component (cualquier framework o HTML): registra <clara-form>
import '@clara.care/form/element';El paquete publicado habla con la API de producción (https://api.clara.care/api)
salvo que pases apiBase / api-base.
Uso sujeto a la licencia: solo clientes con un acuerdo vigente con Clara, para integrar los servicios de Clara.
CDN
<!-- versión fija (recomendado en producción) -->
<script src="https://cdn.clara.care/form/2.0.0/clara-form.js"></script>
<!-- o siempre la última -->
<script src="https://cdn.clara.care/form/latest.js"></script>El script define window.ClaraForm y registra <clara-form>.
React
import { ClaraForm } from '@clara.care/form';
<ClaraForm
templateId="TEMPLATE_ID"
apiKey="PUBLISHABLE_KEY"
lang="es"
onSubmit={(values, { fhir, json, files }) => save(values, fhir)}
onInvalid={(errors) => console.warn(Object.keys(errors))}
/>;Sin red ni API key, con la plantilla en línea (objeto o texto JSON, misma forma que
devuelve GET /api/templates):
<ClaraForm template={template} defaultValues={{ motivo: 'Control' }} />Modo controlado con el hook:
import { ClaraForm, useClaraForm } from '@clara.care/form';
function Consulta() {
const form = useClaraForm({ defaultValues: { motivo: 'Control' } });
return (
<>
<ClaraForm templateId="…" apiKey="…" {...form.formProps} onSubmit={guardar} />
<button onClick={() => form.setValue('temperatura', 37.2)}>37,2 °C</button>
<pre>{JSON.stringify(form.toFhir(), null, 2)}</pre>
</>
);
}form.formProps incluye value, onChange y onReady (si pasas tu propio
onReady después del spread, el hook no captura la plantilla cargada por id).
Props
| Prop | Tipo | Descripción |
| --- | --- | --- |
| template | FormTemplate \| string | Plantilla en línea (objeto o JSON). Tiene prioridad sobre templateId. |
| templateId | string | Plantilla a cargar del API (necesita apiKey o accessToken). |
| apiKey | string | API key publicable (X-API-Key). |
| accessToken | string | Token de sesión (Authorization: Bearer), en lugar de la key. |
| apiBase | string | API de Clara. Por defecto https://api.clara.care/api. |
| analysisId | string | Consulta (análisis) para el modo en vivo. |
| live | boolean | Activa el modo en vivo. |
| liveFrom | 'start' \| 'tail' | Primera lectura del stream: desde el principio (por defecto, se pone al día) o solo lo nuevo. |
| localChannel | boolean | Escuchar los eventos del widget de Clara en la misma página (por defecto true en vivo). |
| lang | 'es' \| 'en' \| 'pt' | Idioma de los textos del formulario. |
| readOnly | boolean | Todo deshabilitado. |
| value / onChange | FormValues / (values, changes) => void | Modo controlado. changes: [{ fieldId, value, source }], source = user, live, program, reset o dom. |
| defaultValues | FormValues | Modo no controlado: valores iniciales (por clave o id de campo; números y "sí"/"no" se convierten). |
| onFieldChange | (fieldId, value, source) => void | Una llamada por campo cambiado. |
| onSubmit | (values, { json, fhir, files, template, templateId, analysisId }) => void | Solo si el formulario es válido. fhir con status: 'completed'. |
| onInvalid | (errors) => void | En lugar de onSubmit, con los errores por campo. |
| onReady | (template) => void | Plantilla lista. |
| onError | (error) => void | { code, message, status? }; códigos: template_not_found, template_load_failed, template_invalid, missing_credentials, unauthorized, live_token_failed, live_stream_failed, widget_load_failed, widget_unavailable, widget_init_failed. |
| onLiveStatus | (status) => void | idle, connecting, live, reconnecting, error, closed. |
| onLiveUpdate | (event) => void | Cada valor en vivo: { fieldId, value?, applied, reason?, provenance? } (reason: manual_edit, unknown_field). |
| lockedFields | string[] | Campos que la persona no puede editar (los valores en vivo y del host sí se aplican). |
| submitLabel | string \| false | Texto del botón de envío; false lo oculta. Se muestra si hay onSubmit, submit.url o texto. |
| submitDisabled | boolean | Deshabilita el botón de envío. |
| submit | { url, format?, headers?, includeFiles?, withCredentials?, method? } | Envío automático del guardado. Sin url solo se emiten los eventos. |
| onSubmitted | ({ ok, status, response }) => void | Respuesta del envío automático (también con ok: false). |
| onSubmitError | (error) => void | { code: 'submit_failed' \| 'submit_http_error', message, status?, response? }. |
| secondaryAction | string | Texto de un botón extra (p. ej. «Guardar borrador»). |
| onSecondaryAction | (values, context) => void | Ese botón, sin validar; fhir con status: 'in-progress'. |
| autosave | { intervalMs?, onSave? } | Borrador periódico; solo salta si algo cambió. Por defecto intervalMs: 15000. |
| widget | boolean \| ClaraWidgetOptions | Trae el widget de Clara a la página. Por defecto false. |
| onWidgetReady | ({ scriptUrl, created }) => void | El widget ya está en la página (created: false si ya estaba). |
| onWidgetError | (error) => void | El widget no se pudo cargar; el formulario sigue funcionando a mano. |
| theme | 'light' \| 'dark' \| 'auto' | auto sigue prefers-color-scheme. Por defecto light. |
| loadFonts | boolean \| string | Opcional: carga Plus Jakarta Sans de Google Fonts (true) o la hoja indicada. Por defecto ninguna fuente externa. |
| showSource | boolean | Mostrar "De la frase: …" bajo los valores en vivo (por defecto true). |
| idPrefix | string | Prefijo de los id de los inputs (varios formularios en una página). Sin prefijo, el id es la clave del campo. |
| className, style | | Del contenedor .clara-form. |
Valores
Los valores se guardan por clave de campo (htmlFieldId, o id si no hay):
| Tipo | Valor |
| --- | --- |
| text, textarea, select, number | string (number tal como se escribe; toJSON y FHIR lo convierten a número) |
| checkbox | boolean |
| odontogram | mapa de dientes ({ "18": { id, surfaces, whole?, wholeType? } }) |
| file | string[] con los nombres; los File en getFiles() y en onSubmit (files) |
| iframe | sin valor (muestra placeholder como URL) |
| event | no se dibuja ni tiene valor |
Las secciones y campos con enabled: false no se muestran.
Métodos (ref)
const ref = useRef<ClaraFormHandle>(null);
<ClaraForm ref={ref} … />
ref.current.getValues(); ref.current.setFieldValue('motivo', 'Control'); ref.current.setValues({ … });
ref.current.validate(); ref.current.submit(); ref.current.clear(); ref.current.reset();
ref.current.toJSON(); ref.current.toFhir({ subject: { reference: 'Patient/123' } });
ref.current.getFiles(); ref.current.getProvenance(); ref.current.getLiveStatus(); ref.current.getTemplate();
ref.current.getSubmitState(); // idle | sending | sent | error
ref.current.getAnalysisId(); // la consulta en uso (la tuya o la que inició el widget)Web component <clara-form>
<script src="https://cdn.clara.care/form/2.0.0/clara-form.js"></script>
<clara-form template-id="TEMPLATE_ID" api-key="PUBLISHABLE_KEY" lang="es" theme="auto"></clara-form>
<script>
const form = document.querySelector('clara-form');
form.addEventListener('clara-submit', (e) => enviar(e.detail.fhir));
</script>Desde npm: import '@clara.care/form/element';.
Se dibuja en un Shadow DOM: los estilos de la página no le afectan y los suyos no
salen. Los id de los campos quedan dentro del shadow root.
| Atributo | Propiedad | |
| --- | --- | --- |
| template-id | templateId | |
| api-key | apiKey | |
| access-token | accessToken | |
| api-base | apiBase | |
| analysis-id | analysisId | |
| live | live | booleano |
| live-from | | start o tail |
| lang | lang | |
| readonly | readOnly | booleano |
| theme | theme | light, dark, auto |
| locked-fields | lockedFields | lista separada por comas / array |
| submit-label | | texto del botón |
| no-submit | | oculta el botón |
| submit-disabled | submitDisabled | booleano |
| widget | widget | booleano en el atributo; objeto de opciones en la propiedad |
| submit-url, submit-format, submit-include-files, submit-with-credentials | submitTarget | envío automático (las cabeceras solo por la propiedad) |
| secondary-action | secondaryAction | texto del botón secundario |
| autosave-interval | autosave | ms entre borradores ({ intervalMs } en la propiedad) |
| show-source="false" | | oculta "De la frase" |
| load-fonts | | true o URL de la hoja |
| template | template | JSON en el atributo; objeto o JSON en la propiedad |
| | defaultValues | objeto |
| | value | lectura: valores actuales; escritura: los fija (se aplica al cargar la plantilla) |
Métodos: validate(), submit(), reset(), clear(), setFieldValue(id, valor),
getValues(), toJSON(), toFhir(opciones).
Eventos (CustomEvent, bubbles y composed):
| Evento | detail |
| --- | --- |
| clara-ready | { template } |
| clara-change | { values, changes, fieldId, value, source } |
| clara-submit | { values, json, fhir, files, templateId, analysisId } (solo si es válido) |
| clara-submitted | { ok, status, response } (respuesta del envío automático) |
| clara-submit-error | { code, message, status?, response? } |
| clara-secondary-action | como clara-submit, sin validar y con fhir in-progress |
| clara-autosave | lo mismo, en cada borrador |
| clara-widget-ready | { scriptUrl, created } |
| clara-widget-error | { code, message } |
| clara-invalid | { errors } |
| clara-error | { code, message, status? } |
| clara-live-status | { status } |
| clara-live-update | { fieldId, value?, applied, reason?, provenance? } |
Vue 3
// vite.config.js: que Vue no trate <clara-form> como componente propio
vue({ template: { compilerOptions: { isCustomElement: (tag) => tag === 'clara-form' } } });<script setup>
import '@clara.care/form/element';
const template = { /* … */ };
const onSubmit = (e) => guardar(e.detail.fhir);
</script>
<template>
<clara-form :template.prop="template" lang="es" @clara-submit="onSubmit" @clara-change="(e) => (valores = e.detail.values)" />
</template>Angular
// app.module.ts (o el componente standalone)
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import '@clara.care/form/element';
@NgModule({ schemas: [CUSTOM_ELEMENTS_SCHEMA] })<clara-form [template]="template" lang="es" (clara-submit)="guardar($event.detail.fhir)"></clara-form>Script del CDN
Las instalaciones existentes siguen funcionando igual:
<div id="clara-form-container"></div>
<script
src="https://cdn.clara.care/form/latest.js"
data-clara-template="TEMPLATE_ID"
data-clara-api-key="PUBLISHABLE_KEY"
data-clara-container="clara-form-container"
></script>Atributos: data-clara-template (o data-clara-template-id, data-template-id),
data-clara-api-key (data-api-key), data-clara-container (data-container-id; id,
o selector #id/.clase), data-clara-read-only (data-read-only), data-clara-lang
(data-lang) y, nuevos: data-clara-api-base, data-clara-analysis-id, data-clara-live,
data-clara-theme, data-clara-locked-fields, data-clara-load-fonts, data-clara-shadow,
data-clara-widget (con el widget), data-clara-widget-script,
data-clara-submit-url, data-clara-submit-format, data-clara-submit-include-files,
data-clara-submit-with-credentials (envío) y data-clara-secondary-action.
Inicio manual:
const api = await ClaraForm.init({
templateId: 'TEMPLATE_ID', apiKey: 'PUBLISHABLE_KEY', containerId: 'clara-form-container',
// nuevos: template, accessToken, apiBase, analysisId, live, liveFrom, defaultValues,
// lockedFields, theme, loadFonts, shadow, submitLabel, submitDisabled, onChange,
// onInvalid, onLiveStatus
widget: true, // o { scriptUrl, position, … }
submit: { url: 'https://tu-servidor/consultas', format: 'both' },
secondaryAction: 'Guardar borrador',
autosave: { intervalMs: 15000, onSave: (values) => {} },
onFieldChange: (fieldId, value) => {},
onSubmit: (data, { fhir }) => {},
onSubmitted: ({ ok, status, response }) => {},
onSubmitError: (error) => {},
onSecondaryAction: (values, ctx) => {},
onWidgetReady: ({ created }) => {},
onWidgetError: (error) => {},
onReady: (api) => {},
onError: (error) => console.error(error.code, error.message),
});
api.getFormData(); api.setFieldValue('motivo', 'Control'); api.clearForm(); api.destroy();
// nuevos: api.validate(), api.submit(), api.reset(), api.toJSON(), api.toFhir(),
// api.getTemplate(), api.getSubmitState(), api.getAnalysisId()init resuelve con la API cuando la plantilla está lista, o con null tras onError
(un Error con code). window.ClaraForm expone además version,
defineElement(), toFhirQuestionnaireResponse(), toLabeledJSON() y validate().
Por defecto el script dibuja en el DOM de la página (los id de los campos son sus
claves, como antes) con todos los estilos acotados a .clara-form; con
shadow: true lo hace en un Shadow DOM. Cambios respecto al script anterior: ver
CHANGELOG.
Formulario + widget
Con widget el propio formulario trae el widget de Clara (el asistente de voz
flotante): una sola integración en lugar de dos scripts.
<ClaraForm templateId="…" apiKey="PUBLISHABLE_KEY" widget />
<ClaraForm templateId="…" apiKey="…" widget={{ position: 'bottom-left', autoOpen: true }} /><clara-form template-id="…" api-key="…" widget></clara-form>
<script>
// Opciones (objeto) por propiedad; el atributo solo lo enciende.
document.querySelector('clara-form').widget = { position: 'bottom-left' };
</script><script
src="https://cdn.clara.care/form/latest.js"
data-clara-template="TEMPLATE_ID"
data-clara-api-key="PUBLISHABLE_KEY"
data-clara-widget="true"
></script>ClaraForm.init({ templateId: '…', apiKey: '…', widget: { autoOpen: true } });Qué hace:
| | |
| --- | --- |
| Lo carga una sola vez | Si ya hay un <script> del widget en la página o ya existe window.ClaraWidget, no lo vuelve a cargar ni lo vuelve a arrancar. |
| De dónde | widget/latest.js del mismo CDN que sirvió el formulario (así el CDN de dev carga el widget de dev); si no se puede deducir, https://cdn.clara.care/widget/latest.js. Fíjalo con widget.scriptUrl (versión concreta o entorno de desarrollo). |
| Qué hereda | apiKey, apiBase, templateId (el de la plantilla cargada), analysisId, lang y el color del tema (--clara-form-primary → primaryColor). Todo se sobrescribe en widget. |
| Cómo se la pasa | Por window.ClaraWidget.init(config) si el widget ya está cargado; si lo carga el formulario, por los atributos data-clara-* del script (que es lo que lee el widget al arrancar solo). Las opciones que no son texto/número/booleano (objetos) solo viajan por init(). |
| Al desmontar | window.ClaraWidget.destroy() solo si lo creó el formulario; si el widget ya estaba en la página, no se toca. |
| Si falla | onError y onWidgetError con widget_load_failed (red o CSP del sitio), widget_unavailable (cargó pero no dejó window.ClaraWidget) o widget_init_failed. El formulario sigue funcionando a mano. |
Opciones de widget: scriptUrl, apiKey, apiBase, templateId, analysisId,
lang, primaryColor, position, autoOpen, draggable, launcherLeft,
launcherBottom, voiceFillMode, useChirpTranscript, googleClientId,
autoUnmount. Cualquier otra clave se pasa tal cual al widget.
La misma consulta en los dos
- Si el formulario tiene
analysisId, se lo pasa al widget: trabajan sobre la misma consulta. - Si no lo tiene y el widget inicia una consulta, el formulario se engancha a
ella: escucha
clara:session-startedde la página, adopta eseanalysisId(getAnalysisId()lo devuelve), lo incluye en los guardados y, en modolivecon credencial, empieza a leer el stream de esa consulta. Sin credencial se queda con los eventos de la página, que es lo que el widget ya emite. - Con
widgetel formulario escucha esos eventos aunque no esté en modolive(el widget rellena el formulario sin red). Apágalo conlocalChannel={false}.
Límites. El widget necesita su propio permiso en la CSP del sitio anfitrión
(script-src https://cdn.clara.care, y lo que pida su micrófono y su proxy de
voz); si la CSP lo bloquea, el error del <script> llega como
widget_load_failed. El bundle del widget habla con la API de su propio build:
apiBase se le pasa por compatibilidad futura, pero hoy no la usa. En un iframe
el widget arranca en modo esclavo (sin interfaz): el formulario lo detecta y no
intenta arrancarlo.
Guardar y enviar
onSubmit / clara-submit siguen igual. Con submit el formulario, además,
envía el guardado al servidor que digas, sin escribir código:
<ClaraForm
templateId="…"
apiKey="…"
submit={{ url: 'https://tu-servidor/consultas', format: 'both', includeFiles: true }}
submitLabel="Guardar consulta"
secondaryAction="Guardar borrador"
autosave={{ intervalMs: 15000, onSave: (values) => localStorage.setItem('borrador', JSON.stringify(values)) }}
onSubmitted={({ status }) => console.info('guardado', status)}
onSubmitError={(e) => console.warn(e.code, e.status)}
onSecondaryAction={(values) => guardarBorrador(values)}
/><clara-form template-id="…" api-key="…"
submit-url="https://tu-servidor/consultas" submit-format="both"
secondary-action="Guardar borrador" autosave-interval="15000"></clara-form>
<script>
const form = document.querySelector('clara-form');
// Cabeceras solo por propiedad (un atributo quedaría escrito en el HTML de la página).
form.submitTarget = { ...form.submitTarget, headers: { 'X-Clinic': 'c-1' } };
form.addEventListener('clara-submitted', (e) => console.info(e.detail.status));
form.addEventListener('clara-submit-error', (e) => console.warn(e.detail.code));
form.addEventListener('clara-secondary-action', (e) => guardarBorrador(e.detail.values));
form.addEventListener('clara-autosave', (e) => guardarBorrador(e.detail.values));
</script><script
src="https://cdn.clara.care/form/latest.js"
data-clara-template="TEMPLATE_ID"
data-clara-api-key="PUBLISHABLE_KEY"
data-clara-submit-url="https://tu-servidor/consultas"
data-clara-submit-format="both"
data-clara-secondary-action="Guardar borrador"
></script>| Opción de submit | |
| --- | --- |
| url | A dónde se envía (POST). Sin url no se envía nada: solo los eventos de siempre. |
| format | json (por defecto), fhir o both. |
| headers | Cabeceras extra. |
| includeFiles | Si hay archivos, envía multipart/form-data; si no, JSON. |
| withCredentials | Manda las cookies del sitio anfitrión (credentials: 'include'). |
| method | Por defecto POST. |
Cuerpo según format (Content-Type: application/json, o application/fhir+json
con fhir):
// json → { "templateId": "…", "analysisId": "…", "values": { … }, "json": { … } }
// fhir → el QuestionnaireResponse tal cual (listo para un servidor FHIR)
// both → { "templateId", "analysisId", "values", "json", "fhir" }Con includeFiles y archivos seleccionados va como multipart/form-data: una
parte payload con ese mismo JSON y una parte por archivo, con el nombre del
campo (Content-Type lo pone el navegador, con su boundary).
Estado en el botón: «Enviando…» (deshabilitado), «Enviado» (en verde) o
«Reintentar» (en rojo). Al lado aparece el resultado: «Guardado en el servidor»
(role="status") o el mensaje con el código HTTP (role="alert").
Al cambiar cualquier valor el botón vuelve a su texto normal, así que corregir y
reenviar es un clic. onSubmitted llega con la respuesta del servidor (también
con ok: false) y onSubmitError con submit_http_error (respuesta 4xx/5xx) o
submit_failed (no salió la petición: red, CORS o CSP).
Cuidado con las cabeceras y la CSP. Todo lo que pongas en
headersviaja en el navegador y lo ve cualquiera que abra la página: usa un token corto o un endpoint que autentique con las cookies del sitio (withCredentials), nunca un secreto de servidor. Tu endpoint necesita CORS para el origen de la página (yAccess-Control-Allow-CredentialsconwithCredentials), yconnect-srcen la CSP del sitio. Los valores son PHI: el formulario solo los manda a esaurl.
Botón secundario y borradores. secondaryAction añade un botón que emite
onSecondaryAction / clara-secondary-action con los mismos datos sin validar
(el fhir va con status: 'in-progress') y sin enviar nada. autosave hace lo
mismo cada intervalMs y solo si algo cambió desde el último borrador.
submitDisabled deshabilita el botón de envío.
Todos los guardados (envío, botón secundario y autosave) llevan templateId y el
analysisId de la consulta cuando lo hay.
Modo en vivo
<ClaraForm templateId="…" apiKey="…" analysisId="ID_DE_LA_CONSULTA" live /><clara-form template-id="…" api-key="…" analysis-id="ID_DE_LA_CONSULTA" live></clara-form>- Pide al API un token de lectura del stream de la consulta:
POST {apiBase}/s2/basin/tokencon{ "streams": [analysisId], "includeUserStream": false, "ops": ["read"] }. El API solo lo emite si la consulta es del dueño de la credencial, para ese único stream y con caducidad fijada por el servidor. - Lee los records por SSE de
https://{basin}.b.aws.s2.dev/v1/streams/{analysisId}/records, desde el principio (seq_num=0, se pone al día con lo ya rellenado) o desde la cola (liveFrom="tail"). - Renueva el token al 80 % de su vida y ante 401/403; al renovar o si el stream se
corta, reconecta desde el record siguiente al último visto (
seq_num = último + 1) y descarta losseq_numya entregados: ningún valor se aplica dos veces. Reintentos con espera exponencial (1 s a 30 s). - Si en la página hay un widget de Clara, escucha también sus eventos de
window(no depende solo de S2). Si el evento traeanalysisIddistinto del del formulario, se ignora. Conlivey sinanalysisId, el formulario escucha solo la página hasta que el widget inicia una consulta: entonces se engancha a ella.
Gana la persona: cuando alguien edita un campo a mano (evento de navegador de
confianza), los valores en vivo que lleguen después para ese campo no se aplican
(onLiveUpdate con applied: false, reason: 'manual_edit'). reset() lo olvida.
Los rellenos programáticos (el widget escribiendo en el DOM, scripts del host) no bloquean.
De qué frase salió: bajo cada valor en vivo se muestra "De la frase: «…»" (y como
title del input, enlazado con aria-describedby) cuando el record trae sourceText,
o sourceUtteranceId y el texto de esa frase llegó en un record transcript. Si no,
"Rellenado por Clara durante la consulta". Los valores escritos por el profesional
(editorSource: 'user') se aplican sin esa línea.
Records que consume
Del stream S2 de la consulta (publicados por el widget y por la plataforma):
| Record | Campos usados |
| --- | --- |
| { action: 'fill' } | fieldId, value, editorSource?, sourceUtteranceId?, sourceText? |
| { action: 'fillMultiple' } | fields[] con fieldId, value, sourceUtteranceId?, sourceText? (y editorSource del record) |
| { type: 'field_interaction', action: 'change' } | fieldId, value |
| { action: 'odontogram' } | fieldId? y value (mapa de dientes), o payload: { fieldId?, value } |
| { action: 'transcript' } | messages[] con id?, content o text (solo para el texto de la frase) |
Se ignoran el resto (chat, assistant_chat, session_status, set_title, pings…).
fieldId se busca por clave, id o htmlFieldId del campo; los valores se adaptan al
tipo (casillas "sí/no", coma decimal, opción de select sin tildes ni mayúsculas).
Eventos de window del widget en la misma página:
| Evento | detail usado |
| --- | --- |
| clara:field-filled | fieldName, value, analysisId?, sourceUtteranceId?, sourceText? |
| clara:odontogram-updated | fieldId, teeth |
| clara:transcript-updated | messages[] con id?, content |
| clara:session-started / clara:session-stopped | analysisId (para filtrar) |
Requisitos
- Credencial (API key publicable o token de sesión) del mismo usuario que lleva la consulta.
- El origen de tu página en la lista CORS del API de Clara.
- La plantilla (
templateIdotemplate) de la consulta: el API no deja leer la consulta con una API key. - CSP con
connect-srcpara el API yhttps://*.b.aws.s2.dev(abajo).
Validación
required (texto vacío, casilla sin marcar, odontograma vacío, sin archivos),
pattern (como el atributo HTML: el valor entero debe encajar; un patrón inválido se
ignora), number (numérico, con min/max si la plantilla los trae). El mensaje es
validationMessage de la plantilla, o uno por defecto en el idioma del formulario.
Los errores se muestran tras salir del campo o al enviar; onSubmit solo se llama si
todo es válido, si no onInvalid y el foco va al primer campo con error.
Fuera de React: validateValues(template, values, lang) y validateField(field, value, lang).
FHIR y JSON
import { toFhirQuestionnaireResponse, toLabeledJSON } from '@clara.care/form';
const qr = toFhirQuestionnaireResponse(template, values, {
status: 'completed', // por defecto 'in-progress'
subject: { reference: 'Patient/123' },
encounter: { reference: 'Encounter/456' },
questionnaire: 'https://tu-servidor/Questionnaire/consulta', // por defecto urn:uuid:<id de la plantilla>
codeSystem: 'https://tu-servidor/CodeSystem/opciones', // system de las respuestas de select
});Recurso R4 QuestionnaireResponse: un item de grupo por sección (linkId = clave de
la sección, text = etiqueta) con un item por campo (linkId = clave del campo).
Respuestas:
| Tipo | answer |
| --- | --- |
| text, textarea | valueString |
| number | valueDecimal (valueString si no es numérico) |
| checkbox | valueBoolean |
| select | valueCoding (code = display = texto de la opción; system opcional) |
| file | valueAttachment con title = nombre del archivo |
| odontogram | valueString con el JSON del array de acciones del odontograma de Clara: [{ "key": "18", "type": "hallazgo" \| "hallazgo_realizado", "area": 0-7, "action": "<condición>" }] (area 0 = diente completo) |
Las preguntas sin respuesta se omiten (includeEmpty: true las incluye sin answer; una
casilla sin marcar va como valueBoolean: false). iframe y event no aparecen.
toLabeledJSON(template, values) devuelve
{ [clave]: { label, type, section, sectionLabel, value } } con los números como número.
Tema y estilos
Sin fuentes externas: la fuente por defecto es la del sistema. Variables CSS (en
cualquier ancestro o en el propio <clara-form>; atraviesan el Shadow DOM):
| Variable | Por defecto (claro) |
| --- | --- |
| --clara-form-primary | #2a4276 |
| --clara-form-primary-contrast | #ffffff |
| --clara-form-font | pila del sistema |
| --clara-form-radius | 10px |
| --clara-form-bg | transparent |
| --clara-form-surface / --clara-form-surface-alt | #ffffff / #f4f6fb |
| --clara-form-text / --clara-form-heading / --clara-form-muted | #1e293b / #1c2f57 / #475569 |
| --clara-form-border | #cbd5e1 |
| --clara-form-error | #b91c1c |
| --clara-form-live | #15803d |
| --clara-form-focus | anillo de foco |
| --clara-form-input-bg, --clara-form-disabled-bg | fondo de inputs |
Tema oscuro con theme="dark", o theme="auto" para seguir prefers-color-scheme;
cada variable tiene su versión -dark (p. ej. --clara-form-bg-dark). El odontograma
se mantiene en claro en ambos temas.
Clases estables para retoques en el DOM de la página: .clara-form,
.clara-form-section, .clara-form-section-header, .clara-form-field,
.clara-form-input, .clara-form-error, .clara-form-source, .clara-form-actions,
.clara-form-submit (con data-state="idle|sending|sent|error"),
.clara-form-secondary y .clara-form-send-status.
Dentro de <clara-form> el contenedor es part="root".
Accesibilidad
- Cada input con
<label for>; los grupos (odontograma, iframe) conaria-labelledby. aria-required,aria-invalidyaria-describedbyhacia el error, la ayuda (helpTextdel campo) y la línea "De la frase"; los errores conrole="alert"y un resumen al enviar; el estado en vivo conrole="status".- Envío: el botón lleva
aria-busymientras envía; el resultado se anuncia junto al botón conrole="status"(enviado) orole="alert"(error). - Foco visible (
:focus-visible). - Odontograma con teclado: cada diente es un botón (Tab), Intro o Espacio marca el diente completo con la condición seleccionada, la tecla de menú o Mayús+F10 abre el menú del diente (el foco entra en él, Escape lo cierra). Límite: las superficies individuales (áreas 1-7) siguen siendo solo de ratón o táctil.
Content Security Policy
| Directiva | Valor | Para |
| --- | --- | --- |
| script-src | https://cdn.clara.care | el script del CDN del formulario y, con widget, el del widget |
| connect-src | https://api.clara.care (o tu apiBase) | plantillas y token del modo en vivo |
| connect-src | https://*.b.aws.s2.dev | modo en vivo (lectura del stream) |
| connect-src | la url de submit | envío automático del guardado |
| style-src | 'unsafe-inline' | el formulario inserta su hoja como <style> dentro de su contenedor o shadow root |
| style-src / font-src | https://fonts.googleapis.com / https://fonts.gstatic.com | solo con loadFonts |
| frame-src | los orígenes de tus campos iframe | solo si la plantilla los usa |
| img-src | los orígenes de los iconos de sección con URL | solo si los usa |
Ejemplos
En examples/: React (controlado, validación y FHIR), HTML puro con
<clara-form>, script del CDN clásico, modo en vivo y
submit/: formulario con widget y guardado enviado a un
servidor, los dos simulados en la propia página (?theme=dark, ?delay=2500,
?fail=500 o ?fail=network para ver los estados del botón).
pnpm --filter @clara.care/form dev # http://localhost:8080/Desarrollo
pnpm --filter @clara.care/form test # vitest (jsdom)
pnpm --filter @clara.care/form typecheck
pnpm --filter @clara.care/form build # dist: ESM, CJS, .d.ts y clara-form.iife.js
pnpm --filter @clara.care/form build:css # regenera los estilos del odontograma (tras cambiar su marcado)- El código del odontograma vive en
packages/clara-voice(privado) y se empaqueta endist. Sus clases Tailwind se compilan asrc/styles/odontogram.generated.ts, acotadas a.clara-formy sin el preflight global. - El paquete publica solo
dist/. En el monorepo el widget lee el código fuente por alias (apps/widget/vite.config.tsypathsdeapps/widget/tsconfig.app.json). - La versión sale de
package.json:scripts/write-version.mjsgenerasrc/version.ts(VERSION) en cadabuildy enpnpm version; un test falla si difieren. Al subir la versión, añadir su entrada enCHANGELOG.md. - API por defecto:
build(lo que va a npm, incluidodist/clara-form.iife.js) usa siempre la de producción e ignoraVITE_API_BASE_URL. Para probar en local contra otra API:CLARA_FORM_API_BASE_URL=https://api-dev.clara.care/api pnpm --filter @clara.care/form build(no publicar ese build:prepublishOnlylo rechaza).pnpm --filter @clara.care/form devsigue leyendoVITE_API_BASE_URL. - El CDN se construye con
pnpm --filter @clara/widget build:form(mismo código,apps/widget/vite.form.config.ts, con la API de cada entorno porVITE_API_BASE_URL) y lo subescripts/cdn-publish.shaform/latest.jsyform/<version>/clara-form.js(una versión publicada no se sobrescribe).
Publicar en npm
prepublishOnly construye, ejecuta typecheck y tests y comprueba dist/ con
scripts/check-dist.mjs (sin URLs de dev/local, sin imports del paquete privado
@clara/voice). El scope @clara.care es la cuenta/organización npm de la empresa
(la misma que publica @clara.care/react). Desde la raíz del monorepo:
npm whoami # clara.care o un miembro de la organización clara.care
npm org ls clara.care # (si es organización) tu usuario con rol owner o admin
unset CLARA_FORM_API_BASE_URL # prepublishOnly rechaza el build si está definida
pnpm --filter @clara.care/form publish --dry-run --no-git-checks
pnpm --filter @clara.care/form publish --otp <código 2FA>
npm view @clara.care/form version # comprobarUsar pnpm publish (no npm publish): pnpm reescribe los workspace:* del
package.json publicado. El acceso público va en publishConfig. Antes de la primera
publicación, completar el LICENSE (borrador pendiente de revisión legal). Los pasos de
los dos paquetes (@clara.care/form y @clara.care/react) están en
apps/widget/docs/NPM_PUBLISH_GUIDE.md.
