Compare commits

...

4 Commits

6 changed files with 97 additions and 28 deletions
+12 -8
View File
@@ -23,8 +23,8 @@ export const QuickEntry: Component = () => {
const [isOpen, setIsOpen] = createSignal(false); const [isOpen, setIsOpen] = createSignal(false);
const [input, setInput] = createSignal(""); const [input, setInput] = createSignal("");
const [priority, setPriority] = createSignal<string>("5"); const [priority, setPriority] = createSignal<string>("");
const [urgency, setUrgency] = createSignal<string>("5"); const [urgency, setUrgency] = createSignal<string>("");
const [tags, setTags] = createSignal<string[]>([]); const [tags, setTags] = createSignal<string[]>([]);
const [description, setDescription] = createSignal(""); const [description, setDescription] = createSignal("");
const [editorInstance, setEditorInstance] = createSignal<any>(null); const [editorInstance, setEditorInstance] = createSignal<any>(null);
@@ -211,8 +211,8 @@ export const QuickEntry: Component = () => {
}); });
setInput(""); setInput("");
setPriority("5"); setPriority("");
setUrgency("5"); setUrgency("");
setTags([]); setTags([]);
setDescription(""); setDescription("");
setSize("5"); setSize("5");
@@ -281,7 +281,9 @@ export const QuickEntry: Component = () => {
{/* Row 1: Priority, Size, Urgency */} {/* 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 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"> <div class="flex-1 space-y-1.5">
<label class="text-[10px] font-bold uppercase tracking-wider text-muted-foreground ml-1">Priority</label> <label class="text-[10px] font-bold uppercase tracking-wider text-muted-foreground ml-1">
Priority <span class="text-destructive">*</span>
</label>
<Select<any> <Select<any>
value={priority()} value={priority()}
onChange={(val) => { onChange={(val) => {
@@ -302,7 +304,7 @@ export const QuickEntry: Component = () => {
<div class="flex items-center gap-2 truncate"> <div class="flex items-center gap-2 truncate">
<ArrowUpCircle size={14} class="text-primary shrink-0" /> <ArrowUpCircle size={14} class="text-primary shrink-0" />
<span class="text-sm font-medium truncate"> <span class="text-sm font-medium truncate">
{PRIORITY_OPTIONS.find(o => o.value === priority())?.label || priority()} {PRIORITY_OPTIONS.find(o => o.value === priority())?.label || (priority() === "" ? "Pick Priority" : priority())}
</span> </span>
</div> </div>
</SelectTrigger> </SelectTrigger>
@@ -341,7 +343,9 @@ export const QuickEntry: Component = () => {
</div> </div>
<div class="flex-1 space-y-1.5"> <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> <label class="text-[10px] font-bold uppercase tracking-wider text-muted-foreground ml-1">
Urgency <span class="text-destructive">*</span>
</label>
<Select<any> <Select<any>
value={urgency()} value={urgency()}
onChange={(val) => onUrgencySimpleChange(val)} onChange={(val) => onUrgencySimpleChange(val)}
@@ -359,7 +363,7 @@ export const QuickEntry: Component = () => {
<div class="flex items-center gap-2 truncate"> <div class="flex items-center gap-2 truncate">
<Clock size={14} class="text-primary shrink-0" /> <Clock size={14} class="text-primary shrink-0" />
<span class="text-sm font-medium truncate"> <span class="text-sm font-medium truncate">
{URGENCY_OPTIONS.find(o => o.value === urgency())?.label || urgency()} {URGENCY_OPTIONS.find(o => o.value === urgency())?.label || (urgency() === "" ? "Pick Urgency" : urgency())}
</span> </span>
</div> </div>
</SelectTrigger> </SelectTrigger>
+64 -1
View File
@@ -1,10 +1,12 @@
import { type Component, createEffect, untrack } from "solid-js"; import { type Component, createEffect, untrack, Show, createSignal } from "solid-js";
import { createTiptapEditor } from "solid-tiptap"; import { createTiptapEditor } from "solid-tiptap";
import StarterKit from "@tiptap/starter-kit"; import StarterKit from "@tiptap/starter-kit";
import Placeholder from "@tiptap/extension-placeholder"; import Placeholder from "@tiptap/extension-placeholder";
import TaskList from "@tiptap/extension-task-list"; import TaskList from "@tiptap/extension-task-list";
import TaskItem from "@tiptap/extension-task-item"; import TaskItem from "@tiptap/extension-task-item";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Indent, Outdent } from "lucide-solid";
import { Button } from "./ui/button";
import { SlashCommands } from "@/lib/slash-command"; import { SlashCommands } from "@/lib/slash-command";
import { suggestion } from "@/lib/slash-renderer"; import { suggestion } from "@/lib/slash-renderer";
@@ -19,6 +21,7 @@ interface TaskEditorProps {
export const TaskEditor: Component<TaskEditorProps> = (props) => { export const TaskEditor: Component<TaskEditorProps> = (props) => {
let editorRef: HTMLDivElement | undefined; let editorRef: HTMLDivElement | undefined;
const [isFocused, setIsFocused] = createSignal(false);
const editor = createTiptapEditor(() => ({ const editor = createTiptapEditor(() => ({
element: editorRef!, element: editorRef!,
@@ -59,6 +62,11 @@ export const TaskEditor: Component<TaskEditorProps> = (props) => {
onUpdate: ({ editor }) => { onUpdate: ({ editor }) => {
props.onUpdate(editor.getHTML()); props.onUpdate(editor.getHTML());
}, },
onFocus: () => setIsFocused(true),
onBlur: () => {
// Delay blur to allow button clicks
setTimeout(() => setIsFocused(false), 200);
},
editorProps: { editorProps: {
attributes: { attributes: {
class: cn( class: cn(
@@ -95,11 +103,66 @@ export const TaskEditor: Component<TaskEditorProps> = (props) => {
} }
}); });
const handleIndent = (e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
const eInstance = editor();
if (!eInstance) return;
if (eInstance.can().sinkListItem('taskItem')) {
eInstance.chain().focus().sinkListItem('taskItem').run();
} else if (eInstance.can().sinkListItem('listItem')) {
eInstance.chain().focus().sinkListItem('listItem').run();
}
};
const handleOutdent = (e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
const eInstance = editor();
if (!eInstance) return;
if (eInstance.can().liftListItem('taskItem')) {
eInstance.chain().focus().liftListItem('taskItem').run();
} else if (eInstance.can().liftListItem('listItem')) {
eInstance.chain().focus().liftListItem('listItem').run();
}
};
return ( return (
<div class="relative w-full">
<div <div
ref={editorRef} ref={editorRef}
class="w-full cursor-text" class="w-full cursor-text"
onClick={() => editor()?.chain().focus().run()} onClick={() => editor()?.chain().focus().run()}
/> />
{/* Mobile Indentation Toolbar */}
<Show when={isFocused() && props.editable !== false}>
<div class="md:hidden fixed bottom-[env(safe-area-inset-bottom,0px)] left-0 right-0 z-[100] bg-background/95 backdrop-blur-md border-t border-border px-4 py-2 flex items-center justify-center gap-6 animate-in slide-in-from-bottom duration-200">
<Button
variant="ghost"
size="sm"
class="h-10 w-12 flex flex-col items-center gap-0.5 text-muted-foreground active:scale-95 active:bg-muted"
onMouseDown={handleOutdent}
>
<Outdent size={18} />
<span class="text-[9px] font-bold uppercase tracking-tighter">Outdent</span>
</Button>
<div class="w-px h-6 bg-border/50" />
<Button
variant="ghost"
size="sm"
class="h-10 w-12 flex flex-col items-center gap-0.5 text-muted-foreground active:scale-95 active:bg-muted"
onMouseDown={handleIndent}
>
<Indent size={18} />
<span class="text-[9px] font-bold uppercase tracking-tighter">Indent</span>
</Button>
</div>
</Show>
</div>
); );
}; };
+1
View File
@@ -34,6 +34,7 @@ export interface ButtonProps extends VariantProps<typeof buttonVariants> {
onClick?: (e: MouseEvent) => void onClick?: (e: MouseEvent) => void
onMouseEnter?: (e: MouseEvent) => void onMouseEnter?: (e: MouseEvent) => void
onMouseLeave?: (e: MouseEvent) => void onMouseLeave?: (e: MouseEvent) => void
onMouseDown?: (e: MouseEvent) => void
children?: JSX.Element children?: JSX.Element
size?: "default" | "sm" | "lg" | "icon" size?: "default" | "sm" | "lg" | "icon"
variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link"
+4 -4
View File
@@ -18,10 +18,10 @@ export const URGENCY_OPTIONS = [
{ value: "7", label: "7 - Half-Week" }, { value: "7", label: "7 - Half-Week" },
{ value: "6", label: "6 - This Week/5 Days" }, { value: "6", label: "6 - This Week/5 Days" },
{ value: "5", label: "5 - Next Week/10 Days" }, { value: "5", label: "5 - Next Week/10 Days" },
{ value: "4", label: "4 - 3 Weeks" }, { value: "4", label: "4 - Three Weeks" },
{ value: "3", label: "3 - 6 Weeks" }, { value: "3", label: "3 - Six Weeks" },
{ value: "2", label: "2 - 3 Months" }, { value: "2", label: "2 - Three Months" },
{ value: "1", label: "1 - 6 Months" } { value: "1", label: "1 - Six Months" }
]; ];
export const URGENCY_HOURS: Record<number, number> = { export const URGENCY_HOURS: Record<number, number> = {
10: 12, 10: 12,
+5 -5
View File
@@ -38,7 +38,7 @@ export interface Task {
export const checkRecurringTasks = () => { export const checkRecurringTasks = () => {
const tasks = store.tasks; const tasks = store.tasks;
const nowObj = new Date(); const nowObj = new Date();
const todayStr = nowObj.toISOString().split('T')[0]; const todayStr = nowObj.toLocaleDateString('en-CA'); // YYYY-MM-DD in local time
tasks.forEach(task => { tasks.forEach(task => {
if (!task.completed || !task.recurrence) return; if (!task.completed || !task.recurrence) return;
@@ -51,14 +51,14 @@ export const checkRecurringTasks = () => {
// Check if we already reset it today/this cycle // Check if we already reset it today/this cycle
if (lastUncompleted) { if (lastUncompleted) {
const lastDate = new Date(lastUncompleted).toISOString().split('T')[0]; const lastDate = new Date(lastUncompleted).toLocaleDateString('en-CA');
if (lastDate === todayStr) return; // Already reset today if (lastDate === todayStr) return; // Already reset today
} }
// Also check 'updated' date. If it was marked completed TODAY, don't reset immediately. // Also check 'updated' date. If it was marked completed TODAY, don't reset immediately.
// We only reset if the completion happened BEFORE the current cycle trigger. // We only reset if the completion happened BEFORE the current cycle trigger.
// E.g. Completed yesterday, today is a new day -> Reset. // E.g. Completed yesterday, today is a new day -> Reset.
const updatedDate = new Date(task.updated).toISOString().split('T')[0]; const updatedDate = new Date(task.updated).toLocaleDateString('en-CA');
if (updatedDate === todayStr) { if (updatedDate === todayStr) {
// If manual loop: user completes it, we shouldn't immediately uncomplete it. // If manual loop: user completes it, we shouldn't immediately uncomplete it.
// But what if it's supposed to be uncompleted today? // But what if it's supposed to be uncompleted today?
@@ -210,8 +210,8 @@ export const matchesFilter = (task: Task) => {
// Edited Today // Edited Today
if (f.editedToday) { if (f.editedToday) {
const today = new Date().toISOString().split('T')[0]; const today = new Date().toLocaleDateString('en-CA');
const updated = new Date(task.updated).toISOString().split('T')[0]; const updated = new Date(task.updated).toLocaleDateString('en-CA');
if (today !== updated) return false; if (today !== updated) return false;
} }
+6 -5
View File
@@ -1,5 +1,5 @@
import { type Component, For, createMemo } from "solid-js"; import { type Component, For, createMemo } from "solid-js";
import { store, calculateUrgencyScore, setActiveTaskId, setStore, getCombinedScore, matchesFilter } from "@/store"; import { store, calculateUrgencyScore, setActiveTaskId, setStore, matchesFilter } from "@/store";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select";
@@ -85,11 +85,11 @@ export const MatrixView: Component = () => {
}; };
const urgencyScore = calculateUrgencyScore(task.dueDate); const urgencyScore = calculateUrgencyScore(task.dueDate);
const combinedScore = getCombinedScore(task);
// Map 1-10 to HSL Hue (0 = Red, 240 = Blue) // Map task size (0-10) to HSL Hue (240 = Blue/Cool, 0 = Red/Warm)
// Using a slightly limited range for better aesthetics (0 to 220) // This creates a "true gradient" from small to large tasks.
const hue = ((10 - combinedScore) / 9) * 220; const taskSize = task.size ?? 5;
const hue = (1 - (taskSize / 10)) * 240;
const dotColor = `hsl(${hue}, 85%, 55%)`; const dotColor = `hsl(${hue}, 85%, 55%)`;
return ( return (
@@ -123,6 +123,7 @@ export const MatrixView: Component = () => {
<p class="text-xs font-bold leading-tight">{task.title}</p> <p class="text-xs font-bold leading-tight">{task.title}</p>
<div class="flex items-center justify-between mt-2 pt-2 border-t border-border"> <div class="flex items-center justify-between mt-2 pt-2 border-t border-border">
<span class="text-[9px] text-muted-foreground">U: {urgencyScore}</span> <span class="text-[9px] text-muted-foreground">U: {urgencyScore}</span>
<span class="text-[9px] text-muted-foreground">S: {task.size ?? 5}</span>
<span class="text-[9px] text-muted-foreground">P: {task.priority}</span> <span class="text-[9px] text-muted-foreground">P: {task.priority}</span>
</div> </div>
</div> </div>