Тема, язык и жизненный цикл
Тема
connect() записывает живую тему приложения в <html> вашей страницы как CSS-переменные и держит их
актуальными:
--ns-<token>для каждого токена:background,background-secondary,background-tertiary,foreground,muted,surface,surface-foreground,surface-secondary,surface-secondary-foreground,surface-tertiary,surface-tertiary-foreground,overlay,overlay-foreground,default,default-foreground,accent,accent-foreground,accent-soft,accent-soft-foreground,success,success-foreground,success-soft,warning,warning-foreground,warning-soft,danger,danger-foreground,danger-soft,border,border-secondary,separator,focus,link,field-background,field-foreground,field-placeholder,field-border,radius,field-radius;--ns-font-sans,--ns-font-mono— наборы шрифтов приложения (сами шрифты не передаются: кладите свои в пакет или используйте системные);color-schemeиdata-ns-scheme="dark"на<html>.
.panel {
background: var(--ns-surface);
color: var(--ns-surface-foreground);
border: 1px solid var(--ns-border);
border-radius: var(--ns-radius);
font-family: var(--ns-font-sans);
}
.panel--alert { background: var(--ns-danger-soft); color: var(--ns-danger); }В коде card.theme — это ThemeSnapshot (scheme, tokens, fontSans, fontMono),
поддерживается в актуальном виде; card.on('theme.changed', …) сообщает об изменении, а
applyTheme(snapshot, element) записывает переменные куда угодно (например, в shadow root).
Сегодня приложение тёмное, поэтому scheme всегда dark, — но не зашивайте это: пользуйтесь
переменными, и светлая тема просто заработает, когда появится. Руководство по оформлению —
UI-кит и оформление.
Язык
Приложение говорит по-английски, по-русски и на упрощённом китайском и переключается на лету.
card.i18n — это { language: 'en' | 'ru' | 'zh', locale } (locale — en-US, ru-RU или
zh-CN, для Intl), <html lang> следует за ним, а тексты из вашего манифеста (displayName,
подписи, описания) приложение подставляет само.
Для ваших собственных строк createTranslator следует за языком приложения на лету:
import { createTranslator } from '@neurosquad/card-sdk'
const t = createTranslator(
{
en: { title: 'Tests', failed_one: '{{count}} test failed', failed_other: '{{count}} tests failed' },
ru: {
title: 'Тесты',
failed_one: '{{count}} тест упал',
failed_few: '{{count}} теста упало',
failed_many: '{{count}} тестов упало',
failed_other: '{{count}} теста упало'
},
zh: { title: '测试', failed_other: '{{count}} 个测试失败' }
},
card
)
render(t('title'), t('failed', { count: 3 }))
t.onChange(() => render(t('title'))) // re-render on a language switch
const when = new Intl.DateTimeFormat(card.i18n.locale, { timeStyle: 'short' }).format(Date.now())
render(when)- Ключи могут быть вложенными (
{ list: { empty: '…' } }→t('list.empty'));enобязателен и служит запасным языком, а за ним — сам ключ. - Подстановки
{{name}}заполняются из второго аргумента; числа форматируются по локали. - При
countформы множественного числаkey_one,key_few,key_many,key_otherвыбираются черезIntl.PluralRules— в русском используются все четыре, в китайском только_other. t.language,t.locale,t.onChange(fn),t.setLanguage(lang),t.dispose().
Жизненный цикл и видимость
Холст на 50 карточек не может гонять 50 веб-приложений на полной скорости, поэтому приложение сообщает карточке, где она, и ставит её на паузу, когда её никто не видит.
card.visibility | Значение | Что делать |
|---|---|---|
visible | На экране, тело показано. | Работать. |
offscreen | Её воркспейс показан, но карточка вне видимой области. | Остановить анимации и опрос. |
overview | Холст отдалён: приложение показывает вашу плитку обзора вместо тела. | Остановить анимации; держать плитку актуальной. |
hidden | Её воркспейс не показан или окно скрыто. | Остановить всё, что нужно только для глаз. |
card.lifecycle.onVisibility((state) => {
if (state === 'visible') startAnimation()
else stopAnimation()
})
card.lifecycle.onSuspend(async (graceMs) => {
// The page is about to be unloaded. You have graceMs (about a second) to save.
await card.storage.set('draft', currentDraft())
})
card.lifecycle.onExpanded((expanded) => render(expanded ? 'big layout' : 'compact layout'))
card.lifecycle.onResized(({ w, h }) => render(w, h))
if (card.launch === 'resumed') render('back from a pause — state restored from storage')
declare function startAnimation(): void
declare function stopAnimation(): void
declare function currentDraft(): stringУсыпление. Карточку, чей воркспейс скрыт уже минуту, усыпляют: она получает lifecycle.suspend,
у неё есть примерно секунда (graceMs), чтобы сохраниться, потом её страница выгружается.
Приложение продолжает показывать её шапку и плитку обзора. Когда
пользователь снова на неё смотрит, страница запускается заново с card.launch === 'resumed'. Во всём
приложении одновременно работает не больше 24 страниц карточек; сверх этого усыпляются и те
карточки вне экрана или в скрытых воркспейсах, что дольше всех не показывались. Карточки с
разрешением background не усыпляются, когда скрыты (до 8 во
всём приложении).
Ленивый запуск. Страница карточки создаётся, когда карточку впервые действительно видно в показанном воркспейсе, а не при открытии воркспейса. До этого в теле заглушка, а в шапке — то, что вы задали в прошлый раз.
Что ещё нужно знать
- Инструменты и входы-запросы будят усыплённую карточку: прежде чем доставить вызов,
приложение запускает её страницу (ждёт до 10 секунд). Потоковые сообщения усыплённой карточке
отбрасываются — используйте
retainна выходах, чтобы карточка могла догнать черезports.read. - Пока холст перетаскивают или масштабируют, ваша карточка не получает событий мыши.
- Колесо мыши внутри карточки прокручивает карточку, а не холст. Горячие клавиши приложения не работают, пока фокус внутри карточки.
- Карточка, которая 15 секунд не отвечает на пульс приложения, показывается как Not responding с кнопкой Reload. SDK отвечает на пульс сам; причиной обычно бывает долгий синхронный цикл в вашем коде.
- Каждый отдельный пакет карточек — это один процесс браузера (~30–60 МБ); копии одной карточки делят его.
События не воспроизводятся заново. Пока страница карточки не работает — ещё не смонтирована,
усыплена или выгружена, — она пропускает события вроде agents.turn. При старте восстанавливайте
показанное из card.agents.list() (status, turnStartedAt и lastTurn — когда и чем закончился
последний ход) и из сохраняемых значений портов, а не рассчитывайте, что видели каждое событие.
Воркспейс
const workspace = await card.getWorkspace() // { id, name, path? }
render(workspace.name, workspace.path ?? 'no fs.read — no path')card.workspace содержит то же и поддерживается в актуальном виде. path (абсолютный) есть только
при fs.read.
Разрешения во время работы
async function copyReport(text: string): Promise<boolean> {
if (!card.permissions.has('clipboard.write')) {
// Declared with "optional": true in the manifest. Only while the card is visible.
const granted = await card.permissions.request('clipboard.write')
if (!granted.includes('clipboard.write')) return false
}
await card.copyText(text)
return true
}
card.permissions.onChange((all) => render(all.filter((p) => p.granted).map((p) => p.id)))| Член | Что это |
|---|---|
all | Все объявленные разрешения: { id, granted, optional, hosts?, reason? }. Поддерживается в актуальном виде. |
has(id) | Выдано напрямую или через включение (fs.write включает fs.read). |
list() | Свежий список из приложения. |
request(...ids) | Просит у пользователя необязательные объявленные разрешения (в диалоге приложения на всё окно). Возвращает идентификаторы, выданные сейчас. Только пока карточка видна, не чаще раза в 5 секунд; запрос того, что не объявлено как необязательное, падает с PERMISSION_DENIED. |
onChange(handler) | Выдача изменилась — пользователь выдал разрешение или отозвал его в настройках. |
Журнал карточки
card.log.info('run started', { filter: 'checkout' })
card.log.warn('slow response', 1834, 'ms')
card.log.error(new Error('parser failed'))Строки попадают в журнал пакета в приложении (card-logs, 256 КБ, по кругу) и — пока запущен
neurosquad-card dev — в ваш терминал. Аргументы склеиваются как в console.log (объекты — как
JSON, ошибки — со стеком), до 2 000 символов в строке, не больше 50 строк в секунду — сверх этого
строки отбрасываются и пишется одно предупреждение «N lines dropped». Необработанные ошибки и
отклонённые промисы автоматически пишутся как error (чтобы отключить, передайте
forwardErrors: false в connect()).