val 0.7.0

An arbitrary precision calculator language
Documentation
import { Button } from '@/components/ui/button';
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from '@/components/ui/dialog';
import { Label } from '@/components/ui/label';
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
import {
  type EditorSettings,
  defaultSettings,
  useEditorSettings,
} from '@/contexts/editor-settings-context';
import { RotateCcw, Settings } from 'lucide-react';
import { ReactNode, useId } from 'react';

const FONT_SIZES = [12, 14, 16, 18] as const;
const TAB_SIZES = [2, 4, 8] as const;

const SettingSection = ({
  children,
  title,
}: {
  children: ReactNode;
  title: string;
}) => (
  <section className='grid gap-3'>
    <h3 className='text-muted-foreground text-xs font-medium'>{title}</h3>
    <div className='grid divide-y rounded-md border'>{children}</div>
  </section>
);

const SettingRow = ({
  children,
  description,
  id,
  label,
}: {
  children: ReactNode;
  description: string;
  id: string;
  label: string;
}) => (
  <div className='grid gap-3 p-3 sm:grid-cols-[1fr_auto] sm:items-center sm:gap-6'>
    <div className='grid gap-1'>
      <Label htmlFor={id} className='cursor-pointer text-sm font-medium'>
        {label}
      </Label>
      <p id={`${id}-description`} className='text-muted-foreground text-xs'>
        {description}
      </p>
    </div>
    {children}
  </div>
);

export const EditorSettingsDialog = () => {
  const { settings, updateSettings } = useEditorSettings();
  const id = useId();
  const hasChanges = Object.entries(defaultSettings).some(
    ([key, value]) => settings[key as keyof EditorSettings] !== value
  );

  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button
          variant='ghost'
          size='icon'
          title='Settings'
          aria-label='Settings'
          className='dark:hover:bg-accent h-7 w-7 cursor-pointer'
        >
          <Settings className='h-4 w-4' aria-hidden='true' />
        </Button>
      </DialogTrigger>
      <DialogContent className='editor-settings-dialog max-h-[calc(100dvh-2rem)] gap-0 overflow-y-auto p-0 sm:max-w-[560px]'>
        <DialogHeader className='px-6 pt-6 pb-2 text-left'>
          <DialogTitle>Settings</DialogTitle>
          <DialogDescription>
            Customize your editor experience with these settings.
          </DialogDescription>
        </DialogHeader>
        <div className='grid gap-5 px-6 py-5'>
          <SettingSection title='Display'>
            <SettingRow
              id={`${id}-line-numbers`}
              label='Line numbers'
              description='Show line numbers beside the editor.'
            >
              <Switch
                id={`${id}-line-numbers`}
                aria-describedby={`${id}-line-numbers-description`}
                checked={settings.lineNumbers}
                onCheckedChange={(checked) =>
                  updateSettings({ lineNumbers: checked })
                }
              />
            </SettingRow>
            <SettingRow
              id={`${id}-word-wrap`}
              label='Word wrap'
              description='Keep long lines within the editor pane.'
            >
              <Switch
                id={`${id}-word-wrap`}
                aria-describedby={`${id}-word-wrap-description`}
                checked={settings.lineWrapping}
                onCheckedChange={(checked) =>
                  updateSettings({ lineWrapping: checked })
                }
              />
            </SettingRow>
          </SettingSection>
          <SettingSection title='Editing'>
            <SettingRow
              id={`${id}-font-size`}
              label='Font size'
              description='Adjust the editor text size.'
            >
              <Select
                value={settings.fontSize.toString()}
                onValueChange={(value) =>
                  updateSettings({ fontSize: parseInt(value) })
                }
              >
                <SelectTrigger
                  id={`${id}-font-size`}
                  aria-describedby={`${id}-font-size-description`}
                  className='w-full sm:w-32'
                >
                  <SelectValue placeholder='Font size' />
                </SelectTrigger>
                <SelectContent>
                  {FONT_SIZES.map((size) => (
                    <SelectItem key={size} value={size.toString()}>
                      {size}px
                    </SelectItem>
                  ))}
                </SelectContent>
              </Select>
            </SettingRow>
            <SettingRow
              id={`${id}-tab-size`}
              label='Tab size'
              description='Set the width of tab characters.'
            >
              <Select
                value={settings.tabSize.toString()}
                onValueChange={(value) =>
                  updateSettings({ tabSize: parseInt(value) })
                }
              >
                <SelectTrigger
                  id={`${id}-tab-size`}
                  aria-describedby={`${id}-tab-size-description`}
                  className='w-full sm:w-32'
                >
                  <SelectValue placeholder='Tab size' />
                </SelectTrigger>
                <SelectContent>
                  {TAB_SIZES.map((size) => (
                    <SelectItem key={size} value={size.toString()}>
                      {size} spaces
                    </SelectItem>
                  ))}
                </SelectContent>
              </Select>
            </SettingRow>
            <SettingRow
              id={`${id}-keybindings`}
              label='Keybindings'
              description='Choose standard shortcuts or Vim bindings.'
            >
              <Select
                value={settings.keybindings}
                onValueChange={(value) =>
                  updateSettings({ keybindings: value as 'default' | 'vim' })
                }
              >
                <SelectTrigger
                  id={`${id}-keybindings`}
                  aria-describedby={`${id}-keybindings-description`}
                  className='w-full sm:w-32'
                >
                  <SelectValue placeholder='Default' />
                </SelectTrigger>
                <SelectContent>
                  <SelectItem value='default'>Default</SelectItem>
                  <SelectItem value='vim'>Vim</SelectItem>
                </SelectContent>
              </Select>
            </SettingRow>
          </SettingSection>
        </div>
        <DialogFooter className='px-6 py-6'>
          <Button
            variant='outline'
            size='sm'
            className='dark:border-input dark:bg-background dark:hover:bg-accent'
            onClick={() => updateSettings(defaultSettings)}
            disabled={!hasChanges}
          >
            <RotateCcw className='h-3.5 w-3.5' aria-hidden='true' />
            Reset
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
};