Make checklists editable and expose workspaces and settings in mobile top and bottom bars

This commit is contained in:
airent106 committed 2026-10-13 11:31:56 -06:00
1 parent 210749fcc8
commit 842c4754a9
4 files changed
+187 -40

No files matched your search

+4
View File
@@ -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) => {
+104 -9
View File
@@ -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>
+11 -1
View File
@@ -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
View File
@@ -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