import { useEffect, useRef, useState } from 'react'; import { ShirtOptionsPanel } from './ShirtOptionsPanel'; import { UploadTab } from './UploadTab'; import { StickersTab } from './StickersTab'; import { EmojiTab } from './EmojiTab'; import { TextTab } from './TextTab'; import { LayersPanel } from '../panels/LayersPanel'; import { formatUsd } from '../../constants/shirt'; import '../../styles/Sidebar.css'; // Tab nav for the right rail. Templates are deliberately absent — templates // are loaded exclusively via the `?template=X` URL parameter (see // `readTemplateFromUrl` in App.jsx). The Layers panel used to be a tab here // too; it's now a permanent section rendered beneath the tab card so the // user can browse / reorder / delete elements without losing access to the // upload / stickers / text tools. const TABS = [ { id: 'upload', label: 'Upload Photo', icon: UploadIcon }, { id: 'stickers', label: 'Stickers', icon: StickersIcon }, { id: 'emoji', label: 'Emoji', icon: EmojiIcon }, { id: 'text', label: 'Text', icon: TextIcon }, ]; /** * Right-rail panel. * * Top: Shirt Options card (color/size/price) * Middle: Tab nav + active tab content * Bottom (sticky): Preview-on-Model toggle + Add to Cart button */ export function Sidebar({ // Editor wiring onAddImage, onAddSticker, onAddText, recentUploads, onRemoveUpload, // Selection-aware editing surface for the text tab. NOTE: // `selectedElement` is used by the layers panel and the mobile // bottom-sheet compact mode. The Text tab now reads from a // SEPARATE prop (`editingTextElement`) so that *selecting* a text // element on the canvas no longer pulls the Text tab into edit // mode — only an explicit edit gesture (pencil affordance on // desktop, Edit-text toolbar button on mobile) sets that prop. // See the Change 7 follow-up in [[Refinements_2026-05-20_Part2]]. selectedElement, editingTextElement, onUpdateSelected, onCommit, // Shirt config shirtColorId, onShirtColorChange, shirtSize, onShirtSizeChange, totalPrice, basePrice, // Cart onAddToCart, cartDisabled = false, cartDisabledReason = null, // Color helpers for the text tab recentColors, onPickColor, // Font helpers for the text tab (recent fonts dropdown) recentFonts, onPickFont, // Layers tab — needs the full elements list and a delete callback. We // already have selectedElement; add elements + onSelectElement + // onDeleteElement so the LayersPanel can show the list and act on it. elements = [], onSelectElement, onDeleteElement, // Layers-panel per-row actions (Change 4). Optional so existing // callers continue to work — the buttons just don't render when // the callback is missing. onDuplicateElement, onUpdateElement, // S3 multi-select wiring. LayersPanel uses these to render the // ✓ chip per row, the bulk-delete affordance, and the drag-reorder // grip. All optional — the panel falls back to single-selection // rendering when they aren't provided. selectedIds, onToggleInSelection, onDeleteMany, onReorderElement, // Controlled active-tab API (Change 7 follow-up). Sidebar used to // own its tab state internally; lifting it to App lets the pencil // affordance / Edit-text gesture set the tab directly (“clicking // the pencil should switch to the Text tab and populate the // form”). Both props are optional so existing call sites that // didn't pass them fall back to internal state — uncontrolled. activeTab: controlledActiveTab, onActiveTabChange, // Desktop focus signal for the Text tab. Threaded straight through // to TextTab — see its docblock for how it's consumed. Optional; // when omitted, TextTab simply never auto-focuses (which is fine // for any caller that doesn't need the pencil-click behaviour). textEditFocusToken, }) { // Controlled or uncontrolled. When the parent supplies `activeTab` // and `onActiveTabChange`, those drive the visible tab; otherwise // we keep our own `useState` and behave as before. Detecting via // `!== undefined` so a deliberate `null` from the parent (= reset // to default) is still respected. const [internalActiveTab, setInternalActiveTab] = useState('upload'); const isControlled = controlledActiveTab !== undefined && typeof onActiveTabChange === 'function'; const activeTab = isControlled ? controlledActiveTab : internalActiveTab; const setActiveTab = isControlled ? onActiveTabChange : setInternalActiveTab; // --------------------------------------------------------------- // Pencil-click focus plumbing (Change 7 follow-up). // // The textarea-focus logic LIVES HERE — in Sidebar — rather than // inside TextTab, on purpose. TextTab is mounted/unmounted as the // user navigates between tabs; pressing the pencil flips the tab // to 'text' AND bumps the focus token in the same React batch, // which means TextTab is mounting FRESH at the moment the token // first changes. Any `useRef(focusToken)` initializer inside // TextTab would capture the new value at mount and the // "transition detection" effect would immediately bail out // (`1 === 1`) — the user has to click the pencil a second time // before TextTab's ref carries an old value. // // Sidebar, by contrast, is persistently mounted on desktop // (App.jsx wraps it in an