Enhance mobile experience: responsive bottom dock, camera upload, drawer, and adaptive card scaling; fix IPv6 localhost healthcheck

This commit is contained in:
airent106 committed 2026-10-13 00:37:25 -06:00
1 parent eb3787dd16
commit 7d9ce809a8
9 files changed
+531 -277

No files matched your search

+2 -2
View File
@@ -17,7 +17,7 @@ COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80 EXPOSE 80
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ HEALTHCHECK --interval=15s --timeout=5s --start-period=5s --retries=3 \
CMD wget --quiet --tries=1 --spider http://localhost:80/ || exit 1 CMD wget --quiet --tries=1 --spider http://127.0.0.1:80/ || exit 1
CMD ["nginx", "-g", "daemon off;"] CMD ["nginx", "-g", "daemon off;"]
+3 -3
View File
@@ -10,11 +10,11 @@ services:
ports: ports:
- "127.0.0.1:8476:80" - "127.0.0.1:8476:80"
healthcheck: healthcheck:
test: ["CMD-SHELL", "wget --quiet --tries=1 --spider http://localhost:80/ || exit 1"] test: ["CMD-SHELL", "wget --quiet --tries=1 --spider http://127.0.0.1:80/ || exit 1"]
interval: 30s interval: 15s
timeout: 5s timeout: 5s
retries: 3 retries: 3
start_period: 10s start_period: 5s
networks: networks:
t3_proxy: t3_proxy:
+1
View File
@@ -1,5 +1,6 @@
server { server {
listen 80; listen 80;
listen [::]:80;
server_name localhost; server_name localhost;
location / { location / {
+22 -3
View File
@@ -60,8 +60,15 @@ export const App: React.FC = () => {
useEffect(() => { useEffect(() => {
const w = typeof window !== 'undefined' ? window.innerWidth : 1400; const w = typeof window !== 'undefined' ? window.innerWidth : 1400;
const h = typeof window !== 'undefined' ? window.innerHeight : 900; const h = typeof window !== 'undefined' ? window.innerHeight : 900;
const isMobile = w < 768;
const initialCount = isMobile ? 9 : 22;
if (isMobile) {
setSettings((s) => ({ ...s, cardCount: initialCount }));
}
const initialCards = generateProceduralCards( const initialCards = generateProceduralCards(
settings.cardCount, initialCount,
w, w,
h, h,
settings.attachChance, settings.attachChance,
@@ -575,8 +582,20 @@ export const App: React.FC = () => {
onClear={() => setStrokes([])} onClear={() => setStrokes([])}
/> />
{/* 6. Bottom Shortcut Bar */} {/* 6. Bottom Shortcut Bar / Mobile Thumb Dock */}
<BottomBar /> <BottomBar
onShuffle={handleShuffle}
onToggleLayout={() =>
setSettings((prev) => ({
...prev,
layoutMode: prev.layoutMode === 'scatter' ? 'grid' : 'scatter'
}))
}
onTogglePen={() => setSettings((prev) => ({ ...prev, penMode: !prev.penMode }))}
onAddCard={() => setIsAddOpen(true)}
penActive={settings.penMode}
isGrid={settings.layoutMode === 'grid'}
/>
{/* 7. Modals */} {/* 7. Modals */}
<AddCardModal <AddCardModal
+31 -5
View File
@@ -97,17 +97,43 @@ export const AddCardModal: React.FC<AddCardModalProps> = ({ isOpen, onClose, onA
/> />
</div> </div>
{/* Image URL for Polaroid */} {/* Image Upload or URL for Polaroid */}
{type === 'polaroid' && ( {type === 'polaroid' && (
<div> <div className="space-y-2">
<label className="block text-neutral-400 mb-1">Image URL (Optional)</label> <label className="block text-neutral-400">Photo / Snapshot</label>
<div className="flex gap-2">
<label className="flex-1 flex items-center justify-center gap-1.5 py-1.5 px-3 bg-neutral-800 border border-neutral-700 hover:border-amber-500 rounded cursor-pointer text-amber-300 font-medium transition text-center">
<span>📸 Camera / Photos</span>
<input
type="file"
accept="image/*"
onChange={(e) => {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (ev) => {
if (typeof ev.target?.result === 'string') {
setImageUrl(ev.target.result);
}
};
reader.readAsDataURL(file);
}}
className="hidden"
/>
</label>
</div>
<input <input
type="url" type="url"
value={imageUrl} value={imageUrl}
onChange={(e) => setImageUrl(e.target.value)} onChange={(e) => setImageUrl(e.target.value)}
placeholder="https://images.unsplash.com/..." placeholder="Or paste an image URL (https://...)"
className="w-full bg-neutral-800 border border-neutral-700 rounded px-2.5 py-1.5 text-neutral-100 focus:outline-none focus:border-amber-500" className="w-full bg-neutral-800 border border-neutral-700 rounded px-2.5 py-1.5 text-neutral-100 focus:outline-none focus:border-amber-500 text-[11px]"
/> />
{imageUrl && (
<div className="w-16 h-16 rounded border border-neutral-700 overflow-hidden relative mt-1">
<img src={imageUrl} alt="Preview" className="w-full h-full object-cover" />
</div>
)}
</div> </div>
)} )}
+78 -21
View File
@@ -1,26 +1,83 @@
import React from 'react'; import React from 'react';
import { Shuffle, Grid, LayoutGrid, PenTool, Plus } from 'lucide-react';
export const BottomBar: React.FC = () => { interface BottomBarProps {
onShuffle?: () => void;
onToggleLayout?: () => void;
onTogglePen?: () => void;
onAddCard?: () => void;
penActive?: boolean;
isGrid?: boolean;
}
export const BottomBar: React.FC<BottomBarProps> = ({
onShuffle,
onToggleLayout,
onTogglePen,
onAddCard,
penActive = false,
isGrid = false
}) => {
return ( return (
<footer className="fixed bottom-2 left-1/2 -translate-x-1/2 z-40 px-3.5 py-1 bg-black/60 text-neutral-400 rounded-full text-[11px] font-mono backdrop-blur-sm pointer-events-none select-none border border-white/5 flex items-center gap-2 shadow-lg"> <>
<span className="flex items-center gap-1.5"> {/* 1. Desktop: Keyboard hints */}
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 inline-block" /> <footer className="fixed bottom-2 left-1/2 -translate-x-1/2 z-40 px-3.5 py-1 bg-black/60 text-neutral-400 rounded-full text-[11px] font-mono backdrop-blur-sm pointer-events-none select-none border border-white/5 hidden md:flex items-center gap-2 shadow-lg">
<span>drag anything</span> <span className="flex items-center gap-1.5">
</span> <span className="w-1.5 h-1.5 rounded-full bg-emerald-400 inline-block" />
<span className="text-neutral-600">·</span> <span>drag anything</span>
<span>flick to throw</span> </span>
<span className="text-neutral-600">·</span> <span className="text-neutral-600">·</span>
<span>double-click to edit</span> <span>flick to throw</span>
<span className="text-neutral-600">·</span> <span className="text-neutral-600">·</span>
<span className="text-neutral-300 font-semibold">S</span> shuffle <span>double-click to edit</span>
<span className="text-neutral-600">·</span> <span className="text-neutral-600">·</span>
<span className="text-neutral-300 font-semibold">G</span> grid <span className="text-neutral-300 font-semibold">S</span> shuffle
<span className="text-neutral-600">·</span> <span className="text-neutral-600">·</span>
<span className="text-neutral-300 font-semibold">P</span> pen <span className="text-neutral-300 font-semibold">G</span> grid
<span className="text-neutral-600">·</span> <span className="text-neutral-600">·</span>
<span className="text-neutral-300 font-semibold">N</span> new <span className="text-neutral-300 font-semibold">P</span> pen
<span className="text-neutral-600">·</span> <span className="text-neutral-600">·</span>
<span className="text-neutral-300 font-semibold">Space</span> scatter <span className="text-neutral-300 font-semibold">N</span> new
</footer> <span className="text-neutral-600">·</span>
<span className="text-neutral-300 font-semibold">Space</span> scatter
</footer>
{/* 2. Mobile: Thumb-friendly floating dock */}
<footer className="fixed bottom-3 left-1/2 -translate-x-1/2 z-40 px-3 py-1.5 bg-[#1C1A17]/95 text-neutral-300 rounded-full shadow-2xl border border-neutral-700/80 backdrop-blur-md flex md:hidden items-center gap-3 select-none">
<button
onClick={onShuffle}
className="p-2 rounded-full bg-neutral-800 hover:bg-neutral-700 active:scale-95 text-neutral-200 transition"
title="Shuffle Desk"
>
<Shuffle size={16} />
</button>
<button
onClick={onToggleLayout}
className="p-2 rounded-full bg-neutral-800 hover:bg-neutral-700 active:scale-95 text-neutral-200 transition"
title="Toggle Grid / Scatter"
>
{isGrid ? <LayoutGrid size={16} className="text-amber-400" /> : <Grid size={16} />}
</button>
<button
onClick={onAddCard}
className="p-2.5 rounded-full bg-amber-500 hover:bg-amber-400 active:scale-95 text-neutral-950 font-bold transition shadow-md"
title="Add Note"
>
<Plus size={18} />
</button>
<button
onClick={onTogglePen}
className={`p-2 rounded-full transition active:scale-95 ${
penActive ? 'bg-amber-500/20 text-amber-300 border border-amber-500' : 'bg-neutral-800 text-neutral-200'
}`}
title="Draw / Pen"
>
<PenTool size={16} />
</button>
</footer>
</>
); );
}; };
+15
View File
@@ -2,6 +2,7 @@ import React, { useState } from 'react';
import { CardItem } from '../types'; import { CardItem } from '../types';
import { getShadowStyle } from '../utils/lighting'; import { getShadowStyle } from '../utils/lighting';
import { Fastener, Stamp, PhotoCorners } from './Fastener'; import { Fastener, Stamp, PhotoCorners } from './Fastener';
import { Pencil } from 'lucide-react';
interface PaperCardProps { interface PaperCardProps {
card: CardItem; card: CardItem;
@@ -70,6 +71,20 @@ export const PaperCard: React.FC<PaperCardProps> = ({
{/* Subtle Paper Grain Filter Overlay */} {/* Subtle Paper Grain Filter Overlay */}
<div className="absolute inset-0 pointer-events-none opacity-25 mix-blend-multiply bg-[radial-gradient(#000_1px,transparent_1px)] [background-size:3px_3px]" /> <div className="absolute inset-0 pointer-events-none opacity-25 mix-blend-multiply bg-[radial-gradient(#000_1px,transparent_1px)] [background-size:3px_3px]" />
{/* Quick Edit button for Mobile & Hover */}
<button
onClick={(e) => {
e.stopPropagation();
onDoubleClick(card);
}}
className={`absolute top-1.5 right-1.5 z-30 p-1 rounded-full bg-black/5 hover:bg-black/15 text-neutral-700 transition ${
isHovered ? 'opacity-90' : 'opacity-0 md:opacity-0 max-md:opacity-60'
}`}
title="Edit note"
>
<Pencil size={11} />
</button>
{/* Paper Corner Curled Illusion for Sticky Notes */} {/* Paper Corner Curled Illusion for Sticky Notes */}
{card.type === 'sticky' && ( {card.type === 'sticky' && (
<div <div
+371 -237
View File
@@ -1,6 +1,6 @@
import React from 'react'; import React, { useState } from 'react';
import { DeskSettings, DeskTexture, FontMode, CardType } from '../types'; import { DeskSettings, DeskTexture, FontMode, CardType } from '../types';
import { Volume2, VolumeX, Plus, Sparkles } from 'lucide-react'; import { Volume2, VolumeX, Plus, Sparkles, SlidersHorizontal, X } from 'lucide-react';
interface TopBarProps { interface TopBarProps {
settings: DeskSettings; settings: DeskSettings;
@@ -21,264 +21,398 @@ export const TopBar: React.FC<TopBarProps> = ({
onOpenPortfolio, onOpenPortfolio,
totalCards totalCards
}) => { }) => {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
return ( return (
<header className="fixed top-2.5 left-1/2 -translate-x-1/2 z-50 w-[98vw] max-w-[1440px] px-3.5 py-1.5 bg-[#1C1A17]/95 text-neutral-300 rounded-xl shadow-2xl border border-neutral-700/60 backdrop-blur-md flex flex-wrap items-center justify-between gap-y-2 text-xs select-none"> <>
{/* 1. Left: Brand Title & Subtitle */} <header className="fixed top-2.5 left-1/2 -translate-x-1/2 z-50 w-[96vw] max-w-[1440px] px-3.5 py-1.5 bg-[#1C1A17]/95 text-neutral-300 rounded-xl shadow-2xl border border-neutral-700/60 backdrop-blur-md flex items-center justify-between gap-2 text-xs select-none">
<div className="flex items-center gap-2.5 mr-1"> {/* 1. Left: Brand Title & Subtitle */}
<div className="flex flex-col"> <div className="flex items-center gap-2.5 shrink-0">
<div className="flex items-center gap-1.5"> <div className="flex flex-col">
<span className="font-bold text-white tracking-tight text-sm"> <div className="flex items-center gap-1.5">
Glimpse <span className="font-bold text-white tracking-tight text-sm">
Glimpse
</span>
<span className="w-1.5 h-1.5 rounded-full bg-amber-500 animate-pulse" />
</div>
<span className="text-[10px] text-neutral-400 font-mono tracking-tight leading-none hidden sm:inline">
procedural paper · visual desk
</span> </span>
<span className="w-1.5 h-1.5 rounded-full bg-amber-500 animate-pulse" />
</div> </div>
<span className="text-[10px] text-neutral-400 font-mono tracking-tight leading-none">
procedural paper · visual desk
</span>
</div>
</div>
{/* 2. Primary Actions: Shuffle, Scatter/Grid */}
<div className="flex items-center gap-2">
<button
onClick={onShuffle}
className="px-3 py-1 rounded bg-[#ECA136] hover:bg-[#F2B04D] active:scale-95 text-neutral-950 font-bold transition shadow-sm flex items-center gap-1"
title="Press 'S' to shuffle"
>
Shuffle
</button>
{/* Scatter / Grid Toggle */}
<div className="flex items-center bg-neutral-800/90 rounded p-0.5 border border-neutral-700/80">
<button
onClick={() => onUpdateSettings({ layoutMode: 'scatter' })}
className={`px-2 py-0.5 rounded transition ${
settings.layoutMode === 'scatter'
? 'bg-neutral-600 text-white font-medium shadow-xs'
: 'text-neutral-400 hover:text-neutral-200'
}`}
title="Press 'G' to toggle"
>
Scatter
</button>
<button
onClick={() => onUpdateSettings({ layoutMode: 'grid' })}
className={`px-2 py-0.5 rounded transition ${
settings.layoutMode === 'grid'
? 'bg-neutral-600 text-white font-medium shadow-xs'
: 'text-neutral-400 hover:text-neutral-200'
}`}
title="Press 'G' to toggle"
>
Grid
</button>
</div> </div>
{/* Typography Mode: Mixed, Handwritten, Typed */} {/* 2. Primary Actions: Shuffle, Scatter/Grid (Always Visible) */}
<div className="flex items-center bg-neutral-800/90 rounded p-0.5 border border-neutral-700/80"> <div className="flex items-center gap-1.5">
{(['mixed', 'handwritten', 'typed'] as FontMode[]).map((mode) => ( <button
onClick={onShuffle}
className="px-2.5 py-1 rounded bg-[#ECA136] hover:bg-[#F2B04D] active:scale-95 text-neutral-950 font-bold transition shadow-sm flex items-center gap-1 shrink-0"
title="Press 'S' to shuffle"
>
Shuffle
</button>
{/* Scatter / Grid Toggle */}
<div className="flex items-center bg-neutral-800/90 rounded p-0.5 border border-neutral-700/80">
<button <button
key={mode} onClick={() => onUpdateSettings({ layoutMode: 'scatter' })}
onClick={() => onUpdateSettings({ fontMode: mode })} className={`px-2 py-0.5 rounded transition ${
className={`px-2 py-0.5 rounded capitalize transition ${ settings.layoutMode === 'scatter'
settings.fontMode === mode
? 'bg-neutral-600 text-white font-medium shadow-xs' ? 'bg-neutral-600 text-white font-medium shadow-xs'
: 'text-neutral-400 hover:text-neutral-200' : 'text-neutral-400 hover:text-neutral-200'
}`} }`}
title="Scatter layout"
> >
{mode} Scatter
</button> </button>
))} <button
</div> onClick={() => onUpdateSettings({ layoutMode: 'grid' })}
</div> className={`px-2 py-0.5 rounded transition ${
settings.layoutMode === 'grid'
? 'bg-neutral-600 text-white font-medium shadow-xs'
: 'text-neutral-400 hover:text-neutral-200'
}`}
title="Grid layout"
>
Grid
</button>
</div>
{/* 3. Desk Texture Selector */} {/* Add New Card (+) Button */}
<div className="flex items-center gap-1.5"> <button
<span className="text-neutral-400">Desk:</span> onClick={onAddCard}
<select className="p-1 rounded bg-neutral-800 hover:bg-amber-500 hover:text-neutral-950 text-neutral-300 border border-neutral-700 transition"
value={settings.deskTexture} title="Add a new custom note or photo"
onChange={(e) => onUpdateSettings({ deskTexture: e.target.value as DeskTexture })} >
className="bg-neutral-800 border border-neutral-700 rounded px-2 py-1 text-neutral-200 focus:outline-none focus:border-amber-500 cursor-pointer text-xs" <Plus size={15} />
> </button>
<option value="oak">Oak desk</option>
<option value="walnut">Walnut</option>
<option value="cutting-mat">Cutting mat</option>
<option value="corkboard">Corkboard</option>
<option value="blueprint">Blueprint</option>
<option value="dark-slate">Dark slate</option>
<option value="marble">Marble</option>
</select>
</div>
{/* 4. Sliders: Cards, Grain, Attach, Light */}
<div className="flex items-center gap-3">
{/* Cards Count */}
<div className="flex items-center gap-1.5" title="Number of paper sheets on desk">
<span className="text-neutral-400">Cards</span>
<input
type="range"
min="10"
max="80"
step="1"
value={settings.cardCount}
onChange={(e) => onUpdateSettings({ cardCount: Number(e.target.value) })}
className="w-14 accent-amber-500 cursor-pointer h-1.5 bg-neutral-700 rounded-lg appearance-none"
/>
<span className="font-mono text-[11px] text-neutral-300 w-5">
{settings.cardCount}
</span>
</div> </div>
{/* Grain */} {/* 3. Desktop-Only Controls (hidden on small/medium screens) */}
<div className="flex items-center gap-1.5" title="Paper texture grain density"> <div className="hidden xl:flex items-center gap-3">
<span className="text-neutral-400">Grain</span> {/* Typography Mode */}
<input <div className="flex items-center bg-neutral-800/90 rounded p-0.5 border border-neutral-700/80">
type="range" {(['mixed', 'handwritten', 'typed'] as FontMode[]).map((mode) => (
min="0" <button
max="1" key={mode}
step="0.05" onClick={() => onUpdateSettings({ fontMode: mode })}
value={settings.grain} className={`px-2 py-0.5 rounded capitalize transition ${
onChange={(e) => onUpdateSettings({ grain: Number(e.target.value) })} settings.fontMode === mode
className="w-14 accent-amber-500 cursor-pointer h-1.5 bg-neutral-700 rounded-lg appearance-none" ? 'bg-neutral-600 text-white font-medium shadow-xs'
/> : 'text-neutral-400 hover:text-neutral-200'
<span className="font-mono text-[11px] text-neutral-300 w-7"> }`}
{settings.grain.toFixed(2)} >
</span> {mode}
</button>
))}
</div>
{/* Desk Texture Selector */}
<div className="flex items-center gap-1.5">
<span className="text-neutral-400">Desk:</span>
<select
value={settings.deskTexture}
onChange={(e) => onUpdateSettings({ deskTexture: e.target.value as DeskTexture })}
className="bg-neutral-800 border border-neutral-700 rounded px-2 py-1 text-neutral-200 focus:outline-none focus:border-amber-500 cursor-pointer text-xs"
>
<option value="oak">Oak desk</option>
<option value="walnut">Walnut</option>
<option value="cutting-mat">Cutting mat</option>
<option value="corkboard">Corkboard</option>
<option value="blueprint">Blueprint</option>
<option value="dark-slate">Dark slate</option>
<option value="marble">Marble</option>
</select>
</div>
{/* Cards Count */}
<div className="flex items-center gap-1.5" title="Number of paper sheets on desk">
<span className="text-neutral-400">Cards</span>
<input
type="range"
min="8"
max="65"
step="1"
value={settings.cardCount}
onChange={(e) => onUpdateSettings({ cardCount: Number(e.target.value) })}
className="w-14 accent-amber-500 cursor-pointer h-1.5 bg-neutral-700 rounded-lg appearance-none"
/>
<span className="font-mono text-[11px] text-neutral-300 w-5">
{settings.cardCount}
</span>
</div>
{/* Grain */}
<div className="flex items-center gap-1.5" title="Paper texture grain density">
<span className="text-neutral-400">Grain</span>
<input
type="range"
min="0"
max="1"
step="0.05"
value={settings.grain}
onChange={(e) => onUpdateSettings({ grain: Number(e.target.value) })}
className="w-14 accent-amber-500 cursor-pointer h-1.5 bg-neutral-700 rounded-lg appearance-none"
/>
<span className="font-mono text-[11px] text-neutral-300 w-7">
{settings.grain.toFixed(2)}
</span>
</div>
{/* Light Angle */}
<div className="flex items-center gap-1.5" title="Directional light angle">
<span className="text-neutral-400">Light</span>
<input
type="range"
min="0"
max="360"
step="5"
value={settings.lightAngle}
onChange={(e) => onUpdateSettings({ lightAngle: Number(e.target.value) })}
className="w-14 accent-amber-400 cursor-pointer h-1.5 bg-neutral-700 rounded-lg appearance-none"
/>
<span className="font-mono text-[11px] text-amber-300 w-8">
{settings.lightAngle}°
</span>
</div>
{/* Checkboxes: Props, Motion, Pen */}
<label className="flex items-center gap-1 cursor-pointer text-neutral-300 hover:text-white">
<input
type="checkbox"
checked={settings.showProps}
onChange={(e) => onUpdateSettings({ showProps: e.target.checked })}
className="rounded accent-amber-500 cursor-pointer"
/>
<span>Props</span>
</label>
<label className="flex items-center gap-1 cursor-pointer text-neutral-300 hover:text-white">
<input
type="checkbox"
checked={settings.penMode}
onChange={(e) => onUpdateSettings({ penMode: e.target.checked })}
className="rounded accent-amber-500 cursor-pointer"
/>
<span>Pen</span>
</label>
{/* Sound Toggle */}
<button
onClick={() => onUpdateSettings({ soundEnabled: !settings.soundEnabled })}
className={`px-2 py-0.5 rounded border transition flex items-center gap-1 ${
settings.soundEnabled
? 'bg-neutral-800 text-amber-400 border-amber-600/50'
: 'bg-neutral-800/60 text-neutral-400 border-neutral-700'
}`}
>
{settings.soundEnabled ? <Volume2 size={13} /> : <VolumeX size={13} />}
</button>
{/* Card Type Filter */}
<select
value={settings.activeFilter}
onChange={(e) => onUpdateSettings({ activeFilter: e.target.value as DeskSettings['activeFilter'] })}
className="bg-neutral-800 border border-neutral-700 rounded px-1.5 py-0.5 text-neutral-300 text-[11px] cursor-pointer"
>
<option value="all">Types: All</option>
<option value="sticky">Sticky Notes</option>
<option value="polaroid">Photos</option>
<option value="tag">Tags</option>
<option value="torn">Letters</option>
<option value="business">Cards</option>
<option value="receipt">Receipts</option>
<option value="graph">Sketches</option>
</select>
{/* Portfolio */}
<button
onClick={onOpenPortfolio}
className="px-2 py-0.5 rounded bg-amber-500/10 hover:bg-amber-500/20 text-amber-300 border border-amber-500/40 transition flex items-center gap-1 font-medium"
>
<Sparkles size={12} />
<span>Presets</span>
</button>
</div> </div>
{/* Attach Chance */} {/* 4. Mobile Settings Trigger (always accessible on mobile & tablet) */}
<div className="flex items-center gap-1.5" title="Attachment frequency (washi tape, pins, clips)">
<span className="text-neutral-400">Attach</span>
<input
type="range"
min="0"
max="1"
step="0.05"
value={settings.attachChance}
onChange={(e) => onUpdateSettings({ attachChance: Number(e.target.value) })}
className="w-14 accent-amber-500 cursor-pointer h-1.5 bg-neutral-700 rounded-lg appearance-none"
/>
<span className="font-mono text-[11px] text-neutral-300 w-7">
{Math.round(settings.attachChance * 100)}%
</span>
</div>
{/* Dynamic Light Direction Dial */}
<div className="flex items-center gap-1.5" title="Directional desk lamp angle (shadow rotation)">
<span className="text-neutral-400">Light</span>
<input
type="range"
min="0"
max="360"
step="5"
value={settings.lightAngle}
onChange={(e) => onUpdateSettings({ lightAngle: Number(e.target.value) })}
className="w-14 accent-amber-400 cursor-pointer h-1.5 bg-neutral-700 rounded-lg appearance-none"
/>
<span className="font-mono text-[11px] text-amber-300 w-8">
{settings.lightAngle}°
</span>
</div>
</div>
{/* 5. Checkboxes: Props, Motion, Pen */}
<div className="flex items-center gap-2.5">
<label className="flex items-center gap-1 cursor-pointer text-neutral-300 hover:text-white">
<input
type="checkbox"
checked={settings.showProps}
onChange={(e) => onUpdateSettings({ showProps: e.target.checked })}
className="rounded accent-amber-500 cursor-pointer"
/>
<span>Props</span>
</label>
<label className="flex items-center gap-1 cursor-pointer text-neutral-300 hover:text-white">
<input
type="checkbox"
checked={settings.enableMotion}
onChange={(e) => onUpdateSettings({ enableMotion: e.target.checked })}
className="rounded accent-amber-500 cursor-pointer"
/>
<span>Motion</span>
</label>
<label className="flex items-center gap-1 cursor-pointer text-neutral-300 hover:text-white">
<input
type="checkbox"
checked={settings.penMode}
onChange={(e) => onUpdateSettings({ penMode: e.target.checked })}
className="rounded accent-amber-500 cursor-pointer"
/>
<span>Pen</span>
</label>
</div>
{/* 6. Action Buttons & Filter */}
<div className="flex items-center gap-2">
<button <button
onClick={onReplay} onClick={() => setMobileMenuOpen(true)}
className="px-2 py-0.5 rounded bg-neutral-800 hover:bg-neutral-700 text-neutral-300 hover:text-white border border-neutral-700/80 transition" className="xl:hidden p-1.5 rounded bg-neutral-800 hover:bg-neutral-700 text-neutral-300 border border-neutral-700 transition flex items-center gap-1"
title="Replay entry animations" title="Open Desk Settings"
> >
Replay <SlidersHorizontal size={14} />
<span className="text-[11px] hidden sm:inline">Settings</span>
</button> </button>
</header>
{/* Sound Toggle */} {/* 5. Mobile Drawer / Bottom Sheet */}
<button {mobileMenuOpen && (
onClick={() => onUpdateSettings({ soundEnabled: !settings.soundEnabled })} <div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center bg-black/60 backdrop-blur-sm p-0 sm:p-4 select-none">
className={`px-2 py-0.5 rounded border transition flex items-center gap-1 ${ <div className="bg-[#1C1A17] border-t sm:border border-neutral-700 w-full sm:max-w-md rounded-t-2xl sm:rounded-2xl p-5 text-neutral-200 shadow-2xl max-h-[85vh] overflow-y-auto space-y-4 text-xs animate-in slide-in-from-bottom duration-200">
settings.soundEnabled <div className="flex justify-between items-center pb-2 border-b border-neutral-800">
? 'bg-neutral-800 text-amber-400 border-amber-600/50' <div className="flex items-center gap-2">
: 'bg-neutral-800/60 text-neutral-400 border-neutral-700' <SlidersHorizontal size={16} className="text-amber-400" />
}`} <h2 className="text-sm font-bold text-white">Desk Controls & Tools</h2>
title="Toggle paper & stationery audio effects" </div>
> <button
{settings.soundEnabled ? <Volume2 size={13} /> : <VolumeX size={13} />} onClick={() => setMobileMenuOpen(false)}
<span>{settings.soundEnabled ? 'Sound on' : 'Sound off'}</span> className="text-neutral-400 hover:text-white p-1"
</button> >
<X size={18} />
</button>
</div>
{/* Card Type Filter */} {/* Desk Surface Texture */}
<select <div>
value={settings.activeFilter} <label className="block text-neutral-400 mb-1 font-semibold">Desk Surface</label>
onChange={(e) => onUpdateSettings({ activeFilter: e.target.value as DeskSettings['activeFilter'] })} <div className="grid grid-cols-3 gap-1.5">
className="bg-neutral-800 border border-neutral-700 rounded px-1.5 py-0.5 text-neutral-300 focus:outline-none text-[11px] cursor-pointer" {[
> { id: 'oak', label: 'Oak Desk' },
<option value="all">Types: All</option> { id: 'walnut', label: 'Walnut' },
<option value="sticky">Sticky Notes</option> { id: 'cutting-mat', label: 'Mat' },
<option value="polaroid">Photos</option> { id: 'corkboard', label: 'Cork' },
<option value="tag">Tags</option> { id: 'blueprint', label: 'Blueprint' },
<option value="torn">Letters</option> { id: 'dark-slate', label: 'Slate' },
<option value="business">Cards</option> { id: 'marble', label: 'Marble' }
<option value="receipt">Receipts</option> ].map((d) => (
<option value="graph">Sketches</option> <button
<option value="moodboard">Moodboards</option> key={d.id}
</select> onClick={() => onUpdateSettings({ deskTexture: d.id as DeskTexture })}
className={`py-1.5 px-2 rounded border text-center transition ${
settings.deskTexture === d.id
? 'bg-amber-500/20 text-amber-300 border-amber-500 font-bold'
: 'bg-neutral-800 text-neutral-400 border-neutral-700 hover:bg-neutral-700'
}`}
>
{d.label}
</button>
))}
</div>
</div>
{/* Add New Card (+) Button */} {/* Typography */}
<button <div>
onClick={onAddCard} <label className="block text-neutral-400 mb-1 font-semibold">Typography</label>
className="p-1 rounded bg-neutral-800 hover:bg-amber-500 hover:text-neutral-950 text-neutral-300 border border-neutral-700 transition" <div className="grid grid-cols-3 gap-1.5">
title="Add a new custom note or card" {(['mixed', 'handwritten', 'typed'] as FontMode[]).map((mode) => (
> <button
<Plus size={14} /> key={mode}
</button> onClick={() => onUpdateSettings({ fontMode: mode })}
className={`py-1.5 rounded capitalize border text-center transition ${
settings.fontMode === mode
? 'bg-amber-500/20 text-amber-300 border-amber-500 font-bold'
: 'bg-neutral-800 text-neutral-400 border-neutral-700'
}`}
>
{mode}
</button>
))}
</div>
</div>
{/* Portfolio Mode / Presets Modal Button */} {/* Sliders: Density, Grain, Light */}
<button <div className="space-y-3 pt-1">
onClick={onOpenPortfolio} <div>
className="px-2 py-0.5 rounded bg-amber-500/10 hover:bg-amber-500/20 text-amber-300 border border-amber-500/40 transition flex items-center gap-1 font-medium" <div className="flex justify-between text-neutral-400 mb-1">
title="Portfolio & Preset Modes" <span>Card Count</span>
> <span className="font-mono text-neutral-200">{settings.cardCount} sheets</span>
<Sparkles size={12} /> </div>
<span>Portfolio</span> <input
</button> type="range"
min="6"
max="45"
value={settings.cardCount}
onChange={(e) => onUpdateSettings({ cardCount: Number(e.target.value) })}
className="w-full accent-amber-500 cursor-pointer h-2 bg-neutral-800 rounded-lg"
/>
</div>
{/* Stats readout like in original */} <div>
<span className="font-mono text-[10px] text-neutral-400 pl-1 border-l border-neutral-700/80 hidden xl:inline"> <div className="flex justify-between text-neutral-400 mb-1">
{totalCards}/{totalCards} sheets <span>Paper Texture Grain</span>
</span> <span className="font-mono text-neutral-200">{settings.grain.toFixed(2)}</span>
</div> </div>
</header> <input
type="range"
min="0"
max="1"
step="0.05"
value={settings.grain}
onChange={(e) => onUpdateSettings({ grain: Number(e.target.value) })}
className="w-full accent-amber-500 cursor-pointer h-2 bg-neutral-800 rounded-lg"
/>
</div>
<div>
<div className="flex justify-between text-neutral-400 mb-1">
<span>Lighting Angle</span>
<span className="font-mono text-amber-300">{settings.lightAngle}°</span>
</div>
<input
type="range"
min="0"
max="360"
step="5"
value={settings.lightAngle}
onChange={(e) => onUpdateSettings({ lightAngle: Number(e.target.value) })}
className="w-full accent-amber-400 cursor-pointer h-2 bg-neutral-800 rounded-lg"
/>
</div>
</div>
{/* Quick Toggles */}
<div className="grid grid-cols-2 gap-2 pt-2 border-t border-neutral-800">
<button
onClick={() => onUpdateSettings({ showProps: !settings.showProps })}
className={`py-2 px-3 rounded border text-left flex items-center justify-between ${
settings.showProps ? 'bg-amber-500/10 border-amber-500 text-amber-300' : 'bg-neutral-800 border-neutral-700 text-neutral-400'
}`}
>
<span>Desk Props</span>
<span className="font-mono text-[10px]">{settings.showProps ? 'ON' : 'OFF'}</span>
</button>
<button
onClick={() => onUpdateSettings({ penMode: !settings.penMode })}
className={`py-2 px-3 rounded border text-left flex items-center justify-between ${
settings.penMode ? 'bg-amber-500/10 border-amber-500 text-amber-300' : 'bg-neutral-800 border-neutral-700 text-neutral-400'
}`}
>
<span>Draw / Pen</span>
<span className="font-mono text-[10px]">{settings.penMode ? 'ACTIVE' : 'OFF'}</span>
</button>
<button
onClick={() => onUpdateSettings({ soundEnabled: !settings.soundEnabled })}
className={`py-2 px-3 rounded border text-left flex items-center justify-between ${
settings.soundEnabled ? 'bg-amber-500/10 border-amber-500 text-amber-300' : 'bg-neutral-800 border-neutral-700 text-neutral-400'
}`}
>
<span>Tactile Audio</span>
<span className="font-mono text-[10px]">{settings.soundEnabled ? 'ON' : 'OFF'}</span>
</button>
<button
onClick={() => {
setMobileMenuOpen(false);
onOpenPortfolio();
}}
className="py-2 px-3 rounded bg-amber-500/20 border border-amber-500 text-amber-300 font-bold text-center flex items-center justify-center gap-1.5"
>
<Sparkles size={14} />
<span>Presets</span>
</button>
</div>
<button
onClick={() => setMobileMenuOpen(false)}
className="w-full py-2.5 rounded-lg bg-amber-500 hover:bg-amber-400 text-neutral-950 font-bold text-sm mt-3"
>
Done
</button>
</div>
</div>
)}
</>
); );
}; };
+8 -6
View File
@@ -286,10 +286,12 @@ export function generateProceduralCards(
fontMode: 'mixed' | 'handwritten' | 'typed' = 'mixed' fontMode: 'mixed' | 'handwritten' | 'typed' = 'mixed'
): CardItem[] { ): CardItem[] {
const cards: CardItem[] = []; const cards: CardItem[] = [];
const paddingX = 80; const isMobile = canvasWidth < 768;
const paddingY = 90; const scale = isMobile ? Math.min(1, Math.max(0.75, canvasWidth / 500)) : 1.0;
const usableWidth = Math.max(500, canvasWidth - paddingX * 2); const paddingX = isMobile ? 25 : 80;
const usableHeight = Math.max(400, canvasHeight - paddingY * 2); const paddingY = isMobile ? 70 : 90;
const usableWidth = Math.max(260, canvasWidth - paddingX * 2);
const usableHeight = Math.max(380, canvasHeight - paddingY * 2);
const availableFonts = FONTS_BY_MODE[fontMode]; const availableFonts = FONTS_BY_MODE[fontMode];
@@ -345,8 +347,8 @@ export function generateProceduralCards(
vx: 0, vx: 0,
vy: 0, vy: 0,
rotation: rotation, rotation: rotation,
width: preset.width || 180, width: Math.round((preset.width || 180) * scale),
height: preset.height || 180, height: Math.round((preset.height || 180) * scale),
zIndex: i + 1, zIndex: i + 1,
isPinned: false, isPinned: false,
attachType: attachType, attachType: attachType,