added task size management features and fixed trash recover feature

This commit is contained in:
2026-02-04 18:05:24 -06:00
parent 54a0da9896
commit fc3f1066e1
8 changed files with 343 additions and 73 deletions
+86 -15
View File
@@ -1,7 +1,8 @@
import { type Component, For, Show } from "solid-js";
import { LayoutDashboard, ListTodo, Settings, Clock, ArrowUpCircle, PanelLeftClose } from "lucide-solid";
import { LayoutDashboard, ListTodo, Settings, Clock, ArrowUpCircle, PanelLeftClose, Snowflake, Pickaxe, ChevronDown, Gauge, TrendingUp } from "lucide-solid";
import { cn } from "@/lib/utils";
import { Button } from "./ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
interface NavItem {
icon: any;
@@ -9,29 +10,99 @@ interface NavItem {
view: string;
}
const navItems: NavItem[] = [
// Desktop nav items (all)
const desktopNavItems: NavItem[] = [
{ icon: ListTodo, label: "Focus", view: "critical" },
{ icon: Clock, label: "Time", view: "urgency" },
{ icon: ArrowUpCircle, label: "Priority", view: "priority" },
{ icon: LayoutDashboard, label: "Matrix", view: "matrix" },
{ icon: Snowflake, label: "Snowball", view: "snowball" },
{ icon: Pickaxe, label: "Dig In", view: "dig_in" },
];
// Mobile nav groups
interface MobileNavGroup {
icon: any;
label: string;
items?: NavItem[];
view?: string; // Direct navigation (no sub-items)
}
const mobileNavGroups: MobileNavGroup[] = [
{ icon: ListTodo, label: "Focus", view: "critical" },
{ icon: LayoutDashboard, label: "Matrix", view: "matrix" },
{
icon: TrendingUp, label: "Value", items: [
{ icon: ArrowUpCircle, label: "Priority", view: "priority" },
{ icon: Clock, label: "Time", view: "urgency" },
]
},
{
icon: Gauge, label: "Flow", items: [
{ icon: Snowflake, label: "Snowball", view: "snowball" },
{ icon: Pickaxe, label: "Dig In", view: "dig_in" },
]
},
{ icon: Settings, label: "Settings", view: "settings" },
];
export const BottomNav: Component<{ currentView: string; setView: (v: string) => void }> = (props) => {
const isGroupActive = (group: MobileNavGroup) => {
if (group.view) return props.currentView === group.view;
return group.items?.some(item => props.currentView === item.view) ?? false;
};
return (
<nav class="fixed bottom-0 left-0 right-0 bg-background border-t border-border flex justify-around items-center h-16 md:hidden z-50">
<For each={navItems}>
{(item) => (
<button
onClick={() => props.setView(item.view)}
class={cn(
"flex flex-col items-center justify-center w-full h-full space-y-1 transition-colors",
props.currentView === item.view ? "text-primary" : "text-muted-foreground"
)}
>
<item.icon size={20} />
<span class="text-[10px] font-medium uppercase tracking-wider">{item.label}</span>
</button>
<For each={mobileNavGroups}>
{(group) => (
<Show when={group.items} fallback={
<button
onClick={() => group.view && props.setView(group.view)}
class={cn(
"flex flex-col items-center justify-center w-full h-full space-y-1 transition-colors",
isGroupActive(group) ? "text-primary" : "text-muted-foreground"
)}
>
<group.icon size={20} />
<span class="text-[10px] font-medium uppercase tracking-wider">{group.label}</span>
</button>
}>
<Popover>
<PopoverTrigger
class={cn(
"flex flex-col items-center justify-center w-full h-full space-y-1 transition-colors",
isGroupActive(group) ? "text-primary" : "text-muted-foreground"
)}
>
<div class="relative">
<group.icon size={20} />
<ChevronDown size={10} class="absolute -right-2 -bottom-0.5 opacity-60" />
</div>
<span class="text-[10px] font-medium uppercase tracking-wider">{group.label}</span>
</PopoverTrigger>
<PopoverContent class="w-40 p-1 bg-card border-border shadow-xl mb-2">
<div class="space-y-0.5">
<For each={group.items}>
{(item) => (
<button
class={cn(
"w-full flex items-center gap-3 p-2.5 rounded-lg transition-colors text-left group",
props.currentView === item.view
? "bg-primary text-primary-foreground"
: "hover:bg-muted text-foreground"
)}
onClick={() => props.setView(item.view)}
>
<item.icon size={16} />
<span class="text-sm font-medium">{item.label}</span>
</button>
)}
</For>
</div>
</PopoverContent>
</Popover>
</Show>
)}
</For>
</nav>
@@ -81,7 +152,7 @@ export const Sidebar: Component<{
</Show>
</div>
<nav class="flex-1 px-3 space-y-1 mt-2">
<For each={navItems.filter(i => i.view !== "settings")}>
<For each={desktopNavItems}>
{(item) => (
<button
onClick={() => props.setView(item.view)}
+78 -41
View File
@@ -1,6 +1,6 @@
import { type Component, createSignal, createEffect, onCleanup, onMount, For, Show } from "solid-js";
import { addTask, calculateDateFromUrgency, calculateUrgencyFromDate, store, upsertTagDefinition, type TaskTemplate } from "@/store";
import { Search, Command, Clock, ArrowUpCircle, Plus, Copy, ChevronDown } from "lucide-solid";
import { Search, Command, Clock, ArrowUpCircle, Plus, Copy, ChevronDown, Gauge } from "lucide-solid";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { TextField, TextFieldInput } from "@/components/ui/textfield";
@@ -11,7 +11,7 @@ import { getThemeAdjustedColor } from "@/lib/colors";
import { lazy, Suspense } from "solid-js";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { toast } from "solid-sonner";
import { PRIORITY_OPTIONS, URGENCY_OPTIONS } from "@/lib/constants";
import { PRIORITY_OPTIONS, URGENCY_OPTIONS, SIZE_OPTIONS } from "@/lib/constants";
const TaskEditor = lazy(() => import("./TaskEditor").then(m => ({ default: m.TaskEditor })));
@@ -28,6 +28,7 @@ export const QuickEntry: Component = () => {
const [tags, setTags] = createSignal<string[]>([]);
const [description, setDescription] = createSignal("");
const [editorInstance, setEditorInstance] = createSignal<any>(null);
const [size, setSize] = createSignal<string>("5");
// Reactive shorthand parsing
createEffect(() => {
@@ -205,7 +206,8 @@ export const QuickEntry: Component = () => {
priority: finalPriority,
dueDate: finalDueDate || undefined,
tags: finalTags,
content: description()
content: description(),
size: parseInt(size())
});
setInput("");
@@ -213,6 +215,7 @@ export const QuickEntry: Component = () => {
setUrgency("5");
setTags([]);
setDescription("");
setSize("5");
lastParsedTags = [];
setDateString("");
setIsOpen(false);
@@ -275,6 +278,7 @@ export const QuickEntry: Component = () => {
</TextField>
</div>
{/* Row 1: Priority, Size, Urgency */}
<div class="flex flex-col md:flex-row items-stretch md:items-center p-4 gap-4 border-b border-border bg-muted/10">
<div class="flex-1 space-y-1.5">
<label class="text-[10px] font-bold uppercase tracking-wider text-muted-foreground ml-1">Priority</label>
@@ -306,6 +310,36 @@ export const QuickEntry: Component = () => {
</Select>
</div>
<div class="flex-1 space-y-1.5">
<label class="text-[10px] font-bold uppercase tracking-wider text-muted-foreground ml-1">Size</label>
<Select<any>
value={size()}
onChange={(val) => {
const v = typeof val === 'object' ? val.value : val;
if (v) setSize(v);
}}
options={SIZE_OPTIONS}
optionValue="value"
optionTextValue="label"
placeholder="Size"
itemComponent={(props) => (
<SelectItem item={props.item}>
{props.item.rawValue.label}
</SelectItem>
)}
>
<SelectTrigger class="w-full bg-background border-none shadow-sm h-10 px-3 overflow-hidden">
<div class="flex items-center gap-2 truncate">
<Gauge size={14} class="text-primary shrink-0" />
<span class="text-sm font-medium truncate">
{SIZE_OPTIONS.find(o => o.value === size())?.label || size()}
</span>
</div>
</SelectTrigger>
<SelectContent />
</Select>
</div>
<div class="flex-1 space-y-1.5">
<label class="text-[10px] font-bold uppercase tracking-wider text-muted-foreground ml-1">Urgency (Time)</label>
<Select<any>
@@ -332,8 +366,48 @@ export const QuickEntry: Component = () => {
<SelectContent />
</Select>
</div>
</div>
<div class="flex-1 space-y-1.5">
{/* Row 2: Tags + Due Date */}
<div class="flex flex-col md:flex-row items-stretch md:items-center px-4 py-3 gap-4 bg-muted/10 border-b border-border">
<div class="flex-1 flex flex-wrap gap-2 items-center">
<TagPicker selectedTags={tags()} onTagsChange={setTags} />
<For each={tags()}>
{(tag) => (
<Badge
variant="secondary"
class="h-7 px-2 text-xs gap-1 bg-background border border-border/50 cursor-pointer hover:bg-destructive/10 hover:border-destructive/30 group/tag transition-all"
style={{
"background-color": (() => {
const def = store.tagDefinitions.find(d => d.name === tag);
const color = getThemeAdjustedColor(def?.color, def?.theme, resolvedTheme());
return color ? `${color}15` : undefined;
})(),
"border-color": (() => {
const def = store.tagDefinitions.find(d => d.name === tag);
const color = getThemeAdjustedColor(def?.color, def?.theme, resolvedTheme());
return color ? `${color}30` : undefined;
})(),
"color": (() => {
const def = store.tagDefinitions.find(d => d.name === tag);
return getThemeAdjustedColor(def?.color, def?.theme, resolvedTheme()) || "inherit";
})()
}}
onClick={() => setTags(tags().filter(t => t !== tag))}
>
<div
class="w-1.5 h-1.5 rounded-full transition-colors group-hover/tag:bg-destructive"
style={{
"background-color": (store.tagDefinitions.find(d => d.name === tag)?.value ?? 5) > 5 ? "#22c55e" : (store.tagDefinitions.find(d => d.name === tag)?.value ?? 5) < 5 ? "#ef4444" : "#94a3b8"
}}
/>
{tag}
</Badge>
)}
</For>
</div>
<div class="w-full md:w-auto md:min-w-[200px] space-y-1.5">
<label class="text-[10px] font-bold uppercase tracking-wider text-muted-foreground ml-1">Due Date</label>
<div class="relative">
<input
@@ -353,43 +427,6 @@ export const QuickEntry: Component = () => {
</div>
</div>
<div class="px-4 pb-4 bg-muted/10 border-b border-border flex flex-wrap gap-2 items-center">
<TagPicker selectedTags={tags()} onTagsChange={setTags} />
<For each={tags()}>
{(tag) => (
<Badge
variant="secondary"
class="h-7 px-2 text-xs gap-1 bg-background border border-border/50 cursor-pointer hover:bg-destructive/10 hover:border-destructive/30 group/tag transition-all"
style={{
"background-color": (() => {
const def = store.tagDefinitions.find(d => d.name === tag);
const color = getThemeAdjustedColor(def?.color, def?.theme, resolvedTheme());
return color ? `${color}15` : undefined;
})(),
"border-color": (() => {
const def = store.tagDefinitions.find(d => d.name === tag);
const color = getThemeAdjustedColor(def?.color, def?.theme, resolvedTheme());
return color ? `${color}30` : undefined;
})(),
"color": (() => {
const def = store.tagDefinitions.find(d => d.name === tag);
return getThemeAdjustedColor(def?.color, def?.theme, resolvedTheme()) || "inherit";
})()
}}
onClick={() => setTags(tags().filter(t => t !== tag))}
>
<div
class="w-1.5 h-1.5 rounded-full transition-colors group-hover/tag:bg-destructive"
style={{
"background-color": (store.tagDefinitions.find(d => d.name === tag)?.value ?? 5) > 5 ? "#22c55e" : (store.tagDefinitions.find(d => d.name === tag)?.value ?? 5) < 5 ? "#ef4444" : "#94a3b8"
}}
/>
{tag}
</Badge>
)}
</For>
</div>
<div class="px-4 py-3 bg-card border-b border-border min-h-[120px] max-h-[300px] overflow-y-auto">
<Suspense fallback={<div class="h-20 animate-pulse bg-muted/20 rounded-lg" />}>
<TaskEditor
+32 -2
View File
@@ -3,12 +3,12 @@ import { Sheet, SheetContent } from "@/components/ui/sheet";
import { type Task, removeTask, restoreTask, updateTask, saveTaskAsTemplate } from "@/store";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { ArrowUpCircle, Clock, Calendar, Type, Trash2, X, Copy, MoreHorizontal } from "lucide-solid";
import { ArrowUpCircle, Clock, Calendar, Type, Trash2, X, Copy, MoreHorizontal, Gauge } from "lucide-solid";
import { calculateDateFromUrgency, calculateUrgencyFromDate } from "@/store";
import { Button } from "./ui/button";
import { TagPicker } from "./TagPicker";
import { Badge } from "./ui/badge";
import { PRIORITY_OPTIONS, URGENCY_OPTIONS } from "@/lib/constants";
import { PRIORITY_OPTIONS, URGENCY_OPTIONS, SIZE_OPTIONS } from "@/lib/constants";
import { store } from "@/store";
import { cn } from "@/lib/utils";
import { toast } from "solid-sonner";
@@ -189,6 +189,36 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
</Select>
</div>
{/* Size */}
<div class="flex items-center gap-1 group shrink-0">
<span class="text-[10px] uppercase font-bold tracking-wider text-muted-foreground/70 hidden sm:inline">Size</span>
<Select<any>
value={(props.task.size ?? 5).toString()}
onChange={(val: any) => {
const value = typeof val === 'object' ? val?.value : val;
if (value) {
const s = parseInt(value);
if (!isNaN(s)) updateTask(props.task.id, { size: s });
}
}}
options={SIZE_OPTIONS}
optionValue="value"
optionTextValue="label"
placeholder="Size"
itemComponent={(props: any) => <SelectItem item={props.item}>{props.item.rawValue.label}</SelectItem>}
>
<SelectTrigger class="h-8 min-w-[40px] max-w-[180px] bg-transparent border-transparent hover:bg-muted/50 px-1 sm:px-2 shadow-none focus:ring-0 shrink-0 overflow-hidden">
<div class="flex items-center gap-1 sm:gap-1.5 text-foreground font-medium truncate">
<Gauge size={14} class="text-primary opacity-70 group-hover:opacity-100 transition-opacity shrink-0" />
<span class="text-xs sm:text-sm truncate">
{SIZE_OPTIONS.find(o => o.value === (props.task.size ?? 5).toString())?.label || props.task.size}
</span>
</div>
</SelectTrigger>
<SelectContent />
</Select>
</div>
{/* Commands Shortcut */}
<div class="flex items-center gap-1 shrink-0">
<Button