diff --git a/Dockerfile b/Dockerfile index 98360bf..c928b79 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 -HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ - CMD wget --quiet --tries=1 --spider http://localhost:80/ || exit 1 +HEALTHCHECK --interval=15s --timeout=5s --start-period=5s --retries=3 \ + CMD wget --quiet --tries=1 --spider http://127.0.0.1:80/ || exit 1 CMD ["nginx", "-g", "daemon off;"] diff --git a/docker-compose.yml b/docker-compose.yml index 84368f1..0871616 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,11 +10,11 @@ services: ports: - "127.0.0.1:8476:80" healthcheck: - test: ["CMD-SHELL", "wget --quiet --tries=1 --spider http://localhost:80/ || exit 1"] - interval: 30s + test: ["CMD-SHELL", "wget --quiet --tries=1 --spider http://127.0.0.1:80/ || exit 1"] + interval: 15s timeout: 5s retries: 3 - start_period: 10s + start_period: 5s networks: t3_proxy: diff --git a/nginx.conf b/nginx.conf index 9f63fe0..1e262a1 100644 --- a/nginx.conf +++ b/nginx.conf @@ -1,5 +1,6 @@ server { listen 80; + listen [::]:80; server_name localhost; location / { diff --git a/src/App.tsx b/src/App.tsx index 56a56e7..7727262 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -60,8 +60,15 @@ export const App: React.FC = () => { useEffect(() => { const w = typeof window !== 'undefined' ? window.innerWidth : 1400; 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( - settings.cardCount, + initialCount, w, h, settings.attachChance, @@ -575,8 +582,20 @@ export const App: React.FC = () => { onClear={() => setStrokes([])} /> - {/* 6. Bottom Shortcut Bar */} - + {/* 6. Bottom Shortcut Bar / Mobile Thumb Dock */} + + 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 */} = ({ isOpen, onClose, onA /> - {/* Image URL for Polaroid */} + {/* Image Upload or URL for Polaroid */} {type === 'polaroid' && ( -
- +
+ +
+ +
setImageUrl(e.target.value)} - placeholder="https://images.unsplash.com/..." - 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" + 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 text-[11px]" /> + {imageUrl && ( +
+ Preview +
+ )}
)} diff --git a/src/components/BottomBar.tsx b/src/components/BottomBar.tsx index 88c0be9..6bbe9ae 100644 --- a/src/components/BottomBar.tsx +++ b/src/components/BottomBar.tsx @@ -1,26 +1,83 @@ 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 = ({ + onShuffle, + onToggleLayout, + onTogglePen, + onAddCard, + penActive = false, + isGrid = false +}) => { return ( -
- - - drag anything - - 路 - flick to throw - 路 - double-click to edit - 路 - S shuffle - 路 - G grid - 路 - P pen - 路 - N new - 路 - Space scatter -
+ <> + {/* 1. Desktop: Keyboard hints */} +
+ + + drag anything + + 路 + flick to throw + 路 + double-click to edit + 路 + S shuffle + 路 + G grid + 路 + P pen + 路 + N new + 路 + Space scatter +
+ + {/* 2. Mobile: Thumb-friendly floating dock */} +
+ + + + + + + +
+ ); }; diff --git a/src/components/PaperCard.tsx b/src/components/PaperCard.tsx index 0ad3b1a..0cc1479 100644 --- a/src/components/PaperCard.tsx +++ b/src/components/PaperCard.tsx @@ -2,6 +2,7 @@ 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'; interface PaperCardProps { card: CardItem; @@ -70,6 +71,20 @@ export const PaperCard: React.FC = ({ {/* Subtle Paper Grain Filter Overlay */}
+ {/* Quick Edit button for Mobile & Hover */} + + {/* Paper Corner Curled Illusion for Sticky Notes */} {card.type === 'sticky' && (
= ({ onOpenPortfolio, totalCards }) => { + const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + return ( -
- {/* 1. Left: Brand Title & Subtitle */} -
-
-
- - Glimpse + <> +
+ {/* 1. Left: Brand Title & Subtitle */} +
+
+
+ + Glimpse + + +
+ + procedural paper 路 visual desk -
- - procedural paper 路 visual desk - -
-
- - {/* 2. Primary Actions: Shuffle, Scatter/Grid */} -
- - - {/* Scatter / Grid Toggle */} -
- -
- {/* Typography Mode: Mixed, Handwritten, Typed */} -
- {(['mixed', 'handwritten', 'typed'] as FontMode[]).map((mode) => ( + {/* 2. Primary Actions: Shuffle, Scatter/Grid (Always Visible) */} +
+ + + {/* Scatter / Grid Toggle */} +
- ))} -
-
+ +
- {/* 3. Desk Texture Selector */} -
- Desk: - -
- - {/* 4. Sliders: Cards, Grain, Attach, Light */} -
- {/* Cards Count */} -
- Cards - onUpdateSettings({ cardCount: Number(e.target.value) })} - className="w-14 accent-amber-500 cursor-pointer h-1.5 bg-neutral-700 rounded-lg appearance-none" - /> - - {settings.cardCount} - + {/* Add New Card (+) Button */} +
- {/* Grain */} -
- Grain - onUpdateSettings({ grain: Number(e.target.value) })} - className="w-14 accent-amber-500 cursor-pointer h-1.5 bg-neutral-700 rounded-lg appearance-none" - /> - - {settings.grain.toFixed(2)} - + {/* 3. Desktop-Only Controls (hidden on small/medium screens) */} +
+ {/* Typography Mode */} +
+ {(['mixed', 'handwritten', 'typed'] as FontMode[]).map((mode) => ( + + ))} +
+ + {/* Desk Texture Selector */} +
+ Desk: + +
+ + {/* Cards Count */} +
+ Cards + onUpdateSettings({ cardCount: Number(e.target.value) })} + className="w-14 accent-amber-500 cursor-pointer h-1.5 bg-neutral-700 rounded-lg appearance-none" + /> + + {settings.cardCount} + +
+ + {/* Grain */} +
+ Grain + onUpdateSettings({ grain: Number(e.target.value) })} + className="w-14 accent-amber-500 cursor-pointer h-1.5 bg-neutral-700 rounded-lg appearance-none" + /> + + {settings.grain.toFixed(2)} + +
+ + {/* Light Angle */} +
+ Light + onUpdateSettings({ lightAngle: Number(e.target.value) })} + className="w-14 accent-amber-400 cursor-pointer h-1.5 bg-neutral-700 rounded-lg appearance-none" + /> + + {settings.lightAngle}掳 + +
+ + {/* Checkboxes: Props, Motion, Pen */} + + + + + {/* Sound Toggle */} + + + {/* Card Type Filter */} + + + {/* Portfolio */} +
- {/* Attach Chance */} -
- Attach - onUpdateSettings({ attachChance: Number(e.target.value) })} - className="w-14 accent-amber-500 cursor-pointer h-1.5 bg-neutral-700 rounded-lg appearance-none" - /> - - {Math.round(settings.attachChance * 100)}% - -
- - {/* Dynamic Light Direction Dial */} -
- Light - onUpdateSettings({ lightAngle: Number(e.target.value) })} - className="w-14 accent-amber-400 cursor-pointer h-1.5 bg-neutral-700 rounded-lg appearance-none" - /> - - {settings.lightAngle}掳 - -
-
- - {/* 5. Checkboxes: Props, Motion, Pen */} -
- - - - - -
- - {/* 6. Action Buttons & Filter */} -
+ {/* 4. Mobile Settings Trigger (always accessible on mobile & tablet) */} +
- {/* Sound Toggle */} - + {/* 5. Mobile Drawer / Bottom Sheet */} + {mobileMenuOpen && ( +
+
+
+
+ +

Desk Controls & Tools

+
+ +
- {/* Card Type Filter */} - + {/* Desk Surface Texture */} +
+ +
+ {[ + { id: 'oak', label: 'Oak Desk' }, + { id: 'walnut', label: 'Walnut' }, + { id: 'cutting-mat', label: 'Mat' }, + { id: 'corkboard', label: 'Cork' }, + { id: 'blueprint', label: 'Blueprint' }, + { id: 'dark-slate', label: 'Slate' }, + { id: 'marble', label: 'Marble' } + ].map((d) => ( + + ))} +
+
- {/* Add New Card (+) Button */} - + {/* Typography */} +
+ +
+ {(['mixed', 'handwritten', 'typed'] as FontMode[]).map((mode) => ( + + ))} +
+
- {/* Portfolio Mode / Presets Modal Button */} - + {/* Sliders: Density, Grain, Light */} +
+
+
+ Card Count + {settings.cardCount} sheets +
+ onUpdateSettings({ cardCount: Number(e.target.value) })} + className="w-full accent-amber-500 cursor-pointer h-2 bg-neutral-800 rounded-lg" + /> +
- {/* Stats readout like in original */} - - {totalCards}/{totalCards} sheets - -
- +
+
+ Paper Texture Grain + {settings.grain.toFixed(2)} +
+ onUpdateSettings({ grain: Number(e.target.value) })} + className="w-full accent-amber-500 cursor-pointer h-2 bg-neutral-800 rounded-lg" + /> +
+ +
+
+ Lighting Angle + {settings.lightAngle}掳 +
+ onUpdateSettings({ lightAngle: Number(e.target.value) })} + className="w-full accent-amber-400 cursor-pointer h-2 bg-neutral-800 rounded-lg" + /> +
+
+ + {/* Quick Toggles */} +
+ + + + + + + +
+ + +
+
+ )} + ); }; diff --git a/src/utils/proceduralCards.ts b/src/utils/proceduralCards.ts index 42101ca..e63275d 100644 --- a/src/utils/proceduralCards.ts +++ b/src/utils/proceduralCards.ts @@ -286,10 +286,12 @@ export function generateProceduralCards( fontMode: 'mixed' | 'handwritten' | 'typed' = 'mixed' ): CardItem[] { const cards: CardItem[] = []; - const paddingX = 80; - const paddingY = 90; - const usableWidth = Math.max(500, canvasWidth - paddingX * 2); - const usableHeight = Math.max(400, canvasHeight - paddingY * 2); + const isMobile = canvasWidth < 768; + const scale = isMobile ? Math.min(1, Math.max(0.75, canvasWidth / 500)) : 1.0; + const paddingX = isMobile ? 25 : 80; + 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]; @@ -345,8 +347,8 @@ export function generateProceduralCards( vx: 0, vy: 0, rotation: rotation, - width: preset.width || 180, - height: preset.height || 180, + width: Math.round((preset.width || 180) * scale), + height: Math.round((preset.height || 180) * scale), zIndex: i + 1, isPinned: false, attachType: attachType,