From a329018f01eec61260a34f8e2f7272ed07bbb737 Mon Sep 17 00:00:00 2001
From: a5sh
Date: Fri, 8 May 2026 23:42:48 +0530
Subject: [PATCH] Refactor builder panel architecture
---
src/components/builder/AdvancedBuilderApp.tsx | 956 ------------------
.../builder/components/LayerPanel.tsx | 519 +++++-----
.../builder/components/layout/Inspector.tsx | 88 +-
.../navigation/BuilderModeToggle.tsx | 45 +
.../components/navigation/PanelSwitcher.tsx | 56 +
.../builder/context/EditorContext.tsx | 2 +-
src/components/builder/index.tsx | 95 +-
.../builder/panels/AdvancedPanelList.tsx | 118 +++
.../builder/panels/AdvancedPanelRenderer.tsx | 76 ++
.../builder/panels/left/LayersPanel.tsx | 17 +
.../builder/panels/left/PosterPanel.tsx | 17 +
.../builder/panels/left/SourcePanel.tsx | 17 +
.../builder/panels/right/BadgesPanel.tsx | 15 +
.../builder/panels/right/SelectionPanel.tsx | 15 +
src/components/builder/types.ts | 2 +
src/pages/abuild.astro | 46 +-
16 files changed, 769 insertions(+), 1315 deletions(-)
delete mode 100644 src/components/builder/AdvancedBuilderApp.tsx
create mode 100644 src/components/builder/components/navigation/BuilderModeToggle.tsx
create mode 100644 src/components/builder/components/navigation/PanelSwitcher.tsx
create mode 100644 src/components/builder/panels/AdvancedPanelList.tsx
create mode 100644 src/components/builder/panels/AdvancedPanelRenderer.tsx
create mode 100644 src/components/builder/panels/left/LayersPanel.tsx
create mode 100644 src/components/builder/panels/left/PosterPanel.tsx
create mode 100644 src/components/builder/panels/left/SourcePanel.tsx
create mode 100644 src/components/builder/panels/right/BadgesPanel.tsx
create mode 100644 src/components/builder/panels/right/SelectionPanel.tsx
diff --git a/src/components/builder/AdvancedBuilderApp.tsx b/src/components/builder/AdvancedBuilderApp.tsx
deleted file mode 100644
index d17759d6..00000000
--- a/src/components/builder/AdvancedBuilderApp.tsx
+++ /dev/null
@@ -1,956 +0,0 @@
-// src/components/builder/AdvancedBuilderApp.tsx
-// Advanced Builder — vertical panel nav on the left, panel content on the right.
-// No horizontal tab bars inside panels.
-
-import React, { useState, useEffect, useRef, useCallback, memo } from 'react';
-import clsx from 'clsx';
-import type { PosterConfig, ExtensionType, ApiKeys, RatingType } from './types';
-import {
- DEFAULT_CONFIG, ALL_BADGES,
- CANVAS_WIDTH, CANVAS_HEIGHT, BASE_BADGE_W, BASE_BADGE_H,
-} from './types';
-import { parseUrlToConfig, DEFAULT_API_BASE, calculateAutoPosition, getScale } from './utils';
-import PreviewCanvas from './components/PreviewCanvas';
-import LayerPanel from './components/LayerPanel';
-import Inspector from './components/layout/Inspector';
-import MobileDock from './components/layout/MobileDock';
-import KeyboardShortcutsModal from './components/KeyboardShortcutsModal';
-import ResetDialog from './components/ResetDialogue';
-import ImportDialog from './components/ImportDialogue';
-import ExportPopover from './components/ExportPopover';
-import { EditorProvider, useEditor } from './context/EditorContext';
-import {
- Film, Layers, Monitor, Sliders, MousePointer2,
- RotateCcw, Undo2, Redo2, Maximize2, Minimize2, ZoomIn, ZoomOut,
- Grid3x3, ShieldCheck, Eye, EyeOff, CheckSquare, MousePointer2Off,
- Download, Contrast, Keyboard, ChevronDown, Search, PanelRight,
-} from 'lucide-react';
-import { usePosterHistory } from './hooks/usePosterHistory';
-import ContextMenu, { type ContextMenuState, type LayerTargetId } from './components/ContextMenu';
-import CommandPalette, { type PaletteCommand } from './components/CommandPalette';
-
-// ── Constants ─────────────────────────────────────────────────────────────────
-const STORAGE_KEY = 'posterium_config_v2'; // shared with main builder
-const COOKIE_KEY = 'posterium_apikeys_v1';
-const MAX_QUERY_CONFIG_LENGTH = 12000;
-
-const saveKeysToCookie = (keys: ApiKeys) => {
- try {
- const val = encodeURIComponent(JSON.stringify(keys));
- const exp = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toUTCString();
- document.cookie = `${COOKIE_KEY}=${val}; expires=${exp}; path=/; SameSite=Strict`;
- } catch {}
-};
-const loadKeysFromCookie = (): ApiKeys => {
- try {
- const match = document.cookie.match(new RegExp(`(?:^|; )${COOKIE_KEY}=([^;]*)`));
- if (!match) return {};
- return JSON.parse(decodeURIComponent(match[1])) || {};
- } catch {
- return {};
- }
-};
-
-// ── Panel definitions ─────────────────────────────────────────────────────────
-const ADV_PANELS = [
- { id: 'source' as const, label: 'Source', Icon: Film, desc: 'Media & poster source' },
- { id: 'layers' as const, label: 'Layers', Icon: Layers, desc: 'Badge & logo layers' },
- { id: 'poster' as const, label: 'Canvas', Icon: Monitor, desc: 'Overlays & effects' },
- { id: 'badges' as const, label: 'Badges', Icon: Sliders, desc: 'Global badge style' },
- { id: 'selection' as const, label: 'Selection', Icon: MousePointer2, desc: 'Selected layer config' },
-] as const;
-
-// ── ToolbarBtn ────────────────────────────────────────────────────────────────
-const ToolbarBtn = memo<{
- onClick?: () => void;
- disabled?: boolean;
- label: string;
- danger?: boolean;
- href?: string;
- active?: boolean;
- children: React.ReactNode;
- hideOnMobile?: boolean;
-}>(({ onClick, disabled, label, danger, href, active, children, hideOnMobile = false }) => {
- const cls = `relative group w-8 h-8 flex items-center justify-center rounded-lg transition-all duration-150 select-none outline-none focus-visible:ring-2 focus-visible:ring-[#C47C2E] ${hideOnMobile ? 'hidden lg:flex' : ''} ${disabled ? 'cursor-not-allowed pointer-events-none' : 'active:scale-95 cursor-pointer'}`;
- const activeStyle = active
- ? { color: 'var(--film-amber)', background: 'rgba(196,124,46,0.1)', border: '1px solid rgba(196,124,46,0.2)' }
- : disabled
- ? { color: 'rgba(255,255,255,0.15)', border: '1px solid transparent', opacity: 0.5 }
- : { color: 'var(--film-text-dim)', border: '1px solid transparent' };
- const tooltip = !disabled && (
-
- {label}
-
- );
- const hoverEvents = !disabled && !active ? {
- onMouseEnter: (e: React.MouseEvent) => {
- const el = e.currentTarget as HTMLElement;
- if (danger) { el.style.color = 'rgba(248,113,113,0.8)'; el.style.background = 'rgba(248,113,113,0.08)'; }
- else { el.style.color = 'var(--film-text-label)'; el.style.background = 'rgba(196,124,46,0.07)'; }
- },
- onMouseLeave: (e: React.MouseEvent) => {
- const el = e.currentTarget as HTMLElement;
- el.style.color = 'var(--film-text-dim)'; el.style.background = 'transparent';
- },
- } : {};
- if (href) return {children}{tooltip} ;
- return {children}{tooltip} ;
-});
-ToolbarBtn.displayName = 'ToolbarBtn';
-
-// ── AdvancedNavSidebar ────────────────────────────────────────────────────────
-const AdvancedNavSidebar = memo<{
- config: PosterConfig;
- selectedCount: number;
-}>(({ config, selectedCount }) => {
- const { activeTab, setActiveTab } = useEditor();
-
- const activePanel = (() => {
- if (activeTab === 'logo') return 'selection';
- if (['source', 'layers', 'poster', 'badges', 'selection'].includes(activeTab)) return activeTab;
- return 'source';
- })();
-
- return (
-
- {/* Strip header */}
-
-
- Panels
-
-
-
- {/* Nav list */}
-
- {ADV_PANELS.map(({ id, label, Icon, desc }) => {
- const isActive = activePanel === id;
- const badge = id === 'selection' && selectedCount > 0 ? selectedCount : null;
- return (
- setActiveTab(id as any)}
- aria-current={isActive ? 'true' : undefined}
- style={{
- width: '100%',
- display: 'flex',
- alignItems: 'center',
- gap: 10,
- padding: '10px 14px',
- background: isActive ? 'rgba(196,124,46,0.07)' : 'transparent',
- border: 'none',
- borderLeft: `3px solid ${isActive ? 'var(--film-amber)' : 'transparent'}`,
- cursor: 'pointer',
- color: isActive ? 'var(--film-cream)' : 'var(--film-text-dim)',
- textAlign: 'left',
- transition: 'all 0.12s ease',
- }}
- onMouseEnter={e => {
- if (!isActive) {
- e.currentTarget.style.background = 'rgba(196,124,46,0.04)';
- e.currentTarget.style.color = 'var(--film-text-label)';
- }
- }}
- onMouseLeave={e => {
- if (!isActive) {
- e.currentTarget.style.background = 'transparent';
- e.currentTarget.style.color = 'var(--film-text-dim)';
- }
- }}
- >
- {/* Icon box */}
-
-
-
-
- {/* Label + desc */}
-
-
-
- {label}
-
- {badge !== null && (
-
- {badge}
-
- )}
-
-
- {desc}
-
-
-
- {/* Active indicator dot */}
- {isActive && (
-
- )}
-
- );
- })}
-
-
- {/* Footer info */}
-
-
- {config.ratings.length} badge{config.ratings.length !== 1 ? 's' : ''} · advanced
-
-
-
- );
-});
-AdvancedNavSidebar.displayName = 'AdvancedNavSidebar';
-
-// ── AdvancedRightPanel ────────────────────────────────────────────────────────
-const AdvancedRightPanel = memo<{
- config: PosterConfig;
- setConfig: React.Dispatch>;
- selectedIds: Set;
- onSelect: (id: RatingType, multi: boolean) => void;
-}>(({ config, setConfig, selectedIds, onSelect }) => {
- const { activeTab } = useEditor();
- const isLayerPanel = ['source', 'layers', 'poster'].includes(activeTab);
-
- const panelMeta = ADV_PANELS.find(p =>
- p.id === activeTab || (activeTab === 'logo' && p.id === 'selection')
- ) ?? ADV_PANELS[0];
- const PanelIcon = panelMeta.Icon;
-
- return (
-
- {/* Panel label strip */}
-
-
-
- {panelMeta.label}
-
-
- {panelMeta.desc}
-
-
-
- {/* Panel content — no tab bar */}
-
- {isLayerPanel ? (
-
- ) : (
-
- )}
-
-
- );
-});
-AdvancedRightPanel.displayName = 'AdvancedRightPanel';
-
-// ── AdvancedStudioLayout ──────────────────────────────────────────────────────
-const AdvancedStudioLayout: React.FC<{
- config: PosterConfig;
- setConfig: React.Dispatch>;
- handleReset: () => void;
- baseUrl: string;
- handleLoadConfig: (url: string) => void;
- undo: () => void;
- redo: () => void;
- canUndo: boolean;
- canRedo: boolean;
-}> = ({ config, setConfig, handleReset, baseUrl, handleLoadConfig, undo, redo, canUndo, canRedo }) => {
- const {
- activeTab, setActiveTab,
- mobileSheetMode, setMobileSheetMode,
- selectedIds, selectedLogo, selectedMinimalElements,
- handleSelection, handleLogoSelection,
- clearSelection, setBatchSelection,
- viewOptions, toggleViewOption,
- } = useEditor();
-
- // ── UI state ────────────────────────────────────────────────────────────
- const [isResetOpen, setIsResetOpen] = useState(false);
- const [isImportOpen, setIsImportOpen] = useState(false);
- const [isFullscreen, setIsFullscreen] = useState(false);
- const [shortcutsOpen, setShortcutsOpen] = useState(false);
- const [exportOpen, setExportOpen] = useState(false);
- const [paletteOpen, setPaletteOpen] = useState(false);
- const [navVisible, setNavVisible] = useState(true);
- const [rightVisible, setRightVisible] = useState(true);
- const [rightW, setRightW] = useState(300);
- const [isDesktop, setIsDesktop] = useState(() => typeof window !== 'undefined' && window.innerWidth >= 1024);
-
- const importBtnRef = useRef(null);
- const exportBtnRef = useRef(null);
- const toggleFullscreen = useCallback(() => setIsFullscreen(v => !v), []);
-
- // Stable refs for keyboard shortcuts
- const selectedIdsRef = useRef(selectedIds);
- const selectedLogoRef = useRef(selectedLogo);
- const selectedMinimalElementsRef = useRef(selectedMinimalElements);
- const configRatingsRef = useRef(config.ratings);
- useEffect(() => { selectedIdsRef.current = selectedIds; });
- useEffect(() => { selectedLogoRef.current = selectedLogo; });
- useEffect(() => { selectedMinimalElementsRef.current = selectedMinimalElements; });
- useEffect(() => { configRatingsRef.current = config.ratings; });
-
- useEffect(() => {
- const mq = window.matchMedia('(min-width: 1024px)');
- const h = (e: MediaQueryListEvent) => setIsDesktop(e.matches);
- mq.addEventListener('change', h);
- return () => mq.removeEventListener('change', h);
- }, []);
-
- // ── Context menu ────────────────────────────────────────────────────────
- const [ctxMenu, setCtxMenu] = useState({ visible: false, x: 0, y: 0, badgeId: null });
- const openCtxMenu = useCallback((badgeId: LayerTargetId, e: React.MouseEvent) => {
- e.preventDefault();
- setCtxMenu({ visible: true, x: e.clientX, y: e.clientY, badgeId });
- }, []);
- const closeCtxMenu = useCallback(() => setCtxMenu(s => ({ ...s, visible: false })), []);
-
- // ── Layer helpers (same logic as main builder) ──────────────────────────
- const handleSelectionOverride = useCallback((id: RatingType, multi: boolean) => {
- handleSelection(id, multi);
- }, [handleSelection]);
-
- const moveLayer = useCallback((id: RatingType, dir: 'front' | 'forward' | 'back' | 'toback') => {
- setConfig(prev => {
- const arr = [...prev.ratings];
- const idx = arr.indexOf(id);
- if (idx === -1) return prev;
- arr.splice(idx, 1);
- if (dir === 'front') arr.push(id);
- else if (dir === 'forward') arr.splice(Math.min(idx + 1, arr.length), 0, id);
- else if (dir === 'back') arr.splice(Math.max(idx - 1, 0), 0, id);
- else arr.unshift(id);
- return { ...prev, ratings: arr };
- });
- }, [setConfig]);
-
- const hideBadge = useCallback((id: RatingType) => {
- setConfig(prev => ({ ...prev, ratings: prev.ratings.filter(r => r !== id) }));
- clearSelection();
- }, [setConfig, clearSelection]);
-
- const showAllBadges = useCallback(() => {
- setConfig(prev => ({
- ...prev,
- ratings: ALL_BADGES.map(b => b.id).filter(id => prev.ratings.includes(id) || !prev.ratings.includes(id)),
- }));
- }, [setConfig]);
-
- const resetBadge = useCallback((id: RatingType) => {
- setConfig(prev => { const ni = { ...prev.items }; delete ni[id]; return { ...prev, items: ni }; });
- }, [setConfig]);
-
- const deleteBadge = useCallback((id: RatingType) => {
- setConfig(prev => ({ ...prev, ratings: prev.ratings.filter(r => r !== id) }));
- clearSelection();
- }, [setConfig, clearSelection]);
-
- const moveLogoLayer = useCallback((dir: 'front' | 'forward' | 'back' | 'toback') => {
- setConfig(prev => {
- const c = prev.logoZ ?? 90;
- if (dir === 'front') return { ...prev, logoZ: 220 };
- if (dir === 'toback') return { ...prev, logoZ: 1 };
- if (dir === 'forward') return { ...prev, logoZ: Math.min(220, c + 1) };
- return { ...prev, logoZ: Math.max(1, c - 1) };
- });
- }, [setConfig]);
-
- const hideLayer = useCallback((id: LayerTargetId) => {
- if (id === 'logo') { setConfig(prev => ({ ...prev, logo: false })); clearSelection(); return; }
- hideBadge(id);
- }, [setConfig, clearSelection, hideBadge]);
-
- const resetLayer = useCallback((id: LayerTargetId) => {
- if (id === 'logo') {
- setConfig(prev => ({
- ...prev,
- logoX: DEFAULT_CONFIG.logoX, logoY: DEFAULT_CONFIG.logoY,
- logoW: DEFAULT_CONFIG.logoW, logoH: DEFAULT_CONFIG.logoH,
- logoOpacity: DEFAULT_CONFIG.logoOpacity, logoZ: DEFAULT_CONFIG.logoZ,
- logoShadow: DEFAULT_CONFIG.logoShadow, logoBgEnabled: DEFAULT_CONFIG.logoBgEnabled,
- logoBgColor: DEFAULT_CONFIG.logoBgColor, logoBgOpacity: DEFAULT_CONFIG.logoBgOpacity,
- logoBgRadius: DEFAULT_CONFIG.logoBgRadius, logoBgPadding: DEFAULT_CONFIG.logoBgPadding,
- logoBgBorderW: DEFAULT_CONFIG.logoBgBorderW, logoBgBorderC: DEFAULT_CONFIG.logoBgBorderC,
- logoBgShadow: DEFAULT_CONFIG.logoBgShadow,
- }));
- return;
- }
- resetBadge(id);
- }, [setConfig, resetBadge]);
-
- const deleteLayer = useCallback((id: LayerTargetId) => {
- if (id === 'logo') { setConfig(prev => ({ ...prev, logo: false })); clearSelection(); return; }
- deleteBadge(id);
- }, [setConfig, clearSelection, deleteBadge]);
-
- // ── Nudge ────────────────────────────────────────────────────────────────
- const nudgeSelection = useCallback((dx: number, dy: number) => {
- const activeBadges = Array.from(selectedIdsRef.current);
- const hasLogo = selectedLogoRef.current;
- if (activeBadges.length === 0 && !hasLogo) return;
- setConfig(prev => {
- const next: PosterConfig = { ...prev, items: { ...prev.items } };
- if (activeBadges.length > 0) {
- activeBadges.forEach(id => {
- const base = next.items[id] ?? {};
- const idx = next.ratings.indexOf(id);
- const auto = calculateAutoPosition(id, Math.max(0, idx), next.ratings.length, next);
- const currX = base.x ?? auto.x, currY = base.y ?? auto.y;
- const scale = getScale(next.size) * (base.scale ?? next.scale ?? 1.0);
- const w = BASE_BADGE_W * scale, h = BASE_BADGE_H * scale;
- next.items[id] = {
- ...base,
- x: Math.max(1 - w, Math.min(currX + dx, CANVAS_WIDTH - 1)),
- y: Math.max(1 - h, Math.min(currY + dy, CANVAS_HEIGHT - 1)),
- };
- });
- next.layout = 'custom'; next.preset = 'custom';
- }
- if (hasLogo) {
- const cx = next.logoX !== null && next.logoX !== undefined
- ? next.logoX : Math.round((CANVAS_WIDTH - next.logoW) / 2);
- next.logoX = Math.max(1 - next.logoW, Math.min(cx + dx, CANVAS_WIDTH - 1));
- next.logoY = Math.max(1 - next.logoH, Math.min(next.logoY + dy, CANVAS_HEIGHT - 1));
- }
- return next;
- });
- }, [setConfig]);
-
- // ── Zoom helpers ────────────────────────────────────────────────────────
- const dispatchZoom = useCallback((delta: number) => window.dispatchEvent(new CustomEvent('canvas-zoom', { detail: delta })), []);
- const dispatchResetView = useCallback(() => window.dispatchEvent(new CustomEvent('reset-canvas-view')), []);
-
- // ── Right panel resize ──────────────────────────────────────────────────
- const startResizeRight = useCallback((e: React.MouseEvent) => {
- e.preventDefault();
- const sx = e.clientX, sw = rightW;
- const move = (m: MouseEvent) => setRightW(Math.max(260, Math.min(sw - (m.clientX - sx), 540)));
- const up = () => {
- document.removeEventListener('mousemove', move);
- document.removeEventListener('mouseup', up);
- document.body.style.cursor = '';
- };
- document.addEventListener('mousemove', move);
- document.addEventListener('mouseup', up);
- document.body.style.cursor = 'col-resize';
- }, [rightW]);
-
- const handleExtensionChange = useCallback((ext: ExtensionType) => {
- setConfig(prev => ({ ...prev, extension: ext }));
- }, [setConfig]);
-
- // ── Keyboard shortcuts ──────────────────────────────────────────────────
- useEffect(() => {
- const onKey = (e: KeyboardEvent) => {
- const t = e.target as HTMLElement;
- const inInput = t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable;
- const mod = e.ctrlKey || e.metaKey;
-
- if (e.key === 'Escape') {
- if (shortcutsOpen) { setShortcutsOpen(false); return; }
- if (paletteOpen) { setPaletteOpen(false); return; }
- if (exportOpen) { setExportOpen(false); return; }
- if (isFullscreen) { setIsFullscreen(false); return; }
- if (selectedIdsRef.current.size > 0 || selectedLogoRef.current) { clearSelection(); return; }
- return;
- }
- if (mod && (e.key.toLowerCase() === 'k' || e.key.toLowerCase() === 'p')) {
- e.preventDefault(); setPaletteOpen(v => !v); return;
- }
- if (mod && (e.key === '/' || e.key === '?')) {
- e.preventDefault(); setShortcutsOpen(v => !v); return;
- }
- if (inInput) return;
- if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key) &&
- (selectedIdsRef.current.size > 0 || selectedLogoRef.current || selectedMinimalElementsRef.current.size > 0)) {
- e.preventDefault();
- const step = e.shiftKey ? 10 : 1;
- if (e.key === 'ArrowUp') nudgeSelection(0, -step);
- else if (e.key === 'ArrowDown') nudgeSelection(0, step);
- else if (e.key === 'ArrowLeft') nudgeSelection(-step, 0);
- else nudgeSelection(step, 0);
- return;
- }
- if (mod && e.key.toLowerCase() === 'a') { e.preventDefault(); setBatchSelection(configRatingsRef.current); return; }
- if (mod && e.key.toLowerCase() === 'd') { e.preventDefault(); clearSelection(); return; }
- if (mod && e.key.toLowerCase() === 'z' && !e.shiftKey) { e.preventDefault(); undo(); return; }
- if (mod && (e.key.toLowerCase() === 'y' || (e.key.toLowerCase() === 'z' && e.shiftKey))) { e.preventDefault(); redo(); return; }
- if ((e.key === 'Delete' || e.key === 'Backspace') && (selectedIdsRef.current.size > 0 || selectedLogoRef.current)) {
- e.preventDefault();
- const rm = new Set(selectedIdsRef.current);
- if (rm.size > 0) setConfig(p => ({ ...p, ratings: p.ratings.filter(r => !rm.has(r)) }));
- clearSelection(); return;
- }
- if (selectedIdsRef.current.size > 0) {
- const sel = Array.from(selectedIdsRef.current);
- if (mod && e.shiftKey && e.key === ']') { e.preventDefault(); sel.forEach(id => moveLayer(id as RatingType, 'front')); return; }
- if (mod && e.shiftKey && e.key === '[') { e.preventDefault(); sel.forEach(id => moveLayer(id as RatingType, 'toback')); return; }
- if (mod && e.key === ']') { e.preventDefault(); sel.forEach(id => moveLayer(id as RatingType, 'forward')); return; }
- if (mod && e.key === '[') { e.preventDefault(); sel.forEach(id => moveLayer(id as RatingType, 'back')); return; }
- if (e.key.toLowerCase() === 'h' && !mod) { e.preventDefault(); sel.forEach(id => hideBadge(id as RatingType)); return; }
- }
- if (e.key.toLowerCase() === 'f' && !mod && isDesktop) { e.preventDefault(); setIsFullscreen(v => !v); return; }
- if (e.key.toLowerCase() === 'g' && !mod) { e.preventDefault(); toggleViewOption('showGrid'); return; }
- if (e.key === "'" && !mod) { e.preventDefault(); toggleViewOption('showSafeArea'); return; }
- if (mod && e.key === '1') { e.preventDefault(); dispatchResetView(); return; }
- if (mod && (e.key === '+' || e.key === '=')) { e.preventDefault(); dispatchZoom(0.25); return; }
- if (mod && e.key === '-') { e.preventDefault(); dispatchZoom(-0.25); return; }
- if (e.key === ']' && !mod && !e.shiftKey) { e.preventDefault(); setRightVisible(v => !v); return; }
- if (e.key === 'Tab' && !mod) {
- const ratings = configRatingsRef.current;
- if (!ratings.length) return;
- e.preventDefault();
- const selArr = Array.from(selectedIdsRef.current);
- const lastSel = selArr[selArr.length - 1];
- const idx = lastSel ? ratings.indexOf(lastSel) : -1;
- const next = ratings[(idx + (e.shiftKey ? -1 + ratings.length : 1)) % ratings.length];
- setBatchSelection([next]);
- return;
- }
- };
- window.addEventListener('keydown', onKey);
- return () => window.removeEventListener('keydown', onKey);
- }, [
- undo, redo, setConfig, clearSelection, setBatchSelection,
- moveLayer, hideBadge, toggleViewOption, dispatchZoom, dispatchResetView, nudgeSelection,
- isFullscreen, paletteOpen, shortcutsOpen, exportOpen,
- selectedIds, selectedLogo, selectedMinimalElements, isDesktop,
- ]);
-
- // ── Command palette commands ────────────────────────────────────────────
- const paletteCommands: PaletteCommand[] = [
- // Panel switching
- { id: 'panel-source', label: 'Open Source Panel', category: 'Panels', icon: , action: () => setActiveTab('source') },
- { id: 'panel-layers', label: 'Open Layers Panel', category: 'Panels', icon: , action: () => setActiveTab('layers') },
- { id: 'panel-canvas', label: 'Open Canvas Panel', category: 'Panels', icon: , action: () => setActiveTab('poster') },
- { id: 'panel-badges', label: 'Open Badges Panel', category: 'Panels', icon: , action: () => setActiveTab('badges') },
- { id: 'panel-selection', label: 'Open Selection Panel', category: 'Panels', icon: , action: () => setActiveTab('selection') },
- // View
- { id: 'zoom-fit', label: 'Zoom to Fit', category: 'View & Canvas', icon: , shortcut: '⌘1', action: dispatchResetView },
- { id: 'zoom-in', label: 'Zoom In', category: 'View & Canvas', icon: , shortcut: '⌘+', action: () => dispatchZoom(0.25) },
- { id: 'zoom-out', label: 'Zoom Out', category: 'View & Canvas', icon: , shortcut: '⌘-', action: () => dispatchZoom(-0.25) },
- { id: 'fullscreen', label: isFullscreen ? 'Exit Fullscreen' : 'Enter Fullscreen', category: 'View & Canvas', icon: isFullscreen ? : , shortcut: 'F', action: toggleFullscreen },
- { id: 'grid', label: `${viewOptions.showGrid ? 'Hide' : 'Show'} Grid`, category: 'View & Canvas', icon: , shortcut: 'G', action: () => toggleViewOption('showGrid') },
- { id: 'safe-area', label: `${viewOptions.showSafeArea ? 'Hide' : 'Show'} Safe Area`, category: 'View & Canvas', icon: , action: () => toggleViewOption('showSafeArea') },
- { id: 'right-panel', label: `${rightVisible ? 'Hide' : 'Show'} Right Panel`, category: 'View & Canvas', icon: , shortcut: ']', action: () => setRightVisible(v => !v) },
- // Selection
- { id: 'select-all', label: 'Select All Badges', category: 'Layers & Selection', icon: , shortcut: '⌘A', action: () => setBatchSelection(config.ratings) },
- { id: 'deselect-all', label: 'Deselect All', category: 'Layers & Selection', icon: , shortcut: '⌘D', action: clearSelection },
- { id: 'show-all', label: 'Show All Badges', category: 'Layers & Selection', icon: , action: showAllBadges },
- { id: 'hide-sel', label: 'Hide Selected', category: 'Layers & Selection', icon: , shortcut: 'H', action: () => Array.from(selectedIds).forEach(id => hideBadge(id as RatingType)) },
- // Canvas
- { id: 'grayscale', label: `${config.grayscale ? 'Remove' : 'Apply'} Grayscale`, category: 'Canvas Properties', icon: , action: () => setConfig(p => ({ ...p, grayscale: !p.grayscale })) },
- // Export
- { id: 'export-svg', label: 'Export as SVG', category: 'Export', icon: , action: () => { setConfig(p => ({ ...p, extension: 'svg' })); setExportOpen(true); } },
- { id: 'export-png', label: 'Export as PNG', category: 'Export', icon: , action: () => { setConfig(p => ({ ...p, extension: 'png' })); setExportOpen(true); } },
- { id: 'export-jpg', label: 'Export as JPG', category: 'Export', icon: , action: () => { setConfig(p => ({ ...p, extension: 'jpg' })); setExportOpen(true); } },
- // History
- { id: 'undo', label: 'Undo', category: 'File', icon: , shortcut: '⌘Z', action: undo },
- { id: 'redo', label: 'Redo', category: 'File', icon: , shortcut: '⌘Y', action: redo },
- { id: 'reset', label: 'Reset All Settings', category: 'File', icon: , action: () => setIsResetOpen(true) },
- // App
- { id: 'shortcuts', label: 'Keyboard Shortcuts', category: 'File', icon: , shortcut: '⌘/', action: () => setShortcutsOpen(true) },
- ];
-
- const ctxBadgeSelected = ctxMenu.badgeId
- ? ctxMenu.badgeId === 'logo' ? selectedLogo : selectedIds.has(ctxMenu.badgeId)
- : false;
-
- const selectedCount = selectedIds.size + (selectedLogo ? 1 : 0);
-
- // ── Render ────────────────────────────────────────────────────────────────
- return (
- <>
-
-
-
-
- {/* ── Modals & overlays ── */}
-
setIsResetOpen(false)} onConfirm={handleReset} />
- setIsImportOpen(false)} onLoad={handleLoadConfig} anchorRef={importBtnRef} />
- setShortcutsOpen(false)} />
- id === 'logo' ? moveLogoLayer('front') : moveLayer(id, 'front')}
- onBringForward={id => id === 'logo' ? moveLogoLayer('forward') : moveLayer(id, 'forward')}
- onSendBackward={id => id === 'logo' ? moveLogoLayer('back') : moveLayer(id, 'back')}
- onSendToBack={id => id === 'logo' ? moveLogoLayer('toback') : moveLayer(id, 'toback')}
- onHide={hideLayer} onShowAll={showAllBadges}
- onSelect={id => id === 'logo' ? handleLogoSelection(false) : handleSelectionOverride(id, false)}
- onDeselect={() => clearSelection()}
- onSelectAll={() => setBatchSelection(config.ratings)}
- onDeselectAll={clearSelection}
- onResetBadge={resetLayer}
- onDelete={deleteLayer}
- />
- setPaletteOpen(false)} commands={paletteCommands} />
- setExportOpen(false)}
- anchorRef={exportBtnRef}
- />
-
- {/* ── Header ────────────────────────────────────────────────────── */}
- {!isFullscreen && (
-
-
-
- {/* Left: logo + badges */}
-
-
-
- POSTERIUM
-
-
- P
-
-
- {/* "Advanced" badge */}
-
- Advanced
-
- {/* Switch to standard */}
-
- Standard
-
-
setShortcutsOpen(v => !v)} label="Keyboard Shortcuts (⌘/)" active={shortcutsOpen} hideOnMobile>
-
-
-
-
- {/* Centre: command palette trigger */}
-
- setPaletteOpen(true)}
- className="hidden min-[751px]:flex items-center gap-2 px-3 h-8 w-full max-w-[420px] rounded-md transition-colors pointer-events-auto"
- style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)', color: 'var(--film-text-dim)' }}
- onMouseEnter={e => { (e.currentTarget as HTMLElement).style.borderColor = 'rgba(196,124,46,0.3)'; }}
- onMouseLeave={e => { (e.currentTarget as HTMLElement).style.borderColor = 'rgba(255,255,255,0.08)'; }}>
-
- Search commands…
- ⌘K
-
-
-
- {/* Right: actions */}
-
-
-
-
-
-
setIsImportOpen(true)}
- className="hidden sm:flex items-center gap-1.5 h-8 px-2.5 rounded-md transition-colors syne-font"
- style={{ color: 'var(--film-text-dim)' }}
- onMouseEnter={e => { (e.currentTarget as HTMLElement).style.background = 'rgba(255,255,255,0.05)'; (e.currentTarget as HTMLElement).style.color = 'var(--film-cream)'; }}
- onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = 'transparent'; (e.currentTarget as HTMLElement).style.color = 'var(--film-text-dim)'; }}>
-
- Import
-
-
- {/* Export CTA */}
-
setExportOpen(v => !v)}
- className="flex items-center gap-1.5 h-8 px-2 sm:px-3 rounded-lg ml-1 syne-font transition-all active:scale-95"
- style={{
- background: exportOpen ? 'rgba(196,124,46,0.9)' : 'var(--film-amber)',
- color: '#070706', fontSize: 11, fontWeight: 700, letterSpacing: '0.08em',
- textTransform: 'uppercase', border: 'none', cursor: 'pointer',
- boxShadow: exportOpen ? 'none' : '0 0 16px rgba(196,124,46,0.2)',
- }}>
-
- Export
-
-
-
-
-
-
setIsResetOpen(true)}
- className="flex items-center gap-1.5 h-8 px-2 sm:px-2.5 rounded-md transition-colors syne-font text-red-400/80 hover:text-red-300 hover:bg-red-500/10">
-
- Reset
-
-
-
- )}
-
- {/* ── Body ──────────────────────────────────────────────────────── */}
-
-
- {/* Left: Advanced nav sidebar (desktop only) */}
- {!isFullscreen && (
-
- )}
-
- {/* Canvas */}
-
{
- if (e.target === e.currentTarget) clearSelection();
- if (mobileSheetMode !== 'hidden') setMobileSheetMode('hidden');
- }}>
-
-
- openCtxMenu('logo', e)}
- />
-
- {/* Film corner accents */}
- {(['tl', 'tr', 'bl', 'br'] as const).map(c => (
-
- ))}
-
- {/* Zoom + fullscreen overlay */}
-
- {[
- { icon:
, label: 'Zoom In', action: () => dispatchZoom(0.25) },
- { icon:
, label: 'Zoom Out', action: () => dispatchZoom(-0.25) },
- { icon:
, label: 'Reset View', action: dispatchResetView },
- ].map(({ icon, label, action }) => (
-
{ (e.currentTarget as HTMLElement).style.color = 'var(--film-amber)'; (e.currentTarget as HTMLElement).style.background = 'rgba(196,124,46,0.1)'; }}
- onMouseLeave={e => { (e.currentTarget as HTMLElement).style.color = 'var(--film-text-dim)'; (e.currentTarget as HTMLElement).style.background = 'transparent'; }}>
- {icon}
-
- ))}
- {isDesktop && (
- <>
-
-
{ (e.currentTarget as HTMLElement).style.color = 'var(--film-amber)'; (e.currentTarget as HTMLElement).style.background = 'rgba(196,124,46,0.1)'; }}
- onMouseLeave={e => { (e.currentTarget as HTMLElement).style.color = isFullscreen ? 'rgba(196,124,46,0.7)' : 'var(--film-text-dim)'; (e.currentTarget as HTMLElement).style.background = 'transparent'; }}>
- {isFullscreen ? : }
-
- >
- )}
-
-
-
- {/* Right: panel content (desktop) */}
- {!isFullscreen && (
-
- {/* Resize handle */}
-
-
-
- )}
-
- {/* Mobile panel sheet */}
- {!isFullscreen && (
-
- {/* Swipe handle */}
-
setMobileSheetMode('hidden')}>
-
-
-
- {(activeTab === 'source' || activeTab === 'layers' || activeTab === 'poster') && (
-
- )}
- {(activeTab === 'badges' || activeTab === 'selection' || activeTab === 'logo') && (
-
- )}
-
-
- )}
-
-
- {/* Mobile dock */}
- 0}
- hasLogo={config.logo}
- isMinimalPreset={(config.uiPreset ?? 'b') === 'm'}
- selectedCount={selectedCount}
- />
-
- >
- );
-};
-
-// ── AdvancedBuilderApp ────────────────────────────────────────────────────────
-const AdvancedBuilderApp: React.FC = () => {
- const { state: config, setState: setConfig, undo, redo, canUndo, canRedo } = usePosterHistory(() => {
- try {
- const saved = localStorage.getItem(STORAGE_KEY);
- const cfg = saved ? (JSON.parse(saved) as PosterConfig) : DEFAULT_CONFIG;
- const cookieKeys = loadKeysFromCookie();
- if (cookieKeys && Object.keys(cookieKeys).some(k => cookieKeys[k as keyof ApiKeys])) {
- return { ...cfg, keys: { ...cookieKeys, ...cfg.keys } };
- }
- return cfg;
- } catch {
- return DEFAULT_CONFIG;
- }
- });
-
- const [baseUrl, setBaseUrl] = useState(DEFAULT_API_BASE);
-
- useEffect(() => {
- localStorage.setItem(STORAGE_KEY, JSON.stringify(config));
- }, [config]);
-
- useEffect(() => {
- if (config.keys) {
- const hasAnyKey = Object.values(config.keys).some(v => v && v.trim());
- if (hasAnyKey) saveKeysToCookie(config.keys);
- }
- }, [config.keys]);
-
- const handleLoadConfig = useCallback((url: string) => {
- setConfig(parseUrlToConfig(url));
- try { setBaseUrl(new URL(url).origin); } catch {}
- }, [setConfig]);
-
- const handleReset = useCallback(() => {
- setConfig(current => ({
- ...DEFAULT_CONFIG,
- mediaType: current.mediaType, tmdbId: current.tmdbId, imdbId: current.imdbId,
- source: current.source, ptype: current.ptype, textless: current.textless, keys: current.keys,
- }));
- window.dispatchEvent(new CustomEvent('reset-canvas-view'));
- }, [setConfig]);
-
- useEffect(() => {
- const params = new URLSearchParams(window.location.search);
- const urlParam = params.get('url');
- if (urlParam) { handleLoadConfig(urlParam); return; }
- const configParam = params.get('config');
- if (!configParam || configParam.length > MAX_QUERY_CONFIG_LENGTH) return;
- try {
- const decoded = atob(decodeURIComponent(configParam));
- const parsed = JSON.parse(decoded) as Partial;
- if (!parsed || !Array.isArray(parsed.ratings)) return;
- setConfig({ ...DEFAULT_CONFIG, ...parsed, items: parsed.items ?? {} } as PosterConfig);
- } catch {}
- }, [handleLoadConfig, setConfig]);
-
- return (
-
-
-
- );
-};
-
-export default AdvancedBuilderApp;
\ No newline at end of file
diff --git a/src/components/builder/components/LayerPanel.tsx b/src/components/builder/components/LayerPanel.tsx
index a47dc3c2..a13e53fc 100644
--- a/src/components/builder/components/LayerPanel.tsx
+++ b/src/components/builder/components/LayerPanel.tsx
@@ -39,6 +39,7 @@ import { BADGE_ICONS } from '../constants';
import { DEFAULT_API_BASE } from '../utils';
import { useEditor } from '../context/EditorContext';
import SidebarLayout from './SidebarLayout';
+import PanelSwitcher from './navigation/PanelSwitcher';
type BadgeIconKey = keyof typeof BADGE_ICONS;
@@ -47,6 +48,9 @@ interface Props {
setConfig: React.Dispatch>;
selectedIds: Set;
onSelect: (id: RatingType, multi: boolean) => void;
+ mode?: 'source' | 'layers' | 'poster';
+ hideTabBar?: boolean;
+ side?: 'left' | 'right' | 'none';
}
interface SearchResult {
@@ -529,7 +533,15 @@ const ApiKeysPanel: React.FC<{
};
// ── Main LayerPanel component ─────────────────────────────────────────────────
-const LayerPanel: React.FC = ({ config, setConfig, selectedIds, onSelect }) => {
+const LayerPanel: React.FC = ({
+ config,
+ setConfig,
+ selectedIds,
+ onSelect,
+ mode,
+ hideTabBar = false,
+ side = 'left',
+}) => {
const {
setBatchSelection,
activeTab,
@@ -545,13 +557,17 @@ const LayerPanel: React.FC = ({ config, setConfig, selectedIds, onSelect
toggleViewOption,
} = useEditor();
- const [localMode, setLocalMode] = useState<'source' | 'layers' | 'poster'>('source');
+ const [localMode, setLocalMode] = useState<'source' | 'layers' | 'poster'>(mode ?? 'source');
const [inactiveOrder, setInactiveOrder] = useState([]);
useEffect(() => {
+ if (mode) {
+ setLocalMode(mode);
+ return;
+ }
if (activeTab === 'source' || activeTab === 'poster' || activeTab === 'layers')
setLocalMode(activeTab);
- }, [activeTab]);
+ }, [activeTab, mode]);
const [searchQuery, setSearchQuery] = useState('');
const [results, setResults] = useState([]);
@@ -819,10 +835,7 @@ const LayerPanel: React.FC = ({ config, setConfig, selectedIds, onSelect
setConfig((prev) => ({
...prev,
ratings: [...badgeTopToBottom].reverse(),
- logoZ:
- logoIndex === -1
- ? prev.logoZ
- : 100 + (ordered.length - logoIndex - 1),
+ logoZ: logoIndex === -1 ? prev.logoZ : 100 + (ordered.length - logoIndex - 1),
}));
} else if (
result.source.droppableId === 'inactive' &&
@@ -1045,122 +1058,102 @@ const LayerPanel: React.FC = ({ config, setConfig, selectedIds, onSelect
setActiveTab('selection');
};
return (
- {
- if (isActive) handleLogoSelection(e.shiftKey || e.ctrlKey || e.metaKey);
- else enableLogoAndFocus();
- }}
- className={clsx(
- 'flex items-center gap-2 px-2 py-2 rounded-lg transition-all select-none',
- selectedLogo && isActive
- ? 'bg-[rgba(196,124,46,0.08)] ring-1 ring-[rgba(196,124,46,0.2)]'
+
{
+ if (isActive) handleLogoSelection(e.shiftKey || e.ctrlKey || e.metaKey);
+ else enableLogoAndFocus();
+ }}
+ className={clsx(
+ 'flex items-center gap-2 px-2 py-2 rounded-lg transition-all select-none',
+ selectedLogo && isActive
+ ? 'bg-[rgba(196,124,46,0.08)] ring-1 ring-[rgba(196,124,46,0.2)]'
: isActive
? 'hover:bg-[rgba(196,124,46,0.06)] cursor-pointer'
: 'opacity-50',
- isDraggingItem && 'shadow-2xl rotate-[0.5deg]'
- )}
- style={
- isDraggingItem
- ? { background: 'var(--film-mid)', ...(provided?.draggableProps.style ?? {}) }
- : (provided?.draggableProps.style ?? {})
- }
- >
- {isActive ? (
+ isDraggingItem && 'shadow-2xl rotate-[0.5deg]'
+ )}
+ style={
+ isDraggingItem
+ ? { background: 'var(--film-mid)', ...(provided?.draggableProps.style ?? {}) }
+ : (provided?.draggableProps.style ?? {})
+ }
+ >
+ {isActive ? (
+
e.stopPropagation()}
+ className="p-0.5 outline-none transition-colors shrink-0"
+ style={{ color: 'var(--film-text-dim)', cursor: 'grab' }}
+ >
+
+
+ ) : (
+
+ )}
e.stopPropagation()}
- className="p-0.5 outline-none transition-colors shrink-0"
- style={{ color: 'var(--film-text-dim)', cursor: 'grab' }}
+ className="shrink-0"
+ onClick={(e) => {
+ e.stopPropagation();
+ if (!isActive) enableLogoAndFocus();
+ else handleLogoSelection(false);
+ }}
>
-
+
+ {selectedLogo && isActive &&
}
+
- ) : (
-
- )}
-
{
- e.stopPropagation();
- if (!isActive) enableLogoAndFocus();
- else handleLogoSelection(false);
- }}
- >
- {selectedLogo && isActive &&
}
+
+
+
+
+ Logo
+
+
+
e.stopPropagation()} className="shrink-0">
+ (config.logo ? updateConfig('logo', false) : enableLogoAndFocus())}
+ className="w-7 h-7 rounded-md flex items-center justify-center transition-colors"
+ style={{ color: config.logo ? 'var(--film-text-dim)' : 'rgba(110,110,120,0.7)' }}
+ title={config.logo ? 'Hide layer' : 'Show layer'}
+ >
+ {config.logo ? : }
+
-
-
-
-
-
- Logo
-
-
-
e.stopPropagation()} className="shrink-0">
- (config.logo ? updateConfig('logo', false) : enableLogoAndFocus())}
- className="w-7 h-7 rounded-md flex items-center justify-center transition-colors"
- style={{ color: config.logo ? 'var(--film-text-dim)' : 'rgba(110,110,120,0.7)' }}
- title={config.logo ? 'Hide layer' : 'Show layer'}
- >
- {config.logo ? : }
-
-
-
);
};
return (
- {([
- { id: 'source', label: 'Source', icon: },
- { id: 'layers', label: 'Layers', icon: },
- { id: 'poster', label: 'Poster', icon: },
- ] as const).map((tab) => (
- setActiveTab(tab.id)}
- className={clsx(
- 'flex-1 flex items-center justify-center gap-1.5 py-1.5 rounded-md text-[11px] font-medium transition-all duration-150 outline-none select-none capitalize syne-font',
- localMode !== tab.id &&
- 'hover:bg-[rgba(196,124,46,0.08)] hover:text-[var(--film-text-label)]'
- )}
- style={{
- background: localMode === tab.id ? 'var(--film-mid)' : 'transparent',
- color: localMode === tab.id ? 'var(--film-cream)' : 'var(--film-text-dim)',
- boxShadow: localMode === tab.id ? '0 1px 4px rgba(0,0,0,0.3)' : 'none',
- }}
- >
- {tab.icon}
- {tab.label}
-
- ))}
-
+ hideTabBar ? undefined : (
+ setActiveTab(tab)}
+ items={[
+ { id: 'source', label: 'Source', icon: },
+ { id: 'layers', label: 'Layers', icon: },
+ { id: 'poster', label: 'Poster', icon: },
+ ]}
+ />
+ )
}
>
{/* ── Source Tab ──────────────────────────────────────────────────────── */}
@@ -1501,7 +1494,10 @@ const LayerPanel: React.FC = ({ config, setConfig, selectedIds, onSelect
>
Badges
-
+
Show/hide all layers with badge behavior
@@ -1670,7 +1666,6 @@ const LayerPanel: React.FC = ({ config, setConfig, selectedIds, onSelect
-
)}
@@ -1678,183 +1673,177 @@ const LayerPanel: React.FC = ({ config, setConfig, selectedIds, onSelect
{localMode === 'layers' && (
<>
-
-
+
+ Badges
+
+
+
{
+ (e.currentTarget as HTMLElement).style.color = 'var(--film-text-label)';
+ }}
+ onMouseLeave={(e) => {
+ (e.currentTarget as HTMLElement).style.color = 'var(--film-text-dim)';
+ }}
>
- Badges
-
-
-
{
- (e.currentTarget as HTMLElement).style.color = 'var(--film-text-label)';
- }}
- onMouseLeave={(e) => {
- (e.currentTarget as HTMLElement).style.color = 'var(--film-text-dim)';
- }}
- >
- {allVisible ? : }
- {allVisible ? 'Hide all' : 'Show all'}
-
-
-
handleSelectAll(!allVisibleSelected)}
- className="flex items-center gap-1.5 transition-colors body-font"
- style={{ fontSize: 10, color: 'var(--film-text-dim)' }}
- onMouseEnter={(e) => {
- (e.currentTarget as HTMLElement).style.color = 'var(--film-text-label)';
- }}
- onMouseLeave={(e) => {
- (e.currentTarget as HTMLElement).style.color = 'var(--film-text-dim)';
+ {allVisible ? : }
+ {allVisible ? 'Hide all' : 'Show all'}
+
+
+
handleSelectAll(!allVisibleSelected)}
+ className="flex items-center gap-1.5 transition-colors body-font"
+ style={{ fontSize: 10, color: 'var(--film-text-dim)' }}
+ onMouseEnter={(e) => {
+ (e.currentTarget as HTMLElement).style.color = 'var(--film-text-label)';
+ }}
+ onMouseLeave={(e) => {
+ (e.currentTarget as HTMLElement).style.color = 'var(--film-text-dim)';
+ }}
+ >
+
+ {allVisibleSelected && }
+
+ Select all
+
+
+
+
+
+ {activeLayers.length > 0 ? (
+
+ {(provided) => (
- {allVisibleSelected && }
+ {activeLayers.map((layer, idx) => (
+
+ {(prov, snap) =>
+ layer.kind === 'logo'
+ ? renderLogoLayerRow(true, prov, snap.isDragging)
+ : renderBadgeRow(
+ { id: layer.id as RatingType, label: layer.label },
+ true,
+ prov,
+ snap.isDragging
+ )
+ }
+
+ ))}
+ {provided.placeholder}
- Select all
-
+ )}
+
+ ) : (
+
+
+
+ No active badges
+
+
+ Enable some from the list below
+
-
+ )}
-
- {activeLayers.length > 0 ? (
-
- {(provided) => (
-
- {activeLayers.map((layer, idx) => (
-
- {(prov, snap) =>
- layer.kind === 'logo'
- ? renderLogoLayerRow(true, prov, snap.isDragging)
- : renderBadgeRow(
- { id: layer.id as RatingType, label: layer.label },
- true,
- prov,
- snap.isDragging
- )
- }
-
- ))}
- {provided.placeholder}
-
- )}
-
- ) : (
-
-
-
0 && (
+ <>
+
+
- No active badges
-
-
- Enable some from the list below
-
-
- )}
-
- {inactiveBadges.length > 0 && (
- <>
-
+ Available
+
+ {/* Fallback toggle */}
+
- Available
+ Fallback
- {/* Fallback toggle */}
-
+ {
+ setFallbackEnabled(v);
+ setConfig((prev) => ({
+ ...prev,
+ fallbackEnabled: v,
+ fallbackPool: v ? inactiveBadges.map((b) => b.id) : [],
+ }));
+ }}
+ className={clsx(
+ 'relative inline-flex h-4 w-7 items-center rounded-full transition-colors focus:outline-none',
+ fallbackEnabled ? 'bg-[#C47C2E]' : 'bg-zinc-700/80'
+ )}
+ >
- Fallback
-
- {
- setFallbackEnabled(v);
- setConfig((prev) => ({
- ...prev,
- fallbackEnabled: v,
- fallbackPool: v ? inactiveBadges.map((b) => b.id) : [],
- }));
- }}
className={clsx(
- 'relative inline-flex h-4 w-7 items-center rounded-full transition-colors focus:outline-none',
- fallbackEnabled ? 'bg-[#C47C2E]' : 'bg-zinc-700/80'
+ 'inline-block w-2.5 h-2.5 rounded-full bg-white shadow-sm transition-transform',
+ fallbackEnabled ? 'translate-x-[13px]' : 'translate-x-[2px]'
)}
- >
-
-
-
+ />
+
-
- {fallbackEnabled ? (
-
- {(provided) => (
-
- {inactiveBadges.map((badge, idx) => (
-
- {(prov, snap) =>
- renderBadgeRow(badge, false, prov, snap.isDragging)
- }
-
- ))}
- {provided.placeholder}
-
- )}
-
- ) : (
-
- {inactiveBadges.map((badge) => (
-
- {renderBadgeRow(badge, false)}
-
- ))}
-
- )}
- >
- )}
- {!config.logo && (
-
0 ? 'mt-2' : 'mt-5')}>
- {renderLogoLayerRow(false)}
- )}
-
+ {fallbackEnabled ? (
+
+ {(provided) => (
+
+ {inactiveBadges.map((badge, idx) => (
+
+ {(prov, snap) => renderBadgeRow(badge, false, prov, snap.isDragging)}
+
+ ))}
+ {provided.placeholder}
+
+ )}
+
+ ) : (
+
+ {inactiveBadges.map((badge) => (
+
+ {renderBadgeRow(badge, false)}
+
+ ))}
+
+ )}
+ >
+ )}
+ {!config.logo && (
+
0 ? 'mt-2' : 'mt-5')}>
+ {renderLogoLayerRow(false)}
+
+ )}
+
>
)}
diff --git a/src/components/builder/components/layout/Inspector.tsx b/src/components/builder/components/layout/Inspector.tsx
index 80657636..27bb793d 100644
--- a/src/components/builder/components/layout/Inspector.tsx
+++ b/src/components/builder/components/layout/Inspector.tsx
@@ -1,23 +1,26 @@
import React, { memo } from 'react';
import { useEditor } from '../../context/EditorContext';
-import PropertyPanel from '../PropertyPanel';
import type { PosterConfig } from '../../types';
import { Badge, MousePointer2 } from 'lucide-react';
-import clsx from 'clsx';
import SidebarLayout from '../SidebarLayout';
+import PanelSwitcher from '../navigation/PanelSwitcher';
+import BadgesPanel from '../../panels/right/BadgesPanel';
+import SelectionPanel from '../../panels/right/SelectionPanel';
interface Props {
config: PosterConfig;
setConfig: React.Dispatch
>;
+ hideTabBar?: boolean;
+ mode?: InspectorTab;
}
type InspectorTab = 'badges' | 'selection';
-const INACTIVE_TAB_HOVER_CLASSES = 'hover:bg-white/[0.05] hover:text-[var(--film-text-dim)]';
const isInspectorTab = (value: string): value is InspectorTab =>
value === 'badges' || value === 'selection';
-const Inspector: React.FC = memo(({ config, setConfig }) => {
- const { activeTab, setActiveTab, selectedIds, selectedLogo, selectedMinimalElements } = useEditor();
+const Inspector: React.FC = memo(({ config, setConfig, hideTabBar = false, mode }) => {
+ const { activeTab, setActiveTab, selectedIds, selectedLogo, selectedMinimalElements } =
+ useEditor();
const selectedCount = selectedIds.size + (selectedLogo ? 1 : 0) + selectedMinimalElements.size;
const isMinimalPreset = (config.uiPreset ?? 'b') === 'm';
const hasBadges = config.ratings.length > 0;
@@ -32,18 +35,23 @@ const Inspector: React.FC = memo(({ config, setConfig }) => {
? 'Logo'
: 'Badges';
- const tabs: { id: InspectorTab; label: string; Icon: React.ElementType; visible: boolean }[] = [
- { id: 'badges', label: primaryTabLabel, Icon: Badge, visible: hasBadges || hasLogo || isMinimalPreset },
+ const tabs = [
{
- id: 'selection',
+ id: 'badges' as const,
+ label: primaryTabLabel,
+ icon: ,
+ visible: hasBadges || hasLogo || isMinimalPreset,
+ },
+ {
+ id: 'selection' as const,
label: selectedCount > 0 ? `${selectedCount} selected` : 'Selection',
- Icon: MousePointer2,
+ icon: ,
visible: true,
},
];
const visibleTabs = tabs.filter((tab) => tab.visible);
- const activeInspectorTab = isInspectorTab(activeTab) ? activeTab : undefined;
+ const activeInspectorTab = mode ?? (isInspectorTab(activeTab) ? activeTab : undefined);
const currentTab = visibleTabs.some((tab) => tab.id === activeInspectorTab)
? activeInspectorTab
: visibleTabs[0]?.id;
@@ -52,44 +60,34 @@ const Inspector: React.FC = memo(({ config, setConfig }) => {
return (
- {visibleTabs.map(({ id, label, Icon }) => (
- setActiveTab(id)}
- aria-pressed={currentTab === id}
- className={clsx(
- 'flex-1 flex items-center justify-center gap-1.5 py-1.5 rounded-md text-[11px] font-medium transition-all duration-150 outline-none select-none syne-font',
- currentTab !== id && INACTIVE_TAB_HOVER_CLASSES
- )}
- style={{
- background: currentTab === id ? 'var(--film-mid)' : 'transparent',
- color: currentTab === id ? 'var(--film-cream)' : 'var(--film-text-dim)',
- boxShadow: currentTab === id ? '0 1px 4px rgba(0,0,0,0.3)' : 'none',
- }}
- >
-
- {label}
-
- ))}
-
+ hideTabBar ? undefined : (
+ setActiveTab(tab)}
+ items={visibleTabs}
+ />
+ )
}
>
-
+ {currentTab === 'badges' ? (
+
+ ) : (
+
+ )}
);
});
diff --git a/src/components/builder/components/navigation/BuilderModeToggle.tsx b/src/components/builder/components/navigation/BuilderModeToggle.tsx
new file mode 100644
index 00000000..e58a003e
--- /dev/null
+++ b/src/components/builder/components/navigation/BuilderModeToggle.tsx
@@ -0,0 +1,45 @@
+import React from 'react';
+import { PanelsTopLeft } from 'lucide-react';
+import type { BuilderMode } from '../../types';
+
+interface Props {
+ mode: BuilderMode;
+ onChange: (mode: BuilderMode) => void;
+}
+
+const OPTIONS: { id: BuilderMode; label: string }[] = [
+ { id: 'simple', label: 'Simple' },
+ { id: 'advanced', label: 'Advanced' },
+];
+
+const BuilderModeToggle: React.FC = ({ mode, onChange }) => (
+
+
+ {OPTIONS.map((option) => {
+ const active = mode === option.id;
+ return (
+
onChange(option.id)}
+ aria-pressed={active}
+ className="h-6 px-2 rounded-md text-[10px] syne-font font-bold uppercase tracking-wider transition-all active:scale-95"
+ style={{
+ background: active ? 'var(--film-amber)' : 'transparent',
+ color: active ? '#070706' : 'var(--film-text-dim)',
+ }}
+ >
+
+ {option.label}
+
+ {option.id === 'advanced' && Adv }
+
+ );
+ })}
+
+);
+
+export default BuilderModeToggle;
diff --git a/src/components/builder/components/navigation/PanelSwitcher.tsx b/src/components/builder/components/navigation/PanelSwitcher.tsx
new file mode 100644
index 00000000..5f7e285b
--- /dev/null
+++ b/src/components/builder/components/navigation/PanelSwitcher.tsx
@@ -0,0 +1,56 @@
+import React, { memo } from 'react';
+import clsx from 'clsx';
+
+export interface PanelSwitcherItem {
+ id: T;
+ label: string;
+ icon?: React.ReactNode;
+ visible?: boolean;
+}
+
+interface Props {
+ items: PanelSwitcherItem[];
+ value: T;
+ onChange: (value: T) => void;
+ className?: string;
+}
+
+const PanelSwitcher = ({ items, value, onChange, className }: Props) => {
+ const visibleItems = items.filter((item) => item.visible !== false);
+
+ return (
+
+ {visibleItems.map((item) => {
+ const active = value === item.id;
+ return (
+ onChange(item.id)}
+ aria-pressed={active}
+ className={clsx(
+ 'flex-1 flex items-center justify-center gap-1.5 py-1.5 rounded-md text-[11px] font-medium transition-all duration-150 outline-none select-none syne-font',
+ !active && 'hover:bg-[rgba(196,124,46,0.08)] hover:text-[var(--film-text-label)]'
+ )}
+ style={{
+ background: active ? 'var(--film-mid)' : 'transparent',
+ color: active ? 'var(--film-cream)' : 'var(--film-text-dim)',
+ boxShadow: active ? '0 1px 4px rgba(0,0,0,0.3)' : 'none',
+ }}
+ >
+ {item.icon}
+ {item.label}
+
+ );
+ })}
+
+ );
+};
+
+export default memo(PanelSwitcher) as typeof PanelSwitcher;
diff --git a/src/components/builder/context/EditorContext.tsx b/src/components/builder/context/EditorContext.tsx
index 6c00c1f4..039862f8 100644
--- a/src/components/builder/context/EditorContext.tsx
+++ b/src/components/builder/context/EditorContext.tsx
@@ -2,7 +2,7 @@
import React, { createContext, useContext, useState, useCallback } from 'react';
import type { RatingType } from '../types';
-type TabType = 'source' | 'layers' | 'poster' | 'badges' | 'logo' | 'selection';
+export type TabType = 'source' | 'layers' | 'poster' | 'badges' | 'logo' | 'selection';
export type SheetMode = 'hidden' | 'half' | 'full';
interface ViewOptions {
diff --git a/src/components/builder/index.tsx b/src/components/builder/index.tsx
index 3a479d88..9a96a377 100644
--- a/src/components/builder/index.tsx
+++ b/src/components/builder/index.tsx
@@ -1,8 +1,15 @@
// src/components/builder/index.tsx
import React, { useState, useEffect, useRef, useCallback, memo } from 'react';
import clsx from 'clsx';
-import type { PosterConfig, ExtensionType, ApiKeys, RatingType } from './types';
-import { DEFAULT_CONFIG, ALL_BADGES, CANVAS_WIDTH, CANVAS_HEIGHT, BASE_BADGE_W, BASE_BADGE_H } from './types';
+import type { BuilderMode, PosterConfig, ExtensionType, ApiKeys, RatingType } from './types';
+import {
+ DEFAULT_CONFIG,
+ ALL_BADGES,
+ CANVAS_WIDTH,
+ CANVAS_HEIGHT,
+ BASE_BADGE_W,
+ BASE_BADGE_H,
+} from './types';
import { parseUrlToConfig, DEFAULT_API_BASE, calculateAutoPosition, getScale } from './utils';
import PreviewCanvas from './components/PreviewCanvas';
import LayerPanel from './components/LayerPanel';
@@ -39,11 +46,13 @@ import {
Type,
ChevronDown,
Search,
- Coffee,
} from 'lucide-react';
import { usePosterHistory } from './hooks/usePosterHistory';
import ContextMenu, { type ContextMenuState, type LayerTargetId } from './components/ContextMenu';
import CommandPalette, { type PaletteCommand } from './components/CommandPalette';
+import BuilderModeToggle from './components/navigation/BuilderModeToggle';
+import AdvancedPanelList from './panels/AdvancedPanelList';
+import AdvancedPanelRenderer from './panels/AdvancedPanelRenderer';
const STORAGE_KEY = 'posterium_config_v2';
const MAX_QUERY_CONFIG_LENGTH = 12000; // Guard against oversized URL payloads/memory abuse in base64 config loading.
@@ -314,6 +323,20 @@ const StudioLayout: React.FC<{
const [leftVisible, setLeftVisible] = useState(true);
const [rightVisible, setRightVisible] = useState(true);
const [isFullscreen, setIsFullscreen] = useState(false);
+ const [builderMode, setBuilderModeState] = useState(() => {
+ if (typeof window === 'undefined') return 'simple';
+ const stored = window.localStorage.getItem('posterium_builder_mode_v1') as BuilderMode | null;
+ if (stored === 'simple' || stored === 'advanced') return stored;
+ return window.location.pathname.includes('abuild') ? 'advanced' : 'simple';
+ });
+ const setBuilderMode = useCallback((mode: BuilderMode) => {
+ setBuilderModeState(mode);
+ try {
+ window.localStorage.setItem('posterium_builder_mode_v1', mode);
+ } catch {
+ /* ignore */
+ }
+ }, []);
const [shortcutsOpen, setShortcutsOpen] = useState(false);
const [exportOpen, setExportOpen] = useState(false);
const importBtnRef = useRef(null);
@@ -397,7 +420,9 @@ const StudioLayout: React.FC<{
}
if (hasLogo) {
const currentX =
- next.logoX !== null && next.logoX !== undefined ? next.logoX : Math.round((CANVAS_WIDTH - next.logoW) / 2);
+ next.logoX !== null && next.logoX !== undefined
+ ? next.logoX
+ : Math.round((CANVAS_WIDTH - next.logoW) / 2);
next.logoX = Math.max(1 - next.logoW, Math.min(currentX + dx, CANVAS_WIDTH - 1));
next.logoY = Math.max(1 - next.logoH, Math.min(next.logoY + dy, CANVAS_HEIGHT - 1));
}
@@ -412,8 +437,14 @@ const StudioLayout: React.FC<{
: Math.max(0, Math.min(CANVAS_HEIGHT - boxH, next.minimalTextY + dy));
}
if (activeMinimal.includes('minimal-year')) {
- next.minimalMetaX = Math.max(0, Math.min(CANVAS_WIDTH - 120, (next.minimalMetaX ?? 26) + dx));
- next.minimalMetaY = Math.max(0, Math.min(CANVAS_HEIGHT - 40, (next.minimalMetaY ?? 672) + dy));
+ next.minimalMetaX = Math.max(
+ 0,
+ Math.min(CANVAS_WIDTH - 120, (next.minimalMetaX ?? 26) + dx)
+ );
+ next.minimalMetaY = Math.max(
+ 0,
+ Math.min(CANVAS_HEIGHT - 40, (next.minimalMetaY ?? 672) + dy)
+ );
}
if (activeMinimal.includes('minimal-duration')) {
next.minimalDurationX = Math.max(
@@ -610,7 +641,10 @@ const StudioLayout: React.FC<{
}
if (inInput) return;
if (
- (e.key === 'ArrowUp' || e.key === 'ArrowDown' || e.key === 'ArrowLeft' || e.key === 'ArrowRight') &&
+ (e.key === 'ArrowUp' ||
+ e.key === 'ArrowDown' ||
+ e.key === 'ArrowLeft' ||
+ e.key === 'ArrowRight') &&
(selectedIdsRef.current.size > 0 ||
selectedLogoRef.current ||
selectedMinimalElementsRef.current.size > 0)
@@ -1106,7 +1140,9 @@ const StudioLayout: React.FC<{
onSendToBack={(id) => (id === 'logo' ? moveLogoLayer('toback') : moveLayer(id, 'toback'))}
onHide={hideLayer}
onShowAll={showAllBadges}
- onSelect={(id) => (id === 'logo' ? handleLogoSelection(false) : handleSelectionOverride(id, false))}
+ onSelect={(id) =>
+ id === 'logo' ? handleLogoSelection(false) : handleSelectionOverride(id, false)
+ }
onDeselect={() => clearSelection()}
onSelectAll={() => setBatchSelection(config.ratings)}
onDeselectAll={clearSelection}
@@ -1180,16 +1216,7 @@ const StudioLayout: React.FC<{
P
-
-
-
- Support
-
-
+
setPaletteOpen(true)}
title="Search commands (⌘K)"
@@ -1408,12 +1435,21 @@ const StudioLayout: React.FC<{
opacity: leftVisible ? 1 : 0,
}}
>
-
+ {builderMode === 'advanced' ? (
+
+ ) : (
+
+ )}
-
+ {builderMode === 'advanced' ? (
+
+ ) : (
+
+ )}
)}
diff --git a/src/components/builder/panels/AdvancedPanelList.tsx b/src/components/builder/panels/AdvancedPanelList.tsx
new file mode 100644
index 00000000..6cb38da8
--- /dev/null
+++ b/src/components/builder/panels/AdvancedPanelList.tsx
@@ -0,0 +1,118 @@
+import React, { memo } from 'react';
+import { Badge, Film, Layers, Monitor, MousePointer2, Sliders } from 'lucide-react';
+import clsx from 'clsx';
+import type { PosterConfig } from '../types';
+import type { TabType } from '../context/EditorContext';
+import { useEditor } from '../context/EditorContext';
+import SidebarLayout from '../components/SidebarLayout';
+
+interface Props {
+ config: PosterConfig;
+ selectedCount: number;
+}
+
+const AdvancedPanelList: React.FC = memo(({ config, selectedCount }) => {
+ const { activeTab, setActiveTab } = useEditor();
+ const isMinimalPreset = (config.uiPreset ?? 'b') === 'm';
+ const hasBadges = config.ratings.length > 0;
+ const hasLogo = config.logo;
+
+ const panels: {
+ id: TabType;
+ label: string;
+ desc: string;
+ Icon: React.ElementType;
+ visible: boolean;
+ }[] = [
+ {
+ id: 'source',
+ label: 'Source',
+ desc: 'Media search, IDs, poster source',
+ Icon: Film,
+ visible: true,
+ },
+ {
+ id: 'layers',
+ label: 'Layers',
+ desc: 'Visibility, order, selection',
+ Icon: Layers,
+ visible: true,
+ },
+ {
+ id: 'poster',
+ label: 'Poster',
+ desc: 'Canvas, overlays, output options',
+ Icon: Monitor,
+ visible: true,
+ },
+ {
+ id: 'badges',
+ label: hasLogo && !hasBadges ? 'Logo style' : 'Badge style',
+ desc: 'Global badge and logo appearance',
+ Icon: hasBadges || isMinimalPreset ? Badge : Sliders,
+ visible: hasBadges || hasLogo || isMinimalPreset,
+ },
+ {
+ id: 'selection',
+ label: selectedCount > 0 ? `${selectedCount} selected` : 'Selection',
+ desc: 'Fine tune selected layers',
+ Icon: MousePointer2,
+ visible: true,
+ },
+ ];
+
+ return (
+
+
+
+ Advanced panels
+
+
+ Choose a system module, then edit it in the inspector on the right.
+
+
+
+ {panels
+ .filter((panel) => panel.visible)
+ .map(({ id, label, desc, Icon }) => {
+ const active = activeTab === id;
+ return (
+ setActiveTab(id)}
+ aria-pressed={active}
+ className={clsx(
+ 'w-full flex items-center gap-3 rounded-xl px-3 py-2.5 text-left transition-all border',
+ active
+ ? 'bg-[rgba(196,124,46,0.14)] border-[rgba(196,124,46,0.28)] text-[var(--film-cream)]'
+ : 'bg-[rgba(255,255,255,0.025)] border-[rgba(255,255,255,0.045)] text-[var(--film-text-label)] hover:bg-[rgba(255,255,255,0.055)] hover:border-[rgba(196,124,46,0.18)]'
+ )}
+ >
+
+
+
+
+
+ {label}
+
+
+ {desc}
+
+
+
+ );
+ })}
+
+
+ );
+});
+
+AdvancedPanelList.displayName = 'AdvancedPanelList';
+export default AdvancedPanelList;
diff --git a/src/components/builder/panels/AdvancedPanelRenderer.tsx b/src/components/builder/panels/AdvancedPanelRenderer.tsx
new file mode 100644
index 00000000..8cc921aa
--- /dev/null
+++ b/src/components/builder/panels/AdvancedPanelRenderer.tsx
@@ -0,0 +1,76 @@
+import React from 'react';
+import { useEditor } from '../context/EditorContext';
+import type { PosterConfig, RatingType } from '../types';
+import SourcePanel from './left/SourcePanel';
+import LayersPanel from './left/LayersPanel';
+import PosterPanel from './left/PosterPanel';
+import BadgesPanel from './right/BadgesPanel';
+import SelectionPanel from './right/SelectionPanel';
+
+interface Props {
+ config: PosterConfig;
+ setConfig: React.Dispatch>;
+ selectedIds: Set;
+ onSelect: (id: RatingType, multi: boolean) => void;
+}
+
+const AdvancedPanelRenderer: React.FC = ({ config, setConfig, selectedIds, onSelect }) => {
+ const { activeTab, selectedLogo, selectedMinimalElements } = useEditor();
+
+ if (activeTab === 'source') {
+ return (
+
+ );
+ }
+ if (activeTab === 'layers') {
+ return (
+
+ );
+ }
+ if (activeTab === 'poster') {
+ return (
+
+ );
+ }
+ if (activeTab === 'badges' || activeTab === 'logo') {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+};
+
+export default AdvancedPanelRenderer;
diff --git a/src/components/builder/panels/left/LayersPanel.tsx b/src/components/builder/panels/left/LayersPanel.tsx
new file mode 100644
index 00000000..a5f38229
--- /dev/null
+++ b/src/components/builder/panels/left/LayersPanel.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import LayerPanel from '../../components/LayerPanel';
+import type { PosterConfig, RatingType } from '../../types';
+
+interface Props {
+ config: PosterConfig;
+ setConfig: React.Dispatch>;
+ selectedIds: Set;
+ onSelect: (id: RatingType, multi: boolean) => void;
+ side?: 'left' | 'right' | 'none';
+}
+
+const LayersPanel: React.FC = (props) => (
+
+);
+
+export default LayersPanel;
diff --git a/src/components/builder/panels/left/PosterPanel.tsx b/src/components/builder/panels/left/PosterPanel.tsx
new file mode 100644
index 00000000..f4f9375d
--- /dev/null
+++ b/src/components/builder/panels/left/PosterPanel.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import LayerPanel from '../../components/LayerPanel';
+import type { PosterConfig, RatingType } from '../../types';
+
+interface Props {
+ config: PosterConfig;
+ setConfig: React.Dispatch>;
+ selectedIds: Set;
+ onSelect: (id: RatingType, multi: boolean) => void;
+ side?: 'left' | 'right' | 'none';
+}
+
+const PosterPanel: React.FC = (props) => (
+
+);
+
+export default PosterPanel;
diff --git a/src/components/builder/panels/left/SourcePanel.tsx b/src/components/builder/panels/left/SourcePanel.tsx
new file mode 100644
index 00000000..00ea888d
--- /dev/null
+++ b/src/components/builder/panels/left/SourcePanel.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import LayerPanel from '../../components/LayerPanel';
+import type { PosterConfig, RatingType } from '../../types';
+
+interface Props {
+ config: PosterConfig;
+ setConfig: React.Dispatch>;
+ selectedIds: Set;
+ onSelect: (id: RatingType, multi: boolean) => void;
+ side?: 'left' | 'right' | 'none';
+}
+
+const SourcePanel: React.FC = (props) => (
+
+);
+
+export default SourcePanel;
diff --git a/src/components/builder/panels/right/BadgesPanel.tsx b/src/components/builder/panels/right/BadgesPanel.tsx
new file mode 100644
index 00000000..d6c89a60
--- /dev/null
+++ b/src/components/builder/panels/right/BadgesPanel.tsx
@@ -0,0 +1,15 @@
+import React from 'react';
+import PropertyPanel from '../../components/PropertyPanel';
+import type { PosterConfig, RatingType } from '../../types';
+
+interface Props {
+ config: PosterConfig;
+ setConfig: React.Dispatch>;
+ selectedIds: Set;
+ selectedLogo: boolean;
+ selectedMinimalElements: Set;
+}
+
+const BadgesPanel: React.FC = (props) => ;
+
+export default BadgesPanel;
diff --git a/src/components/builder/panels/right/SelectionPanel.tsx b/src/components/builder/panels/right/SelectionPanel.tsx
new file mode 100644
index 00000000..c344aecc
--- /dev/null
+++ b/src/components/builder/panels/right/SelectionPanel.tsx
@@ -0,0 +1,15 @@
+import React from 'react';
+import PropertyPanel from '../../components/PropertyPanel';
+import type { PosterConfig, RatingType } from '../../types';
+
+interface Props {
+ config: PosterConfig;
+ setConfig: React.Dispatch>;
+ selectedIds: Set;
+ selectedLogo: boolean;
+ selectedMinimalElements: Set;
+}
+
+const SelectionPanel: React.FC = (props) => ;
+
+export default SelectionPanel;
diff --git a/src/components/builder/types.ts b/src/components/builder/types.ts
index d13b06ca..fdb17ade 100644
--- a/src/components/builder/types.ts
+++ b/src/components/builder/types.ts
@@ -438,6 +438,8 @@ export const DEFAULT_CONFIG: PosterConfig = {
keys: {},
};
+export type BuilderMode = 'simple' | 'advanced';
+
// ── Canvas geometry constants ─────────────────────────────────────────────────
export const CANVAS_WIDTH = 500;
export const CANVAS_HEIGHT = 750;
diff --git a/src/pages/abuild.astro b/src/pages/abuild.astro
index 57e13b6d..8940bb1e 100644
--- a/src/pages/abuild.astro
+++ b/src/pages/abuild.astro
@@ -1,23 +1,23 @@
----
-// src/pages/abuild.astro
-import BaseLayout from '@/layouts/BaseLayout.astro';
-import PageSEO from '@/components/seo/PageSEO.astro';
-import AdvancedBuilderApp from '@/components/builder/AdvancedBuilderApp.tsx';
-import '@/styles/global.css';
----
-
-
-
-
-
-
Posterium Advanced Builder
-
Advanced visual editor with vertical panel navigation for creating custom movie and TV posters with live rating badges.
-
-
-
-
\ No newline at end of file
+---
+// src/pages/abuild.astro
+import BaseLayout from '@/layouts/BaseLayout.astro';
+import PageSEO from '@/components/seo/PageSEO.astro';
+import BuilderApp from '@/components/builder/index.tsx';
+import '@/styles/global.css';
+---
+
+
+
+
+
+
Posterium Unified Builder
+
Use the simple or advanced visual editor to search posters, organize layers, style badges, and export a complete design.
+
+
+
+