anc
Интерфейс по руководству «Стиль Skillok»; одни часы записи на весь экран
По skillok/skillok-develop design/SKILLOK_STYLE.md:
- кнопки и поля — --radius-control 2px; поля — --field-bg, 9/11, 13px,
фокус терракотой внутрь; неактивная кнопка — 0,5; .ui-danger; мишени
44px под пальцем;
- подписи полей — 12px капс с разрядкой; карточки без заливки,
отступы 14–16px; вкладки 32px без жирного; моноширинный — только код,
числа — tabular-nums; тень — только у сообщения;
- значки — icons.ts (SVG 16, контур 1,5) вместо «★ ▶ ❚❚ ✕»;
- обращение на «ты» в интерфейсе и сообщениях ядра; сняты пояснения
«как пользоваться»; блок проверки звука — .audio-check (не пересекается
с галочкой .check).
Светлая тема остаётся единственной — решение владельца для Meet
(руководство просит обе темы).
Шапка и меню показывают время по записанному звуку, как панель записи.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
955f445
4 октября 2026 г.
new.ts
// Экран «Новая запись»: «Записать встречу» ведёт сюда, а не запускает запись
// сразу (решение владельца 01.10.2026). Здесь проверка звука — две живые
// полоски, микрофон и звук компьютера, — и совет про наушники; запись
// начинается только кнопкой «Начать запись».
//
// Проверка — главная защита от встречи, которая молча не записалась: что
// звука собеседников нет, человек видит до встречи, а не после.
import { api, events, type CheckView } from "../api";
import { esc } from "../dom";
import type { Scope } from "../scope";
import { bindPermissionHelp, permissionHelp } from "../recording";
import { startRecording, state } from "../state";
export async function renderNew(root: HTMLElement, scope: Scope): Promise {
// Запись уже идёт — вторую не начать, показываем идущую.
if (state.recording) {
location.replace(`#/m/${encodeURIComponent(state.recording.meeting_id)}`);
return;
}
const view = await api.getSettings();
if (!scope.alive) return;
if (!view.capture_supported) {
root.innerHTML = `
Новая запись
На этой системе запись не поддерживается. Добавь готовый аудиофайл — перетащи его в окно или выбери в меню.
`;
return;
}
root.innerHTML = `
Новая запись
Проверка звука
${checkRow("me", "Микрофон", "скажи что-нибудь")}
${checkRow("others", "Звук компьютера", "включи любой звук — видео или музыку")}
Лучше в наушниках — так голоса собеседников не попадут в микрофон второй раз.
${
view.openrouter_key
? ""
: `Записать можно и без ИИ. Чтобы потом расшифровать, подключи его в настройках.`
}
Начать запись
`;
// Проверка звука: уровни приходят, пока открыт экран; уходим — устройства
// закрываются. «Начать запись» ядро гасит проверку само.
let check: CheckView | null = null;
try {
check = await api.audioCheckStart();
} catch (e) {
check = { mic_error: String(e), system_error: String(e), os: "" };
}
if (!scope.alive) {
void api.audioCheckStop();
return;
}
showError(root, "me", check.mic_error);
showError(root, "others", check.system_error);
const heard: Record = {};
scope.on(
events.recorder(({ meeting_id, event }) => {
if (meeting_id || event.kind !== "level") return;
const key = event.data.speaker.kind === "me" ? "me" : "others";
const row = root.querySelector(`[data-check-row="${key}"]`);
if (!row || row.dataset.error) return;
const fill = row.querySelector(".meter-fill");
if (fill) fill.style.width = `${Math.max(0, Math.min(100, ((event.data.dbfs + 60) / 55) * 100))}%`;
if (event.data.dbfs > -45) heard[key] = performance.now();
const ok = performance.now() - (heard[key] ?? -1e9) < 3_000;
row.classList.toggle("is-ok", ok);
row.querySelector("[data-check-state]")!.textContent = ok ? "слышно" : "тишина";
}),
);
scope.on(Promise.resolve(() => void api.audioCheckStop()));
root.querySelector("[data-start]")!.addEventListener("click", async (e) => {
const b = e.currentTarget as HTMLButtonElement;
b.disabled = true;
b.textContent = "Начинаю…";
await startRecording();
// Не началась — ошибку уже показало сообщение внизу, кнопку возвращаем.
if (scope.alive && !state.recording) {
b.disabled = false;
b.textContent = "Начать запись";
}
});
}
function checkRow(key: "me" | "others", title: string, hint: string): string {
return `
${title}
тишина
${esc(hint)}
`;
}
/** Устройство не открылось — вместо полоски что делать и кнопка в настройки. */
function showError(root: HTMLElement, key: "me" | "others", error: string | null): void {
if (!error) return;
const row = root.querySelector(`[data-check-row="${key}"]`);
if (!row) return;
row.dataset.error = "1";
row.classList.add("is-error");
row.querySelector("[data-check-state]")!.textContent = "не пишется";
const help = permissionHelp(key);
const hint = row.querySelector("[data-check-hint]")!;
hint.innerHTML = help || esc(error);
// Текст ошибки системы — мелко, для того, кто будет разбираться.
if (help) hint.insertAdjacentHTML("beforeend", `${esc(error)}`);
bindPermissionHelp(row);
}