Photorealistic pencil/pen, separate bookmark actions, data-driven graph charts, double-click note creation, vintage tape styles

This commit is contained in:
airent106 committed 2026-10-13 11:24:05 -06:00
1 parent 1d61edd9ce
commit 210749fcc8
6 files changed
+759 -196

No files matched your search

+50
View File
@@ -816,6 +816,55 @@ export const App: React.FC = () => {
return card.type === settings.activeFilter;
});
// Double click anywhere on empty desk surface to instantly create a new sticky note
const handleDeskDoubleClick = (e: React.MouseEvent) => {
const target = e.target as HTMLElement;
if (
target.closest('[id^="card-"]') ||
target.closest('.cursor-grab') ||
target.closest('.cursor-default') ||
target.closest('button') ||
target.closest('input') ||
target.closest('a') ||
target.closest('textarea') ||
target.closest('select')
) {
return;
}
const deskX = Math.round(e.clientX / settings.zoom) - 90;
const deskY = Math.round(e.clientY / settings.zoom) - 80;
const quickNote: CardItem = {
id: `card-quick-${Date.now()}`,
type: 'sticky',
title: '',
content: 'Quick note...',
color: '#FFF9A6',
x: Math.max(20, deskX),
y: Math.max(50, deskY),
vx: 0,
vy: 0,
rotation: (Math.random() - 0.5) * 6,
width: 180,
height: 160,
zIndex: maxZIndex + 1,
isPinned: false,
attachType: 'washi',
attachColor: '#E29578',
fontFamily: settings.fontMode === 'typed' ? 'font-courier' : 'font-caveat'
};
setMaxZIndex((prev) => prev + 1);
setCards((prev) => {
const next = [...prev, quickNote];
saveToServer(next, propsList, strokes, settings, true);
return next;
});
setEditingCard(quickNote);
soundEngine.drop(settings.soundEnabled);
};
return (
<div
className="relative w-screen h-screen overflow-hidden select-none"
@@ -845,6 +894,7 @@ export const App: React.FC = () => {
height: `${(100 / settings.zoom).toFixed(2)}vh`,
transform: `scale(${settings.zoom})`
}}
onDoubleClick={handleDeskDoubleClick}
>
{visibleCards.map((card) => (
<PaperCard
+188 -38
View File
@@ -39,6 +39,7 @@ export const AddCardModal: React.FC<AddCardModalProps> = ({ isOpen, onClose, onA
// Graph state
const [graphType, setGraphType] = useState<'barchart' | 'flowchart' | 'curve'>('curve');
const [graphDataStr, setGraphDataStr] = useState('20, 55, 35, 90, 60, 85');
// Checklist state
const [todoItems, setTodoItems] = useState<{ text: string; done: boolean }[]>([
@@ -129,6 +130,11 @@ export const AddCardModal: React.FC<AddCardModalProps> = ({ isOpen, onClose, onA
else if (type === 'checklist') { defaultWidth = 200; defaultHeight = 240; defaultColor = '#FCFAF2'; }
else if (type === 'ticket') { defaultWidth = 200; defaultHeight = 165; defaultColor = '#F4EDE2'; }
const parsedGraphData = graphDataStr
.split(/[,\s]+/)
.map(Number)
.filter((n) => !isNaN(n) && n > 0);
onAdd({
type,
title: title.trim() || (type === 'bookmark' && formattedBookmarkUrl ? new URL(formattedBookmarkUrl).hostname : undefined),
@@ -146,6 +152,7 @@ export const AddCardModal: React.FC<AddCardModalProps> = ({ isOpen, onClose, onA
stamp: stamp !== 'none' ? stamp : undefined,
stampAngle: stamp !== 'none' ? Math.floor(Math.random() * 20 - 10) : undefined,
graphType: type === 'graph' ? graphType : undefined,
graphData: type === 'graph' ? parsedGraphData : undefined,
todoItems: type === 'checklist' ? todoItems : undefined,
receiptItems: type === 'receipt' ? receiptItems : undefined,
width: defaultWidth,
@@ -260,30 +267,72 @@ export const AddCardModal: React.FC<AddCardModalProps> = ({ isOpen, onClose, onA
</div>
)}
{/* Graph Type Selector */}
{/* Graph Type & Data Selector */}
{type === 'graph' && (
<div className="p-3 bg-neutral-900/80 rounded-lg border border-neutral-800 space-y-2">
<label className="block text-neutral-400 font-semibold">Diagram / Chart Type</label>
<div className="grid grid-cols-3 gap-2">
{[
{ id: 'curve', label: 'Wave / Vector' },
{ id: 'barchart', label: 'Bar Chart' },
{ id: 'flowchart', label: 'Flowchart' }
].map((g) => (
<button
type="button"
key={g.id}
onClick={() => setGraphType(g.id as any)}
className={`py-1.5 px-2 rounded border text-center font-mono ${
graphType === g.id
? 'bg-amber-500/20 text-amber-300 border-amber-500 font-bold'
: 'bg-neutral-800 text-neutral-400 border-neutral-700'
}`}
>
{g.label}
</button>
))}
<div className="p-3 bg-neutral-900/80 rounded-lg border border-neutral-800 space-y-3">
<div>
<label className="block text-neutral-400 font-semibold mb-1">Diagram / Chart Type</label>
<div className="grid grid-cols-3 gap-2">
{[
{ id: 'barchart', label: '📊 Bar Chart' },
{ id: 'curve', label: '📈 Trend Wave' },
{ id: 'flowchart', label: '🔀 Flowchart' }
].map((g) => (
<button
type="button"
key={g.id}
onClick={() => setGraphType(g.id as any)}
className={`py-1.5 px-2 rounded border text-center font-mono ${
graphType === g.id
? 'bg-amber-500/20 text-amber-300 border-amber-500 font-bold'
: 'bg-neutral-800 text-neutral-400 border-neutral-700'
}`}
>
{g.label}
</button>
))}
</div>
</div>
{(graphType === 'barchart' || graphType === 'curve') && (
<div>
<div className="flex justify-between items-center text-neutral-400 mb-1">
<label className="font-semibold">Chart Data Points</label>
<span className="text-[10px] text-neutral-500 font-mono">Numbers (comma-separated)</span>
</div>
<input
type="text"
value={graphDataStr}
onChange={(e) => setGraphDataStr(e.target.value)}
placeholder="e.g. 15, 45, 30, 85, 60, 95"
className="w-full bg-neutral-800 border border-neutral-700 rounded px-2.5 py-1.5 text-neutral-100 font-mono text-xs mb-2"
/>
<div className="flex items-center gap-1.5 text-[10px] font-mono">
<span className="text-neutral-500">Presets:</span>
<button
type="button"
onClick={() => setGraphDataStr('15, 35, 55, 75, 95')}
className="px-2 py-0.5 rounded bg-neutral-800 hover:bg-neutral-700 text-neutral-300 border border-neutral-700"
>
Growth
</button>
<button
type="button"
onClick={() => setGraphDataStr('70, 25, 90, 35, 80, 45')}
className="px-2 py-0.5 rounded bg-neutral-800 hover:bg-neutral-700 text-neutral-300 border border-neutral-700"
>
Spikes
</button>
<button
type="button"
onClick={() => setGraphDataStr('30, 60, 40, 75, 50, 85')}
className="px-2 py-0.5 rounded bg-neutral-800 hover:bg-neutral-700 text-neutral-300 border border-neutral-700"
>
Wave
</button>
</div>
</div>
)}
</div>
)}
@@ -452,14 +501,19 @@ export const AddCardModal: React.FC<AddCardModalProps> = ({ isOpen, onClose, onA
</div>
<div>
<label className="block text-neutral-400 mb-1">Fastener</label>
<label className="block text-neutral-400 mb-1">Fastener & Tape</label>
<select
value={attachType}
onChange={(e) => setAttachType(e.target.value as AttachType)}
className="w-full bg-neutral-800 border border-neutral-700 rounded px-2 py-1.5 text-neutral-200 cursor-pointer"
>
<option value="washi">Washi Tape</option>
<option value="washi">Pastel Washi Tape</option>
<option value="caution-tape">Caution / Police Tape</option>
<option value="black-tape">Black Gaffer Tape</option>
<option value="masking-tape">Beige Masking Tape</option>
<option value="washi-pattern">Grid Pattern Washi</option>
<option value="pin">Red Pushpin</option>
<option value="brass-pin">Brass Pushpin</option>
<option value="paperclip">Paperclip</option>
<option value="none">None (Loose)</option>
</select>
@@ -527,18 +581,28 @@ export const EditCardModal: React.FC<EditCardModalProps> = ({
const [imageUrl, setImageUrl] = useState(card.imageUrl || '');
const [url, setUrl] = useState(card.url || '');
const [stamp, setStamp] = useState<StampType | 'none'>(card.stamp || 'none');
const [attachType, setAttachType] = useState<AttachType>(card.attachType || 'washi');
const [graphType, setGraphType] = useState<'barchart' | 'flowchart' | 'curve'>(card.graphType as any || 'curve');
const [graphDataStr, setGraphDataStr] = useState(card.graphData ? card.graphData.join(', ') : '');
const handleSave = () => {
const formattedUrl = url.trim()
? (url.trim().startsWith('http') ? url.trim() : `https://${url.trim()}`)
: undefined;
const parsedGraphData = graphDataStr.trim()
? graphDataStr.split(/[,\s]+/).map(Number).filter((n) => !isNaN(n) && n > 0)
: undefined;
onSave({
...card,
title: title.trim() || undefined,
subtitle: subtitle.trim() || undefined,
content: content.trim(),
color,
attachType,
graphType: card.type === 'graph' ? graphType : card.graphType,
graphData: card.type === 'graph' ? parsedGraphData : card.graphData,
url: card.type === 'bookmark' ? formattedUrl : card.url,
imageUrl: (card.type === 'polaroid' || card.type === 'bookmark') ? (imageUrl.trim() || undefined) : card.imageUrl,
photoCaption: card.type === 'polaroid' ? (title.trim() || content.trim()) : undefined,
@@ -549,7 +613,7 @@ export const EditCardModal: React.FC<EditCardModalProps> = ({
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4 select-none">
<div className="bg-[#1C1A17] border border-neutral-700 w-full max-w-md rounded-xl p-5 text-neutral-200 shadow-2xl space-y-4 text-xs">
<div className="bg-[#1C1A17] border border-neutral-700 w-full max-w-md rounded-xl p-5 text-neutral-200 shadow-2xl space-y-4 text-xs max-h-[90vh] overflow-y-auto">
<div className="flex justify-between items-center pb-2 border-b border-neutral-800">
<h2 className="text-sm font-bold text-white flex items-center gap-2">
<span>Edit Card ({card.type})</span>
@@ -601,7 +665,93 @@ export const EditCardModal: React.FC<EditCardModalProps> = ({
</div>
)}
{/* Graph diagram and data controls */}
{card.type === 'graph' && (
<div className="p-3 bg-neutral-900/80 rounded-lg border border-neutral-800 space-y-2">
<label className="block text-neutral-400 font-semibold mb-1">Diagram / Chart Type</label>
<div className="grid grid-cols-3 gap-2">
{[
{ id: 'barchart', label: '📊 Bar Chart' },
{ id: 'curve', label: '📈 Trend Wave' },
{ id: 'flowchart', label: '🔀 Flowchart' }
].map((g) => (
<button
type="button"
key={g.id}
onClick={() => setGraphType(g.id as any)}
className={`py-1.5 px-2 rounded border text-center font-mono ${
graphType === g.id
? 'bg-amber-500/20 text-amber-300 border-amber-500 font-bold'
: 'bg-neutral-800 text-neutral-400 border-neutral-700'
}`}
>
{g.label}
</button>
))}
</div>
{(graphType === 'barchart' || graphType === 'curve') && (
<div className="pt-1">
<div className="flex justify-between items-center text-neutral-400 mb-1">
<label className="font-semibold">Chart Data Points</label>
<span className="text-[10px] text-neutral-500 font-mono">Numbers (comma-separated)</span>
</div>
<input
type="text"
value={graphDataStr}
onChange={(e) => setGraphDataStr(e.target.value)}
placeholder="e.g. 15, 45, 30, 85, 60, 95"
className="w-full bg-neutral-800 border border-neutral-700 rounded px-2.5 py-1.5 text-neutral-100 font-mono text-xs mb-2"
/>
<div className="flex items-center gap-1.5 text-[10px] font-mono">
<span className="text-neutral-500">Presets:</span>
<button
type="button"
onClick={() => setGraphDataStr('15, 35, 55, 75, 95')}
className="px-2 py-0.5 rounded bg-neutral-800 hover:bg-neutral-700 text-neutral-300 border border-neutral-700"
>
Growth
</button>
<button
type="button"
onClick={() => setGraphDataStr('70, 25, 90, 35, 80, 45')}
className="px-2 py-0.5 rounded bg-neutral-800 hover:bg-neutral-700 text-neutral-300 border border-neutral-700"
>
Spikes
</button>
<button
type="button"
onClick={() => setGraphDataStr('30, 60, 40, 75, 50, 85')}
className="px-2 py-0.5 rounded bg-neutral-800 hover:bg-neutral-700 text-neutral-300 border border-neutral-700"
>
Wave
</button>
</div>
</div>
)}
</div>
)}
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-neutral-400 mb-1">Fastener & Tape</label>
<select
value={attachType}
onChange={(e) => setAttachType(e.target.value as AttachType)}
className="w-full bg-neutral-800 border border-neutral-700 rounded px-2 py-1.5 text-neutral-200 cursor-pointer"
>
<option value="washi">Pastel Washi Tape</option>
<option value="caution-tape">Caution / Police Tape</option>
<option value="black-tape">Black Gaffer Tape</option>
<option value="masking-tape">Beige Masking Tape</option>
<option value="washi-pattern">Grid Pattern Washi</option>
<option value="pin">Red Pushpin</option>
<option value="brass-pin">Brass Pushpin</option>
<option value="paperclip">Paperclip</option>
<option value="none">None (Loose)</option>
</select>
</div>
<div>
<label className="block text-neutral-400 mb-1">Ink Stamp</label>
<select
@@ -619,20 +769,20 @@ export const EditCardModal: React.FC<EditCardModalProps> = ({
<option value="EVIDENCE">EVIDENCE</option>
</select>
</div>
</div>
<div>
<label className="block text-neutral-400 mb-1">Paper Tint</label>
<div className="flex gap-1.5 flex-wrap pt-1">
{['#FFF9A6', '#FFDFD3', '#E0D4FC', '#BEE7F5', '#C8F7C5', '#FFFFFF', '#282C34'].map((c) => (
<button
type="button"
key={c}
onClick={() => setColor(c)}
className={`w-5 h-5 rounded-full border ${color === c ? 'border-amber-400 scale-110' : 'border-neutral-600'}`}
style={{ backgroundColor: c }}
/>
))}
</div>
<div>
<label className="block text-neutral-400 mb-1">Paper Tint</label>
<div className="flex gap-1.5 flex-wrap pt-1">
{['#FFF9A6', '#FFDFD3', '#E0D4FC', '#BEE7F5', '#C8F7C5', '#FFFFFF', '#FAF8F5', '#E8DCC8', '#282C34'].map((c) => (
<button
type="button"
key={c}
onClick={() => setColor(c)}
className={`w-5 h-5 rounded-full border ${color === c ? 'border-amber-400 scale-110' : 'border-neutral-600'}`}
style={{ backgroundColor: c }}
/>
))}
</div>
</div>
+183 -52
View File
@@ -33,64 +33,195 @@ export const DeskPropsLayer: React.FC<DeskPropsProps> = ({
}}
onPointerDown={(e) => onPointerDown(e, prop)}
>
{/* 1. CLASSIC HB WOODEN PENCIL */}
{/* 1. PHOTO-REALISTIC DIXON TICONDEROGA HB PENCIL */}
{prop.type === 'pencil' && (
<div className="w-52 h-4 relative flex items-center">
{/* Graphite Lead Tip */}
<div
className="w-4 h-full bg-neutral-900"
style={{ clipPath: 'polygon(0% 50%, 100% 0%, 100% 100%)' }}
/>
{/* Sharpened Wood Bevel */}
<div
className="w-5 h-full bg-[#E3C59E]"
style={{ clipPath: 'polygon(0% 25%, 100% 0%, 100% 100%, 0% 75%)' }}
/>
{/* Yellow Hexagonal Body */}
<div
className="w-36 h-full bg-[#F5B025] flex flex-col justify-between py-[1px] px-1 border-y border-amber-600/40"
style={{
backgroundImage: 'linear-gradient(to bottom, rgba(255,255,255,0.4), transparent 40%, rgba(0,0,0,0.15) 80%)'
}}
<svg
width="220"
height="20"
viewBox="0 0 220 20"
fill="none"
className="overflow-visible filter drop-shadow-sm pointer-events-auto"
>
<defs>
<linearGradient id="pencil-facet-top" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#FEF08A" />
<stop offset="50%" stopColor="#FDE047" />
<stop offset="100%" stopColor="#FACC15" />
</linearGradient>
<linearGradient id="pencil-facet-mid" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#F59E0B" />
<stop offset="50%" stopColor="#EAB308" />
<stop offset="100%" stopColor="#CA8A04" />
</linearGradient>
<linearGradient id="pencil-facet-bot" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#B45309" />
<stop offset="50%" stopColor="#92400E" />
<stop offset="100%" stopColor="#78350F" />
</linearGradient>
<linearGradient id="pencil-wood" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#F5DEB3" />
<stop offset="35%" stopColor="#E6C280" />
<stop offset="70%" stopColor="#D4A76A" />
<stop offset="100%" stopColor="#B37D46" />
</linearGradient>
<linearGradient id="pencil-lead" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#475569" />
<stop offset="35%" stopColor="#1E293B" />
<stop offset="100%" stopColor="#0F172A" />
</linearGradient>
<linearGradient id="pencil-ferrule" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#FDE68A" />
<stop offset="25%" stopColor="#D97706" />
<stop offset="50%" stopColor="#FBBF24" />
<stop offset="75%" stopColor="#B45309" />
<stop offset="100%" stopColor="#78350F" />
</linearGradient>
<linearGradient id="pencil-eraser" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#FDA4AF" />
<stop offset="40%" stopColor="#F43F5E" />
<stop offset="100%" stopColor="#BE123C" />
</linearGradient>
</defs>
{/* Graphite Lead Cone Point */}
<polygon points="0,10 14,7 14,13" fill="url(#pencil-lead)" />
<line x1="2" y1="10" x2="14" y2="7.5" stroke="#94A3B8" strokeWidth="0.8" opacity="0.8" />
{/* Sharpened Cedar Wood Cone with 3 scalloped machine facets */}
<polygon points="14,7 40,3 40,17 14,13" fill="url(#pencil-wood)" />
<line x1="16" y1="8" x2="38" y2="5" stroke="#C28E58" strokeWidth="0.6" opacity="0.6" />
<line x1="17" y1="10" x2="39" y2="10" stroke="#B07D48" strokeWidth="0.7" opacity="0.5" />
<line x1="16" y1="12" x2="38" y2="15" stroke="#8C5828" strokeWidth="0.6" opacity="0.6" />
<path d="M 40 3 Q 36 6.5 40 10 Q 36 13.5 40 17" fill="none" stroke="#B45309" strokeWidth="0.8" />
{/* Hexagonal Lacquered Barrel */}
{/* 1. Top Facet */}
<rect x="40" y="3" width="140" height="4.5" fill="url(#pencil-facet-top)" />
<line x1="40" y1="3" x2="180" y2="3" stroke="#FEF9C3" strokeWidth="0.6" />
{/* 2. Middle Facet with Stamped Gold Foil Typography */}
<rect x="40" y="7.5" width="140" height="5" fill="url(#pencil-facet-mid)" />
<line x1="40" y1="7.5" x2="180" y2="7.5" stroke="#CA8A04" strokeWidth="0.5" />
<text
x="110"
y="11.2"
fontSize="4.2"
fontFamily="'Courier New', monospace"
fontWeight="bold"
letterSpacing="0.8"
fill="#78350F"
textAnchor="middle"
style={{ textShadow: '0 0.5px 0.5px rgba(254, 240, 138, 0.7)' }}
>
<span className="text-[7px] font-mono text-amber-900/60 font-bold tracking-widest pl-2">
№ 2 // HB
</span>
</div>
{/* Silver Metal Ferrule */}
<div
className="w-5 h-full bg-neutral-300 border-x border-neutral-400"
style={{
backgroundImage: 'linear-gradient(to bottom, rgba(255,255,255,0.8), rgba(0,0,0,0.2))'
}}
/>
★ GLIMPSE № 2 // HB · CEDAR ★
</text>
{/* 3. Bottom Facet */}
<rect x="40" y="12.5" width="140" height="4.5" fill="url(#pencil-facet-bot)" />
<line x1="40" y1="12.5" x2="180" y2="12.5" stroke="#78350F" strokeWidth="0.5" />
<line x1="40" y1="17" x2="180" y2="17" stroke="#451A03" strokeWidth="0.8" />
{/* Crimped Spun Brass Ferrule */}
<rect x="180" y="2.5" width="24" height="15" rx="0.5" fill="url(#pencil-ferrule)" stroke="#78350F" strokeWidth="0.5" />
<line x1="184" y1="2.5" x2="184" y2="17.5" stroke="#451A03" strokeWidth="1" />
<line x1="185" y1="2.5" x2="185" y2="17.5" stroke="#FEF08A" strokeWidth="0.8" />
<line x1="192" y1="2.5" x2="192" y2="17.5" stroke="#451A03" strokeWidth="1" />
<line x1="193" y1="2.5" x2="193" y2="17.5" stroke="#FEF08A" strokeWidth="0.8" />
{[187, 188, 189, 196, 197, 198, 199, 201].map((kx) => (
<line key={kx} x1={kx} y1="3" x2={kx} y2="17" stroke="rgba(120,53,15,0.4)" strokeWidth="0.5" />
))}
{/* Pink Rubber Eraser */}
<div className="w-4 h-full bg-[#F28B96] rounded-r-sm" />
</div>
<rect x="204" y="3" width="16" height="14" rx="2.5" fill="url(#pencil-eraser)" stroke="#9F1239" strokeWidth="0.5" />
<line x1="205" y1="5" x2="218" y2="5" stroke="#FECDD3" strokeWidth="0.8" opacity="0.7" />
</svg>
)}
{/* 2. BLACK GEL PEN */}
{/* 2. PHOTO-REALISTIC ROLLERBALL / GEL PEN */}
{prop.type === 'pen' && (
<div className="w-48 h-3.5 relative flex items-center">
{/* Fine Metal Tip */}
<div
className="w-5 h-full bg-neutral-300"
style={{ clipPath: 'polygon(0% 50%, 100% 15%, 100% 85%)' }}
/>
{/* Pen Grip */}
<div className="w-8 h-full bg-neutral-800 rounded-l-[1px] border-r border-neutral-700" />
{/* Matte Pen Body */}
<div
className="w-28 h-full bg-neutral-900"
style={{
backgroundImage: 'linear-gradient(to bottom, rgba(255,255,255,0.2), transparent 45%, rgba(0,0,0,0.3))'
}}
/>
{/* Metal Clip & Cap End */}
<div className="w-7 h-full bg-neutral-800 rounded-r-md relative">
<div className="absolute top-[-3px] right-2 w-10 h-1.5 bg-neutral-300 rounded-full border border-neutral-400 shadow-sm" />
</div>
</div>
<svg
width="215"
height="22"
viewBox="0 0 215 22"
fill="none"
className="overflow-visible filter drop-shadow-sm pointer-events-auto"
>
<defs>
<linearGradient id="pen-chrome" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#F8FAFC" />
<stop offset="25%" stopColor="#CBD5E1" />
<stop offset="50%" stopColor="#FFFFFF" />
<stop offset="75%" stopColor="#64748B" />
<stop offset="100%" stopColor="#334155" />
</linearGradient>
<linearGradient id="pen-body" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#334155" />
<stop offset="20%" stopColor="#0F172A" />
<stop offset="45%" stopColor="#475569" />
<stop offset="55%" stopColor="#1E293B" />
<stop offset="85%" stopColor="#090D16" />
<stop offset="100%" stopColor="#020617" />
</linearGradient>
<linearGradient id="pen-grip" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#4B5563" />
<stop offset="35%" stopColor="#1F2937" />
<stop offset="75%" stopColor="#111827" />
<stop offset="100%" stopColor="#030712" />
</linearGradient>
</defs>
{/* Tungsten Carbide Micro-Ball Tip */}
<circle cx="2" cy="11" r="1.5" fill="#0F172A" stroke="#475569" strokeWidth="0.5" />
<circle cx="1.6" cy="10.5" r="0.5" fill="#FFFFFF" opacity="0.9" />
{/* Machined Conical Stainless Steel Cone */}
<polygon points="2,11 18,6.5 18,15.5" fill="url(#pen-chrome)" stroke="#334155" strokeWidth="0.5" />
{/* Ribbed Ergonomic Rubber Grip with Tactile Ridges */}
<rect x="18" y="5.5" width="40" height="11" rx="0.5" fill="url(#pen-grip)" stroke="#111827" strokeWidth="0.5" />
{[22, 26, 30, 34, 38, 42, 46, 50, 54].map((rx) => (
<g key={rx}>
<line x1={rx} y1="5.5" x2={rx} y2="16.5" stroke="#374151" strokeWidth="0.8" />
<line x1={rx + 0.8} y1="5.5" x2={rx + 0.8} y2="16.5" stroke="#111827" strokeWidth="0.8" />
</g>
))}
{/* Polished Chrome Center Band Ring */}
<rect x="58" y="5" width="4" height="12" fill="url(#pen-chrome)" stroke="#475569" strokeWidth="0.5" />
{/* Piano Black Resin Barrel */}
<rect x="62" y="5.2" width="113" height="11.6" fill="url(#pen-body)" stroke="#0F172A" strokeWidth="0.5" />
<line x1="62" y1="8" x2="175" y2="8" stroke="rgba(255,255,255,0.4)" strokeWidth="0.9" />
<text
x="120"
y="12"
fontSize="3.8"
fontFamily="'Helvetica Neue', sans-serif"
fontWeight="600"
letterSpacing="0.6"
fill="#94A3B8"
textAnchor="middle"
opacity="0.85"
>
glimpse · 0.5 rollerball
</text>
{/* Cap & Crown Section */}
<rect x="175" y="4.8" width="30" height="12.4" rx="1" fill="url(#pen-body)" stroke="#020617" strokeWidth="0.5" />
<rect x="205" y="6" width="3.5" height="10" rx="1" fill="url(#pen-chrome)" stroke="#334155" strokeWidth="0.5" />
{/* Spring-Steel Chrome Pocket Clip */}
<path d="M 200 4 L 152 4 L 150 6" stroke="rgba(0,0,0,0.5)" strokeWidth="2.2" strokeLinecap="round" />
<path d="M 201 3.2 L 153 3.2 Q 150 3.2 150 5.5" fill="none" stroke="url(#pen-chrome)" strokeWidth="2.2" strokeLinecap="round" />
<path d="M 200 2.8 L 153 2.8" stroke="#FFFFFF" strokeWidth="0.7" opacity="0.9" />
<circle cx="198" cy="3.2" r="1.2" fill="#475569" stroke="#E2E8F0" strokeWidth="0.4" />
</svg>
)}
{/* 3. HIGHLIGHTER / MARKER */}
+120 -8
View File
@@ -12,6 +12,7 @@ interface FastenerProps {
export const Fastener: React.FC<FastenerProps> = ({ type, color = '#E6C280', angle = -4, lightAngle }) => {
if (type === 'none') return null;
// 1. Classic Pastel Washi Tape
if (type === 'washi') {
return (
<div
@@ -37,30 +38,141 @@ export const Fastener: React.FC<FastenerProps> = ({ type, color = '#E6C280', ang
clipPath: 'polygon(3% 0%, 97% 2%, 100% 95%, 97% 100%, 3% 98%, 0% 5%)'
}}
>
{/* Subtle tape sheen */}
<div className="absolute inset-0 bg-gradient-to-b from-white/20 to-transparent" />
</div>
</div>
);
}
if (type === 'pin') {
// 2. Yellow & Black Caution / Crime Scene Tape
if (type === 'caution-tape') {
return (
<div
className="absolute -top-2 left-1/2 -translate-x-1/2 z-40 pointer-events-none select-none"
className="absolute -top-3 left-1/2 -translate-x-1/2 z-30 pointer-events-none select-none"
style={{
filter: `drop-shadow(${getPinShadow(lightAngle)})`
transform: `translateX(-50%) rotate(${angle}deg)`,
filter: `drop-shadow(${getTapeShadow(lightAngle)})`
}}
>
<div className="w-5 h-5 rounded-full bg-gradient-to-br from-red-400 via-red-600 to-red-800 border border-red-900/40 relative shadow-inner flex items-center justify-center">
{/* Pushpin specular highlight */}
<div className="w-1.5 h-1.5 rounded-full bg-white/70 absolute top-0.5 left-1" />
<div className="w-1 h-1 rounded-full bg-red-950/60" />
<div
className="w-24 h-6 relative shadow-sm border-y border-yellow-600/30 font-mono text-[8px] font-black tracking-widest text-black flex items-center justify-center overflow-hidden"
style={{
backgroundColor: '#FACC15',
backgroundImage: 'repeating-linear-gradient(-45deg, #EAB308, #EAB308 10px, #18181B 10px, #18181B 20px)',
clipPath: 'polygon(2% 0%, 98% 3%, 100% 96%, 98% 100%, 2% 97%, 0% 4%)'
}}
>
<div className="absolute inset-0 bg-gradient-to-b from-white/25 via-transparent to-black/20" />
</div>
</div>
);
}
// 3. Matte Black Studio Gaffer Tape
if (type === 'black-tape') {
return (
<div
className="absolute -top-3 left-1/2 -translate-x-1/2 z-30 pointer-events-none select-none"
style={{
transform: `translateX(-50%) rotate(${angle}deg)`,
filter: `drop-shadow(${getTapeShadow(lightAngle)})`
}}
>
<div
className="w-20 h-6 relative bg-[#1C1917] opacity-95 border-y border-neutral-700/40"
style={{
backgroundImage: 'repeating-linear-gradient(90deg, rgba(255,255,255,0.04) 0px, rgba(255,255,255,0.04) 1px, transparent 1px, transparent 3px)',
clipPath: 'polygon(3% 0%, 97% 1%, 100% 98%, 97% 100%, 3% 97%, 0% 3%)'
}}
>
<div className="absolute inset-0 bg-gradient-to-b from-white/10 to-transparent" />
</div>
</div>
);
}
// 4. Vintage Beige Masking Tape
if (type === 'masking-tape') {
return (
<div
className="absolute -top-3 left-1/2 -translate-x-1/2 z-30 pointer-events-none select-none"
style={{
transform: `translateX(-50%) rotate(${angle}deg)`,
filter: `drop-shadow(${getTapeShadow(lightAngle)})`
}}
>
<div
className="w-22 h-6 relative bg-[#EAD7B8] opacity-90 backdrop-blur-[0.5px]"
style={{
backgroundImage: 'repeating-linear-gradient(45deg, rgba(0,0,0,0.03) 0px, rgba(0,0,0,0.03) 1px, transparent 1px, transparent 4px)',
clipPath: 'polygon(2% 1%, 98% 0%, 100% 95%, 98% 100%, 2% 98%, 0% 5%)'
}}
>
<div className="absolute inset-0 bg-gradient-to-b from-white/30 to-amber-900/10" />
</div>
</div>
);
}
// 5. Japanese Grid / Pattern Washi Tape
if (type === 'washi-pattern') {
return (
<div
className="absolute -top-3 left-1/2 -translate-x-1/2 z-30 pointer-events-none select-none"
style={{
transform: `translateX(-50%) rotate(${angle}deg)`,
filter: `drop-shadow(${getTapeShadow(lightAngle)})`
}}
>
<div
className="w-20 h-6 relative bg-[#DCE4EC] opacity-90"
style={{
backgroundImage: 'linear-gradient(to right, rgba(43,76,126,0.25) 1px, transparent 1px), linear-gradient(to bottom, rgba(43,76,126,0.25) 1px, transparent 1px)',
backgroundSize: '5px 5px',
clipPath: 'polygon(3% 0%, 97% 2%, 100% 95%, 97% 100%, 3% 98%, 0% 5%)'
}}
>
<div className="absolute inset-0 bg-gradient-to-b from-white/20 to-transparent" />
</div>
</div>
);
}
// 6. Classic Red Pushpin
if (type === 'pin') {
return (
<div
className="absolute -top-2.5 left-1/2 -translate-x-1/2 z-40 pointer-events-none select-none"
style={{
filter: `drop-shadow(${getPinShadow(lightAngle)})`
}}
>
<div className="w-5 h-5 rounded-full bg-gradient-to-br from-red-400 via-red-600 to-red-800 border border-red-900/50 relative shadow-inner flex items-center justify-center">
<div className="w-1.5 h-1.5 rounded-full bg-white/75 absolute top-0.5 left-1" />
<div className="w-1 h-1 rounded-full bg-red-950/70" />
</div>
</div>
);
}
// 7. Metallic Polished Brass Tack
if (type === 'brass-pin') {
return (
<div
className="absolute -top-2.5 left-1/2 -translate-x-1/2 z-40 pointer-events-none select-none"
style={{
filter: `drop-shadow(${getPinShadow(lightAngle)})`
}}
>
<div className="w-5 h-5 rounded-full bg-gradient-to-br from-amber-200 via-amber-400 to-amber-700 border border-amber-900/60 relative shadow-md flex items-center justify-center">
<div className="w-1.5 h-1.5 rounded-full bg-white/90 absolute top-0.5 left-1" />
<div className="w-1.5 h-1.5 rounded-full bg-amber-950/50" />
</div>
</div>
);
}
// 8. Silver Wire Paperclip
if (type === 'paperclip') {
return (
<div
+206 -97
View File
@@ -348,71 +348,177 @@ export const PaperCard: React.FC<PaperCardProps> = ({
)}
{/* 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>
{card.type === 'graph' && (() => {
// Parse data points
let rawData = card.graphData;
if (!rawData || !Array.isArray(rawData) || rawData.length === 0) {
const parsedFromContent = card.content
? card.content.split(/[,\s]+/).map(Number).filter((n) => !isNaN(n) && n > 0)
: [];
rawData = parsedFromContent.length >= 2 ? parsedFromContent : [24, 62, 38, 92, 54, 85];
}
const data = rawData.slice(0, 10);
const maxVal = Math.max(...data, 10);
const minVal = Math.min(...data, 0);
return (
<div
className="p-3.5 flex flex-col justify-between h-full"
style={{
backgroundImage: 'linear-gradient(to right, rgba(100,140,200,0.18) 1px, transparent 1px), linear-gradient(to bottom, rgba(100,140,200,0.18) 1px, transparent 1px)',
backgroundSize: '12px 12px'
}}
>
<div className="flex justify-between items-baseline mb-1 border-b border-blue-900/10 pb-1">
<span className="font-courier text-xs font-bold text-neutral-800 tracking-wide">
{card.title || 'Technical Draft'}
</span>
<span className="text-[9px] font-mono text-neutral-500 font-bold">FIG. {data.length}P</span>
</div>
{/* Dynamic SVG Charts */}
<div className="my-auto py-1 flex items-center justify-center overflow-hidden">
{card.graphType === 'barchart' && (() => {
const svgW = 160;
const svgH = 88;
const baselineY = 72;
const barCount = data.length;
const availW = svgW - 30;
const barW = Math.max(8, Math.min(22, availW / barCount - 4));
const stepX = availW / barCount;
return (
<svg width={svgW} height={svgH} viewBox={`0 0 ${svgW} ${svgH}`} fill="none" className="overflow-visible">
<line x1="12" y1={baselineY} x2={svgW - 8} y2={baselineY} stroke="#64748B" strokeWidth="1.2" />
<line x1="12" y1="12" x2="12" y2={baselineY} stroke="#64748B" strokeWidth="1.2" />
{data.map((val, idx) => {
const barH = Math.max(4, (val / maxVal) * 52);
const bx = 16 + idx * stepX + (stepX - barW) / 2;
const by = baselineY - barH;
return (
<g key={idx}>
<rect
x={bx}
y={by}
width={barW}
height={barH}
fill="rgba(59, 130, 246, 0.12)"
stroke="#2563EB"
strokeWidth="1.4"
/>
<clipPath id={`clip-${card.id}-${idx}`}>
<rect x={bx} y={by} width={barW} height={barH} />
</clipPath>
<g clipPath={`url(#clip-${card.id}-${idx})`}>
{[-10, 0, 10, 20, 30, 40, 50, 60].map((offset) => (
<line
key={offset}
x1={bx - 5}
y1={by + offset}
x2={bx + barW + 5}
y2={by + offset - barW}
stroke="#3B82F6"
strokeWidth="1"
opacity="0.8"
/>
))}
</g>
<text
x={bx + barW / 2}
y={by - 2.5}
fontSize="7.5"
fill="#1E3A8A"
fontFamily="monospace"
textAnchor="middle"
fontWeight="bold"
>
{val}
</text>
</g>
);
})}
</svg>
);
})()}
{card.graphType === 'curve' && (() => {
const svgW = 160;
const svgH = 88;
const baselineY = 72;
const points = data.map((val, idx) => {
const px = 18 + idx * ((svgW - 32) / Math.max(1, data.length - 1));
const py = baselineY - ((val - minVal) / Math.max(1, maxVal - minVal)) * 52;
return { x: px, y: py, val };
});
const d = points.reduce((acc, pt, i, arr) => {
if (i === 0) return `M ${pt.x} ${pt.y}`;
const prev = arr[i - 1];
const cpX = (prev.x + pt.x) / 2;
return `${acc} C ${cpX} ${prev.y}, ${cpX} ${pt.y}, ${pt.x} ${pt.y}`;
}, '');
const areaD = `${d} L ${points[points.length - 1].x} ${baselineY} L ${points[0].x} ${baselineY} Z`;
return (
<svg width={svgW} height={svgH} viewBox={`0 0 ${svgW} ${svgH}`} fill="none" className="overflow-visible">
<defs>
<linearGradient id={`curve-fill-${card.id}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#3B82F6" stopOpacity="0.25" />
<stop offset="100%" stopColor="#3B82F6" stopOpacity="0.0" />
</linearGradient>
</defs>
<line x1="12" y1={baselineY} x2={svgW - 8} y2={baselineY} stroke="#94A3B8" strokeWidth="1" />
<line x1="12" y1="12" x2="12" y2={baselineY} stroke="#94A3B8" strokeWidth="1" />
<path d={areaD} fill={`url(#curve-fill-${card.id})`} />
<path d={d} fill="none" stroke="#2563EB" strokeWidth="2" strokeLinecap="round" />
{points.map((pt, i) => (
<g key={i}>
<circle cx={pt.x} cy={pt.y} r="2.8" fill="#FFFFFF" stroke="#2563EB" strokeWidth="1.5" />
<circle cx={pt.x} cy={pt.y} r="1.2" fill="#2563EB" />
<text
x={pt.x}
y={pt.y - 4.5}
fontSize="7"
fill="#1E3A8A"
fontFamily="monospace"
textAnchor="middle"
fontWeight="bold"
>
{pt.val}
</text>
</g>
))}
</svg>
);
})()}
{card.graphType === 'flowchart' && (
<svg width="160" height="85" viewBox="0 0 160 85" fill="none" stroke="#475569" strokeWidth="1.5">
<rect x="8" y="12" width="42" height="24" rx="2" fill="white" stroke="#334155" />
<text x="14" y="27" fontSize="8.5" fill="#334155" fontFamily="monospace" fontWeight="bold">INPUT</text>
<path d="M 50 24 L 68 24" stroke="#475569" strokeWidth="1.5" />
<rect x="68" y="12" width="42" height="24" rx="2" fill="white" stroke="#334155" />
<text x="73" y="27" fontSize="8.5" fill="#334155" fontFamily="monospace" fontWeight="bold">PROCESS</text>
<path d="M 110 24 L 126 24" stroke="#475569" strokeWidth="1.5" />
<circle cx="136" cy="24" r="10" fill="white" stroke="#2563EB" strokeWidth="1.5" />
<text x="131" y="27" fontSize="7.5" fill="#2563EB" fontFamily="monospace">OK</text>
<path d="M 89 36 L 89 54 L 110 54" stroke="#475569" strokeWidth="1.2" strokeDasharray="2 2" />
<rect x="110" y="44" width="42" height="20" rx="2" fill="#FEF2F2" stroke="#EF4444" strokeWidth="1.2" />
<text x="116" y="57" fontSize="7.5" fill="#DC2626" fontFamily="monospace">ERROR</text>
</svg>
)}
</div>
<div className="flex justify-between items-center text-[9px] text-neutral-500 font-courier pt-1 border-t border-blue-900/10">
<span>{card.subtitle || 'SCALE 1:1 · MM'}</span>
<span>MAX: {maxVal}</span>
</div>
</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' && (
@@ -436,36 +542,22 @@ export const PaperCard: React.FC<PaperCardProps> = ({
{/* 9. BOOKMARK / WEB PREVIEW CARD */}
{card.type === 'bookmark' && (
<div className="p-3 bg-[#FAF8F5] text-neutral-900 flex flex-col justify-between h-full rounded border border-neutral-300/80 shadow-xs">
{/* Header: Favicon + Domain + External Link */}
<div className="flex items-center justify-between gap-1.5 pb-1.5 mb-1.5 border-b border-neutral-200/90">
<div className="flex items-center gap-1.5 overflow-hidden">
{card.faviconUrl ? (
<img src={card.faviconUrl} alt="" className="w-3.5 h-3.5 rounded-xs shrink-0" />
) : (
<Globe size={13} className="text-neutral-500 shrink-0" />
)}
<span className="font-mono text-[10px] text-neutral-600 truncate font-semibold">
{card.url ? (() => {
try {
return new URL(card.url.startsWith('http') ? card.url : `https://${card.url}`).hostname.replace('www.', '');
} catch {
return 'link';
}
})() : 'bookmark'}
</span>
</div>
{card.url && (
<a
href={card.url.startsWith('http') ? card.url : `https://${card.url}`}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className="p-1 rounded bg-neutral-200/60 hover:bg-amber-400 hover:text-black text-neutral-600 transition shrink-0"
title="Open link in new window"
>
<ExternalLink size={11} />
</a>
{/* Header: Favicon + Domain (pr-8 keeps clear of the top-right Pencil edit button) */}
<div className="flex items-center gap-1.5 pb-1.5 mb-1.5 border-b border-neutral-200/90 pr-8 overflow-hidden">
{card.faviconUrl ? (
<img src={card.faviconUrl} alt="" className="w-3.5 h-3.5 rounded-xs shrink-0" />
) : (
<Globe size={13} className="text-neutral-500 shrink-0" />
)}
<span className="font-mono text-[10px] text-neutral-600 truncate font-semibold">
{card.url ? (() => {
try {
return new URL(card.url.startsWith('http') ? card.url : `https://${card.url}`).hostname.replace('www.', '');
} catch {
return 'link';
}
})() : 'bookmark'}
</span>
</div>
{/* Thumbnail Preview Image */}
@@ -520,12 +612,29 @@ export const PaperCard: React.FC<PaperCardProps> = ({
)}
</div>
{/* Bottom Dedicated Open Link Button (Completely separate from top-right edit) */}
{card.url && (
<div className="mt-2 pt-1.5 border-t border-neutral-200/80 flex items-center justify-between text-[10px] text-neutral-500 font-mono">
<span className="truncate max-w-[130px]">{card.url}</span>
<span className="text-amber-600 font-bold flex items-center gap-0.5">
↗
<div className="mt-2.5 pt-2 border-t border-neutral-200 flex items-center justify-between gap-2">
<span className="truncate text-[10px] text-neutral-500 font-mono flex-1">
{(() => {
try {
return new URL(card.url.startsWith('http') ? card.url : `https://${card.url}`).hostname.replace('www.', '');
} catch {
return card.url;
}
})()}
</span>
<a
href={card.url.startsWith('http') ? card.url : `https://${card.url}`}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className="px-2.5 py-1 rounded bg-amber-500 hover:bg-amber-400 text-neutral-950 font-bold transition flex items-center gap-1 text-[10px] shrink-0 shadow-xs active:scale-95"
title="Open link in new window"
>
<span>Open Link</span>
<ExternalLink size={10} />
</a>
</div>
)}
</div>
+12 -1
View File
@@ -11,7 +11,16 @@ export type CardType =
| 'checklist'
| 'ticket';
export type AttachType = 'none' | 'washi' | 'paperclip' | 'pin';
export type AttachType =
| 'none'
| 'washi'
| 'paperclip'
| 'pin'
| 'caution-tape'
| 'black-tape'
| 'brass-pin'
| 'masking-tape'
| 'washi-pattern';
export type DeskTexture =
| 'oak'
@@ -58,6 +67,8 @@ export interface CardItem {
fontFamily: string;
todoItems?: { text: string; done: boolean }[];
graphType?: 'barchart' | 'flowchart' | 'wireframe' | 'curve';
graphData?: number[];
graphLabels?: string[];
receiptItems?: { name: string; price: string }[];
tagNumber?: string;
aspectRatio?: number;