Make checklists editable and expose workspaces and settings in mobile top and bottom bars
This commit is contained in:
1 parent
210749fcc8
commit
842c4754a9
4 files changed
+187
-40
No files matched your search
@@ -43,6 +43,7 @@ export const App: React.FC = () => {
|
||||
const [isAddOpen, setIsAddOpen] = useState(false);
|
||||
const [editingCard, setEditingCard] = useState<CardItem | null>(null);
|
||||
const [isPortfolioOpen, setIsPortfolioOpen] = useState(false);
|
||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||
|
||||
// Cross-device sync & backend persistence state
|
||||
const [syncStatus, setSyncStatus] = useState<SyncStatus>('loading');
|
||||
@@ -884,6 +885,8 @@ export const App: React.FC = () => {
|
||||
onOpenPortfolio={() => setIsPortfolioOpen(true)}
|
||||
totalCards={visibleCards.length}
|
||||
syncStatus={syncStatus}
|
||||
isSettingsOpen={isSettingsOpen}
|
||||
onToggleSettings={setIsSettingsOpen}
|
||||
/>
|
||||
|
||||
{/* 3. Scalable Desk Workspace (Cards, Props & Pen) */}
|
||||
@@ -967,6 +970,7 @@ export const App: React.FC = () => {
|
||||
}
|
||||
onTogglePen={() => setSettings((prev) => ({ ...prev, penMode: !prev.penMode }))}
|
||||
onToggleLock={() => setSettings((prev) => ({ ...prev, isLocked: !prev.isLocked }))}
|
||||
onOpenSettings={() => setIsSettingsOpen(true)}
|
||||
onAddCard={() => setIsAddOpen(true)}
|
||||
onCycleZoom={() =>
|
||||
setSettings((prev) => {
|
||||
|
||||
@@ -584,6 +584,10 @@ export const EditCardModal: React.FC<EditCardModalProps> = ({
|
||||
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 [todoItems, setTodoItems] = useState<{ text: string; done: boolean }[]>(
|
||||
card.todoItems ? [...card.todoItems] : []
|
||||
);
|
||||
const [newTodoText, setNewTodoText] = useState('');
|
||||
|
||||
const handleSave = () => {
|
||||
const formattedUrl = url.trim()
|
||||
@@ -603,6 +607,7 @@ export const EditCardModal: React.FC<EditCardModalProps> = ({
|
||||
attachType,
|
||||
graphType: card.type === 'graph' ? graphType : card.graphType,
|
||||
graphData: card.type === 'graph' ? parsedGraphData : card.graphData,
|
||||
todoItems: card.type === 'checklist' ? todoItems : card.todoItems,
|
||||
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,
|
||||
@@ -643,15 +648,105 @@ export const EditCardModal: React.FC<EditCardModalProps> = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-neutral-400 mb-1">Body Text</label>
|
||||
<textarea
|
||||
rows={3}
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
className="w-full bg-neutral-800 border border-neutral-700 rounded px-2.5 py-1.5 text-neutral-100"
|
||||
/>
|
||||
</div>
|
||||
{/* Checklist Task Editor */}
|
||||
{card.type === 'checklist' ? (
|
||||
<div className="p-3 bg-neutral-900/80 rounded-lg border border-neutral-800 space-y-2.5">
|
||||
<div className="flex justify-between items-center">
|
||||
<label className="text-neutral-300 font-semibold flex items-center gap-1.5">
|
||||
<span>Checklist Tasks</span>
|
||||
<span className="font-mono text-[10px] text-amber-400">({todoItems.length})</span>
|
||||
</label>
|
||||
<span className="text-[10px] text-neutral-400 font-mono">
|
||||
{todoItems.filter((t) => t.done).length}/{todoItems.length} completed
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5 max-h-48 overflow-y-auto pr-1">
|
||||
{todoItems.map((item, idx) => (
|
||||
<div key={idx} className="flex items-center gap-2 bg-neutral-800 p-1.5 rounded border border-neutral-700/70">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={item.done}
|
||||
onChange={(e) => {
|
||||
const updated = [...todoItems];
|
||||
updated[idx] = { ...updated[idx], done: e.target.checked };
|
||||
setTodoItems(updated);
|
||||
}}
|
||||
className="accent-amber-500 rounded-sm w-4 h-4 cursor-pointer shrink-0"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={item.text}
|
||||
onChange={(e) => {
|
||||
const updated = [...todoItems];
|
||||
updated[idx] = { ...updated[idx], text: e.target.value };
|
||||
setTodoItems(updated);
|
||||
}}
|
||||
placeholder="Task description..."
|
||||
className={`flex-1 bg-transparent border-b border-transparent focus:border-amber-500 focus:outline-none text-neutral-100 text-xs px-1 ${
|
||||
item.done ? 'line-through text-neutral-500' : ''
|
||||
}`}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTodoItems(todoItems.filter((_, i) => i !== idx))}
|
||||
className="text-neutral-500 hover:text-red-400 p-1 transition"
|
||||
title="Delete task"
|
||||
>
|
||||
<X size={13} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
{todoItems.length === 0 && (
|
||||
<div className="text-xs text-neutral-500 italic py-2 text-center">
|
||||
No tasks yet. Add your first task below!
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 pt-1">
|
||||
<input
|
||||
type="text"
|
||||
value={newTodoText}
|
||||
onChange={(e) => setNewTodoText(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
if (newTodoText.trim()) {
|
||||
setTodoItems([...todoItems, { text: newTodoText.trim(), done: false }]);
|
||||
setNewTodoText('');
|
||||
}
|
||||
}
|
||||
}}
|
||||
placeholder="Add a new task..."
|
||||
className="flex-1 bg-neutral-800 border border-neutral-700 rounded px-2.5 py-1.5 text-neutral-100 text-xs focus:border-amber-500"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (newTodoText.trim()) {
|
||||
setTodoItems([...todoItems, { text: newTodoText.trim(), done: false }]);
|
||||
setNewTodoText('');
|
||||
}
|
||||
}}
|
||||
className="px-3 py-1.5 rounded bg-amber-500 hover:bg-amber-400 text-neutral-950 font-bold transition flex items-center gap-1 shrink-0"
|
||||
>
|
||||
<Plus size={13} />
|
||||
<span>Add</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<label className="block text-neutral-400 mb-1">Body Text</label>
|
||||
<textarea
|
||||
rows={3}
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
className="w-full bg-neutral-800 border border-neutral-700 rounded px-2.5 py-1.5 text-neutral-100"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{card.type === 'bookmark' && (
|
||||
<div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Shuffle, Grid, LayoutGrid, Plus, ZoomIn, Lock, Unlock } from 'lucide-react';
|
||||
import { Shuffle, Grid, LayoutGrid, Plus, ZoomIn, Lock, Unlock, SlidersHorizontal } from 'lucide-react';
|
||||
import { SyncStatus } from '../types';
|
||||
|
||||
interface BottomBarProps {
|
||||
@@ -9,6 +9,7 @@ interface BottomBarProps {
|
||||
onAddCard?: () => void;
|
||||
onCycleZoom?: () => void;
|
||||
onToggleLock?: () => void;
|
||||
onOpenSettings?: () => void;
|
||||
penActive?: boolean;
|
||||
isGrid?: boolean;
|
||||
zoomLevel?: number;
|
||||
@@ -22,6 +23,7 @@ export const BottomBar: React.FC<BottomBarProps> = ({
|
||||
onAddCard,
|
||||
onCycleZoom,
|
||||
onToggleLock,
|
||||
onOpenSettings,
|
||||
isGrid = false,
|
||||
zoomLevel = 1.0,
|
||||
syncStatus = 'synced',
|
||||
@@ -63,6 +65,14 @@ export const BottomBar: React.FC<BottomBarProps> = ({
|
||||
|
||||
{/* 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-2 select-none">
|
||||
<button
|
||||
onClick={onOpenSettings}
|
||||
className="p-2 rounded-full bg-neutral-800 hover:bg-neutral-700 active:scale-95 text-amber-400 transition border border-neutral-700/60"
|
||||
title="Workspaces & Settings"
|
||||
>
|
||||
<SlidersHorizontal size={15} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={onToggleLock}
|
||||
className={`p-2 rounded-full transition active:scale-95 ${
|
||||
|
||||
+68
-30
@@ -11,6 +11,8 @@ interface TopBarProps {
|
||||
onOpenPortfolio: () => void;
|
||||
totalCards: number;
|
||||
syncStatus?: SyncStatus;
|
||||
isSettingsOpen?: boolean;
|
||||
onToggleSettings?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const TopBar: React.FC<TopBarProps> = ({
|
||||
@@ -21,9 +23,13 @@ export const TopBar: React.FC<TopBarProps> = ({
|
||||
onAddCard,
|
||||
onOpenPortfolio,
|
||||
totalCards,
|
||||
syncStatus = 'synced'
|
||||
syncStatus = 'synced',
|
||||
isSettingsOpen,
|
||||
onToggleSettings
|
||||
}) => {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
const isMenuOpen = isSettingsOpen !== undefined ? isSettingsOpen : mobileMenuOpen;
|
||||
const setMenuOpen = onToggleSettings || setMobileMenuOpen;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -80,8 +86,36 @@ export const TopBar: React.FC<TopBarProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 2. Primary Actions: Shuffle, Scatter/Grid (Always Visible) */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
{/* Mobile Header Controls: Workspace Selector + Settings Button */}
|
||||
<div className="flex md:hidden items-center gap-1.5 shrink-0">
|
||||
<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 text-xs font-semibold focus:outline-none focus:border-amber-500 max-w-[130px] truncate"
|
||||
title="Switch Workspace Board"
|
||||
>
|
||||
<option value="oak">1. Oak (Desk)</option>
|
||||
<option value="walnut">2. Walnut (Photos)</option>
|
||||
<option value="cutting-mat">3. Mat (Design)</option>
|
||||
<option value="corkboard">4. Cork (Pinboard)</option>
|
||||
<option value="blueprint">5. Blueprint (Tech)</option>
|
||||
<option value="dark-slate">6. Slate (Night)</option>
|
||||
<option value="marble">7. Marble (Atelier)</option>
|
||||
<option value="evidence-board">8. Evidence Board</option>
|
||||
</select>
|
||||
|
||||
<button
|
||||
onClick={() => setMenuOpen(true)}
|
||||
className="px-2.5 py-1 rounded bg-neutral-800 hover:bg-neutral-700 text-neutral-200 border border-neutral-700 transition flex items-center gap-1 shrink-0"
|
||||
title="Open Workspaces & Desk Settings"
|
||||
>
|
||||
<SlidersHorizontal size={13} className="text-amber-400" />
|
||||
<span className="text-[11px] font-bold">Settings</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 2. Desktop Primary Actions: Shuffle, Scatter/Grid (Hidden on mobile to preserve space) */}
|
||||
<div className="hidden md:flex items-center gap-1.5">
|
||||
<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"
|
||||
@@ -141,7 +175,7 @@ export const TopBar: React.FC<TopBarProps> = ({
|
||||
</div>
|
||||
|
||||
{/* 3. Desktop-Only Controls (Cleaned & streamlined) */}
|
||||
<div className="hidden xl:flex items-center gap-3">
|
||||
<div className="hidden lg:flex items-center gap-2.5">
|
||||
|
||||
{/* Board Selector */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
@@ -253,58 +287,62 @@ export const TopBar: React.FC<TopBarProps> = ({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 4. Settings Trigger (Desktop & Mobile) */}
|
||||
{/* 4. Desktop Settings Trigger */}
|
||||
<button
|
||||
onClick={() => setMobileMenuOpen(true)}
|
||||
className="p-1 sm:px-2.5 sm:py-1 rounded bg-neutral-800 hover:bg-neutral-700 text-neutral-300 border border-neutral-700 transition flex items-center gap-1.5 shrink-0"
|
||||
title="Open Desk Settings & Visual Controls"
|
||||
onClick={() => setMenuOpen(true)}
|
||||
className="hidden md:flex p-1 sm:px-2.5 sm:py-1 rounded bg-neutral-800 hover:bg-neutral-700 text-neutral-300 border border-neutral-700 transition items-center gap-1.5 shrink-0"
|
||||
title="Open Workspaces & Desk Settings"
|
||||
>
|
||||
<SlidersHorizontal size={13} className="text-amber-400" />
|
||||
<span className="text-xs hidden sm:inline">Settings</span>
|
||||
<span className="text-xs">Settings</span>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{/* 5. Mobile Drawer / Bottom Sheet */}
|
||||
{mobileMenuOpen && (
|
||||
{/* 5. Workspaces & Settings Drawer / Bottom Sheet */}
|
||||
{isMenuOpen && (
|
||||
<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">
|
||||
<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">
|
||||
<div className="flex justify-between items-center pb-2 border-b border-neutral-800">
|
||||
<div className="flex items-center gap-2">
|
||||
<SlidersHorizontal size={16} className="text-amber-400" />
|
||||
<h2 className="text-sm font-bold text-white">Desk Controls & Tools</h2>
|
||||
<h2 className="text-sm font-bold text-white">Workspaces & Desk Settings</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
onClick={() => setMenuOpen(false)}
|
||||
className="text-neutral-400 hover:text-white p-1"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Desk Surface / Board */}
|
||||
{/* Desk Surface / Workspaces */}
|
||||
<div>
|
||||
<label className="block text-neutral-400 mb-1 font-semibold">Workspace Board</label>
|
||||
<div className="grid grid-cols-4 gap-1.5">
|
||||
<div className="flex justify-between items-center mb-1.5">
|
||||
<label className="text-neutral-300 font-bold text-xs">Workspaces (8 Distinct Desks)</label>
|
||||
<span className="text-[10px] text-amber-400 font-mono font-semibold">Active: {settings.deskTexture}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{[
|
||||
{ id: 'oak', label: '1. Oak' },
|
||||
{ id: 'walnut', label: '2. Walnut' },
|
||||
{ id: 'cutting-mat', label: '3. Mat' },
|
||||
{ id: 'corkboard', label: '4. Cork' },
|
||||
{ id: 'blueprint', label: '5. Blue' },
|
||||
{ id: 'dark-slate', label: '6. Slate' },
|
||||
{ id: 'marble', label: '7. Marble' },
|
||||
{ id: 'evidence-board', label: '8. Evidence' }
|
||||
{ id: 'oak', label: '1. Oak (Paper Desk)' },
|
||||
{ id: 'walnut', label: '2. Walnut (Photos)' },
|
||||
{ id: 'cutting-mat', label: '3. Cutting Mat' },
|
||||
{ id: 'corkboard', label: '4. Corkboard' },
|
||||
{ id: 'blueprint', label: '5. Blueprint' },
|
||||
{ id: 'dark-slate', label: '6. Dark Slate' },
|
||||
{ id: 'marble', label: '7. Marble Atelier' },
|
||||
{ id: 'evidence-board', label: '8. Evidence Board' }
|
||||
].map((d) => (
|
||||
<button
|
||||
key={d.id}
|
||||
onClick={() => onUpdateSettings({ deskTexture: d.id as DeskTexture })}
|
||||
className={`py-1.5 px-1 rounded border text-center transition text-[11px] truncate ${
|
||||
className={`py-2 px-2.5 rounded-lg border text-left transition text-xs font-medium flex items-center justify-between ${
|
||||
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'
|
||||
? 'bg-amber-500/20 text-amber-300 border-amber-500 font-bold shadow-xs'
|
||||
: 'bg-neutral-800/90 text-neutral-300 border-neutral-700/80 hover:bg-neutral-700'
|
||||
}`}
|
||||
>
|
||||
{d.label}
|
||||
<span className="truncate">{d.label}</span>
|
||||
{settings.deskTexture === d.id && <span className="w-1.5 h-1.5 rounded-full bg-amber-400 ml-1 shrink-0" />}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -490,7 +528,7 @@ export const TopBar: React.FC<TopBarProps> = ({
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setMobileMenuOpen(false);
|
||||
setMenuOpen(false);
|
||||
onOpenPortfolio();
|
||||
}}
|
||||
className="w-full 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"
|
||||
@@ -500,7 +538,7 @@ export const TopBar: React.FC<TopBarProps> = ({
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
onClick={() => setMenuOpen(false)}
|
||||
className="w-full py-2.5 rounded-lg bg-amber-500 hover:bg-amber-400 text-neutral-950 font-bold text-sm mt-2"
|
||||
>
|
||||
Done
|
||||
|
||||
Reference in new issue
Block a user