npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

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

npm

npm install @clara.care/form react react-dom

react 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-started de la página, adopta ese analysisId (getAnalysisId() lo devuelve), lo incluye en los guardados y, en modo live con 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 widget el formulario escucha esos eventos aunque no esté en modo live (el widget rellena el formulario sin red). Apágalo con localChannel={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 headers viaja 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 (y Access-Control-Allow-Credentials con withCredentials), y connect-src en la CSP del sitio. Los valores son PHI: el formulario solo los manda a esa url.

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>
  1. Pide al API un token de lectura del stream de la consulta: POST {apiBase}/s2/basin/token con { "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.
  2. 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").
  3. 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 los seq_num ya entregados: ningún valor se aplica dos veces. Reintentos con espera exponencial (1 s a 30 s).
  4. 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 trae analysisId distinto del del formulario, se ignora. Con live y sin analysisId, 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 (templateId o template) de la consulta: el API no deja leer la consulta con una API key.
  • CSP con connect-src para el API y https://*.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) con aria-labelledby.
  • aria-required, aria-invalid y aria-describedby hacia el error, la ayuda (helpText del campo) y la línea "De la frase"; los errores con role="alert" y un resumen al enviar; el estado en vivo con role="status".
  • Envío: el botón lleva aria-busy mientras envía; el resultado se anuncia junto al botón con role="status" (enviado) o role="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 en dist. Sus clases Tailwind se compilan a src/styles/odontogram.generated.ts, acotadas a .clara-form y 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.ts y paths de apps/widget/tsconfig.app.json).
  • La versión sale de package.json: scripts/write-version.mjs genera src/version.ts (VERSION) en cada build y en pnpm version; un test falla si difieren. Al subir la versión, añadir su entrada en CHANGELOG.md.
  • API por defecto: build (lo que va a npm, incluido dist/clara-form.iife.js) usa siempre la de producción e ignora VITE_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: prepublishOnly lo rechaza). pnpm --filter @clara.care/form dev sigue leyendo VITE_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 por VITE_API_BASE_URL) y lo sube scripts/cdn-publish.sh a form/latest.js y form/<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            # comprobar

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