Zum Inhalt

src/ui/SaveDialog.tsx

import { type Component, createSignal } from 'solid-js';
import { saveAsNew } from '~/stores/presetStore';
import { closePanel, requestSnapshot } from '~/stores/uiStore';
import { X } from './icons/Icon';

export const SaveDialog: Component = () => {
  const [name, setName] = createSignal('');
  let inputRef: HTMLInputElement | undefined;

  setTimeout(() => inputRef?.focus(), 50);

  const submit = () => {
    const n = name().trim();
    if (!n) return;
    try {
      saveAsNew(n);
      // Capture the canvas as the new preset's thumbnail immediately —
      // saveAsNew triggers a respawn that won't be presented until the
      // next RAF, so engine.snapshot() at this point still reads the
      // last-rendered frame (which already shows the preset's look,
      // since the edited state was running pre-save). Without this the
      // new card sits with the gradient placeholder for ~4s until the
      // auto-snapshot at frame 240 catches up.
      requestSnapshot();
      closePanel();
    } catch (err) {
      // Surface failures instead of swallowing them silently — saving used
      // to fail on the reactive proxy without any user-visible signal.
      console.error('Save preset failed:', err);
    }
  };

  return (
    // biome-ignore lint/a11y/useKeyWithClickEvents: backdrop dismissal — Escape key handled by useHotkeys
    <div
      style={{
        position: 'absolute',
        inset: '0',
        'z-index': '40',
        display: 'grid',
        'place-items': 'center',
        background: 'color-mix(in oklab, var(--void-000) 60%, transparent)',
        'backdrop-filter': 'blur(8px)',
      }}
      onClick={(e) => {
        if (e.currentTarget === e.target) closePanel();
      }}
    >
      <form
        onSubmit={(e) => {
          e.preventDefault();
          submit();
        }}
        style={{
          width: '420px',
          background: 'var(--surface-overlay)',
          border: '1px solid var(--border-default)',
          'border-radius': 'var(--radius-lg)',
          'box-shadow': 'var(--shadow-modal)',
          padding: '20px',
          display: 'flex',
          'flex-direction': 'column',
          gap: '16px',
        }}
      >
        <div style={{ display: 'flex', 'align-items': 'center' }}>
          <div style={{ flex: '1', display: 'flex', 'flex-direction': 'column', gap: '4px' }}>
            <span class="phys-eyebrow">Save as new</span>
            <span
              style={{
                'font-family': 'var(--font-display)',
                'font-size': '20px',
                'font-weight': '600',
                color: 'var(--fg-primary)',
              }}
            >
              Name your preset
            </span>
          </div>
          <button type="button" class="phys-iconbtn" onClick={() => closePanel()}>
            <X />
          </button>
        </div>
        <input
          ref={inputRef}
          type="text"
          value={name()}
          onInput={(e) => setName(e.currentTarget.value)}
          placeholder="e.g. Slow Coral"
          maxLength={64}
          style={{
            background: 'var(--surface-inset)',
            border: '1px solid var(--border-default)',
            'border-radius': 'var(--radius-md)',
            color: 'var(--fg-primary)',
            'font-family': 'var(--font-body)',
            'font-size': '14px',
            padding: '10px 12px',
            outline: 'none',
          }}
        />
        <div style={{ display: 'flex', gap: '8px' }}>
          <button type="button" class="phys-btn" data-variant="ghost" onClick={() => closePanel()}>
            Cancel
          </button>
          <div style={{ flex: '1' }} />
          <button type="submit" class="phys-btn" data-variant="primary" disabled={!name().trim()}>
            Save
          </button>
        </div>
      </form>
    </div>
  );
};