import { type Component, For, Show } from 'solid-js';
import type { Preset } from '~/engine/types';
import { SIGNAL_CSS_VAR } from '~/presets/builtIn';
import { thumbnails } from '~/stores/thumbnailStore';
import { ChevDown, ChevUp, Trash } from './icons/Icon';
interface Props {
preset: Preset;
active: boolean;
onClick: () => void;
onDelete?: () => void;
/** Reorder hooks — when provided render up/down arrows on the card. */
onMoveUp?: () => void;
onMoveDown?: () => void;
/** Disable the corresponding arrow at the list boundary. */
canMoveUp?: boolean;
canMoveDown?: boolean;
}
export const PresetCard: Component<Props> = (props) => {
const signal = () => SIGNAL_CSS_VAR[props.preset.tagSignal];
const toHex = (c: readonly number[]) =>
`#${c
.map((v) =>
Math.round(v * 255)
.toString(16)
.padStart(2, '0'),
)
.join('')}`;
const trailHex = () => toHex(props.preset.params.species[0].color);
const bgHex = () => toHex(props.preset.params.backgroundColor);
// Prefer the shipped baked thumbnail (built-ins). Fall back to the
// per-session capture from the runtime thumbnailStore (custom presets
// or any built-in that hasn't been baked yet).
const thumb = () => props.preset.thumbnail ?? thumbnails()[props.preset.id];
const activeSpecies = () => props.preset.params.species.filter((s) => s.populationShare > 0);
return (
<button
type="button"
onClick={props.onClick}
style={{
background: 'var(--surface-raised)',
border: '1px solid var(--border-subtle)',
'border-radius': 'var(--radius-lg)',
overflow: 'hidden',
cursor: 'pointer',
position: 'relative',
// Explicit colour so descendants don't inherit the browser's
// ButtonText system colour (the original cause of the name
// rendering dim grey on dark surface).
color: 'var(--fg-primary)',
'box-shadow': props.active
? `0 0 0 1px ${signal()}, 0 0 24px -8px ${signal()}, var(--shadow-card)`
: 'var(--shadow-card)',
transition:
'transform var(--dur-fast) var(--ease-signal), box-shadow var(--dur-base) var(--ease-signal), border-color var(--dur-base) var(--ease-signal)',
'text-align': 'left',
padding: '0',
}}
>
<div
style={{
position: 'absolute',
top: '0',
left: '0',
right: '0',
height: '2px',
background: signal(),
opacity: props.active ? '1' : '0.7',
'z-index': '2',
}}
/>
<div
style={{
position: 'relative',
'aspect-ratio': '16 / 9',
background: `radial-gradient(ellipse at 50% 50%, ${trailHex()}55, ${bgHex()} 70%)`,
}}
>
<Show when={thumb()}>
{(url) => (
<img
src={url()}
alt=""
style={{
position: 'absolute',
inset: '0',
width: '100%',
height: '100%',
'object-fit': 'cover',
display: 'block',
}}
/>
)}
</Show>
<div
style={{
position: 'absolute',
inset: '0',
'box-shadow':
'inset 0 0 0 1px rgba(255,255,255,0.04), inset 0 -20px 40px -20px rgba(0,0,0,0.6)',
}}
/>
<Show when={props.active}>
<div
style={{
position: 'absolute',
top: '8px',
right: '8px',
padding: '2px 7px',
'border-radius': '999px',
background: signal(),
color: 'var(--fg-on-accent)',
'font-family': 'var(--font-mono)',
'font-size': '8px',
'font-weight': '700',
'letter-spacing': '0.14em',
'text-transform': 'uppercase',
}}
>
Active
</div>
</Show>
<Show when={props.onDelete}>
<button
type="button"
title={`Delete "${props.preset.name}"`}
aria-label={`Delete preset ${props.preset.name}`}
onClick={(e) => {
e.stopPropagation();
if (window.confirm(`Delete "${props.preset.name}"? This can't be undone.`)) {
props.onDelete?.();
}
}}
style={{
position: 'absolute',
bottom: '8px',
right: '8px',
width: '28px',
height: '28px',
display: 'flex',
'align-items': 'center',
'justify-content': 'center',
background: 'color-mix(in oklab, var(--void-000) 60%, transparent)',
border: '1px solid var(--border-subtle)',
'border-radius': 'var(--radius-md)',
color: 'var(--fg-secondary, var(--fg-primary))',
cursor: 'pointer',
padding: '0',
'backdrop-filter': 'blur(4px)',
}}
>
<Trash size={13} />
</button>
</Show>
<Show when={props.onMoveUp || props.onMoveDown}>
<div
style={{
position: 'absolute',
bottom: '8px',
left: '8px',
display: 'flex',
gap: '4px',
}}
>
<Show when={props.onMoveUp}>
<button
type="button"
title="Move earlier in library"
aria-label={`Move preset ${props.preset.name} earlier`}
disabled={!props.canMoveUp}
onClick={(e) => {
e.stopPropagation();
props.onMoveUp?.();
}}
style={{
width: '28px',
height: '28px',
display: 'flex',
'align-items': 'center',
'justify-content': 'center',
background: 'color-mix(in oklab, var(--void-000) 60%, transparent)',
border: '1px solid var(--border-subtle)',
'border-radius': 'var(--radius-md)',
color: 'var(--fg-secondary, var(--fg-primary))',
cursor: props.canMoveUp ? 'pointer' : 'not-allowed',
opacity: props.canMoveUp ? '1' : '0.35',
padding: '0',
'backdrop-filter': 'blur(4px)',
}}
>
<ChevUp size={13} />
</button>
</Show>
<Show when={props.onMoveDown}>
<button
type="button"
title="Move later in library"
aria-label={`Move preset ${props.preset.name} later`}
disabled={!props.canMoveDown}
onClick={(e) => {
e.stopPropagation();
props.onMoveDown?.();
}}
style={{
width: '28px',
height: '28px',
display: 'flex',
'align-items': 'center',
'justify-content': 'center',
background: 'color-mix(in oklab, var(--void-000) 60%, transparent)',
border: '1px solid var(--border-subtle)',
'border-radius': 'var(--radius-md)',
color: 'var(--fg-secondary, var(--fg-primary))',
cursor: props.canMoveDown ? 'pointer' : 'not-allowed',
opacity: props.canMoveDown ? '1' : '0.35',
padding: '0',
'backdrop-filter': 'blur(4px)',
}}
>
<ChevDown size={13} />
</button>
</Show>
</div>
</Show>
</div>
<div style={{ padding: '12px 14px 14px' }}>
<div
style={{
display: 'flex',
'align-items': 'baseline',
'justify-content': 'space-between',
gap: '8px',
'margin-bottom': '6px',
}}
>
<span
style={{
'font-family': 'var(--font-display)',
'font-size': '17px',
'font-weight': '600',
color: 'var(--fg-primary)',
}}
>
{props.preset.name}
</span>
<span
class="phys-mono"
style={{
'font-size': '10px',
color: 'var(--fg-secondary)',
'letter-spacing': '0.12em',
'text-transform': 'uppercase',
}}
>
{(props.preset.params.agentCount / 1000).toFixed(0)}k
</span>
</div>
<div style={{ display: 'flex', gap: '5px', 'margin-bottom': '8px' }}>
<For each={activeSpecies()}>
{(s) => (
<span
style={{
width: '10px',
height: '10px',
'border-radius': '50%',
background: toHex(s.color),
'box-shadow': `0 0 6px ${toHex(s.color)}`,
}}
/>
)}
</For>
</div>
<div style={{ display: 'flex', 'flex-wrap': 'wrap', gap: '4px' }}>
<For each={props.preset.tags}>
{(t) => (
<span
class="phys-mono"
style={{
'font-size': '10px',
color: 'var(--fg-secondary)',
'letter-spacing': '0.06em',
padding: '3px 7px',
border: '1px solid var(--border-default)',
'border-radius': '3px',
background: 'var(--surface-inset)',
}}
>
{t}
</span>
)}
</For>
</div>
</div>
</button>
);
};