Settings Page

A settings page with profile and preferences sections. Composed from foldcn primitives.

Preview
Style

Settings

Manage your account settings and preferences.

Profile

This information will be displayed publicly.

Preferences

Control how you receive notifications.

Receive emails about your account activity.

Add an extra layer of security to your account.

src/demo/views/settings-page.ts123 lines
import { Schema as S } from 'effect'
import { evo } from 'foldkit/struct'
import { defineMessageUnion } from 'foldkit/message'
import type { Html, HtmlBuilder } from 'foldkit/html'

import { settingsPage } from '../../generated/registry/blocks/settings-page/settings-page'

import { defineSlice, type UpdateReturn } from '../slice'
import type { Model, Message as AppMessage } from '../assemble'

const Message = defineMessageUnion({
  UpdatedSettingsName: { value: S.String },
  UpdatedSettingsEmail: { value: S.String },
  UpdatedSettingsBio: { value: S.String },
  UpdatedSettingsLanguage: { value: S.String },
  ToggledSettingsEmailNotifs: { isChecked: S.Boolean },
  ToggledSettingsTfa: { isChecked: S.Boolean },
  ClickedSaveSettings: {},
})

export const settingsPageView = (model: Model, h: HtmlBuilder<AppMessage>): Html =>
  h.div(
    [h.Class('w-full overflow-hidden rounded-xl border border-border')],
    [
      settingsPage<AppMessage>(
        {
          name: model.settingsName,
          onNameInput: (value) => Message.UpdatedSettingsName({ value }),
          email: model.settingsEmail,
          onEmailInput: (value) => Message.UpdatedSettingsEmail({ value }),
          bio: model.settingsBio,
          onBioInput: (value) => Message.UpdatedSettingsBio({ value }),
          language: model.settingsLanguage,
          onLanguageChange: (value) => Message.UpdatedSettingsLanguage({ value }),
          isEmailNotificationsEnabled: model.settingsEmailNotifs,
          onToggleEmailNotifications: (isChecked) =>
            Message.ToggledSettingsEmailNotifs({ isChecked }),
          isTwoFactorEnabled: model.settingsTfa,
          onToggleTwoFactor: (isChecked) => Message.ToggledSettingsTfa({ isChecked }),
          onSave: Message.ClickedSaveSettings(),
        },
        h,
      ),
      ...(model.settingsSaved
        ? [
            h.p(
              [
                h.Class(
                  'mx-auto max-w-2xl px-6 pb-6 text-sm text-emerald-600 dark:text-emerald-400',
                ),
              ],
              ['Settings saved (demo).'],
            ),
          ]
        : []),
    ],
  )

const fields = {
  settingsName: S.String,
  settingsEmail: S.String,
  settingsBio: S.String,
  settingsLanguage: S.String,
  settingsEmailNotifs: S.Boolean,
  settingsTfa: S.Boolean,
  settingsSaved: S.Boolean,
}

const stateSchema = S.Struct(fields)
type State = typeof stateSchema.Type

export const slice = defineSlice({
  fields,
  init: {
    settingsName: '',
    settingsEmail: '',
    settingsBio: '',
    settingsLanguage: 'en',
    settingsEmailNotifs: true,
    settingsTfa: false,
    settingsSaved: false,
  },
  messages: [
    Message.UpdatedSettingsName,
    Message.UpdatedSettingsEmail,
    Message.UpdatedSettingsBio,
    Message.UpdatedSettingsLanguage,
    Message.ToggledSettingsEmailNotifs,
    Message.ToggledSettingsTfa,
    Message.ClickedSaveSettings,
  ],
  handlers: (model: State) => ({
    UpdatedSettingsName: ({ value }: typeof Message.UpdatedSettingsName.Type): UpdateReturn => ({
      model: evo(model, { settingsName: () => value }),
    }),
    UpdatedSettingsEmail: ({ value }: typeof Message.UpdatedSettingsEmail.Type): UpdateReturn => ({
      model: evo(model, { settingsEmail: () => value }),
    }),
    UpdatedSettingsBio: ({ value }: typeof Message.UpdatedSettingsBio.Type): UpdateReturn => ({
      model: evo(model, { settingsBio: () => value }),
    }),
    UpdatedSettingsLanguage: ({
      value,
    }: typeof Message.UpdatedSettingsLanguage.Type): UpdateReturn => ({
      model: evo(model, { settingsLanguage: () => value }),
    }),
    ToggledSettingsEmailNotifs: ({
      isChecked,
    }: typeof Message.ToggledSettingsEmailNotifs.Type): UpdateReturn => ({
      model: evo(model, { settingsEmailNotifs: () => isChecked }),
    }),
    ToggledSettingsTfa: ({ isChecked }: typeof Message.ToggledSettingsTfa.Type): UpdateReturn => ({
      model: evo(model, { settingsTfa: () => isChecked }),
    }),
    ClickedSaveSettings: (): UpdateReturn => ({ model: evo(model, { settingsSaved: () => true }) }),
  }),
  samples: [
    Message.UpdatedSettingsName({ value: 'Ada' }),
    Message.ToggledSettingsTfa({ isChecked: true }),
    Message.ClickedSaveSettings(),
  ],
})

This code is shown verbatim — it’s the exact file used for this preview. Much of the surrounding wiring (slice, fields, messages, handlers) belongs to this site’s demo harness; you only need the view and the state that matters for your own app. Adapt what you need.

Installation

Add this component to your project:

pnpm dlx shadcn@latest add @foldcn/settings-page

Source

The component ships as plain source — no build step, no wrapper. Copy it and make it yours.

registry/default/blocks/settings-page/settings-page.ts139 lines
import type { Html, HtmlBuilder } from 'foldkit/html'

import { button } from '@/components/ui/button'
import { fieldset } from '@/components/ui/fieldset'
import { input } from '@/components/ui/input'
import { nativeSelect } from '@/components/ui/native-select'
import { switch_ } from '@/components/ui/switch'
import { textarea } from '@/components/ui/textarea'

export type SettingsPageConfig<M> = Readonly<{
  name: string
  onNameInput: (value: string) => M
  email: string
  onEmailInput: (value: string) => M
  bio: string
  onBioInput: (value: string) => M
  language: string
  onLanguageChange: (value: string) => M
  isEmailNotificationsEnabled: boolean
  onToggleEmailNotifications: (isChecked: boolean) => M
  isTwoFactorEnabled: boolean
  onToggleTwoFactor: (isChecked: boolean) => M
  onSave: M
  className?: string
}>

/** Settings page block: profile + preferences sections composed from foldcn
 *  primitives. */
export const settingsPage = <M>(config: SettingsPageConfig<M>, h: HtmlBuilder<M>): Html =>
  h.div(
    [h.Class('mx-auto flex w-full max-w-2xl flex-col gap-8 p-6')],
    [
      h.div(
        [],
        [
          h.h2([h.Class('text-3xl font-bold tracking-tight')], ['Settings']),
          h.p(
            [h.Class('text-muted-foreground')],
            ['Manage your account settings and preferences.'],
          ),
        ],
      ),

      fieldset<M>(
        {
          id: 'settings-profile',
          legend: 'Profile',
          description: 'This information will be displayed publicly.',
          children: [
            input<M>(
              {
                id: 'settings-name',
                label: 'Name',
                value: config.name,
                onInput: config.onNameInput,
                placeholder: 'Ada Lovelace',
              },
              h,
            ),
            input<M>(
              {
                id: 'settings-email',
                label: 'Email',
                type: 'email',
                value: config.email,
                onInput: config.onEmailInput,
                placeholder: 'ada@example.com',
              },
              h,
            ),
            textarea<M>(
              {
                id: 'settings-bio',
                label: 'Bio',
                value: config.bio,
                onInput: config.onBioInput,
                rows: 4,
                placeholder: 'Tell us about yourself...',
              },
              h,
            ),
            nativeSelect<M>(
              {
                id: 'settings-language',
                label: 'Language',
                value: config.language,
                onChange: config.onLanguageChange,
                options: [
                  h.option([h.Value('en')], ['English']),
                  h.option([h.Value('id')], ['Bahasa Indonesia']),
                  h.option([h.Value('ja')], ['日本語']),
                  h.option([h.Value('es')], ['Español']),
                ],
              },
              h,
            ),
          ],
        },
        h,
      ),

      fieldset<M>(
        {
          id: 'settings-preferences',
          legend: 'Preferences',
          description: 'Control how you receive notifications.',
          children: [
            switch_<M>(
              {
                id: 'settings-email-notifications',
                label: 'Email notifications',
                description: 'Receive emails about your account activity.',
                isChecked: config.isEmailNotificationsEnabled,
                onToggle: config.onToggleEmailNotifications,
              },
              h,
            ),
            switch_<M>(
              {
                id: 'settings-two-factor',
                label: 'Two-factor authentication',
                description: 'Add an extra layer of security to your account.',
                isChecked: config.isTwoFactorEnabled,
                onToggle: config.onToggleTwoFactor,
              },
              h,
            ),
          ],
        },
        h,
      ),

      h.div(
        [h.Class('flex justify-end')],
        [button<M>({ onClick: config.onSave }, 'Save changes', h)],
      ),
    ],
  )