GrFormSection

Пакет: @feugene/granularityядроГруппа: Формы

Берут, когда форма длинная.

Когда брать

  • форма длинная — «Контакты», «Доставка», «Оплата»: разделы дают структуру и точки навигации;
  • раздел нужен незрячему — заголовок рендерится тегом h2h6, и форма обходится по заголовкам;
  • у раздела есть действия — «Добавить», «Сбросить» в правой части шапки;
  • раздел нуждается в пояснении — описание под заголовком, а не подсказка у каждого поля.

Когда взять другое

НужноБерите
Правила и отправкаGrForm
Обвязка одного поляGrFormField
Разделы переключаются, а не идут подрядGrTabs
Раздел сворачиваетсяGrCollapse
Нужна просто карточкаGrCard

Заголовок — настоящий

Длинную форму незрячий пользователь обходит по заголовкам, поэтому заголовок секции рендерится тегом h2h6, а не жирным текстом:

<GrFormSection title="Профиль" :heading-level="2" description="Публичные данные">
  <GrFormField label="Имя"><GrInput v-model="name" /></GrFormField>
</GrFormSection>

headingLevel по умолчанию 3 и читается из GrConfigProvider (componentDefaults.GrFormSection.headingLevel) — уровень задаётся один раз на приложение, если у формы своя структура страницы.

Лендмарк по запросу

Корень — <section>, но без имени: именованная секция становится лендмарком region, и пять секций формы дали бы пять регионов в обзоре диктора. Структуру несёт заголовок.

Там, где секция действительно крупная и к ней хочется прыгать как к региону, включите landmark — имя придёт от заголовка:

<GrFormSection landmark title="Безопасность" />

Описание связывается с секцией через aria-describedby в обоих режимах.

Слоты

СлотЧто заменяет
titleзаголовок целиком — ссылка, бейдж, счётчик
descriptionописание целиком
actionsправая часть шапки: «Добавить», «Сбросить»
defaultсодержимое секции

Шапка — одна строка: заголовок с описанием слева, действия справа; на узком экране действия переносятся под заголовок. Без заголовка, описания и действий шапка не рендерится вовсе.

Рамка и сворачивание

Пропов bordered/collapsible нет намеренно — это композиция, а не настройка:

<GrCard class="p-4">
  <GrFormSection title="Профиль" />
</GrCard>

Сворачивание даёт GrCollapse — у него тот же headingLevel, поэтому структура заголовков не ломается.

Playground 3

Загружается…

Код
<GrFormSection />

Установка

npm i @feugene/granularity

Импорт

import { GrFormSection } from '@feugene/granularity/components/GrFormSection'

API

Props

PropTypeпо умолчаниюОписание
titlestring | undefinedundefined
descriptionstring | undefinedundefined
headingLevel2 | 3 | 4 | 5 | 6 | undefinedundefinedУровень заголовка под структуру страницы. Не задан — берётся из `GrConfigProvider`, иначе `3`.
landmarkboolean | undefinedfalseОбъявить секцию лендмарком `region`. По умолчанию выключено: пять секций формы дали бы пять лендмарков и засорили обзор, а структуру и так несёт заголовок. Включать стоит там, где секция действительно крупная и к ней хочется прыгать как к региону.

Slots

SlotTypeОписание
defaultanyКонтент секции.
titleanyЗаголовок целиком — вместо строки `title`.
descriptionanyОписание целиком — вместо строки `description`.
actionsanyДействия в правой части шапки: «Добавить», «Сбросить».

Примеры 5

Уровень заголовка, действия и слоты

Заголовок секции — настоящий h2h6 (heading-level, по умолчанию h3): именно по заголовкам незрячий пользователь обходит длинную форму. Слот #title собирает заголовок разметкой, #actions кладёт кнопки в правую часть шапки. Лендмарком секция становится только по пропу landmark — иначе пять секций дали бы пять регионов в обзоре.

Участники проекта 2

Приглашения уходят на почту сразу после сохранения.

Actions
<script setup lang="ts">
import { ref } from 'vue'

import { GrBadge, GrButton, GrCard, GrFormField, GrFormSection, GrInput } from '@feugene/granularity'

const members = ref([
  { id: 1, email: '[email protected]' },
  { id: 2, email: '[email protected]' },
])

function addMember(): void {
  members.value = [...members.value, { id: Date.now(), email: '' }]
}
</script>

<template>
  <GrCard class="p-4">
    <!-- Заголовок секции — настоящий `h4`, поэтому форма обходится по заголовкам. -->
    <GrFormSection
      title="Участники проекта"
      description="Приглашения уходят на почту сразу после сохранения."
      :heading-level="4"
    >
      <template #title>
        Участники проекта
        <GrBadge tone="neutral">
          {{ members.length }}
        </GrBadge>
      </template>

      <template #actions>
        <GrButton variant="outline" size="sm" @click="addMember">
          Добавить
        </GrButton>
      </template>

      <div class="grid gap-3">
        <GrFormField v-for="(member, index) in members" :key="member.id" :label="`Участник ${index + 1}`">
          <GrInput v-model="member.email" type="email" placeholder="[email protected]" />
        </GrFormField>
      </div>
    </GrFormSection>
  </GrCard>
</template>

Заголовок раздела и поля профиля

Показываем базовую роль GrFormSection: лёгкий heading-wrapper для связанных полей и описания секции.

Team profile

Use `GrFormSection` when a group of fields needs shared title and supporting copy.

Profile
<script setup lang="ts">
import { ref } from 'vue'

import { GrFormField, GrFormSection, GrInput, GrTextarea } from '@feugene/granularity'

const teamName = ref('Platform operations')
const summary = ref('Coordinates deployments, release notes and service health updates.')
</script>

<template>
  <GrFormSection
    title="Team profile"
    description="Use `GrFormSection` when a group of fields needs shared title and supporting copy."
  >
    <div class="grid gap-4 md:grid-cols-2">
      <GrFormField label="Team name" for-id="team-name">
        <GrInput id="team-name" v-model="teamName" placeholder="Operations" />
      </GrFormField>

      <GrFormField label="Summary" for-id="team-summary" class="md:col-span-2">
        <GrTextarea id="team-summary" v-model="summary" :rows="4" />
      </GrFormField>
    </div>
  </GrFormSection>
</template>

Сгруппированные контролы внутри одной секции

Отдельный пример фиксирует composition-паттерн, где GrFormSection оборачивает и form fields, и более свободные control groups.

Notification routing

Section wrappers keep longer forms readable when fields are grouped by intent.
Include business stakeholders in the launch message
Require manual approval before notifications are sent

Nested Groups
<script setup lang="ts">
import { ref } from 'vue'

import { GrCheckbox, GrFormField, GrFormSection, GrInput } from '@feugene/granularity'

const channel = ref('release-updates')
const includeStakeholders = ref(true)
const requireApproval = ref(false)
</script>

<template>
  <GrFormSection
    title="Notification routing"
    description="Section wrappers keep longer forms readable when fields are grouped by intent."
  >
    <div class="grid gap-4">
      <GrFormField label="Slack channel" for-id="notify-channel">
        <GrInput id="notify-channel" v-model="channel" placeholder="release-updates" />
      </GrFormField>

      <div class="grid gap-3 rounded-2xl border border-[var(--gr-brd)] bg-[var(--gr-card)] p-4">
        <GrCheckbox v-model="includeStakeholders">
          Include business stakeholders in the launch message
        </GrCheckbox>
        <GrCheckbox v-model="requireApproval">
          Require manual approval before notifications are sent
        </GrCheckbox>
      </div>
    </div>
  </GrFormSection>
</template>

Несколько секций подряд

Такой сценарий показывает, что несколько GrFormSection подряд могут собирать skeleton полноценной settings-страницы без тяжёлой layout-обвязки.

Ownership

Operational assets

Multiple sections can be stacked to create a light-weight form page skeleton.
Owner assignedRunbook linked

Stacked Flow
<script setup lang="ts">
import { ref } from 'vue'

import { GrBadge, GrFormField, GrFormSection, GrInput } from '@feugene/granularity'

const owner = ref('[email protected]')
const runbook = ref('docs/runbooks/incident-handoff')
</script>

<template>
  <div class="grid gap-6">
    <GrFormSection title="Ownership">
      <GrFormField label="Primary owner" for-id="incident-owner">
        <GrInput id="incident-owner" v-model="owner" placeholder="[email protected]" />
      </GrFormField>
    </GrFormSection>

    <GrFormSection title="Operational assets" description="Multiple sections can be stacked to create a light-weight form page skeleton.">
      <div class="grid gap-4">
        <GrFormField label="Runbook path" for-id="runbook-path">
          <GrInput id="runbook-path" v-model="runbook" placeholder="docs/runbooks/..." />
        </GrFormField>

        <div class="flex flex-wrap gap-2">
          <GrBadge tone="success" radius="round">Owner assigned</GrBadge>
          <GrBadge tone="info" radius="round">Runbook linked</GrBadge>
        </div>
      </div>
    </GrFormSection>
  </div>
</template>

Секции с рамкой и скруглением

У GrFormSection один корневой <section>, поэтому class проходит на него насквозь (fallthrough) и мержится с внутренним grid gap-4. Так секцию легко превратить в карточку — прямоугольная рамка (border) или скруглённая (rounded-2xl border) задаётся обычными utility-классами, без пропсов.

Bordered section

Border via utility classes — GrFormSection forwards `class` to its root <section>.

Rounded-border section

Same wrapper with rounded corners and a card surface.

Bordered
<script setup lang="ts">
import { ref } from 'vue'

import { GrFormField, GrFormSection, GrInput, GrTextarea } from '@feugene/granularity'

const projectName = ref('Granularity')
const apiToken = ref('')
const notes = ref('Rotate the token every 90 days.')
</script>

<template>
  <div class="grid gap-6">
    <!-- Прямоугольная рамка: класс проходит на корневой <section> секции. -->
    <GrFormSection
      title="Bordered section"
      description="Border via utility classes — GrFormSection forwards `class` to its root <section>."
      class="border border-[var(--gr-brd)] p-5"
    >
      <GrFormField label="Project name" for-id="bordered-project">
        <GrInput id="bordered-project" v-model="projectName" placeholder="Acme" />
      </GrFormField>
    </GrFormSection>

    <!-- Скруглённая рамка: те же классы + rounded-2xl и мягкая подложка. -->
    <GrFormSection
      title="Rounded-border section"
      description="Same wrapper with rounded corners and a card surface."
      class="rounded-2xl border border-[var(--gr-brd)] bg-[var(--gr-card)] p-5"
    >
      <div class="grid gap-4">
        <GrFormField label="API token" for-id="rounded-token">
          <GrInput id="rounded-token" v-model="apiToken" placeholder="sk_live_…" />
        </GrFormField>

        <GrFormField label="Notes" for-id="rounded-notes">
          <GrTextarea id="rounded-notes" v-model="notes" :rows="3" />
        </GrFormField>
      </div>
    </GrFormSection>
  </div>
</template>

Внутренний вертикальный ритм секции (grid gap-4) сохраняется — добавляемые классы только расширяют оформление (рамка, паддинги, фон, скругление).

Документация компонентаВсе компоненты