Add desk lock mode, web bookmark card type, sound default, and buried visual effects
This commit is contained in:
1 parent
15fbe1db88
commit
19eb968f86
7 files changed
+493
-187
No files matched your search
+132
-16
@@ -2,12 +2,13 @@ import React, { useState } from 'react';
|
||||
import { CardItem } from '../types';
|
||||
import { getShadowStyle } from '../utils/lighting';
|
||||
import { Fastener, Stamp, PhotoCorners } from './Fastener';
|
||||
import { Pencil } from 'lucide-react';
|
||||
import { Pencil, Globe, ExternalLink } from 'lucide-react';
|
||||
|
||||
interface PaperCardProps {
|
||||
card: CardItem;
|
||||
lightAngle: number;
|
||||
isDragging: boolean;
|
||||
isLocked?: boolean;
|
||||
onPointerDown: (e: React.PointerEvent, card: CardItem) => void;
|
||||
onDoubleClick: (card: CardItem) => void;
|
||||
onToggleTodo?: (cardId: string, index: number) => void;
|
||||
@@ -17,6 +18,7 @@ export const PaperCard: React.FC<PaperCardProps> = ({
|
||||
card,
|
||||
lightAngle,
|
||||
isDragging,
|
||||
isLocked = false,
|
||||
onPointerDown,
|
||||
onDoubleClick,
|
||||
onToggleTodo
|
||||
@@ -34,9 +36,9 @@ export const PaperCard: React.FC<PaperCardProps> = ({
|
||||
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]' : ''
|
||||
}`}
|
||||
className={`absolute select-none transition-transform will-change-transform ${
|
||||
isLocked ? 'cursor-default' : 'cursor-grab active:cursor-grabbing'
|
||||
} ${isDragging ? 'z-50 scale-[1.03]' : ''}`}
|
||||
style={{
|
||||
left: `${card.x}px`,
|
||||
top: `${card.y}px`,
|
||||
@@ -48,6 +50,12 @@ export const PaperCard: React.FC<PaperCardProps> = ({
|
||||
}}
|
||||
onPointerDown={(e) => onPointerDown(e, card)}
|
||||
onDoubleClick={() => onDoubleClick(card)}
|
||||
onClick={(e) => {
|
||||
if (isLocked && card.type === 'bookmark' && card.url) {
|
||||
e.stopPropagation();
|
||||
window.open(card.url.startsWith('http') ? card.url : `https://${card.url}`, '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
}}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
@@ -186,27 +194,36 @@ export const PaperCard: React.FC<PaperCardProps> = ({
|
||||
{/* 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%)]"
|
||||
className="p-3.5 pb-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-5 h-5 rounded-full border-2 border-amber-900/60 bg-amber-100/50 shadow-inner mt-0.5 mb-2 flex items-center justify-center shrink-0">
|
||||
<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 className="text-center w-full my-auto space-y-1 px-1">
|
||||
<div className="text-[10px] font-mono font-bold tracking-widest text-neutral-700/80 uppercase">
|
||||
{card.tagNumber || card.subtitle || '#TAG'}
|
||||
</div>
|
||||
{card.title && (
|
||||
<div className={`text-sm font-bold tracking-wide uppercase text-neutral-900 ${card.fontFamily}`}>
|
||||
{card.title}
|
||||
</div>
|
||||
)}
|
||||
{card.content && (
|
||||
<div className={`text-xs text-neutral-900/90 leading-snug whitespace-pre-line py-1.5 border-y border-amber-900/20 my-1 ${card.fontFamily}`}>
|
||||
{card.content}
|
||||
</div>
|
||||
)}
|
||||
{card.date && (
|
||||
<div className="text-[10px] font-mono text-neutral-700">
|
||||
{card.date}
|
||||
</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">
|
||||
<div className="w-full text-center border-t border-amber-900/20 pt-1.5 text-[9px] tracking-widest uppercase font-mono text-neutral-700 shrink-0">
|
||||
PRIORITY PARCEL
|
||||
</div>
|
||||
</div>
|
||||
@@ -415,6 +432,105 @@ export const PaperCard: React.FC<PaperCardProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Thumbnail Preview Image */}
|
||||
<div
|
||||
className="w-full aspect-[16/10] bg-neutral-200 rounded overflow-hidden relative shadow-inner mb-2 cursor-pointer group"
|
||||
onClick={(e) => {
|
||||
if (card.url) {
|
||||
e.stopPropagation();
|
||||
window.open(card.url.startsWith('http') ? card.url : `https://${card.url}`, '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
}}
|
||||
>
|
||||
{card.imageUrl ? (
|
||||
<img
|
||||
src={card.imageUrl}
|
||||
alt={card.title || 'Web preview'}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-200"
|
||||
loading="lazy"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex flex-col items-center justify-center text-neutral-400 gap-1 bg-neutral-100">
|
||||
<Globe size={18} className="text-neutral-400" />
|
||||
<span className="text-[10px] font-mono">Web Snapshot</span>
|
||||
</div>
|
||||
)}
|
||||
{/* Hover visit pill */}
|
||||
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/15 transition flex items-center justify-center">
|
||||
<span className="opacity-0 group-hover:opacity-100 bg-black/80 text-white text-[10px] font-mono px-2 py-0.5 rounded shadow transition flex items-center gap-1">
|
||||
<ExternalLink size={10} /> Visit
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Title & Description */}
|
||||
<div className="flex-1 flex flex-col justify-between">
|
||||
<div>
|
||||
<h4
|
||||
className="font-bold text-xs leading-snug line-clamp-2 text-neutral-900 cursor-pointer hover:text-amber-600 transition"
|
||||
onClick={(e) => {
|
||||
if (card.url) {
|
||||
e.stopPropagation();
|
||||
window.open(card.url.startsWith('http') ? card.url : `https://${card.url}`, '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
}}
|
||||
>
|
||||
{card.title || card.url || 'Web Bookmark'}
|
||||
</h4>
|
||||
{card.content && (
|
||||
<p className="text-[11px] text-neutral-600 line-clamp-2 mt-1 font-sans">
|
||||
{card.content}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{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">
|
||||
↗
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user