Skip to Content
Card SDKСправочник APIТема, язык и жизненный цикл

Тема, язык и жизненный цикл

Тема

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()).