import { useState } from 'react' import { Minus, Quote, Square } from 'lucide-react' import type { BookElement, BookPageSetup, BookShapeId } from '@trek/shared' import { useStudioStore } from '../../store/studioStore' import { FRAME_SHAPES, HOLED_SHAPES, SHAPE_GROUPS, SHAPE_PATHS } from './shapes' import { GRIDS, defaultGridBox, gridElements } from './grids' import { PanelHead } from './StudioPanelHead' /** * Everything you can add that does not come from the journey: type, shapes, * picture frames and photo grids. * * Three tabs rather than three more entries on the rail. They are all the same * kind of answer to "what do I put on this page", and a rail long enough to * hold them separately stops being faster to hit than a word would be. */ const uid = (p: string) => `${p}-${Math.random().toString(36).slice(2, 9)}` type FrameStyle = 'none' | 'polaroid' | 'white' | 'shadow' | 'film' | 'tape' /** * A shape as a tile. * * The same path the page will draw, so what you pick is what you get. In frame * mode it is filled with a sky-and-hill gradient, which is Canva's trick and a * good one: an outline tells you the silhouette, but only something * photograph-shaped inside it says "a picture goes here". */ export function ShapeGlyph({ shape, photo = false }: { shape: BookShapeId; photo?: boolean }) { const gid = `sg-${photo ? 'p' : 's'}-${shape}` return ( {photo && ( {/* theme-lint-disable — a frame preview stands in for a photograph, not for UI chrome */} )} ) } /** A miniature of what the decoration around a picture does. */ function FrameStyleGlyph({ style }: { style: FrameStyle }) { const pad = style === 'polaroid' ? 3 : style === 'white' ? 2 : style === 'film' ? 4 : 0 const bottom = style === 'polaroid' ? 9 : pad return ( {/* Both strips, because both is what gets placed — a preview showing one made the second one look like a bug the first time it appeared. */} {style === 'tape' && <>} ) } export function StudioElementsPanel({ page, t }: { page: BookPageSetup; t: (k: string) => string }) { const [tab, setTab] = useState<'shapes' | 'frames' | 'grids'>('shapes') const addElement = useStudioStore(s => s.addElement) const commit = useStudioStore(s => s.commit) const select = useStudioStore(s => s.select) const active = useStudioStore(s => s.activeSpread) const doc = useStudioStore(s => s.doc) const spread = doc?.spreads[active] const single = !!spread && spread.role !== 'inner' const centre = (w: number, h: number) => { const W = single ? page.pageWidth : page.pageWidth * 2 return { x: (W - w) / 2, y: (page.pageHeight - h) / 2, w, h } } const addText = (size: number, weight: 400 | 500 | 600 | 700, sample: string, extra: Partial = {}) => addElement(active, { id: uid('t'), kind: 'text', frame: centre(page.pageWidth * 0.7, size * 0.5 + 8), rotation: 0, opacity: 1, locked: false, text: sample, font: 'sans', size, weight, italic: false, align: 'left', leading: size > 16 ? 1.1 : 1.5, tracking: size > 16 ? -0.02 : 0, color: '#1a1a1a', binding: null, overridden: false, ...extra, } as BookElement) /** * Placed at a size that suits the shape rather than always as a square. * * A banner wants to be wide and a pin wants to be tall; dropping every shape * as one 60mm square means resizing each one before it is any use, which is * a tax on the most common action in the panel. */ const addShape = (shape: BookShapeId, opts: { outline?: boolean } = {}) => { const side = Math.min(page.pageWidth, page.pageHeight) * 0.3 const wide = shape.startsWith('banner') || shape === 'ticket' || shape === 'wave' || shape === 'arrow-right' || shape === 'arrow-left' || shape === 'arrow-both' || shape === 'capsule' const tall = shape === 'pin' || shape === 'drop' || shape === 'arrow-up' || shape === 'arrow-down' const isRule = shape === 'line' addElement(active, { id: uid('s'), kind: 'shape', frame: isRule ? centre(page.pageWidth * 0.5, 0.5) : centre(wide ? side * 1.7 : side, tall ? side * 1.35 : wide ? side * 0.55 : side), rotation: 0, opacity: 1, locked: false, // A rule is a shape too — a hairline box rather than its own element type, // so it moves, colours and snaps like everything else on the page. shape: isRule ? 'rect' : shape, fill: opts.outline ? null : '#141414', gradient: 'none', stroke: opts.outline ? '#141414' : null, strokeWidth: opts.outline ? 0.5 : 0, strokeStyle: 'solid', radius: 0, } as BookElement) } /** An empty frame, cut to a shape and waiting for a picture. */ const addFrame = (mask: BookShapeId | null, frameStyle: FrameStyle = 'none') => { const side = Math.min(page.pageWidth, page.pageHeight) * 0.45 addElement(active, { id: uid('p'), kind: 'photo', // A Polaroid is taller than it is wide, because of the chin. Placing it // square would put the picture in a box the decoration then eats. frame: centre(side, frameStyle === 'polaroid' ? side * 1.16 : side), rotation: 0, opacity: 1, locked: false, photoId: null, fit: 'cover', focalX: 0.5, focalY: 0.5, radius: 0, filter: 'none', mask, frameStyle, } as BookElement) } return ( <>
{tab === 'shapes' && ( <>
{t('journey.studio.text')}
{t('journey.studio.lines')}
{SHAPE_GROUPS.map(group => (
{t(`journey.studio.shapeGroup.${group.id}`)}
{group.shapes.map(shape => ( ))}
))} )} {tab === 'frames' && ( <>
{t('journey.studio.frameStyles')}
{([ ['none', 'plainFrame'], ['polaroid', 'polaroidFrame'], ['white', 'whiteFrame'], ['shadow', 'shadowFrame'], ['film', 'filmFrame'], ['tape', 'tapeFrame'], ] as [FrameStyle, string][]).map(([style, key]) => ( ))}
{t('journey.studio.frameShapes')}
{FRAME_SHAPES.map(shape => ( ))}

{t('journey.studio.frameHint')}

)} {tab === 'grids' && (
{t('journey.studio.grids')}
{GRIDS.map(grid => ( ))}

{t('journey.studio.gridHint')}

)}
) }