diff --git a/package.json b/package.json
index e72cb21..f7a59af 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "pages",
- "version": "0.0.4",
+ "version": "0.0.5",
"description": "Doctorina Pages - Multiple lightweight HTML pages",
"type": "module",
"scripts": {
diff --git a/pages/chat/main.ts b/pages/chat/main.ts
index 9f1558d..e5c2193 100644
--- a/pages/chat/main.ts
+++ b/pages/chat/main.ts
@@ -24,10 +24,8 @@ const i18n = {
attrDefault: 'Default',
attrTargetUrl: 'App URL to redirect to',
attrTargetUrlDefault: 'app.doctorina.com',
- attrPlaceholder: 'Static placeholder text',
- attrPlaceholderDefault: 'None',
- attrPhrases: 'JSON array of phrases for typing animation',
- attrPhrasesDefault: 'None',
+ attrChildren: 'Child <p> elements: 1 = static placeholder, 2+ = typing animation',
+ attrChildrenDefault: 'None',
attrParams: 'JSON object of extra query parameters (e.g. UTM tags)',
attrParamsDefault: 'None',
examples: 'Examples',
@@ -39,6 +37,9 @@ const i18n = {
exPlaceholderDesc: 'Simple static placeholder text:',
exParamsTitle: 'With UTM Parameters',
exParamsDesc: 'Pass custom query parameters (e.g. UTM tags) to the target URL:',
+ demoPhrase1: 'What symptoms do you have?',
+ demoPhrase2: 'Ask about medications',
+ demoPhrase3: 'Describe how you feel',
multiWidget: 'Multiple Widgets',
multiWidgetDesc: 'Use the class doctorina-chat instead of id to place multiple widgets on the same page. Each can have its own configuration.',
programmatic: 'Programmatic API',
@@ -60,10 +61,8 @@ const i18n = {
attrDefault: 'Predeterminado',
attrTargetUrl: 'URL de la app para redirigir',
attrTargetUrlDefault: 'app.doctorina.com',
- attrPlaceholder: 'Texto estático del placeholder',
- attrPlaceholderDefault: 'Ninguno',
- attrPhrases: 'Array JSON de frases para la animación de escritura',
- attrPhrasesDefault: 'Ninguno',
+ attrChildren: 'Elementos hijos <p>: 1 = placeholder estático, 2+ = animación de escritura',
+ attrChildrenDefault: 'Ninguno',
attrParams: 'Objeto JSON con parámetros extra (ej. etiquetas UTM)',
attrParamsDefault: 'Ninguno',
examples: 'Ejemplos',
@@ -75,6 +74,9 @@ const i18n = {
exPlaceholderDesc: 'Texto estático simple en el campo de entrada:',
exParamsTitle: 'Con parámetros UTM',
exParamsDesc: 'Pasa parámetros personalizados (ej. etiquetas UTM) a la URL de destino:',
+ demoPhrase1: '¿Qué síntomas tienes?',
+ demoPhrase2: 'Pregunta sobre medicamentos',
+ demoPhrase3: 'Describe cómo te sientes',
multiWidget: 'Múltiples widgets',
multiWidgetDesc: 'Usa la clase doctorina-chat en lugar de id para colocar múltiples widgets en la misma página. Cada uno puede tener su propia configuración.',
programmatic: 'API programática',
@@ -96,10 +98,8 @@ const i18n = {
attrDefault: 'По умолчанию',
attrTargetUrl: 'URL приложения для редиректа',
attrTargetUrlDefault: 'app.doctorina.com',
- attrPlaceholder: 'Статический текст placeholder',
- attrPlaceholderDefault: 'Нет',
- attrPhrases: 'JSON-массив фраз для анимации печатания',
- attrPhrasesDefault: 'Нет',
+ attrChildren: 'Дочерние элементы <p>: 1 = статический placeholder, 2+ = анимация печатания',
+ attrChildrenDefault: 'Нет',
attrParams: 'JSON-объект с дополнительными query-параметрами (напр. UTM-метки)',
attrParamsDefault: 'Нет',
examples: 'Примеры',
@@ -111,6 +111,9 @@ const i18n = {
exPlaceholderDesc: 'Простой статический текст в поле ввода:',
exParamsTitle: 'С UTM-параметрами',
exParamsDesc: 'Передача произвольных query-параметров (напр. UTM-меток) в целевой URL:',
+ demoPhrase1: 'Какие у вас симптомы?',
+ demoPhrase2: 'Спросите о лекарствах',
+ demoPhrase3: 'Опишите как вы себя чувствуете',
multiWidget: 'Несколько виджетов',
multiWidgetDesc: 'Используйте класс doctorina-chat вместо id, чтобы разместить несколько виджетов на одной странице. Каждый может иметь свою конфигурацию.',
programmatic: 'Программный API',
@@ -131,13 +134,15 @@ const SNIPPET_BASIC = `
`;
-const SNIPPET_PHRASES = `
+const SNIPPET_PHRASES = `
+
What symptoms do you have?
+
Ask about medications
+
Get a health consultation
`;
-const SNIPPET_PLACEHOLDER = `
+const SNIPPET_PLACEHOLDER = `
+
Type your health question...
`;
@@ -172,8 +177,10 @@ function render(lang: Lang): void {
${t.demoDesc}
@@ -197,8 +204,7 @@ function render(lang: Lang): void {
| data-target-url | ${t.attrTargetUrl} | ${t.attrTargetUrlDefault} |
- | data-placeholder | ${t.attrPlaceholder} | ${t.attrPlaceholderDefault} |
- | data-phrases | ${t.attrPhrases} | ${t.attrPhrasesDefault} |
+ | <p> children | ${t.attrChildren} | ${t.attrChildrenDefault} |
| data-params | ${t.attrParams} | ${t.attrParamsDefault} |
diff --git a/src/widget/config.ts b/src/widget/config.ts
index 129f14e..9cb955d 100644
--- a/src/widget/config.ts
+++ b/src/widget/config.ts
@@ -14,21 +14,16 @@ export function parseConfig(container: HTMLElement): WidgetConfig {
targetUrl: targetUrl.replace(/\/+$/, ''),
};
- const placeholder = container.getAttribute('data-placeholder');
- if (placeholder) {
- config.placeholder = placeholder;
- }
+ // Child elements = phrases for placeholder animation (translation-plugin friendly)
+ // 1 element → static placeholder, 2+ elements → animated cycling
+ const childTexts = Array.from(container.children)
+ .map((el) => el.textContent?.trim() ?? '')
+ .filter((text) => text.length > 0);
- const phrasesAttr = container.getAttribute('data-phrases');
- if (phrasesAttr) {
- try {
- const parsed = JSON.parse(phrasesAttr);
- if (Array.isArray(parsed) && parsed.length > 0) {
- config.phrases = parsed.filter((p): p is string => typeof p === 'string' && p.trim() !== '');
- }
- } catch {
- console.warn('[DoctorinaChat] Invalid data-phrases JSON:', phrasesAttr);
- }
+ if (childTexts.length === 1) {
+ config.placeholder = childTexts[0];
+ } else if (childTexts.length > 1) {
+ config.phrases = childTexts;
}
const paramsAttr = container.getAttribute('data-params');
diff --git a/src/widget/doctorina-chat.ts b/src/widget/doctorina-chat.ts
index 09b75bb..4921f65 100644
--- a/src/widget/doctorina-chat.ts
+++ b/src/widget/doctorina-chat.ts
@@ -12,6 +12,7 @@ export function init(): void {
container.dataset.dchatInited = '1';
const config = parseConfig(container);
+ container.innerHTML = '';
injectStyles();
createWidget(container, config);
diff --git a/src/widget/placeholder-animation.ts b/src/widget/placeholder-animation.ts
index 06b9d93..c49eb2f 100644
--- a/src/widget/placeholder-animation.ts
+++ b/src/widget/placeholder-animation.ts
@@ -10,6 +10,12 @@ export function startPlaceholderAnimation(
): () => void {
if (phrases.length === 0) return () => {};
+ // Respect user motion preferences — show first phrase as static placeholder
+ if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
+ textarea.placeholder = phrases[0];
+ return () => {};
+ }
+
let phraseIndex = 0;
let charIndex = 0;
let deleting = false;
diff --git a/src/widget/styles.ts b/src/widget/styles.ts
index 02794ca..6cdf272 100644
--- a/src/widget/styles.ts
+++ b/src/widget/styles.ts
@@ -37,7 +37,6 @@ const CSS = `
.dchat-textarea:focus {
outline: none;
- box-shadow: none;
}
.dchat-textarea::placeholder {
@@ -75,6 +74,10 @@ const CSS = `
position: relative;
}
+.dchat-btn:focus-visible {
+ box-shadow: 0 0 0 2px rgba(37, 211, 102, 0.35);
+}
+
.dchat-btn:active:not(:disabled) {
transform: scale(0.94);
}
diff --git a/src/widget/ui.ts b/src/widget/ui.ts
index 6c73398..d663ed2 100644
--- a/src/widget/ui.ts
+++ b/src/widget/ui.ts
@@ -5,17 +5,16 @@ import { startPlaceholderAnimation } from './placeholder-animation';
const SEND_ICON_SVG = `
`;
export function createWidget(container: HTMLElement, config: WidgetConfig): void {
- // Build DOM
const box = document.createElement('div');
box.className = 'dchat-box';
box.setAttribute('role', 'form');
- box.setAttribute('aria-label', 'Chat message');
+ box.setAttribute('aria-label', container.getAttribute('data-aria-label') || 'Chat message');
const textarea = document.createElement('textarea');
textarea.className = 'dchat-textarea';
textarea.rows = 1;
textarea.setAttribute('autocomplete', 'off');
- textarea.setAttribute('aria-label', 'Chat message');
+ textarea.setAttribute('aria-label', container.getAttribute('data-aria-input') || 'Message');
textarea.maxLength = 4096;
if (config.placeholder) {
@@ -25,8 +24,7 @@ export function createWidget(container: HTMLElement, config: WidgetConfig): void
const btn = document.createElement('button');
btn.className = 'dchat-btn';
btn.type = 'button';
- btn.setAttribute('aria-label', 'Send message');
- btn.title = 'Send message';
+ btn.setAttribute('aria-label', container.getAttribute('data-aria-send') || 'Send');
btn.disabled = true;
btn.setAttribute('aria-disabled', 'true');
btn.innerHTML = SEND_ICON_SVG;
@@ -63,12 +61,13 @@ export function createWidget(container: HTMLElement, config: WidgetConfig): void
btn.addEventListener('click', submit);
- textarea.addEventListener('keydown', (e) => {
+ function onKeydown(e: KeyboardEvent): void {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
submit();
}
- });
+ }
+ textarea.addEventListener('keydown', onKeydown);
// Placeholder animation
let destroyAnimation: (() => void) | undefined;
@@ -76,13 +75,15 @@ export function createWidget(container: HTMLElement, config: WidgetConfig): void
destroyAnimation = startPlaceholderAnimation(textarea, config.phrases);
}
- // Expose cleanup for SPA / dynamic removal
+ // Cleanup for SPA / dynamic removal
container.addEventListener(
'dchat:destroy',
() => {
destroyAnimation?.();
textarea.removeEventListener('input', autoResize);
textarea.removeEventListener('input', updateButton);
+ textarea.removeEventListener('keydown', onKeydown);
+ btn.removeEventListener('click', submit);
},
{ once: true },
);