1 звезда на GitHub = 1 новый компонент.

Поставить звезду
Приложение и дашбордappdashboardsaassettingsformaccount

Settings Section

Блок настроек: заголовок и подсказка рядом с карточкой полей, кнопки «Сохранить/Отмена» и подтверждение.

Живое превью

Profile

This information is visible to your team.

Settings Section

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

Скопируйте и вставьте этот код в ваш React-компонент:

tsx
import { SettingsSection } from 'react-component-kit-ui/app';

<SettingsSection title="Notifications" description="Choose what reaches you.">
  <SwitchRow name="bookings" label="New bookings" />
</SettingsSection>

Ручная установка

Если вы не используете пакет npm, создайте файл и вставьте следующий код: src/components/SettingsSection.tsx + SettingsSection.css:

SettingsSection.tsx
'use client';

import { useEffect, useRef } from 'react';
import type { ReactNode } from 'react';
import './SettingsSection.css';

/** class:list → className */
function cx(...args: unknown[]): string {
  const out: string[] = [];
  const walk = (v: unknown) => {
    if (!v) return;
    if (typeof v === 'string' || typeof v === 'number') out.push(String(v));
    else if (Array.isArray(v)) v.forEach(walk);
    else if (typeof v === 'object') for (const [k, on] of Object.entries(v as Record<string, unknown>)) if (on) out.push(k);
  };
  args.forEach(walk);
  return out.join(' ');
}

/**
 * SettingsSection — Settings page block: title and description on the left, fields in a card on
 * the right, with an optional footer holding Save / Cancel and a "saved" confirmation.
 *
 * @param {string} title - Section title. Default 'Profile'.
 * @param {string} description - Help text under the title.
 * @param {boolean} form - Wrap the fields in a <form> with Save/Cancel. Default true.
 * @param {string} saveLabel - Save button label. Default 'Save changes'.
 * @param {string} savedText - Confirmation after saving. Default 'Changes saved'.
 * @param {string} id - Base id. Default 'settings-section'.
 * @param {string} variant - Layout (split|stacked). Default 'split'.
 * @param {string} className - Extra class.
 *
 * Slots: default (fields — SwitchRow, AnimatedInput, ModernSelect…), `footer-start` (extra text).
 *
 * Accessibility: a labelled section; the form validates natively and announces the saved state
 * in a polite live region. Emits `app:settings-save` with the FormData.
 */
export interface SettingsSectionProps {
  title?: string;
  description?: string;
  form?: boolean;
  saveLabel?: string;
  savedText?: string;
  id?: string;
  variant?: 'split' | 'stacked';
  className?: string;
  children?: ReactNode;
  footerStart?: ReactNode;
}

export function SettingsSection({
  title = 'Profile',
  description = 'This information is visible to your team.',
  form = true,
  saveLabel = 'Save changes',
  savedText = 'Changes saved',
  id = 'settings-section',
  variant = 'split',
  className = '',
  children,
  footerStart
}: SettingsSectionProps) {
  const Body = form ? 'form' : 'div';

  // Ported from the Astro <script>: runs once after mount (DOM logic kept as in the Astro kit).
  const mounted = useRef(false);
  useEffect(() => {
    if (mounted.current) return;
    mounted.current = true;
    document.querySelectorAll<HTMLFormElement>('form[data-settings-form]:not([data-bound])').forEach((f) => {
        f.dataset.bound = '';
        const saved = f.querySelector<HTMLElement>('[data-saved]');
        f.addEventListener('submit', (e) => {
          e.preventDefault();
          if (!f.checkValidity()) {
            f.querySelector<HTMLElement>(':invalid')?.focus();
            return;
          }
          if (saved) {
            saved.textContent = `✓ ${saved.dataset.text}`;
            window.setTimeout(() => { saved.textContent = ''; }, 2600);
          }
          f.dispatchEvent(new CustomEvent('app:settings-save', { bubbles: true, detail: new FormData(f) }));
        });
      });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <>
      <section className={cx(['set', `set--${variant}`, className])} aria-labelledby={`${id}-title`} data-rk-d579d6="">
        <div className="set__intro" data-rk-d579d6="">
          <h2 className="set__title" id={`${id}-title`} data-rk-d579d6="">{title}</h2>
          {description && <p className="set__desc" data-rk-d579d6="">{description}</p>}
        </div>
        <Body className="set__card" data-settings-form={form ? '' : undefined} noValidate={form ? true : undefined} data-rk-d579d6="">
          <div className="set__fields" data-rk-d579d6="">{children}</div>
          {form && (
            <div className="set__foot" data-rk-d579d6="">
              {footerStart}
              <p className="set__saved" aria-live="polite" data-saved data-text={savedText} data-rk-d579d6=""></p>
              <button type="reset" className="set__btn" data-rk-d579d6="">Cancel</button>
              <button type="submit" className="set__btn set__btn--primary" data-rk-d579d6="">{saveLabel}</button>
            </div>
          )}
        </Body>
      </section>
    </>
  );
}

export default SettingsSection;
SettingsSection.css
/* SettingsSection — ported from astro-component-kit (app/SettingsSection.astro) */

.set:where([data-rk-d579d6]) { container-type: inline-size; display: grid; grid-template-columns: minmax(0, 0.8fr) minmax(0, 2fr); gap: var(--sp-6, 1.5rem); padding-block: var(--sp-6, 1.5rem); border-bottom: 1px solid var(--c-border, rgba(255, 255, 255, 0.08)); }
.set--stacked:where([data-rk-d579d6]) { grid-template-columns: 1fr; }
.set__title:where([data-rk-d579d6]) { margin: 0; font-size: 1rem; font-weight: 700; color: var(--c-text-1, #fff); }
.set__desc:where([data-rk-d579d6]) { margin: 4px 0 0; font-size: 0.85rem; line-height: 1.55; color: var(--c-text-2, #a3a3a3); }
.set__card:where([data-rk-d579d6]) { border-radius: var(--r-lg, 16px); border: 1px solid var(--c-border, rgba(255, 255, 255, 0.08)); background: var(--c-bg-1, #0a0a0a); overflow: hidden; }
.set__fields:where([data-rk-d579d6]) { display: grid; gap: var(--sp-4, 1rem); padding: var(--sp-5, 1.25rem); }
.set__foot:where([data-rk-d579d6]) { display: flex; align-items: center; justify-content: flex-end; gap: var(--sp-2, 0.5rem); padding: var(--sp-3, 0.75rem) var(--sp-5, 1.25rem); border-top: 1px solid var(--c-border, rgba(255, 255, 255, 0.08)); background: rgb(var(--ink-rgb, 255 255 255) / 0.02); }
.set__saved:where([data-rk-d579d6]) { margin: 0 auto 0 0; font-size: 0.8rem; font-weight: 600; color: var(--c-success, #34d399); }
.set__btn:where([data-rk-d579d6]) { min-height: 38px; padding: 0 0.9rem; border-radius: 10px; border: 1px solid var(--c-border, rgba(255, 255, 255, 0.12)); background: transparent; color: var(--c-text-1, #fff); font: inherit; font-size: 0.82rem; font-weight: 600; cursor: pointer; }
.set__btn--primary:where([data-rk-d579d6]) { border-color: transparent; background: var(--app-accent, var(--c-primary-light, #d4a017)); color: var(--c-on-primary, #050505); }
.set__btn:focus-visible:where([data-rk-d579d6]) { outline: 2px solid var(--app-accent, var(--c-primary-light, #d4a017)); outline-offset: 2px; }
.set:where([data-rk-d579d6]) .anim-input, .set:where([data-rk-d579d6]) .custom-select { background: var(--c-bg, #050505); }
@container (max-width: 640px) { .set__intro:where([data-rk-d579d6]), .set__card:where([data-rk-d579d6]) { grid-column: 1 / -1; } }

Краткая информация

Категория
Приложение и дашборд
Имя файла
SettingsSection.tsx
Зависимости
Нет — чистый React + CSS
Теги
appdashboardsaassettingsformaccount