diff --git a/src/components/letter/LetterStickers.tsx b/src/components/letter/LetterStickers.tsx index 55652d07..b30e977a 100644 --- a/src/components/letter/LetterStickers.tsx +++ b/src/components/letter/LetterStickers.tsx @@ -14,47 +14,10 @@ import { sanitizeSvg } from '@/lib/sanitizeSvg'; const MIN_SCALE = 0.5; const MAX_SCALE = 4; -const BASE_SIZE_CQW = 14; +const BASE_SIZE_CQW = 14; // cqw — base sticker size at scale=1, scales with card width /** Inward buffer (%) so sticker centers can't reach the very edge of the card. */ const EDGE_BUFFER = 5; -/** Aspect ratio of the letter card (width / height). */ -const CARD_ASPECT = 5 / 4; - -/** - * Compute an edge-proximity scale factor so stickers shrink as their center - * approaches any edge of the card. The sticker's nominal half-size (in %) - * is compared against the distance from the center to the nearest edge; - * when the distance is less than the half-size the sticker shrinks - * proportionally so it always fits within the card boundary. - * - * Returns a multiplier in (0, 1] that should be applied on top of the - * sticker's own `scale` value. - */ -function edgeScale(x: number, y: number, baseScale: number): number { - // Half-size of the sticker in percentage of container width - const halfW = (BASE_SIZE_CQW * baseScale) / 2; // in cqw, which == % of width - // Container height in cqw units = 100 / aspectRatio - const containerHeightCqw = 100 / CARD_ASPECT; // 80 for 5:4 - // Half-size as % of container height - const halfH = (BASE_SIZE_CQW * baseScale) / 2 / containerHeightCqw * 100; - - // Distance from center to each edge (in %) - const dLeft = x; - const dRight = 100 - x; - const dTop = y; - const dBottom = 100 - y; - - // How much room we need vs how much we have, per axis - const fitX = Math.min(dLeft, dRight) / halfW; - const fitY = Math.min(dTop, dBottom) / halfH; - - // Take the tightest constraint, clamp to (0, 1] - const fit = Math.min(fitX, fitY, 1); - // Floor at a tiny size so stickers don't vanish completely - return Math.max(fit, 0.15); -} - function isSafeUrl(url: string): boolean { try { return new URL(url).protocol === 'https:'; @@ -67,7 +30,7 @@ function StickerMedia({ sticker, sizeCqw, className }: { sticker: LetterSticker; if (sticker.svg) { return (
@@ -78,7 +41,7 @@ function StickerMedia({ sticker, sizeCqw, className }: { sticker: LetterSticker; {sticker.shortcode} @@ -87,8 +50,7 @@ function StickerMedia({ sticker, sizeCqw, className }: { sticker: LetterSticker; function StaticSticker({ sticker }: { sticker: LetterSticker }) { const s = sticker.scale ?? 1; - const es = edgeScale(sticker.x, sticker.y, s); - const sizeCqw = `${BASE_SIZE_CQW * s * es}cqw`; + const sizeCqw = `${BASE_SIZE_CQW * s}cqw`; return (
(undefined); + if (isColorMoment && selected?.event) lastColorMomentRef.current = selected.event; + + // Flat mode = color moment was selected but event has been stripped (only color remains) + const isFlatMode = !!(selected && !selected.event && lastColorMomentRef.current); const toggleSingleColor = () => { - if (!selected || !isColorMoment) return; - if (isSingleColor) { - const { colors: _, ...rest } = selected; + if (!selected) return; + if (isFlatMode && lastColorMomentRef.current) { + // Restore: put the event back + onSelect({ ...selected, event: lastColorMomentRef.current }); + } else if (isColorMoment && selected.event) { + // Flatten: strip the event, keep only color + const { event: _, ...rest } = selected; onSelect(rest as Stationery); - } else { - onSelect({ ...selected, colors: [] }); } }; @@ -359,7 +367,7 @@ export function StationeryPicker({ selected, onSelect }: StationeryPickerProps) {tab === 'themes' && }
- {((hasEmoji && !isTheme) || isColorMoment) && ( + {((hasEmoji && !isTheme) || isColorMoment || isFlatMode) && (
{hasEmoji && !isTheme && ( )} - {isColorMoment && ( + {(isColorMoment || isFlatMode) && ( )} diff --git a/src/lib/letterTypes.ts b/src/lib/letterTypes.ts index 3a83c221..a85ce247 100644 --- a/src/lib/letterTypes.ts +++ b/src/lib/letterTypes.ts @@ -29,7 +29,8 @@ export interface LetterSticker { } export interface LetterContent { - body: string; + /** Main letter text. Optional — a letter must have either a non-empty body or at least one sticker. */ + body?: string; closing?: string; signature?: string; /** Stickers placed on the letter card — stored in encrypted content for privacy */ @@ -53,8 +54,7 @@ export interface Stationery { emoji?: string; /** Emoji display mode: 'tile' (faint repeating pattern) or 'emblem' (single large centered glyph). */ emojiMode?: 'tile' | 'emblem'; - /** Palette colors override. Empty array = "flat" mode (suppress palette). */ - colors?: string[]; + /** CSS font-family string (e.g. "Caveat, cursive"). Set from the sender's font choice. */ fontFamily?: string; /** Frame style ID. */ @@ -110,14 +110,9 @@ export function resolveStationery(s: Stationery): ResolvedStationery { const event = s.event; if (event?.kind === COLOR_MOMENT_KIND) { - // Colors override: empty array = flat mode, undefined = read from event - if (s.colors !== undefined) { - base.colors = s.colors.length > 0 ? s.colors : undefined; - } else { - const hexRe = /^#[0-9A-Fa-f]{6}$/; - const eventColors = event.tags.filter(([n]) => n === 'c').map(([, c]) => c).filter((c) => hexRe.test(c)); - if (eventColors.length >= 2) base.colors = eventColors; - } + const hexRe = /^#[0-9A-Fa-f]{6}$/; + const eventColors = event.tags.filter(([n]) => n === 'c').map(([, c]) => c).filter((c) => hexRe.test(c)); + if (eventColors.length >= 2) base.colors = eventColors; base.layout = s.layout ?? event.tags.find(([n]) => n === 'layout')?.[1]; if (!base.emoji) { const raw = event.content?.trim(); @@ -150,7 +145,6 @@ export function resolveStationery(s: Stationery): ResolvedStationery { // No event or unknown kind — use legacy flat fallbacks (old letters, presets) base.textColor = s.textColor; base.primaryColor = s.primaryColor; - base.colors = s.colors; base.layout = s.layout; base.imageUrl = s.imageUrl; base.imageMode = s.imageMode ?? 'cover';