Структура листа
Восемь действий — одна операция: оболочка называет диапазон, ось и действие, а уровни и что спрятать считает ядро.

В рамке выше работает тот самый проект, чей код лежит ниже, — собранный, но не переписанный: те же файлы, только пропущенные через сборку. Считает в нём не ядро, а дублёр транспорта, поэтому числа в ячейках ничего не значат: смотреть нужно на код.
Страница, которую открывает браузер.
<!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="outline" 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>apply_outline_subtotal</code>: оболочка называет диапазон, ось и действие, а какой у дорожки уровень, что спрятать при сворачивании и сколько уровней всего — считает тот, кто стоит за швом, и присылает состоянием. Ни одного уровня на этой странице не вычисляется. </div> </header>
<section class="m-4 flex flex-col gap-3 rounded-card border border-base-300 p-4" data-outline-panel> <div class="flex flex-wrap items-end gap-2"> <label class="form-control"> <span class="label-text text-xs">Диапазон</span> <input data-outline-range value="A2:C4" class="input input-sm input-bordered w-28" /> </label> <!-- Подписи — Excel для macOS, вкладка «Данные», группа «Структура» (те же строки, что у нашей ленты: `data-groups.ts`). --> <button data-outline-group class="btn btn-sm btn-primary">Группировать</button> <button data-outline-ungroup class="btn btn-sm">Разгруппировать</button> <button data-outline-hide class="btn btn-sm">Скрыть детали</button> <button data-outline-show class="btn btn-sm">Показать детали</button> <button data-outline-auto class="btn btn-sm">Создать структуру</button> <button data-outline-clear class="btn btn-sm">Удалить структуру</button> </div>
<div data-outline-answer class="text-xs opacity-60">ещё не спрашивали</div> <div data-outline-state class="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 { OutlineAction, OutlineResult } from "@f1/facade";import { EXECUTOR, transport } from "./book.js";import { hostAsks } from "./host.js";import "./styles.css";
/** * СТРУКТУРА ЛИСТА ЧЕРЕЗ SDK — ОДНА ОПЕРАЦИЯ НА ВОСЕМЬ ДЕЙСТВИЙ. * * Показывает то, что видно только на живом вызове: у структуры НЕТ восьми * методов. Есть `FRange.outlineAsync(action, options)` и словарь действий * контракта (`group`, `ungroup`, `collapse`, `expand`, `show_level`, * `auto_outline`, `clear_outline`, `read_state`). Восемь кнопок Excel — это * восемь ЗНАЧЕНИЙ одного поля, и заводить под них восемь дорог значило бы выдать * наше членение за членение ядра. * * ЧЕГО СТРАНИЦА НЕ СЧИТАЕТ: уровней, границ групп и того, какие строки прятать. * Всё это приезжает состоянием (`state.rowLevels`), и показывается как есть. * * ЗАКОННЫЙ ВЫЗОВ МОЖЕТ НИЧЕГО НЕ ИЗМЕНИТЬ, и это не отказ: разгруппировать то, * что не сгруппировано, — законно. Слово об этом говорит ядро (`didChange`), а * не наш вывод по длине списка. */const { locale } = hostAsks();
const range = document.querySelector<HTMLInputElement>("[data-outline-range]")!;const answer = document.querySelector<HTMLElement>("[data-outline-answer]")!;const state = document.querySelector<HTMLElement>("[data-outline-state]")!;
const core = createSheetsCore({ mount: document.getElementById("app")!, transport, locale });const sheet = core.f1.getActiveWorkbook().getActiveSheet()!;
function show(said: OutlineResult, what: string): void { if (!said.ok) { answer.className = "text-xs text-error"; answer.setAttribute("data-outline-refusal", said.code ?? "refused"); answer.textContent = `${what}: отказ — ${said.reason}${said.code ? ` (${said.code})` : ""}`; return; } answer.removeAttribute("data-outline-refusal"); answer.className = "text-xs opacity-70"; answer.setAttribute("data-outline-changed", String(said.didChange)); answer.textContent = `${what} — сделано ${EXECUTOR}; лист ${said.didChange ? "изменился" : "не изменился (законный вызов, менять было нечего)"}.`;
const rows = said.state.rowLevels; state.setAttribute("data-outline-max-level", String(said.state.maxRowLevel)); state.textContent = rows.length ? `Уровни строк (счёт ЯДРА, максимум ${said.state.maxRowLevel}): ` + rows .map((track) => `${track.index + 1}→${track.level}${track.collapsed ? " свёрнута" : ""}${track.hidden ? " скрыта" : ""}`) .join(", ") : "Структуры на листе нет — ни одной дорожки с уровнем.";}
function ask(action: OutlineAction, what: string): void { const area = sheet.getRange(range.value); if (!area) { answer.className = "text-xs text-error"; answer.textContent = `${what}: «${range.value}» — не адрес диапазона`; return; } void area.outlineAsync(action, { axis: "row" }).then((said) => show(said, what));}
document.querySelector("[data-outline-group]")!.addEventListener("click", () => ask("group", "Группировать"));document.querySelector("[data-outline-ungroup]")!.addEventListener("click", () => ask("ungroup", "Разгруппировать"));document.querySelector("[data-outline-hide]")!.addEventListener("click", () => ask("collapse", "Скрыть детали"));document.querySelector("[data-outline-show]")!.addEventListener("click", () => ask("expand", "Показать детали"));document.querySelector("[data-outline-auto]")!.addEventListener("click", () => ask("auto_outline", "Создать структуру"));document.querySelector("[data-outline-clear]")!.addEventListener("click", () => ask("clear_outline", "Удалить структуру"));
/* ПЕРВЫЙ ВОПРОС — ИМЕННО ВОПРОС: `read_state` ничего не меняет, и пустой ответ на него честно означает «структуры нет». */void core.f1.loadAsync().then(() => ask("read_state", "Состояние структуры"));
import.meta.hot?.dispose(() => core.destroy());Книга примера: данные, с которыми лист открывается.
import { createTransport, transportExecutorName, transportSettingFromSearch } from "@f1/contract";
/** * КНИГА-ЗАГЛУШКА — ВМЕСТО НАСТОЯЩЕГО ЯДРА. * * Отчёт по кварталам: три строки данных и строка итога — то, что в Excel и * группируют. Ни одного уровня структуры здесь нет: уровни считает тот, кто * стоит за швом, и присылает состоянием (инвариант №1). * * ТРАНСПОРТ ВЫБИРАЕТСЯ НАСТРОЙКОЙ, А НЕ ВПАЯНА: умолчание — заглушка, * `?core=http://127.0.0.1:5507` уводит на живое ядро. Код примера не меняется. */const TABLE: readonly (readonly string[])[] = [ ["Квартал", "Регион", "Выручка"], ["I", "Север", "18 480"], ["I", "Восток", "9 110"], ["I", "Юг", "28 125"], ["Итог I", "", "55 715"],];
const setting = transportSettingFromSearch(globalThis.location?.search ?? "", { sheets: ["Лист1"], usedRange: { rowEnd: 4, 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-outline", "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.