← back to Norma
components/email-analyzer/LayoutSettings.tsx
230 lines
'use client';
import { useEffect, useState } from 'react';
import { Settings, ChevronDown, ChevronRight, ArrowLeft, ArrowRight, Eye, EyeOff, RotateCcw } from 'lucide-react';
export type PanelId =
| 'original'
| 'stats'
| 'generated'
| 'proscons'
| 'chat'
| 'score'
| 'controls'
| 'suggestions'
| 'references'
| 'versions';
export type Column = 'left' | 'right';
export interface PanelSetting {
id: PanelId;
enabled: boolean;
column: Column;
}
export const PANEL_LABELS: Record<PanelId, string> = {
original: 'Original Email (paste box)',
stats: 'Analysis Stats',
generated: 'Generated Version',
proscons: 'Version Comparison (Pros/Cons)',
chat: 'Rewrite Chat',
score: 'Version Score & Thermometer',
controls: 'Rewrite Controls',
suggestions: 'Auto-Suggested References',
references: 'Reference Materials',
versions: 'Versions Timeline',
};
export const DEFAULT_LAYOUT: PanelSetting[] = [
{ id: 'original', enabled: true, column: 'left' },
{ id: 'stats', enabled: true, column: 'left' },
{ id: 'generated', enabled: true, column: 'left' },
{ id: 'proscons', enabled: true, column: 'left' },
{ id: 'chat', enabled: true, column: 'right' },
{ id: 'score', enabled: true, column: 'right' },
{ id: 'controls', enabled: true, column: 'right' },
{ id: 'suggestions', enabled: true, column: 'right' },
{ id: 'references', enabled: true, column: 'right' },
{ id: 'versions', enabled: true, column: 'right' },
];
const CACHE_KEY = 'norma-analyzer-layout-v1';
export function loadLayout(): PanelSetting[] {
if (typeof window === 'undefined') return DEFAULT_LAYOUT;
try {
const raw = localStorage.getItem(CACHE_KEY);
if (!raw) return DEFAULT_LAYOUT;
const parsed = JSON.parse(raw) as PanelSetting[];
// Ensure every panel is present (new panels default to enabled)
const byId = new Map(parsed.map((p) => [p.id, p]));
return DEFAULT_LAYOUT.map((d) => byId.get(d.id) ?? d);
} catch {
return DEFAULT_LAYOUT;
}
}
export function saveLayout(layout: PanelSetting[]): void {
try {
localStorage.setItem(CACHE_KEY, JSON.stringify(layout));
} catch { /* noop */ }
}
interface LayoutSettingsProps {
layout: PanelSetting[];
onChange: (next: PanelSetting[]) => void;
}
export default function LayoutSettings({ layout, onChange }: LayoutSettingsProps) {
const [collapsed, setCollapsed] = useState(true);
function updatePanel(id: PanelId, patch: Partial<PanelSetting>) {
onChange(layout.map((p) => (p.id === id ? { ...p, ...patch } : p)));
}
function reset() {
onChange(DEFAULT_LAYOUT);
}
function move(id: PanelId, dir: -1 | 1) {
const idx = layout.findIndex((p) => p.id === id);
const target = idx + dir;
if (idx < 0 || target < 0 || target >= layout.length) return;
const next = [...layout];
[next[idx], next[target]] = [next[target], next[idx]];
onChange(next);
}
return (
<div className="card" style={{ padding: 12 }}>
<div
role="button"
tabIndex={0}
onClick={() => setCollapsed((p) => !p)}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setCollapsed((p) => !p); } }}
style={{ display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer' }}
>
{collapsed ? <ChevronRight size={14} style={{ color: 'var(--color-text-muted)' }} /> : <ChevronDown size={14} style={{ color: 'var(--color-text-muted)' }} />}
<Settings size={14} style={{ color: 'var(--color-text-muted)' }} />
<span style={{ fontSize: 13, fontWeight: 600, color: 'var(--color-text)' }}>
Layout Settings
</span>
<span style={{ fontSize: 11, color: 'var(--color-text-muted)' }}>
— show/hide & rearrange panels
</span>
{!collapsed && (
<button
type="button"
className="btn btn-ghost btn-sm"
onClick={(e) => { e.stopPropagation(); reset(); }}
style={{ marginLeft: 'auto', minHeight: 24, padding: '2px 8px', fontSize: 11, gap: 4 }}
title="Reset to defaults"
>
<RotateCcw size={11} />
Reset
</button>
)}
</div>
{!collapsed && (
<div style={{ marginTop: 10, display: 'flex', flexDirection: 'column', gap: 4 }}>
<div style={{ fontSize: 10, color: 'var(--color-text-muted)', marginBottom: 4 }}>
Click a column name to move a panel. Use ↑↓ to reorder within the column.
</div>
{layout.map((p, i) => (
<div
key={p.id}
style={{
display: 'flex',
alignItems: 'center',
gap: 6,
padding: '5px 8px',
borderRadius: 'var(--radius-sm)',
background: p.enabled ? 'var(--color-surface-el)' : 'transparent',
border: `1px solid ${p.enabled ? 'var(--color-border)' : 'transparent'}`,
opacity: p.enabled ? 1 : 0.5,
}}
>
<button
type="button"
onClick={() => updatePanel(p.id, { enabled: !p.enabled })}
title={p.enabled ? 'Hide this panel' : 'Show this panel'}
style={{
background: 'transparent',
border: 'none',
cursor: 'pointer',
color: p.enabled ? 'var(--color-primary)' : 'var(--color-text-muted)',
padding: 2,
display: 'flex',
}}
>
{p.enabled ? <Eye size={13} /> : <EyeOff size={13} />}
</button>
<span style={{ fontSize: 11, color: 'var(--color-text)', flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{PANEL_LABELS[p.id]}
</span>
<button
type="button"
onClick={() => updatePanel(p.id, { column: p.column === 'left' ? 'right' : 'left' })}
title={`Move to ${p.column === 'left' ? 'right' : 'left'} column`}
style={{
background: 'var(--color-surface)',
border: '1px solid var(--color-border)',
borderRadius: 4,
cursor: 'pointer',
padding: '1px 6px',
fontSize: 10,
color: 'var(--color-text-secondary)',
display: 'flex',
alignItems: 'center',
gap: 3,
}}
>
{p.column === 'left' ? <ArrowRight size={10} /> : <ArrowLeft size={10} />}
{p.column}
</button>
<button
type="button"
onClick={() => move(p.id, -1)}
disabled={i === 0}
title="Move up"
style={{
background: 'transparent',
border: 'none',
cursor: i === 0 ? 'not-allowed' : 'pointer',
color: 'var(--color-text-muted)',
padding: 2,
display: 'flex',
fontSize: 12,
lineHeight: 1,
}}
>
↑
</button>
<button
type="button"
onClick={() => move(p.id, 1)}
disabled={i === layout.length - 1}
title="Move down"
style={{
background: 'transparent',
border: 'none',
cursor: i === layout.length - 1 ? 'not-allowed' : 'pointer',
color: 'var(--color-text-muted)',
padding: 2,
display: 'flex',
fontSize: 12,
lineHeight: 1,
}}
>
↓
</button>
</div>
))}
</div>
)}
</div>
);
}