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

> Как следовать теме и языку приложения на лету, видимость и усыпление, воркспейс, необязательные разрешения во время работы и журнал карточки.

Source: https://docs.neurosquad.ai/ru/card-sdk/api/environment

## Тема

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

```css
.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-кит и оформление](https://docs.neurosquad.ai/ru/card-sdk/styling).

## Язык

Приложение говорит по-английски, по-русски и на упрощённом китайском и переключается на лету.
`card.i18n` — это `{ language: 'en' | 'ru' | 'zh', locale }` (`locale` — `en-US`, `ru-RU` или
`zh-CN`, для `Intl`), `<html lang>` следует за ним, а тексты из вашего манифеста (`displayName`,
подписи, описания) приложение подставляет само.

Для ваших собственных строк `createTranslator` следует за языком приложения на лету:

```ts
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` | Холст отдалён: приложение показывает вашу [плитку обзора](https://docs.neurosquad.ai/ru/card-sdk/api/card-ui#overview) вместо тела. | Остановить анимации; держать плитку актуальной. |
| `hidden` | Её воркспейс не показан или окно скрыто. | Остановить всё, что нужно только для глаз. |

```ts
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`), чтобы сохраниться, потом её страница выгружается.
Приложение продолжает показывать её шапку и [плитку обзора](https://docs.neurosquad.ai/ru/card-sdk/api/card-ui#overview). Когда
пользователь снова на неё смотрит, страница запускается заново с `card.launch === 'resumed'`. Во всём
приложении одновременно работает не больше 24 страниц карточек; сверх этого усыпляются и те
карточки вне экрана или в скрытых воркспейсах, что дольше всех не показывались. Карточки с
разрешением [`background`](https://docs.neurosquad.ai/ru/card-sdk/permissions#background) не усыпляются, когда скрыты (до 8 во
всём приложении).

**Ленивый запуск.** Страница карточки создаётся, когда карточку впервые действительно видно в
показанном воркспейсе, а не при открытии воркспейса. До этого в теле заглушка, а в шапке — то, что
вы задали в прошлый раз.

**Что ещё нужно знать**

- Инструменты и входы-запросы **будят** усыплённую карточку: прежде чем доставить вызов,
приложение запускает её страницу (ждёт до 10 секунд). Потоковые сообщения усыплённой карточке
отбрасываются — используйте `retain` на выходах, чтобы карточка могла догнать через `ports.read`.
- Пока холст перетаскивают или масштабируют, ваша карточка не получает событий мыши.
- Колесо мыши внутри карточки прокручивает карточку, а не холст. Горячие клавиши приложения не
работают, пока фокус внутри карточки.
- Карточка, которая 15 секунд не отвечает на пульс приложения, показывается как **Not responding**
с кнопкой Reload. SDK отвечает на пульс сам; причиной обычно бывает долгий синхронный цикл в вашем
коде.
- Каждый отдельный пакет карточек — это один процесс браузера (~30–60 МБ); копии одной карточки
делят его.

**События не воспроизводятся заново.** Пока страница карточки не работает — ещё не смонтирована,
усыплена или выгружена, — она пропускает события вроде `agents.turn`. При старте восстанавливайте
показанное из `card.agents.list()` (`status`, `turnStartedAt` и `lastTurn` — когда и чем закончился
последний ход) и из сохраняемых значений портов, а не рассчитывайте, что видели каждое событие.

## Воркспейс

```ts
const workspace = await card.getWorkspace()  // { id, name, path? }
render(workspace.name, workspace.path ?? 'no fs.read — no path')
```

`card.workspace` содержит то же и поддерживается в актуальном виде. `path` (абсолютный) есть только
при `fs.read`.

## Разрешения во время работы

```ts
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)` | Выдача изменилась — пользователь выдал разрешение или отозвал его в настройках. |

## Журнал карточки

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