Initial commit of Glimpse: Procedural paper desk and moodboard workspace
This commit is contained in:
1 parent
eb0ad24141
commit
eb3787dd16
29 files changed
+6333
No files matched your search
@@ -0,0 +1,406 @@
|
||||
import React, { useState } from 'react';
|
||||
import { CardItem } from '../types';
|
||||
import { getShadowStyle } from '../utils/lighting';
|
||||
import { Fastener, Stamp, PhotoCorners } from './Fastener';
|
||||
|
||||
interface PaperCardProps {
|
||||
card: CardItem;
|
||||
lightAngle: number;
|
||||
isDragging: boolean;
|
||||
onPointerDown: (e: React.PointerEvent, card: CardItem) => void;
|
||||
onDoubleClick: (card: CardItem) => void;
|
||||
onToggleTodo?: (cardId: string, index: number) => void;
|
||||
}
|
||||
|
||||
export const PaperCard: React.FC<PaperCardProps> = ({
|
||||
card,
|
||||
lightAngle,
|
||||
isDragging,
|
||||
onPointerDown,
|
||||
onDoubleClick,
|
||||
onToggleTodo
|
||||
}) => {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
// Get dynamic shadow based on light angle and drag state
|
||||
const shadowStyle = getShadowStyle(
|
||||
lightAngle,
|
||||
isDragging,
|
||||
isHovered ? 1.25 : 1.0,
|
||||
card.type === 'sticky'
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
id={card.id}
|
||||
className={`absolute cursor-grab active:cursor-grabbing select-none transition-transform will-change-transform ${
|
||||
isDragging ? 'z-50 scale-[1.03]' : ''
|
||||
}`}
|
||||
style={{
|
||||
left: `${card.x}px`,
|
||||
top: `${card.y}px`,
|
||||
width: `${card.width}px`,
|
||||
zIndex: card.zIndex,
|
||||
transform: `rotate(${card.rotation}deg)`,
|
||||
transformOrigin: 'center center',
|
||||
touchAction: 'none'
|
||||
}}
|
||||
onPointerDown={(e) => onPointerDown(e, card)}
|
||||
onDoubleClick={() => onDoubleClick(card)}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
{/* Top Attachment (Washi tape, pin, or paperclip) */}
|
||||
<Fastener
|
||||
type={card.attachType}
|
||||
color={card.attachColor}
|
||||
angle={card.attachAngle}
|
||||
lightAngle={lightAngle}
|
||||
/>
|
||||
|
||||
{/* Main Paper Body */}
|
||||
<div
|
||||
className="relative rounded-sm overflow-hidden text-neutral-900 transition-shadow duration-150"
|
||||
style={{
|
||||
backgroundColor: card.color || '#FFF9A6',
|
||||
boxShadow: shadowStyle.boxShadow,
|
||||
minHeight: `${card.height}px`
|
||||
}}
|
||||
>
|
||||
{/* Subtle Paper Grain Filter Overlay */}
|
||||
<div className="absolute inset-0 pointer-events-none opacity-25 mix-blend-multiply bg-[radial-gradient(#000_1px,transparent_1px)] [background-size:3px_3px]" />
|
||||
|
||||
{/* Paper Corner Curled Illusion for Sticky Notes */}
|
||||
{card.type === 'sticky' && (
|
||||
<div
|
||||
className="absolute -bottom-1 -right-1 w-6 h-6 pointer-events-none"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, transparent 50%, rgba(0,0,0,0.12) 50%)',
|
||||
borderTopLeftRadius: '2px'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Rubber Stamp */}
|
||||
<Stamp type={card.stamp} angle={card.stampAngle} />
|
||||
|
||||
{/* 1. POLAROID CARD */}
|
||||
{card.type === 'polaroid' && (
|
||||
<div className="p-2.5 pb-4 bg-white flex flex-col items-center">
|
||||
<PhotoCorners />
|
||||
<div className="w-full aspect-[4/3] bg-neutral-200 overflow-hidden relative shadow-inner">
|
||||
{card.imageUrl ? (
|
||||
<img
|
||||
src={card.imageUrl}
|
||||
alt={card.title || 'Polaroid'}
|
||||
className="w-full h-full object-cover pointer-events-none"
|
||||
loading="lazy"
|
||||
draggable={false}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-xs text-neutral-400">
|
||||
No Image
|
||||
</div>
|
||||
)}
|
||||
{/* Glossy photo finish sheen */}
|
||||
<div className="absolute inset-0 bg-gradient-to-tr from-transparent via-white/10 to-transparent pointer-events-none" />
|
||||
</div>
|
||||
|
||||
<div className="mt-3 text-center w-full px-1">
|
||||
<span className={`text-base font-medium text-neutral-800 whitespace-pre-line tracking-wide ${card.fontFamily}`}>
|
||||
{card.photoCaption || card.title}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 2. STICKY NOTE */}
|
||||
{card.type === 'sticky' && (
|
||||
<div className={`p-4 flex flex-col justify-between h-full ${card.fontFamily}`}>
|
||||
{card.title && (
|
||||
<div className="text-base font-bold tracking-tight text-neutral-900/90 mb-1 border-b border-black/5 pb-1">
|
||||
{card.title}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{card.todoItems && card.todoItems.length > 0 ? (
|
||||
<div className="space-y-1 my-1">
|
||||
{card.todoItems.map((item, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="flex items-center gap-2 cursor-pointer group"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggleTodo?.(card.id, idx);
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={item.done}
|
||||
readOnly
|
||||
className="cursor-pointer accent-neutral-800 rounded-sm w-3.5 h-3.5 pointer-events-none"
|
||||
/>
|
||||
<span
|
||||
className={`text-sm leading-tight transition-all ${
|
||||
item.done ? 'line-through text-neutral-500/70' : 'text-neutral-800'
|
||||
}`}
|
||||
>
|
||||
{item.text}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={`text-base leading-snug whitespace-pre-line ${
|
||||
card.color === '#282C34' ? 'text-white/90 text-2xl text-center my-auto py-2' : 'text-neutral-800'
|
||||
}`}
|
||||
>
|
||||
{card.content}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{card.date && (
|
||||
<div className="text-[11px] text-neutral-500 text-right mt-2 font-mono">
|
||||
{card.date}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 3. KRAFT LUGGAGE / PARCEL TAG */}
|
||||
{card.type === 'tag' && (
|
||||
<div
|
||||
className="p-4 flex flex-col items-center justify-between text-neutral-800 relative [clip-path:polygon(20%_0%,80%_0%,100%_15%,100%_100%,0%_100%,0%_15%)]"
|
||||
style={{ backgroundColor: card.color || '#CBB296', minHeight: `${card.height}px` }}
|
||||
>
|
||||
{/* Tag string & brass eyelet */}
|
||||
<div className="w-5 h-5 rounded-full border-2 border-amber-900/60 bg-amber-100/50 shadow-inner mt-1 mb-3 flex items-center justify-center">
|
||||
<div className="w-2.5 h-2.5 rounded-full bg-neutral-900/80 shadow" />
|
||||
</div>
|
||||
|
||||
<div className="text-center w-full my-auto space-y-2">
|
||||
<div className="text-xs font-mono font-bold tracking-widest text-neutral-700 uppercase">
|
||||
{card.tagNumber || '#1828'}
|
||||
</div>
|
||||
<div className="text-xl font-bold tracking-wider font-courier uppercase text-neutral-900">
|
||||
{card.subtitle || 'fragile'}
|
||||
</div>
|
||||
<div className="text-xs font-mono text-neutral-600">
|
||||
{card.date || 'Sept 4'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full text-center border-t border-amber-900/20 pt-2 text-[10px] tracking-widest uppercase font-mono text-neutral-600">
|
||||
PRIORITY PARCEL
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 4. TORN PAPER / LETTER */}
|
||||
{card.type === 'torn' && (
|
||||
<div
|
||||
className="p-5 flex flex-col justify-between"
|
||||
style={{
|
||||
clipPath: 'polygon(0% 2%, 8% 0%, 18% 2.5%, 28% 0.5%, 40% 3%, 52% 0.5%, 65% 2%, 78% 0%, 90% 2.5%, 100% 0%, 100% 100%, 0% 100%)'
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-between items-baseline border-b border-neutral-400/30 pb-2 mb-3">
|
||||
<span className="font-mono text-xs uppercase tracking-wider text-neutral-500 font-bold">
|
||||
{card.title || 'Memo'}
|
||||
</span>
|
||||
<span className="text-[11px] font-mono text-neutral-400">
|
||||
{card.date || 'Fri — late'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{card.subtitle && (
|
||||
<div className="text-sm font-semibold mb-2 font-courier text-neutral-700">
|
||||
{card.subtitle}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{card.todoItems && card.todoItems.length > 0 ? (
|
||||
<div className="space-y-1.5 font-caveat text-lg">
|
||||
{card.todoItems.map((item, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="flex items-center gap-2 cursor-pointer group"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggleTodo?.(card.id, idx);
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={item.done}
|
||||
readOnly
|
||||
className="cursor-pointer accent-neutral-800 rounded-sm w-4 h-4 pointer-events-none"
|
||||
/>
|
||||
<span className={`${item.done ? 'line-through text-neutral-400' : 'text-neutral-800'}`}>
|
||||
{item.text}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className={`text-sm leading-relaxed whitespace-pre-line text-neutral-800 ${card.fontFamily}`}>
|
||||
{card.content}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4 pt-2 border-t border-dashed border-neutral-300 text-[10px] text-neutral-400 font-mono flex justify-between">
|
||||
<span>NOTES // LOG</span>
|
||||
<span>CONFIDENTIAL</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 5. MINIMAL BUSINESS CARD */}
|
||||
{card.type === 'business' && (
|
||||
<div className="p-5 flex flex-col justify-between h-full font-courier border border-black/5">
|
||||
<div className="text-right text-[10px] tracking-widest text-neutral-500 font-mono">
|
||||
[ JL ]
|
||||
</div>
|
||||
<div className="my-auto">
|
||||
<div className="text-sm font-bold tracking-widest text-neutral-900 uppercase">
|
||||
{card.title || 'JULES LAURENT'}
|
||||
</div>
|
||||
<div className="text-[11px] text-neutral-600 tracking-wider mt-0.5 uppercase">
|
||||
{card.subtitle || 'PRODUCT DESIGNER'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[10px] text-neutral-500 whitespace-pre-line font-mono tracking-tight pt-2 border-t border-neutral-400/20">
|
||||
{card.content}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 6. STORE RECEIPT */}
|
||||
{card.type === 'receipt' && (
|
||||
<div
|
||||
className="p-4 flex flex-col justify-between text-neutral-800 font-mono text-[11px]"
|
||||
style={{
|
||||
clipPath: 'polygon(0% 0%, 5% 1.5%, 10% 0%, 15% 1.5%, 20% 0%, 25% 1.5%, 30% 0%, 35% 1.5%, 40% 0%, 45% 1.5%, 50% 0%, 55% 1.5%, 60% 0%, 65% 1.5%, 70% 0%, 75% 1.5%, 80% 0%, 85% 1.5%, 90% 0%, 95% 1.5%, 100% 0%, 100% 100%, 95% 98.5%, 90% 100%, 85% 98.5%, 80% 100%, 75% 98.5%, 70% 100%, 65% 98.5%, 60% 100%, 55% 98.5%, 50% 100%, 45% 98.5%, 40% 100%, 35% 98.5%, 30% 100%, 25% 98.5%, 20% 100%, 15% 98.5%, 10% 100%, 5% 98.5%, 0% 100%)'
|
||||
}}
|
||||
>
|
||||
<div className="text-center pb-2 border-b border-dashed border-neutral-400">
|
||||
<div className="font-bold text-xs tracking-wider">{card.title || 'MALL STORE'}</div>
|
||||
<div className="text-[10px] text-neutral-500">{card.subtitle}</div>
|
||||
<div className="text-[10px] text-neutral-400 mt-1">{card.date || '10/24 15:34'}</div>
|
||||
</div>
|
||||
|
||||
<div className="py-3 space-y-1">
|
||||
{card.receiptItems?.map((it, idx) => (
|
||||
<div key={idx} className="flex justify-between items-center text-[10px]">
|
||||
<span className={idx >= (card.receiptItems?.length || 0) - 2 ? 'font-bold' : ''}>
|
||||
{it.name}
|
||||
</span>
|
||||
<span className="font-mono">{it.price}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Barcode representation */}
|
||||
<div className="pt-2 border-t border-dashed border-neutral-400 flex flex-col items-center">
|
||||
<div className="h-6 w-32 flex items-stretch gap-[1.5px] justify-center opacity-80 my-1">
|
||||
{[2, 1, 3, 1, 2, 4, 1, 3, 2, 1, 3, 1, 2, 1, 4, 2, 1, 3, 2, 1].map((w, i) => (
|
||||
<div key={i} className="bg-neutral-900" style={{ width: `${w}px` }} />
|
||||
))}
|
||||
</div>
|
||||
<span className="text-[9px] tracking-widest text-neutral-500 font-mono">
|
||||
* 8 4 9 2 0 1 8 5 *
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 7. GRAPH / WIREFRAME SKETCH */}
|
||||
{card.type === 'graph' && (
|
||||
<div
|
||||
className="p-4 flex flex-col justify-between"
|
||||
style={{
|
||||
backgroundImage: 'linear-gradient(to right, rgba(100,140,200,0.15) 1px, transparent 1px), linear-gradient(to bottom, rgba(100,140,200,0.15) 1px, transparent 1px)',
|
||||
backgroundSize: '12px 12px'
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-between items-baseline mb-2">
|
||||
<span className="font-courier text-xs font-bold text-neutral-700">
|
||||
{card.title}
|
||||
</span>
|
||||
<span className="text-[10px] font-mono text-neutral-400">Fig. 1A</span>
|
||||
</div>
|
||||
|
||||
{/* SVG Hand-drawn Charts */}
|
||||
<div className="my-auto py-2 flex items-center justify-center">
|
||||
{card.graphType === 'barchart' && (
|
||||
<svg width="150" height="90" viewBox="0 0 150 90" fill="none" stroke="#2563EB" strokeWidth="1.8">
|
||||
<path d="M 10 80 L 140 80" stroke="#64748B" />
|
||||
<path d="M 10 10 L 10 80" stroke="#64748B" />
|
||||
{/* Hatched bars */}
|
||||
<rect x="25" y="45" width="18" height="35" fill="none" />
|
||||
<path d="M 25 75 L 43 55 M 25 65 L 43 45 M 25 55 L 35 45" stroke="#3B82F6" strokeWidth="1.2" />
|
||||
<rect x="55" y="25" width="18" height="55" fill="none" />
|
||||
<path d="M 55 75 L 73 55 M 55 65 L 73 45 M 55 55 L 73 35 M 55 45 L 73 25" stroke="#3B82F6" strokeWidth="1.2" />
|
||||
<rect x="85" y="15" width="18" height="65" fill="none" />
|
||||
<path d="M 85 75 L 103 55 M 85 65 L 103 45 M 85 55 L 103 35 M 85 45 L 103 25 M 85 35 L 103 15" stroke="#3B82F6" strokeWidth="1.2" />
|
||||
<rect x="115" y="38" width="18" height="42" fill="none" />
|
||||
<path d="M 115 75 L 133 55 M 115 65 L 133 45 M 115 55 L 133 35" stroke="#3B82F6" strokeWidth="1.2" />
|
||||
</svg>
|
||||
)}
|
||||
|
||||
{card.graphType === 'flowchart' && (
|
||||
<svg width="160" height="85" viewBox="0 0 160 85" fill="none" stroke="#475569" strokeWidth="1.5">
|
||||
<rect x="10" y="10" width="40" height="25" rx="2" fill="white" />
|
||||
<text x="16" y="26" fontSize="9" fill="#475569" fontFamily="monospace">INPUT</text>
|
||||
<path d="M 50 22 L 70 22" stroke="#475569" markerEnd="url(#arrow)" />
|
||||
<rect x="70" y="10" width="40" height="25" rx="2" fill="white" />
|
||||
<text x="74" y="26" fontSize="9" fill="#475569" fontFamily="monospace">TRANS</text>
|
||||
<path d="M 110 22 L 130 22" stroke="#475569" />
|
||||
<circle cx="140" cy="22" r="10" fill="white" />
|
||||
{/* Branch down */}
|
||||
<path d="M 90 35 L 90 55 L 110 55" stroke="#475569" />
|
||||
<rect x="110" y="45" width="40" height="22" rx="2" fill="white" />
|
||||
<text x="115" y="59" fontSize="8" fill="#475569" fontFamily="monospace">OUTPUT</text>
|
||||
</svg>
|
||||
)}
|
||||
|
||||
{card.graphType === 'curve' && (
|
||||
<svg width="150" height="85" viewBox="0 0 150 85" fill="none" stroke="#2563EB" strokeWidth="2">
|
||||
<path d="M 10 75 L 140 75" stroke="#94A3B8" strokeWidth="1" />
|
||||
<path d="M 10 10 L 10 75" stroke="#94A3B8" strokeWidth="1" />
|
||||
<path d="M 10 65 Q 40 70 70 40 T 135 15" stroke="#2563EB" strokeWidth="2" strokeDasharray="3 2" />
|
||||
<circle cx="70" cy="40" r="3" fill="#2563EB" />
|
||||
<circle cx="135" cy="15" r="3" fill="#EF4444" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-[10px] text-neutral-500 font-courier text-right">
|
||||
scale 1:1 · mm
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 8. MOODBOARD CARD */}
|
||||
{card.type === 'moodboard' && (
|
||||
<div className="p-4 flex flex-col justify-between h-full font-caveat">
|
||||
<div>
|
||||
<div className="text-xl font-bold text-neutral-900 border-b border-neutral-300/60 pb-1 mb-2">
|
||||
{card.title || 'Moodboard'}
|
||||
</div>
|
||||
<div className="text-base text-neutral-800 leading-snug whitespace-pre-line">
|
||||
{card.content}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 pt-2 border-t border-neutral-200 flex items-center justify-between text-xs text-neutral-500 font-sans">
|
||||
<span>{card.subtitle || 'Creative Direction'}</span>
|
||||
<span className="font-mono text-[10px]">#mood</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in new issue
Block a user