Skip to content
Open
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,542 changes: 1,586 additions & 956 deletions src/components/builder/AdvancedBuilderApp.tsx

Large diffs are not rendered by default.

901 changes: 262 additions & 639 deletions src/components/builder/components/LayerPanel.tsx

Large diffs are not rendered by default.

1,564 changes: 561 additions & 1,003 deletions src/components/builder/components/PropertyPanel.tsx

Large diffs are not rendered by default.

456 changes: 456 additions & 0 deletions src/components/builder/components/controls/BuilderControls.tsx

Large diffs are not rendered by default.

45 changes: 45 additions & 0 deletions src/components/builder/components/layout/BuilderModeToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from 'react';
import clsx from 'clsx';

type BuilderMode = 'simple' | 'advanced';

interface Props {
mode: BuilderMode;
className?: string;
}

const items: Array<{ mode: BuilderMode; label: string; href: string }> = [
{ mode: 'simple', label: 'Simple', href: '/build' },
{ mode: 'advanced', label: 'Advanced', href: '/abuild' },
];

const BuilderModeToggle: React.FC<Props> = ({ mode, className }) => (
<nav
aria-label="Builder mode"
className={clsx(
'inline-flex items-center rounded-lg border border-[rgba(196,124,46,0.18)] bg-[rgba(255,255,255,0.035)] p-0.5 shadow-inner shadow-black/20',
className
)}
>
{items.map((item) => {
const active = item.mode === mode;
return (
<a
key={item.mode}
href={item.href}
aria-current={active ? 'page' : undefined}
className={clsx(
'h-6 px-2.5 rounded-md text-[9px] syne-font font-bold uppercase tracking-[0.11em] transition-all flex items-center',
active
? 'bg-[var(--film-amber)] text-[var(--film-dark)] shadow-sm shadow-[rgba(196,124,46,0.25)]'
: 'text-[var(--film-text-dim)] hover:text-[var(--film-cream)] hover:bg-white/[0.045]'
)}
>
{item.label}
</a>
);
})}
</nav>
);

export default BuilderModeToggle;
59 changes: 26 additions & 33 deletions src/components/builder/components/layout/Inspector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,23 @@ 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 './PanelSwitcher';

interface Props {
config: PosterConfig;
setConfig: React.Dispatch<React.SetStateAction<PosterConfig>>;
mode?: InspectorTab;
hideTabBar?: boolean;
}

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<Props> = memo(({ config, setConfig }) => {
const { activeTab, setActiveTab, selectedIds, selectedLogo, selectedMinimalElements } = useEditor();
const Inspector: React.FC<Props> = memo(({ config, setConfig, mode, hideTabBar = false }) => {
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;
Expand All @@ -33,7 +35,12 @@ const Inspector: React.FC<Props> = memo(({ config, setConfig }) => {
: 'Badges';

const tabs: { id: InspectorTab; label: string; Icon: React.ElementType; visible: boolean }[] = [
{ id: 'badges', label: primaryTabLabel, Icon: Badge, visible: hasBadges || hasLogo || isMinimalPreset },
{
id: 'badges',
label: primaryTabLabel,
Icon: Badge,
visible: hasBadges || hasLogo || isMinimalPreset,
},
{
id: 'selection',
label: selectedCount > 0 ? `${selectedCount} selected` : 'Selection',
Expand All @@ -43,7 +50,7 @@ const Inspector: React.FC<Props> = memo(({ config, setConfig }) => {
];

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;
Expand All @@ -52,34 +59,20 @@ const Inspector: React.FC<Props> = memo(({ config, setConfig }) => {

return (
<SidebarLayout
side="right"
header={
<div
className="flex rounded-lg p-0.5 gap-0.5"
style={{
background: 'var(--film-char)',
border: '1px solid rgba(255,255,255,0.05)',
}}
>
{visibleTabs.map(({ id, label, Icon }) => (
<button
key={id}
onClick={() => 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',
}}
>
<Icon size={11} strokeWidth={2} />
{label}
</button>
))}
</div>
hideTabBar ? null : (
<PanelSwitcher
ariaLabel="Builder inspector panels"
activeId={currentTab}
onChange={(id) => setActiveTab(id)}
items={visibleTabs.map(({ id, label, Icon }) => ({
id,
label,
icon: <Icon size={11} strokeWidth={2} />,
}))}
/>
)
}
>
<PropertyPanel
Expand Down
51 changes: 51 additions & 0 deletions src/components/builder/components/layout/PanelSwitcher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from 'react';
import clsx from 'clsx';

export interface PanelSwitcherItem<T extends string = string> {
id: T;
label: string;
icon?: React.ReactNode;
}

interface Props<T extends string> {
items: readonly PanelSwitcherItem<T>[];
activeId: T;
onChange: (id: T) => void;
ariaLabel?: string;
}

const PanelSwitcher = <T extends string>({ items, activeId, onChange, ariaLabel }: Props<T>) => (
<div
role="tablist"
aria-label={ariaLabel}
className="flex rounded-lg p-0.5"
style={{ background: 'var(--film-char)', border: '1px solid rgba(255,255,255,0.05)' }}
>
{items.map((item) => {
const active = activeId === item.id;
return (
<button
key={item.id}
type="button"
role="tab"
aria-selected={active}
onClick={() => onChange(item.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',
!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}
</button>
);
})}
</div>
);

export default PanelSwitcher;
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import clsx from 'clsx';
import type { LucideIcon } from 'lucide-react';

export interface AdvancedPanelListItem<T extends string = string> {
id: T;
label: string;
desc: string;
Icon: LucideIcon;
badge?: number | null;
}

interface Props<T extends string> {
items: readonly AdvancedPanelListItem<T>[];
activeId: T;
onSelect: (id: T) => void;
}

const AdvancedPanelList = <T extends string>({ items, activeId, onSelect }: Props<T>) => (
<div className="space-y-1.5 p-2">
{items.map(({ id, label, desc, Icon, badge }) => {
const active = activeId === id;
return (
<button
key={id}
type="button"
onClick={() => onSelect(id)}
aria-current={active ? 'true' : undefined}
className={clsx(
'w-full flex items-center gap-2.5 rounded-xl border-l-2 px-3 py-2.5 text-left transition-all',
active
? 'border-l-[var(--film-amber)] bg-[rgba(196,124,46,0.08)] text-[var(--film-cream)]'
: 'border-l-transparent text-[var(--film-text-dim)] hover:bg-[rgba(196,124,46,0.04)] hover:text-[var(--film-text-label)]'
)}
>
<span
className={clsx(
'flex h-8 w-8 shrink-0 items-center justify-center rounded-lg border',
active
? 'border-[rgba(196,124,46,0.28)] bg-[rgba(196,124,46,0.14)] text-[var(--film-amber)]'
: 'border-white/[0.06] bg-white/[0.03]'
)}
>
<Icon size={13} />
</span>
<span className="min-w-0 flex-1">
<span className="flex items-center gap-1.5 text-[11px] font-bold uppercase tracking-[0.08em] syne-font">
{label}
{!!badge && (
<span className="rounded-full bg-[rgba(196,124,46,0.16)] px-1.5 py-0.5 text-[8px] text-[var(--film-amber)]">
{badge}
</span>
)}
</span>
<span className="mt-0.5 block truncate text-[9px] body-font text-[var(--film-text-dim)]">
{desc}
</span>
</span>
</button>
);
})}
</div>
);

export default AdvancedPanelList;
9 changes: 9 additions & 0 deletions src/components/builder/components/panels/left/LayersPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react';
import LayerPanel from '../../LayerPanel';
import type { BuilderLeftPanelProps } from './types';

const LayersPanel: React.FC<BuilderLeftPanelProps> = (props) => (
<LayerPanel {...props} forcedPanel="layers" />
);

export default LayersPanel;
9 changes: 9 additions & 0 deletions src/components/builder/components/panels/left/PosterPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react';
import LayerPanel from '../../LayerPanel';
import type { BuilderLeftPanelProps } from './types';

const PosterPanel: React.FC<BuilderLeftPanelProps> = (props) => (
<LayerPanel {...props} forcedPanel="poster" />
);

export default PosterPanel;
9 changes: 9 additions & 0 deletions src/components/builder/components/panels/left/SourcePanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react';
import LayerPanel from '../../LayerPanel';
import type { BuilderLeftPanelProps } from './types';

const SourcePanel: React.FC<BuilderLeftPanelProps> = (props) => (
<LayerPanel {...props} forcedPanel="source" />
);

export default SourcePanel;
9 changes: 9 additions & 0 deletions src/components/builder/components/panels/left/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type React from 'react';
import type { PosterConfig, RatingType } from '../../../types';

export interface BuilderLeftPanelProps {
config: PosterConfig;
setConfig: React.Dispatch<React.SetStateAction<PosterConfig>>;
selectedIds: Set<RatingType>;
onSelect: (id: RatingType, multi: boolean) => void;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';
import SourcePanel from '../left/SourcePanel';
import LayersPanel from '../left/LayersPanel';
import PosterPanel from '../left/PosterPanel';
import BadgesPanel from './BadgesPanel';
import SelectionPanel from './SelectionPanel';
import type { BuilderLeftPanelProps } from '../left/types';
import type { BuilderRightPanelProps } from './types';

type PanelId = 'source' | 'layers' | 'poster' | 'badges' | 'selection' | 'logo';

type Props = BuilderLeftPanelProps & BuilderRightPanelProps & { activePanel: PanelId };

const AdvancedPanelHost: React.FC<Props> = ({ activePanel, ...props }) => {
if (activePanel === 'source') return <SourcePanel {...props} />;
if (activePanel === 'layers') return <LayersPanel {...props} />;
if (activePanel === 'poster') return <PosterPanel {...props} />;
if (activePanel === 'badges') return <BadgesPanel {...props} />;
return <SelectionPanel {...props} />;
};

export default AdvancedPanelHost;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react';
import Inspector from '../../layout/Inspector';
import type { BuilderRightPanelProps } from './types';

const BadgesPanel: React.FC<BuilderRightPanelProps> = (props) => (
<Inspector {...props} mode="badges" hideTabBar />
);

export default BadgesPanel;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react';
import Inspector from '../../layout/Inspector';
import type { BuilderRightPanelProps } from './types';

const SelectionPanel: React.FC<BuilderRightPanelProps> = (props) => (
<Inspector {...props} mode="selection" hideTabBar />
);

export default SelectionPanel;
7 changes: 7 additions & 0 deletions src/components/builder/components/panels/right/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type React from 'react';
import type { PosterConfig } from '../../../types';

export interface BuilderRightPanelProps {
config: PosterConfig;
setConfig: React.Dispatch<React.SetStateAction<PosterConfig>>;
}
Loading