Примечания
Шесть действий примечаний одной операцией контракта — перечислить, создать, удалить, ответить, отменить, повторить; текст, номер и нормализованный адрес называет ядро.

В рамке выше работает тот самый проект, чей код лежит ниже, — собранный, но не переписанный: те же файлы, только пропущенные через сборку. Считает в нём не ядро, а дублёр транспорта, поэтому числа в ячейках ничего не значат: смотреть нужно на код.
Страница, которую открывает браузер.
<!doctype html><!-- Примечания — НАСТОЯЩИЙ МАЛЕНЬКИЙ ПРОЕКТ.
Не «страница витрины», а тот проект, который встраивающий копирует к себе целиком. Галерея примеров ЗАПУСКАЕТ эти файлы и ПОКАЗЫВАЕТ их же.--><html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Примечания — @f1</title> </head> <body> <div data-example="annotations" class="flex h-screen w-screen flex-col bg-base-100 text-base-content"> <header class="border-b border-base-300 px-4 py-2"> <div class="text-sm font-semibold">Примечания</div> <div class="text-xs opacity-70"> Шесть пунктов вкладки «Рецензирование» — ОДНА операция контракта <code>review_annotations_lifecycle</code> и поле <code>intent</code>, которым они различаются. Текст, номер записи, автора, нормализованный адрес и время правки называет тот, кто стоит за швом: оболочка не сочиняет ничего и примечаний не хранит. До первого вопроса список пуст — это «не спрашивали», а не «их нет». </div> </header>
<section class="m-4 flex flex-col gap-3 rounded-card border border-base-300 p-4" data-notes-panel> <div class="flex flex-wrap items-end gap-2"> <label class="form-control"> <span class="label-text text-xs">Ячейка</span> <input data-notes-cell value="b3" class="input input-sm input-bordered w-24" /> </label> <label class="form-control grow"> <span class="label-text text-xs">Текст примечания</span> <input data-notes-text value="Курс уточнить у поставщика" class="input input-sm input-bordered w-full" /> </label> <!-- Подписи кнопок — Excel для macOS, вкладка «Рецензирование», группа «Примечания» (те же строки, что у нашей ленты: `review-groups.ts`). --> <button data-notes-new class="btn btn-sm btn-primary">Создать примечание</button> <button data-notes-delete class="btn btn-sm">Удалить</button> <button data-notes-show-hide class="btn btn-sm">Показать или скрыть примечание</button> <button data-notes-show-all class="btn btn-sm">Показать все примечания</button> </div>
<!-- Расписка о последнем ответе: чей он и что в нём. До первого вопроса здесь честное «ещё не спрашивали», а не пустой список, притворившийся ответом. --> <div data-notes-answer class="text-xs opacity-60">ещё не спрашивали</div> <div data-notes-list class="flex flex-col gap-1 text-xs"></div> </section>
<div id="app" class="flex min-h-0 flex-1 flex-col"></div> </div>
<script type="module" src="./src/main.ts"></script> </body></html>Точка входа: здесь лист создаётся и сюда же приходят события.
import { createSheetsCore } from "@f1/preset-sheets-core";import type { AnnotationsResult, SheetAnnotation } from "@f1/facade";import { EXECUTOR, transport } from "./book.js";import { hostAsks } from "./host.js";import "./styles.css";
/** * ПРИМЕЧАНИЯ ЧЕРЕЗ SDK — ШЕСТЬ ПУНКТОВ EXCEL И ОДНА ОПЕРАЦИЯ КОНТРАКТА. * * Показывает то, что при первом знакомстве обычно понимают неправильно: у * примечаний НЕТ шести методов контракта. Есть одна операция * (`review_annotations_lifecycle`) и намерение внутри неё, а разными их делают * подписи Excel: у операции шесть действий — перечислить, создать или * изменить, удалить, ответить, отменить, повторить, — и под каждой кнопкой * этой страницы лежит один и тот же путь, меняется только намерение. * * ЧЕГО СТРАНИЦА НЕ ДЕЛАЕТ. Она не хранит примечаний и не сочиняет их полей: * номер записи, автора, время правки и НОРМАЛИЗОВАННЫЙ адрес («b3» → «B3») * присылает тот, кто стоит за швом. Список ниже — это последний его ответ, а не * наша память: инвариант №1 звучит именно так, а не «мы стараемся». * * ЖЁЛТУЮ КОРОБКУ У ЯЧЕЙКИ РИСУЕТ ЛИСТ, и в этом примере её на листе не * появляется: слой примечаний принадлежит сетке, а его срез ещё не сделан. * Обещать её здесь картинкой было бы fake-green; страница показывает ровно то, * что уже есть, — ответ ядра целиком. */const { locale } = hostAsks();
const cell = document.querySelector<HTMLInputElement>("[data-notes-cell]")!;const text = document.querySelector<HTMLInputElement>("[data-notes-text]")!;const answer = document.querySelector<HTMLElement>("[data-notes-answer]")!;const list = document.querySelector<HTMLElement>("[data-notes-list]")!;
const core = createSheetsCore({ mount: document.getElementById("app")!, transport, locale });const sheet = core.f1.getActiveWorkbook().getActiveSheet()!;
let current: SheetAnnotation | null = null;
/** * ОДНА РАСПИСКА НА ВСЕ ШЕСТЬ КНОПОК: кто ответил, что он сделал и что теперь * знает. Отказ — тоже ответ, и он показывается словами и кодом ЯДРА. */function show(said: AnnotationsResult, what: string): void { if (!said.ok) { answer.className = "text-xs text-error"; answer.setAttribute("data-notes-refusal", said.code ?? "refused"); answer.textContent = `${what}: отказ — ${said.reason}${said.code ? ` (${said.code})` : ""}`; return; } answer.removeAttribute("data-notes-refusal"); answer.className = "text-xs opacity-70"; answer.setAttribute("data-notes-status", said.status); answer.textContent = `${what} — сделано ${EXECUTOR}; ответ «${said.status}», записей ${said.annotations.length}.`;
current = said.annotations.at(-1) ?? null; list.replaceChildren( ...said.annotations.map((note) => { const row = document.createElement("div"); row.setAttribute("data-notes-item", note.cellRef); row.className = "rounded border border-base-300 px-2 py-1"; /* Поля перечислены поимённо и без домысла: показывается то, что прислало ядро, — включая вид записи и видимость, у которой смысл есть только у заметки (у обсуждения её нет вовсе). */ row.textContent = `${note.cellRef} · ${note.author} · «${note.text}» · ${note.kind}` + `${note.visible === undefined ? "" : note.visible ? " · видна" : " · скрыта"}`; return row; }), );}
document.querySelector("[data-notes-new]")!.addEventListener("click", () => { void sheet .upsertAnnotationAsync({ cellRef: cell.value, text: text.value, /* Автора называет ХОЗЯИН страницы: личность приходит от него, а не выдумывается в TypeScript (то же правило, что у присутствия). */ author: "Мира", visible: true, }) .then((said) => show(said, "Создать примечание"));});
document.querySelector("[data-notes-delete]")!.addEventListener("click", () => { if (!current) { answer.className = "text-xs opacity-70"; answer.textContent = "Удалить нечего: сначала создайте примечание."; return; } void sheet.deleteAnnotationAsync(current.annotationId).then((said) => show(said, "Удалить"));});
document.querySelector("[data-notes-show-hide]")!.addEventListener("click", () => { if (!current) { answer.className = "text-xs opacity-70"; answer.textContent = "Показывать нечего: сначала создайте примечание."; return; } void sheet .setNoteVisibilityAsync(current.annotationId, current.visible !== true) .then((said) => show(said, "Показать или скрыть примечание"));});
document.querySelector("[data-notes-show-all]")!.addEventListener("click", () => { void sheet.showAllNotesAsync(true).then((said) => show(said, "Показать все примечания"));});
/* ПЕРВЫЙ ВОПРОС ЗАДАЁТСЯ САМ, и он именно ВОПРОС: `list` ничего не меняет. Пустой ответ на него — честное «примечаний нет», а не «мы не спрашивали». */void core.f1.loadAsync().then(() => sheet.listAnnotationsAsync().then((said) => show(said, "Список примечаний")));
import.meta.hot?.dispose(() => core.destroy());Книга примера: данные, с которыми лист открывается.
import { createTransport, transportExecutorName, transportSettingFromSearch } from "@f1/contract";
const TABLE: readonly (readonly string[])[] = [ ["Поставщик", "Курс", "Сумма"], ["Северный", "92,40", "18 480"], ["Восточный", "91,10", "9 110"], ["Южный", "93,75", "28 125"],];
/** * КНИГА-ЗАГЛУШКА — ВМЕСТО НАСТОЯЩЕГО ЯДРА. * * Транспорт контракта — единственное место, где лист разговаривает с ядром * (инвариант №1). В продукте здесь стоит `wasm` или мост хоста, и меняется * РОВНО эта строка. * * ТРАНСПОРТ ВЫБИРАЕТСЯ НАСТРОЙКОЙ, А НЕ ВПАЯНА. Умолчание — заглушка; * `?core=http://127.0.0.1:5507` уводит на живое ядро через мост `@f1/core-host`. * Ни одна строка примера от этого не меняется: примечания живут на операции * `review_annotations_lifecycle`, и её исполняет тот, кто стоит за швом. */const setting = transportSettingFromSearch(globalThis.location?.search ?? "", { sheets: ["Лист1"], usedRange: { rowEnd: 3, colEnd: 2 }, cellAt: (row, col) => TABLE[row]?.[col],});
export const transport = createTransport(setting);
/** * КТО НА САМОМ ДЕЛЕ ХРАНИТ ТЕКСТ ПРИМЕЧАНИЯ — и почему это написано рядом с * транспортом. * * Расписка «Сделано ядром» там, где отвечает заглушка, — тот же fake-green, ради * запрета которого заведён инвариант №3, только в словах, а не в кнопке. Имя * исполнителя приходит от САМОГО транспорта: заменят его — правка на соседней * строке, а не в другом конце файла, и подпись не соврёт на живом ядре. */export const EXECUTOR = transportExecutorName(setting);Сторона хозяина страницы: что приложение отвечает листу и что от него получает.
import { applyLocaleAttributes, resolveLocale, setLocale, type Locale } from "@f1/i18n";
/** * ЧТО ХОЗЯИН СТРАНИЦЫ ДАЁТ ЛИСТУ — тема и язык. * * У встраивающего это уже есть своё: тему он знает из настроек, язык — из * профиля пользователя. Здесь и то и другое читается из адреса * (`?theme=dark&locale=ar`) ровно затем, чтобы на любое состояние можно было * дать ссылку и чтобы его проверял обход: настройка, доступная только щелчком, * недоступна ни ссылке, ни автоматической проверке. * * ПРИЗНАКИ СТАВЯТСЯ НА КОРЕНЬ ДОКУМЕНТА — потому что это делает ХОЗЯИН, а не * лист. Лист читает тему оттуда, где стоит (`:host-context`), а язык получает * СВОЙСТВОМ сборки: два листа на одной странице вправе говорить на разном. * * НАПРАВЛЕНИЕ ПИСЬМА — ИНТЕРФЕЙСА, А НЕ ДОКУМЕНТА. `dir="rtl"` зеркалит хром * страницы; «Лист справа налево» из Excel — свойство КНИГИ, и сетка от * арабского языка интерфейса не переворачивается. Это не пропуск, а решение * (`locale.ts`). */export function hostAsks(): { readonly theme: "light" | "dark"; readonly locale: Locale } { const params = new URLSearchParams(location.search); const theme = params.get("theme") === "dark" ? "dark" : "light"; const locale = resolveLocale(params.get("locale") ?? undefined, "ru");
document.documentElement.setAttribute("data-theme", theme); /* Язык — один на процесс: словарь модульный, и ставит его хозяин страницы. */ setLocale(locale); applyLocaleAttributes(document.documentElement, locale);
return { theme, locale };}Оформление страницы вокруг листа.
/* ЛИСТ СТИЛЕЙ НАБОРА КОМПОНЕНТОВ — ЕДИНСТВЕННАЯ СТРОКА, КОТОРУЮ НЕЛЬЗЯ ЗАБЫТЬ. Собственное оформление лист приносит в теневые корни сам, но утилиты раскладки, которыми он держит свои органы, компилируются ЭТИМ листом. Без него страница не просто выглядит иначе — вкладка ЗАВИСАЕТ (бесконечный цикл замера у сетки без размеров). У потребителя вне репозитория это `@fastoffices/formula-kit` из его node_modules. */@import "@fastoffices/formula-kit"; /* см. примечание выше */html,body { height: 100%; margin: 0;}Из каких пакетов собран пример.
{ "name": "f1-example-annotations", "private": true, "type": "module", "description": "Примечания: шесть пунктов «Рецензирования» — одна операция контракта, а текст хранит ядро.", "scripts": { "dev": "vite", "build": "vite build" }, "dependencies": { "@f1/preset-sheets-core": "*", "@f1/facade": "*", "@f1/contract": "*", "@f1/i18n": "*" }, "devDependencies": { "vite": "^7.0.0" }}- Остальные примеры — Примеры F1.
- Правила, на которые ссылается код, — пять правил.
- Из чего собирается лист — Архитектура.
- Что можно вызвать сегодня — справочник Compute Contract.