Zum Inhalt

src/ui/SettingsMenu.tsx

import { type Component, For, Show } from 'solid-js';
import {
  closePanel,
  openPanel,
  setDensity,
  toggleFps,
  triggerRespawn,
  uiState,
} from '~/stores/uiStore';

const HOTKEYS: readonly [string, string][] = [
  ['L', 'Library'],
  ['E', 'Editor'],
  ['C', 'Cinema mode'],
  ['R', 'Respawn agents'],
  ['Space', 'Play / pause'],
  ['Esc', 'Close panel'],
];

export interface SettingsActions {
  onToggleAudio: () => void;
  onToggleRecord: () => void;
  onShare: () => void;
  onCopyEmbed: () => void;
  onExport: () => void;
}

export const SettingsMenu: Component<SettingsActions> = (props) => {
  return (
    // biome-ignore lint/a11y/useKeyWithClickEvents: backdrop dismissal — Escape key handled by useHotkeys
    <div
      style={{
        position: 'absolute',
        inset: '0',
        'z-index': '30',
        background: 'color-mix(in oklab, var(--void-000) 40%, transparent)',
      }}
      onClick={(e) => {
        if (e.currentTarget === e.target) closePanel();
      }}
    >
      <div
        style={{
          position: 'absolute',
          top: `calc(env(safe-area-inset-top, 0px) + ${uiState.isNarrow ? '64px' : '76px'})`,
          right: `calc(env(safe-area-inset-right, 0px) + ${uiState.isNarrow ? '8px' : '16px'})`,
          left: uiState.isNarrow ? 'calc(env(safe-area-inset-left, 0px) + 8px)' : 'auto',
          width: uiState.isNarrow ? 'auto' : '260px',
          'max-width': uiState.isNarrow ? 'none' : '260px',
          'max-height':
            'calc(100dvh - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px) - 100px)',
          'overflow-y': 'auto',
          background: 'var(--surface-overlay)',
          border: '1px solid var(--border-default)',
          'border-radius': 'var(--radius-lg)',
          'box-shadow': 'var(--shadow-modal)',
          padding: '12px',
          display: 'flex',
          'flex-direction': 'column',
          gap: '6px',
        }}
      >
        <Show when={uiState.isNarrow}>
          <span class="phys-eyebrow" style={{ padding: '4px 8px' }}>
            Actions
          </span>
          <MenuRow
            label={
              uiState.audio.active ? `Audio — stop (${uiState.audio.source})` : 'Audio — start'
            }
            active={uiState.audio.active}
            onClick={() => {
              props.onToggleAudio();
              closePanel();
            }}
          />
          <MenuRow
            label={uiState.recording.active ? 'Recording — stop' : 'Record video'}
            active={uiState.recording.active}
            onClick={() => {
              props.onToggleRecord();
              closePanel();
            }}
          />
          <MenuRow
            label="Export PNG"
            onClick={() => {
              props.onExport();
              closePanel();
            }}
          />
          <MenuRow
            label="Share link"
            onClick={() => {
              props.onShare();
              closePanel();
            }}
          />
          <MenuRow
            label="Respawn agents"
            onClick={() => {
              triggerRespawn();
              closePanel();
            }}
          />
          <div class="phys-hr" />
          <MenuRow label="Open library" onClick={() => openPanel('library')} />
          <div class="phys-hr" />
        </Show>
        <span class="phys-eyebrow" style={{ padding: '4px 8px' }}>
          Settings
        </span>
        <MenuRow
          label="Copy embed code"
          onClick={() => {
            props.onCopyEmbed();
            closePanel();
          }}
        />
        <div class="phys-hr" />
        <MenuRow
          label="Density: Comfortable"
          active={uiState.density === 'comfortable'}
          onClick={() => setDensity('comfortable')}
        />
        <MenuRow
          label="Density: Compact"
          active={uiState.density === 'compact'}
          onClick={() => setDensity('compact')}
        />
        <div class="phys-hr" />
        <MenuRow
          label={uiState.showFps ? 'Hide FPS / Agent count' : 'Show FPS / Agent count'}
          onClick={toggleFps}
        />
        <div class="phys-hr" />
        <span class="phys-eyebrow" style={{ padding: '4px 8px' }}>
          Keyboard
        </span>
        <div
          style={{ padding: '0 8px 4px', display: 'flex', 'flex-direction': 'column', gap: '4px' }}
        >
          <For each={HOTKEYS}>
            {([key, label]) => (
              <div
                style={{
                  display: 'flex',
                  'align-items': 'center',
                  gap: '10px',
                  'font-size': '12px',
                  color: 'var(--fg-secondary, var(--fg-primary))',
                }}
              >
                <kbd
                  class="phys-mono"
                  style={{
                    'min-width': '32px',
                    'text-align': 'center',
                    padding: '2px 6px',
                    'font-size': '10px',
                    background: 'var(--surface-inset)',
                    border: '1px solid var(--border-subtle)',
                    'border-radius': 'var(--radius-sm)',
                    color: 'var(--fg-primary)',
                  }}
                >
                  {key}
                </kbd>
                <span>{label}</span>
              </div>
            )}
          </For>
        </div>
        <div class="phys-hr" />
        <div
          style={{ padding: '4px 8px', display: 'flex', 'flex-direction': 'column', gap: '2px' }}
        >
          <span
            class="phys-mono"
            style={{ 'font-size': '10px', color: 'var(--fg-tertiary)', 'letter-spacing': '0.08em' }}
          >
            v1.2 · physarum.lab
          </span>
          <a
            href="https://git.jkaindl.de/jkaindl/physarum"
            target="_blank"
            rel="noopener noreferrer"
            class="phys-mono"
            style={{
              'font-size': '11px',
              color: 'var(--signal-circuit)',
              'text-decoration': 'none',
            }}
          >
            View source ↗
          </a>
        </div>
      </div>
    </div>
  );
};

const MenuRow: Component<{ label: string; active?: boolean; onClick: () => void }> = (p) => (
  <button
    type="button"
    onClick={p.onClick}
    style={{
      'text-align': 'left',
      background: p.active ? 'var(--surface-raised)' : 'transparent',
      border: '0',
      color: 'var(--fg-primary)',
      'font-family': 'var(--font-body)',
      'font-size': '13px',
      padding: '8px 10px',
      'border-radius': 'var(--radius-md)',
      cursor: 'pointer',
    }}
  >
    {p.label}
  </button>
);