Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/builder/components/Inspector.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { memo } from 'react';
import { useEditor } from '../EditorContext';
import { BadgesPanel, SelectionPanel } from './panels';
import { BadgesPanel, SelectionPanel } from './Panels';
import type { PosterConfig } from '../types';
import { Badge, MousePointer2 } from 'lucide-react';
import SidebarLayout from './SidebarLayout';
Expand Down
230 changes: 118 additions & 112 deletions src/builder/components/ui/SourceTabContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -270,108 +270,112 @@ const SourceTabContent: React.FC<Props> = ({
</Combobox>
</div>

{/* Media type + ID */}
<div className="grid grid-cols-[1fr_auto] gap-2">
<div>
<p
className="syne-font uppercase tracking-widest mb-1.5"
style={{ fontSize: 9, color: 'var(--film-text-dim)', fontWeight: 700 }}
>
Media Type
</p>
<SelectBox
value={config.mediaType}
onChange={(v) => updateConfig('mediaType', v as PosterConfig['mediaType'])}
options={[
{ id: 'movie', label: '🎬 Movie' },
{ id: 'tv', label: '📺 TV Series' },
{ id: 'anime', label: '🎌 Anime' },
]}
/>
</div>
<div className="w-24">
<p
className="syne-font uppercase tracking-widest mb-1.5"
style={{ fontSize: 9, color: 'var(--film-text-dim)', fontWeight: 700 }}
>
IMDb ID
</p>
<input
type="text"
value={config.imdbId || config.tmdbId}
onChange={(e) => {
const val = e.target.value.trim();
if (val.startsWith('tt')) {
setConfig((prev) => ({ ...prev, imdbId: val }));
} else {
setConfig((prev) => ({ ...prev, tmdbId: val, imdbId: undefined }));
}
}}
className="w-full h-9 px-2 rounded-lg mono-font text-center focus:outline-none transition-colors"
style={{
background: 'rgba(255,255,255,0.04)',
border: '1px solid rgba(255,255,255,0.1)',
fontSize: 11,
color: 'var(--film-pale)',
}}
onMouseEnter={(e) => {
(e.currentTarget as HTMLElement).style.borderColor = 'rgba(196,124,46,0.4)';
}}
onMouseLeave={(e) => {
if (document.activeElement !== e.currentTarget) {
(e.currentTarget as HTMLElement).style.borderColor = 'rgba(255,255,255,0.1)';
}
}}
onFocus={(e) => {
(e.currentTarget as HTMLElement).style.borderColor = 'rgba(196,124,46,0.4)';
}}
onBlur={(e) => {
(e.currentTarget as HTMLElement).style.borderColor = 'rgba(255,255,255,0.1)';
}}
/>
</div>
</div>
{isAdvanced && (
<>
{/* Media type + ID */}
<div className="grid grid-cols-[1fr_auto] gap-2">
<div>
<p
className="syne-font uppercase tracking-widest mb-1.5"
style={{ fontSize: 9, color: 'var(--film-text-dim)', fontWeight: 700 }}
>
Media Type
</p>
<SelectBox
value={config.mediaType}
onChange={(v) => updateConfig('mediaType', v as PosterConfig['mediaType'])}
options={[
{ id: 'movie', label: '🎬 Movie' },
{ id: 'tv', label: '📺 TV Series' },
{ id: 'anime', label: '🎌 Anime' },
]}
/>
</div>
<div className="w-24">
<p
className="syne-font uppercase tracking-widest mb-1.5"
style={{ fontSize: 9, color: 'var(--film-text-dim)', fontWeight: 700 }}
>
IMDb ID
</p>
<input
type="text"
value={config.imdbId || config.tmdbId}
onChange={(e) => {
const val = e.target.value.trim();
if (val.startsWith('tt')) {
setConfig((prev) => ({ ...prev, imdbId: val }));
} else {
setConfig((prev) => ({ ...prev, tmdbId: val, imdbId: undefined }));
}
}}
className="w-full h-9 px-2 rounded-lg mono-font text-center focus:outline-none transition-colors"
style={{
background: 'rgba(255,255,255,0.04)',
border: '1px solid rgba(255,255,255,0.1)',
fontSize: 11,
color: 'var(--film-pale)',
}}
onMouseEnter={(e) => {
(e.currentTarget as HTMLElement).style.borderColor = 'rgba(196,124,46,0.4)';
}}
onMouseLeave={(e) => {
if (document.activeElement !== e.currentTarget) {
(e.currentTarget as HTMLElement).style.borderColor = 'rgba(255,255,255,0.1)';
}
}}
onFocus={(e) => {
(e.currentTarget as HTMLElement).style.borderColor = 'rgba(196,124,46,0.4)';
}}
onBlur={(e) => {
(e.currentTarget as HTMLElement).style.borderColor = 'rgba(255,255,255,0.1)';
}}
/>
</div>
</div>

{/* Source + ptype */}
<div className="grid grid-cols-2 gap-2">
<div>
<p
className="syne-font uppercase tracking-widest mb-1.5"
style={{ fontSize: 9, color: 'var(--film-text-dim)', fontWeight: 700 }}
>
Poster Source
</p>
<SelectBox
value={config.source}
onChange={(v) => updateConfig('source', v as PosterConfig['source'])}
options={sourceOptions}
/>
</div>
{['fanart', 'tmdb', 'imdb'].includes(config.source) && (
<div>
<p
className="syne-font uppercase tracking-widest mb-1.5"
style={{ fontSize: 9, color: 'var(--film-text-dim)', fontWeight: 700 }}
>
Poster Type
</p>
<SelectBox
value={config.ptype || 'auto'}
onChange={(v) => updateConfig('ptype', v)}
options={ptypeOptions}
/>
{/* Source + ptype */}
<div className="grid grid-cols-2 gap-2">
<div>
<p
className="syne-font uppercase tracking-widest mb-1.5"
style={{ fontSize: 9, color: 'var(--film-text-dim)', fontWeight: 700 }}
>
Poster Source
</p>
<SelectBox
value={config.source}
onChange={(v) => updateConfig('source', v as PosterConfig['source'])}
options={sourceOptions}
/>
</div>
{['fanart', 'tmdb', 'imdb'].includes(config.source) && (
<div>
<p
className="syne-font uppercase tracking-widest mb-1.5"
style={{ fontSize: 9, color: 'var(--film-text-dim)', fontWeight: 700 }}
>
Poster Type
</p>
<SelectBox
value={config.ptype || 'auto'}
onChange={(v) => updateConfig('ptype', v)}
options={ptypeOptions}
/>
</div>
)}
</div>
)}
</div>

{/* Textless toggle */}
<ToggleRow
label="Textless Poster"
sub="Remove title text from image"
checked={['metahub', 'imdb'].includes(config.source) ? false : config.textless}
onChange={(v) => updateConfig('textless', v)}
disabled={['metahub', 'imdb'].includes(config.source)}
/>
{/* Textless toggle */}
<ToggleRow
label="Textless Poster"
sub="Remove title text from image"
checked={['metahub', 'imdb'].includes(config.source) ? false : config.textless}
onChange={(v) => updateConfig('textless', v)}
disabled={['metahub', 'imdb'].includes(config.source)}
/>
</>
)}

<div className="pt-1 space-y-2">
<ToggleRow
Expand Down Expand Up @@ -469,19 +473,21 @@ const SourceTabContent: React.FC<Props> = ({
/>
</Switch>
</div>
<div className="px-1">
<SegmentedRow
label="Logo Source"
value={String(config.logoSource ?? 'auto')}
onChange={(v) =>
updateConfig(
'logoSource',
v === 'auto' ? null : (v as PosterConfig['logoSource'])
)
}
options={logoSourceOptions}
/>
</div>
{isAdvanced && (
<div className="px-1">
<SegmentedRow
label="Logo Source"
value={String(config.logoSource ?? 'auto')}
onChange={(v) =>
updateConfig(
'logoSource',
v === 'auto' ? null : (v as PosterConfig['logoSource'])
)
}
options={logoSourceOptions}
/>
</div>
)}
<div
className="mt-5 mx-1"
style={{ height: 1, background: 'rgba(255,255,255,0.04)' }}
Expand Down
50 changes: 11 additions & 39 deletions src/builder/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import {
saveKeysToCookie,
} from './builderStorage';
import PreviewCanvas from './components/PreviewCanvas';
import LayerPanel from './components/LayerPanel';
import Inspector from './components/Inspector';
import AdvancedPanelNav, { type BuilderPanelId } from './components/AdvancedPanelNav';
import type { BuilderMode } from './components/ModeToggle';
Expand All @@ -29,7 +28,7 @@ import {
PosterPanel,
BadgesPanel,
SelectionPanel,
} from './components/panels';
} from './components/Panels';
import BuilderDesktopHeader from './components/BuilderDesktopHeader';
import ZoomOverlay from './components/ZoomOverlay';
import { EditorProvider, useEditor } from './EditorContext';
Expand Down Expand Up @@ -150,7 +149,7 @@ const StudioLayout: React.FC<{
const mobileRootRef = useRef<HTMLDivElement>(null);
const [leftPanelOpen, setLeftPanelOpen] = useState(false);
const [rightPanelOpen, setRightPanelOpen] = useState(false);
const [bottomPanelTab, setBottomPanelTab] = useState<'source' | 'canvas' | 'badges'>('source');
const [bottomPanelTab, setBottomPanelTab] = useState<'source' | 'badges'>('source');

const {
bottomPanelOpen,
Expand All @@ -163,7 +162,7 @@ const StudioLayout: React.FC<{
} = useMobileBottomSheet(mobileRootRef);

const openBottomPanel = useCallback(
(tab: 'source' | 'canvas' | 'badges') => {
(tab: 'source' | 'badges') => {
setBottomPanelTab(tab);
openBottomPanelSheet();
},
Expand Down Expand Up @@ -1230,9 +1229,7 @@ const StudioLayout: React.FC<{
: bottomPanelOpen
? bottomPanelTab === 'source'
? 'Source'
: bottomPanelTab === 'canvas'
? 'Canvas'
: 'Badges'
: 'Badges'
: 'Builder'}
</span>
</div>
Expand Down Expand Up @@ -1970,7 +1967,6 @@ const StudioLayout: React.FC<{
{(
[
{ id: 'source', label: 'Source' },
{ id: 'canvas', label: 'Canvas' },
{ id: 'badges', label: 'Badges' },
] as const
).map(({ id, label }) => {
Expand Down Expand Up @@ -2007,7 +2003,7 @@ const StudioLayout: React.FC<{
</div>

{/* CONTENT AREA */}
{/* All three panels are mounted simultaneously. Only one is visible via display. */}
{/* Both panels are mounted simultaneously. Only one is visible via display. */}
{/* This eliminates remounting, re-running effects, and scroll position loss on tab switch. */}
{/* Each content div has its own overflow-y:auto with touch scrolling. */}
<div
Expand Down Expand Up @@ -2040,29 +2036,6 @@ const StudioLayout: React.FC<{
/>
</div>

{/* CANVAS TAB CONTENT */}
<div
style={{
position: 'absolute',
inset: 0,
overflowY: 'auto',
overflowX: 'hidden',
WebkitOverflowScrolling: 'touch',
overscrollBehavior: 'contain',
display: bottomPanelTab === 'canvas' ? 'block' : 'none',
paddingBottom: 24,
}}
>
<PosterPanel
config={config}
setConfig={setConfig}
selectedIds={selectedIds}
onSelect={handleSelectionOverride}
chrome={false}
detailLevel="simple"
/>
</div>

{/* BADGES TAB CONTENT */}
<div
style={{
Expand Down Expand Up @@ -2125,23 +2098,22 @@ const StudioLayout: React.FC<{
}}
/>
{/* Active indicator bar — 28px wide, 2px tall, translates X based on active tab */}
{/* Tab indices: source=0, canvas=1, badges=2. Each tab is 1/3 of nav width. */}
{/* Indicator centered within its tab: left = (tabIndex * 33.333%) + (33.333% - 28px) / 2 */}
{/* Tab indices: source=0, badges=1. Each tab is 1/2 of nav width. */}
{/* Indicator centered within its tab: left = (tabIndex * 50%) + (50% - 28px) / 2 */}
{/* Use transform: translateX for GPU-accelerated animation instead of left: */}
<div
aria-hidden="true"
style={
{
position: 'absolute',
top: 0,
left: 'calc(33.333% * var(--active-tab-index, 0) + (33.333% - 28px) / 2)',
left: 'calc(50% * var(--active-tab-index, 0) + (50% - 28px) / 2)',
width: 28,
height: 2,
background: bottomPanelOpen ? 'var(--film-amber)' : 'transparent',
borderRadius: '0 0 2px 2px',
transition: 'left 0.22s cubic-bezier(0.4,0,0.2,1), background 0.15s',
'--active-tab-index':
bottomPanelTab === 'source' ? 0 : bottomPanelTab === 'canvas' ? 1 : 2,
'--active-tab-index': bottomPanelTab === 'source' ? 0 : 1,
} as React.CSSProperties
}
/>
Expand Down Expand Up @@ -2175,7 +2147,6 @@ const StudioLayout: React.FC<{
{(
[
{ id: 'source', label: 'Source', Icon: Film },
{ id: 'canvas', label: 'Canvas', Icon: Monitor },
{ id: 'badges', label: 'Badges', Icon: Sliders },
] as const
).map(({ id, label, Icon }) => {
Expand Down Expand Up @@ -2241,11 +2212,12 @@ const StudioLayout: React.FC<{
{builderMode === 'advanced' ? (
<AdvancedPanelNav activePanel={advancedPanel} onChange={switchAdvancedPanel} />
) : (
<LayerPanel
<SourcePanel
config={config}
setConfig={setConfig}
selectedIds={selectedIds}
onSelect={handleSelectionOverride}
chrome={false}
detailLevel="simple"
/>
)}
Expand Down