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>
);