Перейти к содержимому

Интерфейс

FAQ аккордеон

Аккордеон часто задаваемых вопросов с плавным раскрытием и поддержкой кастомных вопросов.


FAQ аккордеон

Аккордеон для отображения часто задаваемых вопросов с плавным раскрытием через slide-переход. Поддерживает одновременно открытые несколько элементов. Передаётся кастомный список вопросов через пропс items.

Превью

<script lang="ts">
	import { cn } from '$lib/utils/cn';
	import { slide } from 'svelte/transition';
	import Add from 'carbon-icons-svelte/lib/Add.svelte';

	type FaqItem = {
		question: string;
		answer: string;
	};

	type Props = {
		class?: string;
		items?: FaqItem[];
	};

	const defaultItems: FaqItem[] = [
		{
			question: 'Что это за продукт?',
			answer:
				'Это минималистичный фреймворк для разработки быстрых визуальных эффектов. Он даёт чёткий путь от одного эффекта до полноценного визуального слоя в вашем приложении.'
		},
		{
			question: 'Для кого он предназначен?',
			answer:
				'Он создан для разработчиков, которые хотят современные высокопроизводительные визуальные эффекты без необходимости строить рендер-стек с нуля.'
		},
		{
			question: 'Нужны ли специальные знания для начала работы?',
			answer:
				'Нет. Вы можете начать с минимального шейдера и быстро итерировать. По мере роста проекта вы получаете полный контроль над юниформами, текстурами и постобработкой.'
		},
		{
			question: 'Насколько быстро можно что-то выпустить?',
			answer:
				'Обычно первый визуальный эффект можно запустить за несколько минут: установить, определить материал, смонтировать компонент, а затем настроить в плейграунде.'
		},
		{
			question: 'Подходит ли это для продакшена?',
			answer:
				'Да. Продукт спроектирован для предсказуемого поведения, явного управления рантаймом и чёткой диагностики, чтобы команды могли уверенно переходить от прототипа к продакшену.'
		},
		{
			question: 'Это полноценный 3D-движок?',
			answer:
				'Нет. Продукт фокусируется на полноэкранных фрагментных рабочих процессах и пайплайнах постобработки. Для полных сцен и 3D-инструментария используйте специализированный 3D-движок.'
		},
		{
			question: 'С чего лучше начать?',
			answer:
				'Начните с раздела "Быстрый старт" для мгновенной обратной связи, затем следуйте руководству "Начало работы", чтобы перейти к реальному коду приложения и продакшен-паттернам.'
		}
	];

	let { class: className = '', items = defaultItems }: Props = $props();

	let openItems = $state<Record<string, boolean>>({});

	const isOpen = (question: string): boolean => openItems[question] ?? false;

	function toggle(question: string) {
		openItems[question] = !isOpen(question);
	}
</script>

<div class={cn('inset-shadow w-full overflow-hidden rounded-xl bg-background-inset p-2', className)}>
	<div class="grid gap-2">
		{#each items as item, index (item.question)}
			<div class="inset-shadow relative overflow-hidden rounded-lg bg-background-inset p-1.5">
				<article class="rounded-md bg-background card">
					<button
						type="button"
						id={`faq-trigger-${index.toString()}`}
						class="flex w-full cursor-pointer items-center justify-between gap-4 px-4 py-3 text-left text-base text-foreground sm:px-6"
						aria-expanded={isOpen(item.question)}
						aria-controls={`faq-panel-${index.toString()}`}
						onclick={() => { toggle(item.question); }}
					>
						<span class="font-medium tracking-tight">{item.question}</span>
						<span
							aria-hidden="true"
							class="inset-shadow inline-flex shrink-0 rounded-sm bg-background-inset p-1.5 text-foreground-muted"
						>
							<div
								class:rotate-45={isOpen(item.question)}
								class="transition-transform duration-150"
							>
								<Add size={24} />
							</div>
						</span>
					</button>
					{#if isOpen(item.question)}
						<div
							id={`faq-panel-${index.toString()}`}
							role="region"
							aria-labelledby={`faq-trigger-${index.toString()}`}
							class="px-4 pb-4 sm:px-6"
							transition:slide={{ duration: 220 }}
						>
							<p class="text-sm font-normal tracking-normal text-pretty text-foreground-muted sm:text-base">
								{item.answer}
							</p>
						</div>
					{/if}
				</article>
			</div>
		{/each}
	</div>
</div>
<script lang="ts">
	import { cn } from '$lib/utils/cn';
	import { slide } from 'svelte/transition';
	import Add from 'carbon-icons-svelte/lib/Add.svelte';

	type FaqItem = {
		question: string;
		answer: string;
	};

	type Props = {
		class?: string;
		items?: FaqItem[];
	};

	const defaultItems: FaqItem[] = [
		{
			question: 'Что это за продукт?',
			answer:
				'Это минималистичный фреймворк для разработки быстрых визуальных эффектов. Он даёт чёткий путь от одного эффекта до полноценного визуального слоя в вашем приложении.'
		},
		{
			question: 'Для кого он предназначен?',
			answer:
				'Он создан для разработчиков, которые хотят современные высокопроизводительные визуальные эффекты без необходимости строить рендер-стек с нуля.'
		},
		{
			question: 'Нужны ли специальные знания для начала работы?',
			answer:
				'Нет. Вы можете начать с минимального шейдера и быстро итерировать. По мере роста проекта вы получаете полный контроль над юниформами, текстурами и постобработкой.'
		},
		{
			question: 'Насколько быстро можно что-то выпустить?',
			answer:
				'Обычно первый визуальный эффект можно запустить за несколько минут: установить, определить материал, смонтировать компонент, а затем настроить в плейграунде.'
		},
		{
			question: 'Подходит ли это для продакшена?',
			answer:
				'Да. Продукт спроектирован для предсказуемого поведения, явного управления рантаймом и чёткой диагностики, чтобы команды могли уверенно переходить от прототипа к продакшену.'
		},
		{
			question: 'Это полноценный 3D-движок?',
			answer:
				'Нет. Продукт фокусируется на полноэкранных фрагментных рабочих процессах и пайплайнах постобработки. Для полных сцен и 3D-инструментария используйте специализированный 3D-движок.'
		},
		{
			question: 'С чего лучше начать?',
			answer:
				'Начните с раздела "Быстрый старт" для мгновенной обратной связи, затем следуйте руководству "Начало работы", чтобы перейти к реальному коду приложения и продакшен-паттернам.'
		}
	];

	let { class: className = '', items = defaultItems }: Props = $props();

	let openItems = $state<Record<string, boolean>>({});

	const isOpen = (question: string): boolean => openItems[question] ?? false;

	function toggle(question: string) {
		openItems[question] = !isOpen(question);
	}
</script>

<div class={cn('inset-shadow w-full overflow-hidden rounded-xl bg-background-inset p-2', className)}>
	<div class="grid gap-2">
		{#each items as item, index (item.question)}
			<div class="inset-shadow relative overflow-hidden rounded-lg bg-background-inset p-1.5">
				<article class="rounded-md bg-background card">
					<button
						type="button"
						id={`faq-trigger-${index.toString()}`}
						class="flex w-full cursor-pointer items-center justify-between gap-4 px-4 py-3 text-left text-base text-foreground sm:px-6"
						aria-expanded={isOpen(item.question)}
						aria-controls={`faq-panel-${index.toString()}`}
						onclick={() => { toggle(item.question); }}
					>
						<span class="font-medium tracking-tight">{item.question}</span>
						<span
							aria-hidden="true"
							class="inset-shadow inline-flex shrink-0 rounded-sm bg-background-inset p-1.5 text-foreground-muted"
						>
							<div
								class:rotate-45={isOpen(item.question)}
								class="transition-transform duration-150"
							>
								<Add size={24} />
							</div>
						</span>
					</button>
					{#if isOpen(item.question)}
						<div
							id={`faq-panel-${index.toString()}`}
							role="region"
							aria-labelledby={`faq-trigger-${index.toString()}`}
							class="px-4 pb-4 sm:px-6"
							transition:slide={{ duration: 220 }}
						>
							<p class="text-sm font-normal tracking-normal text-pretty text-foreground-muted sm:text-base">
								{item.answer}
							</p>
						</div>
					{/if}
				</article>
			</div>
		{/each}
	</div>
</div>

Особенности

  • Плавное раскрытие через svelte/transitionslide
  • Иконка + поворачивается на 45° при открытии (→ крестик закрытия)
  • Несколько элементов могут быть открыты одновременно
  • aria-expanded и aria-controls для доступности
  • Полностью кастомизируемый список через пропс items

Использование

Дефолтный список

<script>
	import Faq from '$lib/components/ui/Faq.svelte';
</script>

<Faq />
<script>
	import Faq from '$lib/components/ui/Faq.svelte';
</script>

<Faq />

Кастомные вопросы

<Faq
	items={[
		{
			question: 'Как начать работу?',
			answer: 'Установите пакет и следуйте руководству по быстрому старту.'
		},
		{
			question: 'Есть ли поддержка TypeScript?',
			answer: 'Да, все компоненты имеют полную типизацию.'
		}
	]}
/>
<Faq
	items={[
		{
			question: 'Как начать работу?',
			answer: 'Установите пакет и следуйте руководству по быстрому старту.'
		},
		{
			question: 'Есть ли поддержка TypeScript?',
			answer: 'Да, все компоненты имеют полную типизацию.'
		}
	]}
/>

Пропсы

Пропс Тип По умолчанию Описание
items FaqItem[] 7 дефолтных вопросов Список вопросов и ответов
class string '' Дополнительные CSS-классы

Тип FaqItem

Поле Тип Описание
question string Текст вопроса
answer string Текст ответа