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, Gauge } from "lucide-solid"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { TextField, TextFieldInput } from "@/components/ui/textfield"; import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select"; import { TagPicker } from "@/components/TagPicker"; import { useTheme } from "./ThemeProvider"; 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, SIZE_OPTIONS } from "@/lib/constants"; const TaskEditor = lazy(() => import("./TaskEditor").then(m => ({ default: m.TaskEditor }))); export const QuickEntry: Component = () => { const { resolvedTheme } = useTheme(); // Diff tracking for reactive tags let lastParsedTags: string[] = []; const [isOpen, setIsOpen] = createSignal(false); const [input, setInput] = createSignal(""); const [priority, setPriority] = createSignal(""); const [urgency, setUrgency] = createSignal(""); const [tags, setTags] = createSignal([]); const [description, setDescription] = createSignal(""); const [editorInstance, setEditorInstance] = createSignal(null); const [size, setSize] = createSignal("5"); // Reactive shorthand parsing createEffect(() => { const text = input(); const pMatch = text.match(/\/p(\d+)/); if (pMatch) { const val = Math.min(10, Math.max(1, parseInt(pMatch[1]))).toString(); setPriority(val); } const uMatch = text.match(/\/u(\d+)/); if (uMatch) { const val = Math.min(10, Math.max(1, parseInt(uMatch[1]))).toString(); if (val !== urgency()) { onUrgencySimpleChange(val); } } // Tag Shortcut Parsing: /t [tag name]:[optional value] // This is reactive. To avoid "prefix storm", only add if terminated by : or / // Tag Shortcut Parsing: /t [tag name]:[optional value] // Regex: /t, name (no : or /), lookahead for terminator (: or /), optional value group const tagRegex = /\/t\s*([^:/]+)(?=(?:[:/]))(?:[:](\d+))?/g; const newParsedTags: string[] = []; let match; while ((match = tagRegex.exec(text)) !== null) { const tagName = match[1].trim(); if (tagName) { newParsedTags.push(tagName); } } const currentTags = [...tags()]; // Determine changes const tagsToRemove = lastParsedTags.filter(t => !newParsedTags.includes(t)); const tagsToAdd = newParsedTags.filter(t => !currentTags.includes(t)); // Apply changes let nextTags = currentTags.filter(t => !tagsToRemove.includes(t)); nextTags = [...nextTags, ...tagsToAdd]; if (JSON.stringify(nextTags) !== JSON.stringify(currentTags)) { setTags(nextTags); } lastParsedTags = newParsedTags; }); // Explicit date string for the input (YYYY-MM-DDTHH:mm) const [dateString, setDateString] = createSignal(""); let inputRef: HTMLInputElement | undefined; createEffect(() => { if (isOpen()) { // Slight delay to ensure DOM is ready and animations don't interfere setTimeout(() => inputRef?.focus(), 50); } }); const handleKeyDown = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key === "k") { e.preventDefault(); if (isOpen()) { inputRef?.focus(); } else { setIsOpen(true); } } if (e.key === "Escape") { setIsOpen(false); } }; onMount(() => window.addEventListener("keydown", handleKeyDown)); onCleanup(() => window.removeEventListener("keydown", handleKeyDown)); // When urgency changes via dropdown, update the date const onUrgencySimpleChange = (val: any) => { const value = typeof val === 'object' ? val.value : val; if (!value) return; setUrgency(value); const level = parseInt(value); if (!isNaN(level)) { const newDateIso = calculateDateFromUrgency(level); // Format for datetime-local input: YYYY-MM-DDTHH:mm const dateObj = new Date(newDateIso); const localIso = new Date(dateObj.getTime() - (dateObj.getTimezoneOffset() * 60000)).toISOString().slice(0, 16); setDateString(localIso); } }; // When date changes via input, update urgency level const onDateInputChange = (e: Event) => { const val = (e.currentTarget as HTMLInputElement).value; setDateString(val); if (val) { const level = calculateUrgencyFromDate(new Date(val).toISOString()); setUrgency(level.toString()); } }; const parseAndAdd = async () => { let text = input(); if (!text || !priority() || !urgency()) return; let finalPriority = parseInt(priority()); let finalUrgency = parseInt(urgency()); const finalTags = [...tags()]; // 1. Handle Priority / Urgency and clean them from text const pMatch = text.match(/\/p\s*(\d+)/); if (pMatch) { finalPriority = Math.min(10, Math.max(1, parseInt(pMatch[1]))); text = text.replace(pMatch[0], ""); } const uMatch = text.match(/\/u\s*(\d+)/); if (uMatch) { finalUrgency = Math.min(10, Math.max(1, parseInt(uMatch[1]))); text = text.replace(uMatch[0], ""); } // 2. Handle Tags: /t [name]:[value] const segments = text.split(/\/t\s*/); let title = segments[0]; for (let i = 1; i < segments.length; i++) { const seg = segments[i]; const firstColon = seg.indexOf(':'); let tagName = ""; let tagValue: number | undefined; let remainingText = ""; if (firstColon === -1) { tagName = seg.trim(); } else { tagName = seg.slice(0, firstColon).trim(); const afterColon = seg.slice(firstColon + 1); const valueMatch = afterColon.match(/^\s*(\d+)/); if (valueMatch) { tagValue = Math.min(10, Math.max(0, parseInt(valueMatch[1]))); remainingText = afterColon.slice(valueMatch[0].length); } else { remainingText = afterColon; } } if (tagName) { if (!finalTags.includes(tagName)) { finalTags.push(tagName); } if (tagValue !== undefined) { // update global definition await upsertTagDefinition(tagName, tagValue, undefined, resolvedTheme()); } } title += " " + remainingText; } let finalDueDate: Date | null = null; if (dateString()) { finalDueDate = new Date(dateString()); } else { const u = finalUrgency; const dateStr = calculateDateFromUrgency(u); finalDueDate = new Date(dateStr); } addTask(title.replace(/\s+/g, " ").trim(), { priority: finalPriority, dueDate: finalDueDate || undefined, tags: finalTags, content: description(), size: parseInt(size()) }); setInput(""); setPriority(""); setUrgency(""); setTags([]); setDescription(""); setSize("5"); lastParsedTags = []; setDateString(""); setIsOpen(false); // Reset editor content const instance = editorInstance(); if (instance) instance.commands.setContent(""); }; const applyTemplate = (template: TaskTemplate) => { setInput(template.title); setPriority(template.priority.toString()); setUrgency(template.urgency.toString()); setTags([...(template.tags || [])]); setDescription(template.content || ""); // Sync urgency to date const level = template.urgency; const newDateIso = calculateDateFromUrgency(level); const dateObj = new Date(newDateIso); const localIso = new Date(dateObj.getTime() - (dateObj.getTimezoneOffset() * 60000)).toISOString().slice(0, 16); setDateString(localIso); // Update editor if ready const instance = editorInstance(); if (instance) { instance.commands.setContent(template.content || ""); } toast.info(`Applied template: ${template.name}`); }; return ( <> {isOpen() && (
setIsOpen(false)} />
setInput(e.currentTarget.value)} onKeyDown={(e) => e.key === "Enter" && parseAndAdd()} placeholder="Task title... type /p1-10 for priority and /u1-10 for urgency" class="border-none shadow-none focus-visible:ring-0 text-lg" />
{/* Row 1: Priority, Size, Urgency */}
value={priority()} onChange={(val) => { const v = typeof val === 'object' ? val.value : val; if (v) setPriority(v); }} options={PRIORITY_OPTIONS} optionValue="value" optionTextValue="label" placeholder="Priority" itemComponent={(props) => ( {props.item.rawValue.label} )} >
{PRIORITY_OPTIONS.find(o => o.value === priority())?.label || (priority() === "" ? "Pick Priority" : priority())}
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) => ( {props.item.rawValue.label} )} >
{SIZE_OPTIONS.find(o => o.value === size())?.label || size()}
value={urgency()} onChange={(val) => onUrgencySimpleChange(val)} options={URGENCY_OPTIONS} optionValue="value" optionTextValue="label" placeholder="Urgency" itemComponent={(props) => ( {props.item.rawValue.label} )} >
{URGENCY_OPTIONS.find(o => o.value === urgency())?.label || (urgency() === "" ? "Pick Urgency" : urgency())}
{/* Row 2: Tags + Due Date */}
{(tag) => ( { 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))} >
d.name === tag)?.value ?? 5) > 5 ? "#22c55e" : (store.tagDefinitions.find(d => d.name === tag)?.value ?? 5) < 5 ? "#ef4444" : "#94a3b8" }} /> {tag} )}
(e.target as HTMLInputElement).showPicker?.()} onKeyDown={(e) => { if (e.key !== "Tab" && e.key !== "Escape") { e.preventDefault(); } }} step="900" class="w-full h-10 px-3 bg-background border-none rounded-md text-sm shadow-sm focus:outline-none focus:ring-1 focus:ring-ring z-[102] relative cursor-pointer" />
}>
Enter to Add
0}>
{(template) => ( )}
)} ); };