A web app to help you design things, local, offline, on device. In your browser.

TypeScript 53.1% JSON 45.6% CSS 0.9% Markdown 0.3% JavaScript 0.1%
AdjustmentsPanel.tsx 248 lines (8 KB)
'use client';

import React from 'react';
import { RotateCcw, Sun, Contrast, Droplets, Sparkles } from 'lucide-react';
import { useEditorStore } from '@/lib/store/editor-store';
import type { ImageLayerData, ImageFilters } from '@/types';

const DEFAULT_FILTERS: ImageFilters = {
  brightness: 0,
  contrast: 0,
  saturation: 0,
  blur: 0,
};

interface FilterSliderProps {
  label: string;
  icon: React.ReactNode;
  value: number;
  min: number;
  max: number;
  onChange: (value: number) => void;
  unit?: string;
}

function FilterSlider({ label, icon, value, min, max, onChange, unit = '' }: FilterSliderProps) {
  return (
    <div className="space-y-2">
      <div className="flex items-center justify-between">
        <div className="flex items-center gap-2 text-neutral-400">
          {icon}
          <span className="text-xs">{label}</span>
        </div>
        <span className="text-xs text-white tabular-nums">
          {value > 0 ? '+' : ''}{value}{unit}
        </span>
      </div>
      <input
        type="range"
        min={min}
        max={max}
        value={value}
        onChange={(e) => onChange(Number(e.target.value))}
        className="w-full h-1.5 bg-neutral-700 rounded-full appearance-none cursor-pointer
          [&::-webkit-slider-thumb]:appearance-none
          [&::-webkit-slider-thumb]:w-3
          [&::-webkit-slider-thumb]:h-3
          [&::-webkit-slider-thumb]:rounded-full
          [&::-webkit-slider-thumb]:bg-white
          [&::-webkit-slider-thumb]:cursor-pointer
          [&::-webkit-slider-thumb]:shadow-md
        "
      />
    </div>
  );
}

export function AdjustmentsPanel() {
  const { layers, selectedLayerIds, updateLayer } = useEditorStore();

  const selectedLayer = selectedLayerIds.length === 1
    ? layers.find((l) => l.id === selectedLayerIds[0])
    : null;

  const isImageLayer = selectedLayer?.type === 'image';
  const imageData = isImageLayer ? (selectedLayer.data as ImageLayerData) : null;
  const filters = imageData?.filters || DEFAULT_FILTERS;

  const updateFilter = (key: keyof ImageFilters, value: number) => {
    if (!selectedLayer || !isImageLayer) return;

    updateLayer(selectedLayer.id, {
      data: {
        ...selectedLayer.data,
        filters: {
          ...filters,
          [key]: value,
        },
      } as ImageLayerData,
    });
  };

  const resetFilters = () => {
    if (!selectedLayer || !isImageLayer) return;

    updateLayer(selectedLayer.id, {
      data: {
        ...selectedLayer.data,
        filters: DEFAULT_FILTERS,
      } as ImageLayerData,
    });
  };

  const hasChanges = filters.brightness !== 0 || filters.contrast !== 0 ||
    filters.saturation !== 0 || filters.blur !== 0;

  if (!isImageLayer) {
    return (
      <div className="p-4">
        <h2 className="text-lg font-semibold text-white mb-4">Adjustments</h2>
        <div className="flex flex-col items-center justify-center py-12 text-neutral-500">
          <Sparkles className="w-12 h-12 mb-2 opacity-50" />
          <span className="text-sm text-center">Select an image layer to adjust its appearance</span>
        </div>
      </div>
    );
  }

  return (
    <div className="p-4">
      <div className="flex items-center justify-between mb-4">
        <h2 className="text-lg font-semibold text-white">Adjustments</h2>
        {hasChanges && (
          <button
            onClick={resetFilters}
            className="flex items-center gap-1 px-2 py-1 rounded text-xs text-neutral-400 hover:text-white hover:bg-neutral-800 transition-colors"
          >
            <RotateCcw className="w-3 h-3" />
            Reset
          </button>
        )}
      </div>

      {/* Selected layer info */}
      <div className="mb-4 p-2 rounded-lg bg-neutral-800/50 border border-neutral-700/50">
        <p className="text-xs text-neutral-400">
          Editing: <span className="text-white">{selectedLayer.name}</span>
        </p>
      </div>

      {/* Filter controls */}
      <div className="space-y-5">
        <FilterSlider
          label="Brightness"
          icon={<Sun className="w-4 h-4" />}
          value={filters.brightness}
          min={-100}
          max={100}
          onChange={(v) => updateFilter('brightness', v)}
        />

        <FilterSlider
          label="Contrast"
          icon={<Contrast className="w-4 h-4" />}
          value={filters.contrast}
          min={-100}
          max={100}
          onChange={(v) => updateFilter('contrast', v)}
        />

        <FilterSlider
          label="Saturation"
          icon={<Droplets className="w-4 h-4" />}
          value={filters.saturation}
          min={-100}
          max={100}
          onChange={(v) => updateFilter('saturation', v)}
        />

        <FilterSlider
          label="Blur"
          icon={<Sparkles className="w-4 h-4" />}
          value={filters.blur}
          min={0}
          max={20}
          unit="px"
          onChange={(v) => updateFilter('blur', v)}
        />
      </div>

      {/* Presets */}
      <div className="mt-6">
        <label className="text-xs text-neutral-500 uppercase tracking-wider block mb-2">
          Quick Presets
        </label>
        <div className="grid grid-cols-2 gap-2">
          <button
            onClick={() => {
              if (!selectedLayer) return;
              updateLayer(selectedLayer.id, {
                data: {
                  ...selectedLayer.data,
                  filters: { brightness: 10, contrast: 10, saturation: 15, blur: 0 },
                } as ImageLayerData,
              });
            }}
            className="px-3 py-2 rounded-lg bg-neutral-800 hover:bg-neutral-700 text-white text-xs transition-colors"
          >
            Vibrant
          </button>
          <button
            onClick={() => {
              if (!selectedLayer) return;
              updateLayer(selectedLayer.id, {
                data: {
                  ...selectedLayer.data,
                  filters: { brightness: 5, contrast: -10, saturation: -100, blur: 0 },
                } as ImageLayerData,
              });
            }}
            className="px-3 py-2 rounded-lg bg-neutral-800 hover:bg-neutral-700 text-white text-xs transition-colors"
          >
            B&W
          </button>
          <button
            onClick={() => {
              if (!selectedLayer) return;
              updateLayer(selectedLayer.id, {
                data: {
                  ...selectedLayer.data,
                  filters: { brightness: 15, contrast: -15, saturation: -20, blur: 0 },
                } as ImageLayerData,
              });
            }}
            className="px-3 py-2 rounded-lg bg-neutral-800 hover:bg-neutral-700 text-white text-xs transition-colors"
          >
            Faded
          </button>
          <button
            onClick={() => {
              if (!selectedLayer) return;
              updateLayer(selectedLayer.id, {
                data: {
                  ...selectedLayer.data,
                  filters: { brightness: -10, contrast: 20, saturation: 10, blur: 0 },
                } as ImageLayerData,
              });
            }}
            className="px-3 py-2 rounded-lg bg-neutral-800 hover:bg-neutral-700 text-white text-xs transition-colors"
          >
            Dramatic
          </button>
        </div>
      </div>

      {/* Info */}
      <div className="mt-6 p-3 rounded-lg bg-neutral-800/50 border border-neutral-700/50">
        <h3 className="text-sm font-medium text-white mb-2">Tips</h3>
        <ul className="text-xs text-neutral-400 space-y-1">
          <li>• Adjustments are non-destructive</li>
          <li>• Use brightness for exposure fixes</li>
          <li>• Contrast adds depth to images</li>
          <li>• Saturation controls color intensity</li>
        </ul>
      </div>
    </div>
  );
}

About

A web app to help you design things, local, offline, on device. In your browser.

0 stars
0 forks