Photorealistic pencil/pen, separate bookmark actions, data-driven graph charts, double-click note creation, vintage tape styles
This commit is contained in:
1 parent
1d61edd9ce
commit
210749fcc8
6 files changed
+759
-196
No files matched your search
+50
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user