Формат: переопределения вложенных инстансов (references/instance-overrides.md)

Источник: ds-docs/src/skills/astra-figma-to-md/references/instance-overrides.md — справочник скилла astra-figma-to-md

Как записывать то, что компонент меняет у чужого инстанса внутри своей структуры. Порядок разбора, без которого этот формат не работает, — в documentation-order.md.

Задача

Basic List Item вставляет Divider. У Divider по умолчанию ширина FIXED 190, а внутри строки списка он стоит FILL и растянут STRETCH. Это не свойство Divider и не свойство Basic List Item — это свойство их стыка, и живёт оно только в карточке родителя.

Сейчас такие вещи теряются. references/document-format.md уже говорит «если документируемый компонент что-то у вложенного переопределяет — пишется только переопределение», но не задаёт ни списка, что проверять, ни формы записи. Из-за этого результат зависит от исполнителя, а check-card.js не может проверить, что проверку вообще делали.

Два формата, а не один

Сборка — машинный JSON, который отдаёт Figma. Один вызов на компонент, состав полей задан жёстко. Это то, что нельзя собрать «на глаз».

Запись — то, что попадает в markdown-карточку. Короче сборки: из неё выкинуто всё, что совпадает с умолчанием, и всё, что является следствием, а не решением.


1. Сборка: что читать из Figma

get_design_context дельты не отдаёт — он рендерит инстанс уже применённым, и отличить «здесь так решили» от «так у компонента по умолчанию» по его выводу нельзя. Дельта берётся только через use_figma.

// Дельты прямых инстансов внутри базового варианта мастера.
// PAGE_ID и MASTER_NAME подставить из figma-navigation-mcp.md.
const p = await figma.getNodeByIdAsync(PAGE_ID);
await p.loadAsync();
const m = p.findAllWithCriteria({ types: ["COMPONENT_SET", "COMPONENT"] })
           .find(n => n.name === MASTER_NAME);
const base = m.type === "COMPONENT_SET" ? m.children[0] : m;

// прямые инстансы: внутрь чужого инстанса не спускаемся — его содержимое принадлежит владельцу
const stack = [...base.children], insts = [];
while (stack.length) {
  const n = stack.pop();
  if (n.type === "INSTANCE") { insts.push(n); continue; }
  if (n.children) stack.push(...n.children);
}

const out = [];
for (const inst of insts) {
  const mc = await inst.getMainComponentAsync();
  if (!mc) continue;
  const owner = mc.parent && mc.parent.type === "COMPONENT_SET" ? mc.parent : mc;
  const props = {};
  for (const [k, v] of Object.entries(inst.componentProperties || {})) props[k.split("#")[0]] = v.value;
  out.push({
    layer: inst.name,
    component: owner.name,
    variant: mc.name,
    props,
    sizing:     inst.layoutSizingHorizontal + "/" + inst.layoutSizingVertical,
    mainSizing: mc.layoutSizingHorizontal   + "/" + mc.layoutSizingVertical,
    size:     Math.round(inst.width) + "x" + Math.round(inst.height),
    mainSize: Math.round(mc.width)   + "x" + Math.round(mc.height),
    align: inst.layoutAlign, grow: inst.layoutGrow,
    positioning: inst.layoutPositioning,
    visible: inst.visible, opacity: inst.opacity,
    boundVars: Object.keys(inst.boundVariables || {}),
    fields: [...new Set((inst.overrides || []).flatMap(o => o.overriddenFields))],
  });
}
return out;

Ключ ко всему — instance.overrides. Figma сама помечает, какие поля разошлись с мастером, и возвращает их именами: boundVariables, explicitVariableModes, fills, height, width, layoutPositioning, name, characters, componentProperties, strokeTopWeight и так далее. Это единственный источник, где «изменено» отделено от «унаследовано».

Один вызов на компонент. В бюджет чтения MCP добавляется одной строкой.

2. Словарь: что считается переопределением

Проверяется весь список, в этом порядке. Пункт, по которому расхождения нет, в запись не идёт — но проверен должен быть каждый.

1Вариантprops (оси вариантов)Icon Button внутри Navigation Bar запиннен в Size=Small, Type=Ghost
2Булев слотprops вида Show *SYSTEM Status Bar внутри Navigation Bar: Show Background = false
3Swapprops вида Swap *✱ Icon Container внутри Button: подставлена конкретная иконка
4Раскладкаsizing ≠ mainSizing, align, growDivider внутри Basic List Item: FIXED/HUG → FILL/HUG, STRETCH
5Потокpositioning⚒︎ State Layer внутри Basic List Item вынут в ABSOLUTE
6Размерfields: width, height, min*, max*Checkbox внутри Basic List Item: зафиксированы min/max по обеим осям
7Токен и режимfields: boundVariables, explicitVariableModes, fillsIcon Button внутри Navigation Bar: переключён режим цвета под тёмную подложку
8Рамкаfields: stroke*Weight✱ Icon Container внутри Button
9Видимостьvisible, opacity—
10Имя слояfields: nameIcon Button внутри Navigation Bar переименован в Icon Button Back

Что переопределением не считается

  • Текст-образец. characters, styledTextSegments, значения строковых свойств вроде Title = "Заголовок". Это наполнение примера, а не решение о компоненте.
  • Демо-иконка, если слот всё равно заменяемый снаружи. Пишется факт «слот заполнен по умолчанию», а не имя конкретной иконки.
  • Следствия FILL. Если ширина стала FILL, то width: 375 — это результат, а не решение. Пишется FILL, число не пишется.
  • Расхождение размера мастера и инстанса без флага в fields. Мастер варианта может стоять в сетке других габаритов; пока Figma не пометила width/height как overridden, это не дельта.

3. Запись: как это выглядит в карточке

Дельта живёт в карточке родителя, в узле «Анатомии» того самого вложенного компонента — там, где он лежит в дереве. Отдельного раздела не заводится: сводная таблица переопределений оторвана от места, где они действуют, и первой же расходится с макетом.

Узел вложенного компонента несёт строго эти строки:

<details>
<summary><code>Divider</code> — отбивка строки</summary>

- **Роль:** отделяет строку от следующей
- **Свойства здесь:** `Сторона = Both` · `Толщина = 3xs` · `Отступ = Off`
- **Переопределено:** `width`: 190 FIXED → 100% FILL · `align-self`: INHERIT → STRETCH

</details>

Ключи — те же, что во всей «Анатомии»; отдельных Компонент и Вариант не заводится: ссылка на карточку стоит в <summary> узла, а значения осей идут в Свойства здесь.

Правила обеих строк:

  • Свойства здесь — пропсы: оси вариантов, булевы, свапы. Проверяются все, а не только переключённые: ось в умолчании тоже называется, иначе непонятно, проверяли её или пропустили;
  • Переопределено — то, что правлено поверх компонента: раскладка, поток, размер, токены, рамка, видимость, имя слоя. По словарю выше, сверху вниз;
  • формат элемента — два яруса. Значение совпало с умолчанием — Имя = Значение. Значение изменено — Имя: умолчание → здесь. Стрелка означает ровно одно: здесь принято решение, отличное от компонента;
  • разделитель между элементами — ·;
  • имена свойств — как в CSS, теми же словами, что в остальной «Анатомии» (width, align-self, position, background), кроме тех, у которых CSS-эквивалента нет (Swap, constraints) — эти остаются фигмовскими;
  • пустота записывается словом. свойств нет · всё по умолчанию · совпадает с умолчанием · не проверено. Отсутствие строки и не проверено означают разное: первое читается как «забыли», второе — как честно отмеченный пробел.

Когда дельт больше четырёх, строка заменяется таблицей — иначе она не читается:

- **Свойства здесь:** `Size`: `Medium` → `Small` · `Type`: `Primary` → `Ghost`
- **Переопределено:**

| Свойство | У компонента | Здесь | Зачем |
|---|---|---|---|
| `background` | `--color-surface-primary` | `--color-surface-on-color` | шапка лежит на тёмной подложке |
| `color` | `--color-icon-primary` | `--color-icon-on-color` | то же |
| `name` | `Icon Button` | `Icon Button Back` | слой отвечает за возврат |

Колонка «Зачем» заполняется только тогда, когда причина прочитана — из аннотации автора или из очевидной механики (растяжение по ширине родителя). Догадка сюда не идёт: пустая клетка честнее выдуманной причины.

4. Когда вложенный компонент ещё не разобран

Такого быть не должно — на то и порядок. Но если карточки ребёнка нет, а родителя разбирают:

  1. дельту всё равно собрать и записать — значения «у компонента» берутся прямо из мастера;
  2. ссылку в строке **Компонент:** поставить на секцию Figma, а не на карточку;
  3. в audit.md завести находку класса GAP: «родитель разобран раньше ребёнка».

5. Что добавить в проверку

check-card.js может стеречь формат механически:

  • у каждого узла-инстанса ◆ обязаны быть обе строки — Свойства здесь и Переопределено;
  • значение — либо служебное слово из закрытого списка (свойств нет, всё по умолчанию, совпадает с умолчанием, не проверено), либо перечень элементов через ·;
  • элемент имеет вид Имя = Значение (умолчание) или Имя: было → стало (решение);
  • имена свойств — из того же словаря, что и остальная «Анатомия»;
  • токены в дельте существуют в foundations/tokens/*.json — та же проверка, что и везде.

Включать проверку сразу нельзя. На 27.08.2026 в базе 583 узла-инстанса: у 133 нет ни одной из строк, ни один не записан ярусами. Проверка ставится после переразбора, иначе она заблокирует отправку всей документации.

Оговорки

  • Формат покрывает прямые инстансы внутри мастера. Что переопределено на два уровня вглубь — забота владельца того уровня; иначе карточки начнут дублировать друг друга и разъезжаться.
  • Дельты снимаются с базового варианта. Если варианты родителя переопределяют ребёнка по-разному, различие идёт в раздел «Варианты» — там, где уже описываются отличия вариантов.
  • Все примеры в этом файле прочитаны из макета «Astra DS (Copy)», а не придуманы.