- Accordion
- Alert
- Alert Dialog
- Autocomplete
- Auth Surface
- Avatar
- Badge
- Browse Catalog Dialog
- Button
- Card
- Checkbox
- Checkbox Group
- Collapsible
- Combobox
- Command
- Connector Setup Dialog
- Cookie Banner
- Dialog
- Directory Card
- Directory Detail
- Directory Skeleton
- DrawerНовое
- Token Parts Input
- Empty
- Field
- Fieldset
- File Preview Modal
- File Preview Skeleton
- Form
- Frame
- Group
- Icon
- Input
- Input Group
- Kbd
- Label
- Legal Shell
- Menu
- Mermaid Diagram
- Mind Map Diagram
- Not Found Screen
- Onboarding Frame
- Popover
- PDF Thumbnail
- Personalization Landing
- Preview Card
- Pricing Page
- Progress
- Radio Group
- Ring Spinner
- Scroll Area
- Select
- Separator
- Settings Page
- Settings Skills
- Settings Connectors
- Settings Capabilities
- Settings Usage
- Settings Account
- Settings Billing
- Sheet
- Sidebar
- Skeleton
- Skill Create Dialog
- Slider
- Spinner
- Stat
- Switch
- Table
- Tabs
- Textarea
- Toast
- Toggle
- Tooltip
- Компоненты AI
- Chat Conversation
- Chat Message
- Chat Response
- Chat Suggestion
- Chat Prompt Input
- Slash Highlighted Textarea
- Chat Search Dialog
- Chat Skill Doc
- Chat Connector Detail
- Chat Attachments
- Chat File Card
- Chat Token Chip
- Chat Code Block
- Chat Image
- Chat Inline Citation
- Chat Sources
- Chat Web Search
- Chat Research
- Chat Source
- Chat Actions
- Chat Context
- Chat Loader
- Chat Compaction
- Chat Timeline
- Chat Snippet
- Chat Terminal
- Chat Stack Trace
- Chat Test Results
- Chat File Tree
- Chat Environment Variables
- Chat Audio Player
- Chat Transcription
- Chat Speech Input
- Chat Mic Selector
- Chat Voice Selector
- Chat Agent
- Chat Persona
- Chat Connection
- Chat Connector Suggestion
- Chat Queue
- Chat Checkpoint
- Chat Confirmation
- Chat Artifact
- Chat JSX Preview
- Chat Schema Display
- Chat Package Info
- Chat Commit
- Chat Plan
- Chat Open In Chat
- Chat Sandbox
- Chat Model Selector
- Chat Canvas
- Chat Node
- Chat Edge
Chat Message
Surface сообщения в чате 1:1 с живым стримом — пузырь пользователя, serif-тело ассистента, ряд действий (копировать / редактировать / перегенерировать), инлайн-редактор, навигация по веткам, scroll-to-bottom FAB и тулбар «Ответить» над выделением.
"use client";
import { IconCopy, IconPencil, IconRefresh } from "@tabler/icons-react";
import { useEffect, useState } from "react";
import {
ChatMessage,
ChatMessageAction,
ChatMessageActions,
ChatMessageContent,
ChatMessageTime,
} from "@/components/ui/chat-message";
export default function Particle() {
const [now, setNow] = useState<number | null>(null);
useEffect(() => setNow(Date.now()), []);
return (
<div className="flex w-full max-w-2xl flex-col gap-10 p-4">
<ChatMessage from="user">
<ChatMessageContent>Сравни Oracul DS и shadcn/ui</ChatMessageContent>
<ChatMessageActions variant="user">
<ChatMessageAction label="Редактировать">
<IconPencil aria-hidden="true" stroke={1} />
</ChatMessageAction>
<ChatMessageAction label="Копировать">
<IconCopy aria-hidden="true" stroke={1} />
</ChatMessageAction>
<ChatMessageTime now={now} ts={Date.now() - 1000 * 60 * 5} />
</ChatMessageActions>
</ChatMessage>
<ChatMessage from="assistant">
<ChatMessageContent>
Oracul построен на Base UI (преемник Radix): использует `render` prop
вместо `asChild` и идёт с большой коллекцией particles. Установка и
темизация совместимы с shadcn CLI, а тёплые токены — корректная
система Claude.
</ChatMessageContent>
<ChatMessageActions variant="assistant" visibility="always">
<ChatMessageAction label="Копировать">
<IconCopy aria-hidden="true" stroke={1} />
</ChatMessageAction>
<ChatMessageAction label="Перегенерировать">
<IconRefresh aria-hidden="true" stroke={1} />
</ChatMessageAction>
<ChatMessageTime now={now} ts={Date.now() - 1000 * 60} />
</ChatMessageActions>
</ChatMessage>
<ChatMessage from="system">
<ChatMessageContent>
Системное сообщение: модель переключена на claude-opus-4.8
</ChatMessageContent>
</ChatMessage>
</div>
);
}
Установка
pnpm dlx shadcn@latest add @oracul/chat-message
Использование
import {
ChatMessage,
ChatMessageAction,
ChatMessageActions,
ChatMessageContent,
ChatMessageTime,
} from "@/components/ui/chat-message";Базовое сообщение
<ChatMessage from="user">
<ChatMessageContent>Покажи погоду в Москве</ChatMessageContent>
</ChatMessage>
<ChatMessage from="assistant">
<ChatMessageContent>Привет! Чем могу помочь?</ChatMessageContent>
</ChatMessage>Пузырь пользователя — secondary-плитка (15px sans, leading-relaxed). Тело
ассистента — без пузыря, serif 16px / lh 1.65, и помечено
data-assistant-message, чтобы тулбар выделения цеплялся только к нему. Роли
system (приглушённый пузырь) и tool (карточка с обводкой) сохранены.
Ряд действий
ChatMessageActions рисует hover-ряд. variant="user" даёт gap-2, variant="assistant" — gap-1; visibility="always" закрепляет ряд (последний ответ), visibility="hover" (по умолчанию) показывает его на ховере сообщения.
<ChatMessage from="assistant">
<ChatMessageContent>...</ChatMessageContent>
<ChatMessageActions variant="assistant" visibility="always">
<ChatMessageAction label="Копировать"><CopyIcon strokeWidth={1} /></ChatMessageAction>
<ChatMessageAction label="Перегенерировать"><RefreshCwIcon strokeWidth={1} /></ChatMessageAction>
<ChatMessageTime now={now} ts={message.createdAt} />
</ChatMessageActions>
</ChatMessage>ChatMessageAction — это 32px / r8 ghost-квадрат с тонкими (strokeWidth 1) иконками 20px, приглушённый по умолчанию, foreground-on-accent на ховере. ChatMessageTime рендерит относительное время (только что, 5м назад, …) и ничего не рисует, пока now не станет числом после гидрации.
Инлайн-редактор
"use client";
import { IconCopy, IconPencil } from "@tabler/icons-react";
import { useState } from "react";
import {
ChatMessage,
ChatMessageAction,
ChatMessageActions,
ChatMessageContent,
ChatMessageEdit,
ChatMessageEditActions,
} from "@/components/ui/chat-message";
export default function Particle() {
const [content, setContent] = useState("Сравни Oracul DS и shadcn/ui");
const [editing, setEditing] = useState(false);
const [draft, setDraft] = useState(content);
const save = (value: string) => {
setContent(value);
setEditing(false);
};
return (
<div className="flex w-full max-w-2xl flex-col gap-6 p-4">
<ChatMessage from="user">
{editing ? (
<div className="flex w-full max-w-[80%] flex-col gap-2">
<ChatMessageEdit
aria-label="Редактировать сообщение"
onCancel={() => setEditing(false)}
onChange={(event) => setDraft(event.target.value)}
onSave={save}
value={draft}
/>
<ChatMessageEditActions
onCancel={() => setEditing(false)}
onSave={() => save(draft)}
/>
</div>
) : (
<>
<ChatMessageContent>{content}</ChatMessageContent>
<ChatMessageActions variant="user">
<ChatMessageAction
label="Редактировать"
onClick={() => {
setDraft(content);
setEditing(true);
}}
>
<IconPencil aria-hidden="true" stroke={1.5} />
</ChatMessageAction>
<ChatMessageAction label="Копировать">
<IconCopy aria-hidden="true" stroke={1.5} />
</ChatMessageAction>
</ChatMessageActions>
</>
)}
</ChatMessage>
<ChatMessage from="assistant">
<ChatMessageContent>
Нажмите карандаш у сообщения выше, чтобы открыть инлайн-редактор.
Enter сохраняет, Shift+Enter — перенос строки, Escape — отмена.
</ChatMessageContent>
</ChatMessage>
</div>
);
}
<ChatMessage from="user">
{editing ? (
<div className="flex w-full max-w-[80%] flex-col gap-2">
<ChatMessageEdit
value={draft}
onChange={(e) => setDraft(e.target.value)}
onSave={save}
onCancel={() => setEditing(false)}
/>
<ChatMessageEditActions onCancel={() => setEditing(false)} onSave={() => save(draft)} />
</div>
) : (
<ChatMessageContent>{content}</ChatMessageContent>
)}
</ChatMessage>ChatMessageEdit — рукописная textarea (не базовый Input): r18 на bg-card, 14px / leading-snug, фокус-кольцо ring-2/30. Enter сохраняет, Shift+Enter — перенос строки, Escape — отмена. ChatMessageEditActions — правый ряд Отмена (outline) / Сохранить (primary).
Ветки (альтернативные ответы)
"use client";
import {
ChatMessage,
ChatMessageBranch,
ChatMessageBranchContent,
ChatMessageBranchControls,
ChatMessageBranchNext,
ChatMessageBranchPage,
ChatMessageBranchPrevious,
ChatMessageContent,
} from "@/components/ui/chat-message";
export default function Particle() {
return (
<div className="flex w-full max-w-2xl flex-col gap-6 p-4">
<ChatMessage from="user">
<ChatMessageContent>Объясни замыкания в JavaScript</ChatMessageContent>
</ChatMessage>
<ChatMessage from="assistant">
<ChatMessageBranch defaultBranch={0}>
<ChatMessageBranchContent>
<ChatMessageContent>
Замыкание — это функция, которая помнит окружение, в котором была
создана: она сохраняет доступ к переменным внешней области даже
после того, как та завершилась.
</ChatMessageContent>
<ChatMessageContent>
Если коротко: внутренняя функция «захватывает» переменные внешней
функции. Это даёт приватное состояние без классов.
</ChatMessageContent>
<ChatMessageContent>
Представь рюкзак: функция уносит с собой переменные из места
своего рождения и достаёт их при каждом вызове.
</ChatMessageContent>
</ChatMessageBranchContent>
<ChatMessageBranchControls>
<ChatMessageBranchPrevious />
<ChatMessageBranchPage />
<ChatMessageBranchNext />
</ChatMessageBranchControls>
</ChatMessageBranch>
</ChatMessage>
</div>
);
}
<ChatMessage from="assistant">
<ChatMessageBranch defaultBranch={0}>
<ChatMessageBranchContent>
<ChatMessageContent>Вариант 1</ChatMessageContent>
<ChatMessageContent>Вариант 2</ChatMessageContent>
<ChatMessageContent>Вариант 3</ChatMessageContent>
</ChatMessageBranchContent>
<ChatMessageBranchControls>
<ChatMessageBranchPrevious />
<ChatMessageBranchPage />
<ChatMessageBranchNext />
</ChatMessageBranchControls>
</ChatMessageBranch>
</ChatMessage>Управление контекстное: ChatMessageBranchControls рендерит null, когда вариант один (totalBranches <= 1). ChatMessageBranchPage показывает текущий / всего (tabular-nums).
Scroll-to-bottom FAB
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import {
ChatMessage,
ChatMessageContent,
ChatMessageScrollButton,
} from "@/components/ui/chat-message";
const TURNS = [
[
"Покажи план миграции на Base UI",
"Разбил миграцию на четыре фазы: аудит, токены, примитивы, particles.",
],
[
"С чего начать?",
"Начни с токенов — они снимают большинство хардкод-цветов сразу.",
],
[
"А что с radius?",
"Radius lg = 0.5rem (8px). Используй шкалу sm/md/lg/xl/2xl везде.",
],
["Сколько займёт?", "Около спринта на ядро и ещё один на частные экраны."],
["Спасибо!", "Обращайся — допишу чек-лист, если нужно."],
];
export default function Particle() {
const ref = useRef<HTMLDivElement>(null);
const [isAtBottom, setIsAtBottom] = useState(true);
const updatePin = useCallback(() => {
const el = ref.current;
if (!el) return;
const distance = el.scrollHeight - el.scrollTop - el.clientHeight;
setIsAtBottom(distance < 48);
}, []);
useEffect(() => {
const el = ref.current;
if (!el) return;
updatePin();
el.addEventListener("scroll", updatePin, { passive: true });
return () => el.removeEventListener("scroll", updatePin);
}, [updatePin]);
const scrollToBottom = () => {
const el = ref.current;
if (!el) return;
el.scrollTo({ top: el.scrollHeight, behavior: "smooth" });
};
return (
<div className="relative w-full max-w-2xl">
<div
className="flex max-h-80 flex-col gap-8 overflow-y-auto rounded-xl border bg-background p-4"
ref={ref}
>
{TURNS.map(([user, ai]) => (
<div className="flex flex-col gap-8" key={user}>
<ChatMessage from="user">
<ChatMessageContent>{user}</ChatMessageContent>
</ChatMessage>
<ChatMessage from="assistant">
<ChatMessageContent>{ai}</ChatMessageContent>
</ChatMessage>
</div>
))}
</div>
{!isAtBottom ? (
<div className="absolute bottom-4 left-1/2 z-10 -translate-x-1/2">
<ChatMessageScrollButton onClick={scrollToBottom} />
</div>
) : null}
</div>
);
}
{!isAtBottom ? (
<div className="absolute bottom-4 left-1/2 z-10 -translate-x-1/2">
<ChatMessageScrollButton onClick={scrollToBottom} />
</div>
) : null}ChatMessageScrollButton — круглый 36px FAB c прохладной рамкой 0.5px, frosted-панелью и двухслойной тенью (1:1 с живым стримом). Позиционирование, монтирование и анимацию владеет потребитель.
Тулбар «Ответить» над выделением
"use client";
import { useState } from "react";
import {
ChatMessage,
ChatMessageContent,
ChatMessageSelectionReply,
} from "@/components/ui/chat-message";
export default function Particle() {
const [excerpt, setExcerpt] = useState<string | null>(null);
return (
<div className="flex w-full max-w-2xl flex-col gap-4 p-4">
<ChatMessage from="assistant">
<ChatMessageContent>
Выделите любой фрагмент этого ответа мышью — над выделением появится
тёмная пилюля «Ответить». Клик по ней передаст выбранный текст в
обработчик как цитату, ровно как в живом чате. Тулбар реагирует только
на выделение внутри тела ассистента.
</ChatMessageContent>
</ChatMessage>
<ChatMessage from="user">
<ChatMessageContent>
А по этому пузырю выделение тулбар не покажет.
</ChatMessageContent>
</ChatMessage>
{excerpt ? (
<div className="rounded-lg border bg-muted px-3 py-2 text-muted-foreground text-sm">
Цитата в композер: «{excerpt}»
</div>
) : (
<div className="rounded-lg border border-dashed px-3 py-2 text-muted-foreground text-sm">
Выделите текст ответа, чтобы увидеть цитату здесь.
</div>
)}
<ChatMessageSelectionReply onReply={setExcerpt} />
</div>
);
}
<ChatMessageSelectionReply onReply={(text) => quoteIntoComposer(text)} />ChatMessageSelectionReply слушает выделение внутри [data-assistant-message="true"] и портит fixed тёмную пилюлю над выделением. Клик передаёт обрезанный фрагмент в onReply и снимает выделение.
Видимость действий
"use client";
import { IconCopy, IconRefresh } from "@tabler/icons-react";
import { useEffect, useState } from "react";
import {
ChatMessage,
ChatMessageAction,
ChatMessageActions,
ChatMessageContent,
ChatMessageTime,
} from "@/components/ui/chat-message";
export default function Particle() {
const [now, setNow] = useState<number | null>(null);
useEffect(() => setNow(Date.now()), []);
return (
<div className="flex w-full max-w-2xl flex-col gap-10 p-4">
<ChatMessage from="assistant">
<ChatMessageContent>
Это прошлый ответ в ленте. Ряд действий скрыт по умолчанию и
проявляется только при наведении на сообщение.
</ChatMessageContent>
<ChatMessageActions variant="assistant" visibility="hover">
<ChatMessageAction label="Копировать">
<IconCopy aria-hidden="true" stroke={1.5} />
</ChatMessageAction>
<ChatMessageTime now={now} ts={Date.now() - 1000 * 60 * 60 * 2} />
</ChatMessageActions>
</ChatMessage>
<ChatMessage from="assistant">
<ChatMessageContent>
Это последний ответ. Здесь ряд действий закреплён и виден всегда,
включая кнопку перегенерации.
</ChatMessageContent>
<ChatMessageActions variant="assistant" visibility="always">
<ChatMessageAction label="Копировать">
<IconCopy aria-hidden="true" stroke={1.5} />
</ChatMessageAction>
<ChatMessageAction label="Перегенерировать">
<IconRefresh aria-hidden="true" stroke={1.5} />
</ChatMessageAction>
<ChatMessageTime now={now} ts={Date.now() - 1000 * 12} />
</ChatMessageActions>
</ChatMessage>
</div>
);
}
Вложения
"use client";
import {
ChatMessage,
ChatMessageAttachment,
ChatMessageAttachments,
ChatMessageContent,
} from "@/components/ui/chat-message";
export default function Particle() {
const open = (label: string) => () => {
window.alert(`Открыть превью: ${label}`);
};
return (
<div className="flex w-full max-w-2xl flex-col gap-4 p-4">
<ChatMessage from="user">
<ChatMessageAttachments>
<ChatMessageAttachment
name="diagram.png"
onClick={open("diagram.png")}
src="https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=240&q=80"
variant="image"
/>
<ChatMessageAttachment
ext="PDF"
meta="отчёт"
name="quarterly-report-2024.pdf"
onClick={open("quarterly-report-2024.pdf")}
variant="file"
/>
<ChatMessageAttachment
ext="TXT"
meta="248 строк"
name="server-config-notes.txt"
onClick={open("server-config-notes.txt")}
variant="file"
/>
</ChatMessageAttachments>
<ChatMessageContent>
Посмотри вложения и подскажи, что не так с конфигом.
</ChatMessageContent>
</ChatMessage>
</div>
);
}
<ChatMessage from="user">
<ChatMessageAttachments>
<ChatMessageAttachment
variant="image"
src={image.url}
name={image.name}
onClick={() => preview(image)}
/>
<ChatMessageAttachment
variant="file"
name="report.pdf"
meta="248 строк"
ext="PDF"
onClick={() => preview(doc)}
/>
</ChatMessageAttachments>
<ChatMessageContent>{content}</ChatMessageContent>
</ChatMessage>ChatMessageAttachments — ряд плиток над пузырём (выровнен вправо, перенос, gap-3). ChatMessageAttachment — плитка 120×120: вариант image рисует cover-миниатюру (на ховере подсвечивается), вариант file — имя (обрезка в 3 строки), мета-строку и чип-расширение ChatMessageFileBadge. Обе плитки — кнопки с onClick. Геометрия (120px, рамка 0.5px, тень) — bespoke; цвета — токены DS (bg-muted/bg-card, border, text-foreground/text-muted-foreground).
Цитаты и вставленный текст
"use client";
import {
ChatMessage,
ChatMessageAttachments,
ChatMessageContent,
ChatMessageQuote,
} from "@/components/ui/chat-message";
const PASTED_PREVIEW = `import { createServer } from "node:http";
const server = createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("hello world");
});
server.listen(3000);`;
const EXCERPT_PREVIEW =
"Транзакции в этой схеме изолируются на уровне SERIALIZABLE, поэтому два параллельных перевода между одними и теми же счетами никогда не приведут к двойному списанию — движок откатит более позднюю транзакцию с ошибкой сериализации.";
export default function Particle() {
const preview = (label: string) => () => {
window.alert(`Открыть превью: ${label}`);
};
return (
<div className="flex w-full max-w-2xl flex-col gap-6 p-4">
{/* Pasted-text tile with the PASTED badge, in a user attachment row. */}
<ChatMessage from="user">
<ChatMessageAttachments>
<ChatMessageQuote
badge="Вставлено"
onClick={preview("вставленный текст")}
preview={PASTED_PREVIEW}
title="Вставленный текст"
variant="pasted"
/>
</ChatMessageAttachments>
<ChatMessageContent>
Объясни, что делает этот фрагмент.
</ChatMessageContent>
</ChatMessage>
{/* Quoted excerpt of a previous assistant answer (block-quote pill). */}
<ChatMessage from="user">
<ChatMessageQuote
onClick={preview("цитата из ответа")}
preview={EXCERPT_PREVIEW}
title="Цитата из ответа"
variant="excerpt"
/>
<ChatMessageContent>
А что будет при уровне READ COMMITTED?
</ChatMessageContent>
</ChatMessage>
</div>
);
}
{/* вставленный текст — плитка 120px + бейдж */}
<ChatMessageAttachments>
<ChatMessageQuote variant="pasted" preview={text} badge="Вставлено" onClick={preview} />
</ChatMessageAttachments>
{/* цитата фрагмента ответа — блок-цитата */}
<ChatMessageQuote variant="excerpt" preview={excerpt} onClick={preview} />ChatMessageQuote рисует два вида карточек. variant="pasted" повторяет геометрию плитки 120px и ставит чип ChatMessageFileBadge («Вставлено»). variant="excerpt" — авто-ширина r12 bg-secondary блок-цитата: вертикальная акцент-черта (bg-foreground/25) + обрезанный текст в 6 строк, на ховере bg-secondary/80. ChatMessageFileBadge — uppercase-чип h-18 / r4 на bg-muted (semibold 11px muted).
API
| Компонент | Описание |
|---|---|
ChatMessage | Внешний контейнер. from: 'user' | 'assistant' | 'system' | 'tool' управляет выравниванием/стилями и hover-ховером ряда действий. |
ChatMessageContent | Тело сообщения. Рисует пузырь/тело по роли; для ассистента выставляет data-assistant-message. |
ChatMessageAvatar | Тонкая обёртка над Avatar (src / fallback). |
ChatMessageActions | Ряд действий. variant (user/assistant gap), visibility (hover/always). |
ChatMessageAction | Icon-only действие — 32px / r8 ghost, иконки 20px. |
ChatMessageTime | Относительный таймстамп (ts, now); экспортирует formatAgo. |
ChatMessageEdit | Инлайн textarea-редактор (onSave / onCancel, Enter/Escape). |
ChatMessageEditActions | Ряд Отмена / Сохранить. |
ChatMessageBranch* | Селектор альтернативных версий ответа (prev / page / next). |
ChatMessageScrollButton | Круглый scroll-to-bottom FAB. |
ChatMessageSelectionReply | Плавающий тулбар «Ответить» над выделением в теле ассистента. |
ChatMessageAttachments | Ряд плиток вложений над пузырём (выровнен вправо, перенос). |
ChatMessageAttachment | Плитка вложения 120px. variant (image / file), src / name / meta / ext. |
ChatMessageQuote | Карточка цитаты / вставленного текста. variant (pasted / excerpt), preview / badge. |
ChatMessageFileBadge | Uppercase-чип расширения / «Вставлено» (h-18 / r4). |
ChatMessageToolbar | Строка раскладки под пузырём (для обратной совместимости). |
Состояния
- role=user — пузырь secondary справа, 15px sans / leading-relaxed.
- role=assistant — без пузыря, serif 16px / lh 1.65,
data-assistant-message. - role=system / tool — приглушённый пузырь / карточка с обводкой (примитив).
- actions hover / pinned —
opacity-0 group-hover/msg:opacity-100противopacity-100. - edit idle / active — пузырь + действия против textarea + Отмена/Сохранить (Enter сохраняет, Shift+Enter — перенос, Escape — отмена).
- branch single / multi — управление скрыто при одном варианте, иначе prev/next +
N / M. - scroll at-bottom / away — FAB скрыт / показан (потребитель анимирует).
- selection active / collapsed — пилюля «Ответить» над выделением / удалена при сбросе.
- attachment image idle / hover — cover-миниатюра 120px /
hover:brightness-95. - attachment file idle / hover — имя + мета + чип-расширение /
hover:border-foreground/30. - quote pasted — плитка 120px превью + бейдж «Вставлено».
- quote excerpt idle / hover — блок-цитата с акцент-чертой /
hover:bg-secondary/80. - file-badge — uppercase-чип h-18 / r4 на
bg-muted.
Стили роли применяются через [data-role=user], [data-role=assistant] и т.д. — можно переопределить классами. Текст и подписи интернационализируемы через пропсы (label, cancelLabel, saveLabel, format).