Skip to content

Settings page

A settings page, finished: pass its sections as items (each with its rows), its draft’s save and the router’s unsaved-changes guard. The sections are one form that the save bar at its foot saves together; settings that apply at once need no save.

Language
Appearance

A draft saved together

Change the greeting: the save bar appears. Save checks the fields first (clear the greeting to see it refuse).

Agent settings

How the front desk agent talks to callers.

Greeting

What callers hear first.

Greeting

Said when the agent answers.

Calls

import { useState } from 'react';
import { SettingRow, SettingToggle, SettingsPage, TextField } from '@arbol-ai/ui';

const SAVED_GREETING = 'Hi, this is Clínica Norte. How can I help?';

const AgentSettings = () => {
  const [saved, setSaved] = useState({ greeting: SAVED_GREETING, endSilentCalls: true });
  const [draft, setDraft] = useState(saved);
  const [isSaved, setIsSaved] = useState(false);
  return (
    <SettingsPage
      className="px-0 md:px-0"
      description="How the front desk agent talks to callers."
      label="Agent settings"
      save={{
        idle: isSaved ? 'All changes saved.' : undefined,
        isDirty: draft.greeting !== saved.greeting || draft.endSilentCalls !== saved.endSilentCalls,
        onDiscard: () => {
          setDraft(saved);
        },
        onSave: () => {
          setSaved(draft);
          setIsSaved(true);
        },
      }}
      sections={[
        {
          id: 'greeting',
          title: 'Greeting',
          description: 'What callers hear first.',
          content: (
            <SettingRow description="Said when the agent answers." label="Greeting" layout="stacked">
              <TextField
                aria-label="Greeting"
                isRequired
                multiline
                onChange={(greeting) => {
                  setDraft({ ...draft, greeting });
                  setIsSaved(false);
                }}
                value={draft.greeting}
              />
            </SettingRow>
          ),
        },
        {
          id: 'calls',
          title: 'Calls',
          content: (
            <SettingToggle
              description="The agent hangs up after 30 seconds of silence."
              isSelected={draft.endSilentCalls}
              label="End silent calls"
              onChange={(endSilentCalls) => {
                setDraft({ ...draft, endSilentCalls });
                setIsSaved(false);
              }}
            />
          ),
        },
      ]}
      title="Agent settings"
    />
  );
};

<AgentSettings />

Settings that apply at once

No save: each control applies as it changes.

Profile

Preferences

How this browser shows Account.

Language

import { useState } from 'react';
import { SegmentedControl, SettingRow, SettingsPage } from '@arbol-ai/ui';

const Preferences = () => {
  const [language, setLanguage] = useState('es');
  return (
    <SettingsPage
      className="px-0 md:px-0"
      sections={[
        {
          id: 'preferences',
          title: 'Preferences',
          description: 'How this browser shows Account.',
          content: (
            <SettingRow label="Language" layout="responsive">
              <SegmentedControl
                label="Language"
                onChange={setLanguage}
                options={[
                  { label: 'Español', value: 'es' },
                  { label: 'English', value: 'en' },
                ]}
                size="sm"
                value={language}
              />
            </SettingRow>
          ),
        },
      ]}
      title="Profile"
    />
  );
};

<Preferences />

An area with tabs and a preview

The area’s pages as tabs, a notice first, and what the settings change beside them.

import { UserPlusIcon } from '@phosphor-icons/react';
import { Notice, SettingRow, SettingsPage, TextField } from '@arbol-ai/ui';

<SettingsPage
  className="px-0 md:px-0"
  preview={
    <div className="rounded-card bg-card text-muted-foreground grid place-items-center border p-8 text-sm">
      Welcome to Clínica Norte
    </div>
  }
  primaryAction={{ label: 'Invite people', icon: <UserPlusIcon />, href: '#/settings-page' }}
  sections={[
    {
      id: 'identity',
      title: 'Identity',
      description: 'How the organization appears in every app.',
      content: (
        <SettingRow label="Name" layout="stacked">
          <TextField aria-label="Name" defaultValue="Clínica Norte" />
        </SettingRow>
      ),
    },
  ]}
  summary={<Notice tone="info">Only administrators change these settings.</Notice>}
  tabs={{
    items: [
      { id: 'organization', label: 'Organization', href: '#/settings-page' },
      { id: 'team', label: 'Team', href: '#/settings-page' },
      { id: 'billing', label: 'Billing', href: '#/settings-page' },
    ],
    label: 'Settings pages',
    value: 'organization',
  }}
  title="Organization"
/>

Source

The examples above are src/blocks/settings-page/settings-page.examples.tsx, which the library’s tests render too.

Type to search…

↑↓ navigate↵ selectEsc close