deduplicated quick add for embedding
This commit is contained in:
+9
-6
@@ -16,8 +16,8 @@ const SnowballView = lazy(() => import('./views/SnowballView').then(m => ({ defa
|
||||
const DigInView = lazy(() => import('./views/DigInView').then(m => ({ default: m.DigInView })));
|
||||
const ProgressView = lazy(() => import('./views/ProgressView').then(m => ({ default: m.ProgressView })));
|
||||
const HelpView = lazy(() => import('./views/HelpView').then(m => ({ default: m.HelpView })));
|
||||
const EmbedNotesView = lazy(() => import('./views/EmbedNotesView').then(m => ({ default: m.EmbedNotesView })));
|
||||
const EmbedQuickAddView = lazy(() => import('./views/EmbedQuickAddView').then(m => ({ default: m.EmbedQuickAddView })));
|
||||
const NotepadView = lazy(() => import('./views/NotepadView').then(m => ({ default: m.NotepadView })));
|
||||
const QuickEntryForm = lazy(() => import('./components/QuickEntryForm').then(m => ({ default: m.QuickEntryForm })));
|
||||
|
||||
const App: Component = () => {
|
||||
// Basic routing state
|
||||
@@ -99,11 +99,14 @@ const App: Component = () => {
|
||||
<EmbedAuthWrapper>
|
||||
<EmbedLayout>
|
||||
<Suspense fallback={<div class="flex items-center justify-center h-full"><div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div></div>}>
|
||||
<Show when={getEmbedView() === "embed_notes"}>
|
||||
<EmbedNotesView />
|
||||
<Show when={getEmbedView() === 'embed_notes'}>
|
||||
{(() => {
|
||||
const [selectedNoteId, setSelectedNoteId] = createSignal<string | null>(null);
|
||||
return <NotepadView selectedNoteId={selectedNoteId()} setSelectedNoteId={setSelectedNoteId} />;
|
||||
})()}
|
||||
</Show>
|
||||
<Show when={getEmbedView() === "embed_quick_add"}>
|
||||
<EmbedQuickAddView />
|
||||
<Show when={getEmbedView() === 'embed_quick_add'}>
|
||||
<QuickEntryForm autoFocus={true} />
|
||||
</Show>
|
||||
</Suspense>
|
||||
</EmbedLayout>
|
||||
|
||||
+26
-478
@@ -1,39 +1,18 @@
|
||||
import { type Component, createSignal, createEffect, onCleanup, onMount, For, Show } from "solid-js";
|
||||
import { addTask, calculateDateFromUrgency, calculateUrgencyFromDate, store, upsertTagDefinition, currentTaskContext, type TaskTemplate } from "@/store";
|
||||
import { Search, Command, Clock, ArrowUpCircle, Plus, Copy, ChevronDown, Gauge, X } from "lucide-solid";
|
||||
import { type Component, createSignal, createEffect, onCleanup, onMount } from "solid-js";
|
||||
import { store, currentTaskContext } from "@/store";
|
||||
import { Plus, X } 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";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const TaskEditor = lazy(() => import("./TaskEditor").then(m => ({ default: m.TaskEditor })));
|
||||
import { QuickEntryForm } from "./QuickEntryForm";
|
||||
|
||||
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<string>("");
|
||||
const [urgency, setUrgency] = createSignal<string>("");
|
||||
const [tags, setTags] = createSignal<string[]>([]);
|
||||
const [description, setDescription] = createSignal("");
|
||||
const [editorInstance, setEditorInstance] = createSignal<any>(null);
|
||||
const [size, setSize] = createSignal<string>("3");
|
||||
const [initialTags, setInitialTags] = createSignal<string[]>([]);
|
||||
|
||||
// Autofill tags based on context when opening
|
||||
createEffect(() => {
|
||||
if (isOpen()) {
|
||||
const tags: string[] = [];
|
||||
const ctx = currentTaskContext();
|
||||
if (store.isNotepadMode) {
|
||||
// Not in a specific context, but in Notes mode.
|
||||
@@ -43,98 +22,28 @@ export const QuickEntry: Component = () => {
|
||||
const note = store.notes.find(n => n.id === activeNoteId);
|
||||
if (note && note.title) {
|
||||
const noteTagName = note.title.trim();
|
||||
if (noteTagName && !tags().includes(noteTagName)) {
|
||||
setTags(prev => [...prev, noteTagName]);
|
||||
}
|
||||
if (noteTagName) tags.push(noteTagName);
|
||||
}
|
||||
}
|
||||
} else if (typeof ctx === 'object') {
|
||||
if ('bucketId' in ctx) {
|
||||
// Bucket View: Autofill bucket name
|
||||
const bucketName = ctx.name;
|
||||
if (bucketName && !tags().includes(bucketName)) {
|
||||
setTags(prev => [...prev, bucketName]);
|
||||
}
|
||||
if (bucketName) tags.push(bucketName);
|
||||
} else if ('userId' in ctx) {
|
||||
// Shared User View: Autofill user name (assuming name IS the tag for sharing)
|
||||
const userName = ctx.name;
|
||||
if (userName && !tags().includes(userName)) {
|
||||
setTags(prev => [...prev, userName]);
|
||||
}
|
||||
if (userName) tags.push(userName);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 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<string>("");
|
||||
let inputRef: HTMLInputElement | undefined;
|
||||
|
||||
createEffect(() => {
|
||||
if (isOpen()) {
|
||||
// Slight delay to ensure DOM is ready and animations don't interfere
|
||||
setTimeout(() => inputRef?.focus(), 50);
|
||||
setInitialTags(tags);
|
||||
}
|
||||
});
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
|
||||
e.preventDefault();
|
||||
if (isOpen()) {
|
||||
inputRef?.focus();
|
||||
} else {
|
||||
setIsOpen(true);
|
||||
}
|
||||
setIsOpen(prev => !prev);
|
||||
}
|
||||
if (e.key === "Escape") {
|
||||
setIsOpen(false);
|
||||
@@ -144,145 +53,6 @@ export const QuickEntry: Component = () => {
|
||||
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("3");
|
||||
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 (
|
||||
<>
|
||||
<Button
|
||||
@@ -301,245 +71,23 @@ export const QuickEntry: Component = () => {
|
||||
<div class="absolute inset-0 bg-background/80 backdrop-blur-sm" onClick={() => setIsOpen(false)} />
|
||||
|
||||
<div class="relative w-full max-w-xl max-h-[100vh] md:max-h-[80vh] bg-card border border-border rounded-t-2xl md:rounded-2xl shadow-2xl overflow-hidden animate-in fade-in slide-in-from-bottom-4 md:zoom-in-95 duration-200 flex flex-col">
|
||||
{/* Scrollable content area - includes header so entire card can scroll on mobile */}
|
||||
<div class="flex-1 overflow-y-auto overscroll-contain">
|
||||
<div class="flex items-center px-4 py-3 border-b border-border sticky top-0 bg-card z-10">
|
||||
<Search class="text-muted-foreground mr-3" size={20} />
|
||||
<TextField class="flex-1">
|
||||
<TextFieldInput
|
||||
id="quick-entry-input"
|
||||
name="task-title"
|
||||
ref={inputRef}
|
||||
value={input()}
|
||||
onInput={(e) => 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"
|
||||
/>
|
||||
</TextField>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={() => setIsOpen(false)}
|
||||
class="ml-2 md:hidden h-9 w-9 rounded-full text-muted-foreground hover:text-foreground hover:bg-muted"
|
||||
>
|
||||
<X size={20} />
|
||||
</Button>
|
||||
</div>
|
||||
{/* Row 1: Priority, Size, Urgency */}
|
||||
<div class="flex flex-col md:flex-row flex-wrap items-stretch md:items-center p-4 gap-4 border-b border-border bg-muted/10">
|
||||
<div class="flex-1 min-w-0 md:min-w-[140px] space-y-1.5">
|
||||
<label class="text-[0.625rem] font-bold uppercase tracking-wider text-muted-foreground ml-1">
|
||||
Priority <span class="text-destructive">*</span>
|
||||
</label>
|
||||
<Select<any>
|
||||
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) => (
|
||||
<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">
|
||||
<ArrowUpCircle size={14} class="text-primary shrink-0" />
|
||||
<span class="text-sm font-medium truncate">
|
||||
{PRIORITY_OPTIONS.find(o => o.value === priority())?.label || (priority() === "" ? "Pick Priority" : priority())}
|
||||
</span>
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent />
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-w-0 md:min-w-[140px] space-y-1.5">
|
||||
<label class="text-[0.625rem] 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 min-w-0 md:min-w-[140px] space-y-1.5">
|
||||
<label class="text-[0.625rem] font-bold uppercase tracking-wider text-muted-foreground ml-1">
|
||||
Urgency <span class="text-destructive">*</span>
|
||||
</label>
|
||||
<Select<any>
|
||||
value={urgency()}
|
||||
onChange={(val) => onUrgencySimpleChange(val)}
|
||||
options={URGENCY_OPTIONS}
|
||||
optionValue="value"
|
||||
optionTextValue="label"
|
||||
placeholder="Urgency"
|
||||
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">
|
||||
<Clock size={14} class="text-primary shrink-0" />
|
||||
<span class="text-sm font-medium truncate">
|
||||
{URGENCY_OPTIONS.find(o => o.value === urgency())?.label || (urgency() === "" ? "Pick Urgency" : urgency())}
|
||||
</span>
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent />
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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-[0.625rem] font-bold uppercase tracking-wider text-muted-foreground ml-1">Due Date</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={dateString()}
|
||||
onInput={onDateInputChange}
|
||||
onClick={(e) => (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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-3 bg-card min-h-[80px]">
|
||||
<Suspense fallback={<div class="h-20 animate-pulse bg-muted/20 rounded-lg" />}>
|
||||
<TaskEditor
|
||||
content={description()}
|
||||
onUpdate={setDescription}
|
||||
onEditorReady={setEditorInstance}
|
||||
class="min-h-[60px] prose-sm"
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
<div class="absolute top-3 right-4 z-[101]">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={() => setIsOpen(false)}
|
||||
class="md:hidden h-9 w-9 rounded-full text-muted-foreground hover:text-foreground hover:bg-muted"
|
||||
>
|
||||
<X size={20} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="p-4 bg-muted/30 flex justify-between items-center shrink-0 border-t border-border">
|
||||
<div class="flex items-center space-x-2 text-[0.625rem] font-bold text-muted-foreground uppercase tracking-widest">
|
||||
<Command size={10} />
|
||||
<span class="hidden sm:inline">K to focus • Enter to Add</span>
|
||||
<span class="sm:hidden">Enter to Add</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<Show when={(store.templates || []).length > 0}>
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
as={Button}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-9 px-3 gap-2 hover:bg-primary/10 hover:text-primary text-muted-foreground transition-all rounded-xl border border-border/40"
|
||||
>
|
||||
<Copy size={14} />
|
||||
<span class="text-[0.625rem] font-bold uppercase tracking-wider hidden sm:inline">Use Template</span>
|
||||
<ChevronDown size={12} class="opacity-50" />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-64 p-2 bg-card border-border shadow-xl">
|
||||
<div class="space-y-1">
|
||||
<For each={store.templates}>
|
||||
{(template) => (
|
||||
<button
|
||||
class="w-full flex items-center gap-3 p-2 rounded-lg hover:bg-muted transition-colors text-left group"
|
||||
onClick={() => applyTemplate(template)}
|
||||
>
|
||||
<div class="w-8 h-8 rounded-lg bg-primary/10 flex items-center justify-center text-primary shrink-0 group-hover:bg-primary group-hover:text-primary-foreground transition-all">
|
||||
<Copy size={14} />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm font-bold truncate tracking-tight">{template.name}</p>
|
||||
<p class="text-[0.5625rem] uppercase font-black text-muted-foreground opacity-60 tracking-widest">
|
||||
P:{template.priority} / U:{template.urgency}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</Show>
|
||||
|
||||
<Button size="sm" onClick={parseAndAdd} disabled={!input() || !priority() || !urgency()}>
|
||||
Create Task
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<QuickEntryForm
|
||||
onSuccess={() => setIsOpen(false)}
|
||||
autoFocus={true}
|
||||
initialTags={initialTags()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,452 @@
|
||||
import { type Component, createSignal, createEffect, onMount, For, Show, Suspense, lazy } from "solid-js";
|
||||
import { addTask, calculateDateFromUrgency, calculateUrgencyFromDate, store, upsertTagDefinition, type TaskTemplate } from "@/store";
|
||||
import { Search, Command, Clock, ArrowUpCircle, 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 { 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 })));
|
||||
|
||||
interface QuickEntryFormProps {
|
||||
onSuccess?: () => void;
|
||||
autoFocus?: boolean;
|
||||
initialTitle?: string;
|
||||
initialTags?: string[];
|
||||
}
|
||||
|
||||
export const QuickEntryForm: Component<QuickEntryFormProps> = (props) => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
// Diff tracking for reactive tags
|
||||
let lastParsedTags: string[] = [];
|
||||
|
||||
const [input, setInput] = createSignal(props.initialTitle || "");
|
||||
const [priority, setPriority] = createSignal<string>("");
|
||||
const [urgency, setUrgency] = createSignal<string>("");
|
||||
const [tags, setTags] = createSignal<string[]>(props.initialTags || []);
|
||||
const [description, setDescription] = createSignal("");
|
||||
const [editorInstance, setEditorInstance] = createSignal<any>(null);
|
||||
const [size, setSize] = createSignal<string>("3");
|
||||
const [dateString, setDateString] = createSignal<string>("");
|
||||
|
||||
let inputRef: HTMLInputElement | undefined;
|
||||
|
||||
onMount(() => {
|
||||
if (props.autoFocus) {
|
||||
setTimeout(() => inputRef?.focus(), 100);
|
||||
}
|
||||
});
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
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()];
|
||||
const tagsToRemove = lastParsedTags.filter(t => !newParsedTags.includes(t));
|
||||
const tagsToAdd = newParsedTags.filter(t => !currentTags.includes(t));
|
||||
|
||||
let nextTags = currentTags.filter(t => !tagsToRemove.includes(t));
|
||||
nextTags = [...nextTags, ...tagsToAdd];
|
||||
|
||||
if (JSON.stringify(nextTags) !== JSON.stringify(currentTags)) {
|
||||
setTags(nextTags);
|
||||
}
|
||||
lastParsedTags = newParsedTags;
|
||||
});
|
||||
|
||||
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);
|
||||
const dateObj = new Date(newDateIso);
|
||||
const localIso = new Date(dateObj.getTime() - (dateObj.getTimezoneOffset() * 60000)).toISOString().slice(0, 16);
|
||||
setDateString(localIso);
|
||||
}
|
||||
};
|
||||
|
||||
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()];
|
||||
|
||||
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], "");
|
||||
}
|
||||
|
||||
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) {
|
||||
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("3");
|
||||
lastParsedTags = [];
|
||||
setDateString("");
|
||||
|
||||
const instance = editorInstance();
|
||||
if (instance) instance.commands.setContent("");
|
||||
|
||||
if (props.onSuccess) props.onSuccess();
|
||||
};
|
||||
|
||||
const applyTemplate = (template: TaskTemplate) => {
|
||||
setInput(template.title);
|
||||
setPriority(template.priority.toString());
|
||||
setUrgency(template.urgency.toString());
|
||||
setTags([...(template.tags || [])]);
|
||||
setDescription(template.content || "");
|
||||
|
||||
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);
|
||||
|
||||
const instance = editorInstance();
|
||||
if (instance) {
|
||||
instance.commands.setContent(template.content || "");
|
||||
}
|
||||
toast.info(`Applied template: ${template.name}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="flex flex-col h-full bg-card">
|
||||
<div class="flex items-center px-4 py-3 border-b border-border sticky top-0 bg-card z-10">
|
||||
<Search class="text-muted-foreground mr-3" size={20} />
|
||||
<TextField class="flex-1">
|
||||
<TextFieldInput
|
||||
id="quick-entry-input"
|
||||
name="task-title"
|
||||
ref={inputRef}
|
||||
value={input()}
|
||||
onInput={(e) => 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"
|
||||
/>
|
||||
</TextField>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto overscroll-contain">
|
||||
<div class="flex flex-col md:flex-row flex-wrap items-stretch md:items-center p-4 gap-4 border-b border-border bg-muted/10">
|
||||
<div class="flex-1 min-w-0 md:min-w-[140px] space-y-1.5">
|
||||
<label class="text-[0.625rem] font-bold uppercase tracking-wider text-muted-foreground ml-1">
|
||||
Priority <span class="text-destructive">*</span>
|
||||
</label>
|
||||
<Select<any>
|
||||
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) => (
|
||||
<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">
|
||||
<ArrowUpCircle size={14} class="text-primary shrink-0" />
|
||||
<span class="text-sm font-medium truncate">
|
||||
{PRIORITY_OPTIONS.find(o => o.value === priority())?.label || (priority() === "" ? "Pick Priority" : priority())}
|
||||
</span>
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent />
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-w-0 md:min-w-[140px] space-y-1.5">
|
||||
<label class="text-[0.625rem] 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 min-w-0 md:min-w-[140px] space-y-1.5">
|
||||
<label class="text-[0.625rem] font-bold uppercase tracking-wider text-muted-foreground ml-1">
|
||||
Urgency <span class="text-destructive">*</span>
|
||||
</label>
|
||||
<Select<any>
|
||||
value={urgency()}
|
||||
onChange={(val) => onUrgencySimpleChange(val)}
|
||||
options={URGENCY_OPTIONS}
|
||||
optionValue="value"
|
||||
optionTextValue="label"
|
||||
placeholder="Urgency"
|
||||
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">
|
||||
<Clock size={14} class="text-primary shrink-0" />
|
||||
<span class="text-sm font-medium truncate">
|
||||
{URGENCY_OPTIONS.find(o => o.value === urgency())?.label || (urgency() === "" ? "Pick Urgency" : urgency())}
|
||||
</span>
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent />
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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-[0.625rem] font-bold uppercase tracking-wider text-muted-foreground ml-1">Due Date</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={dateString()}
|
||||
onInput={onDateInputChange}
|
||||
onClick={(e) => (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 relative cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-3 bg-card min-h-[80px]">
|
||||
<Suspense fallback={<div class="h-20 animate-pulse bg-muted/20 rounded-lg" />}>
|
||||
<TaskEditor
|
||||
content={description()}
|
||||
onUpdate={setDescription}
|
||||
onEditorReady={setEditorInstance}
|
||||
class="min-h-[60px] prose-sm"
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 bg-muted/30 flex justify-between items-center shrink-0 border-t border-border">
|
||||
<div class="flex items-center space-x-2 text-[0.625rem] font-bold text-muted-foreground uppercase tracking-widest">
|
||||
<Command size={10} />
|
||||
<span class="hidden sm:inline">K to focus • Enter to Add</span>
|
||||
<span class="sm:hidden">Enter to Add</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<Show when={(store.templates || []).length > 0}>
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
as={Button}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-9 px-3 gap-2 hover:bg-primary/10 hover:text-primary text-muted-foreground transition-all rounded-xl border border-border/40"
|
||||
>
|
||||
<Copy size={14} />
|
||||
<span class="text-[0.625rem] font-bold uppercase tracking-wider hidden sm:inline">Use Template</span>
|
||||
<ChevronDown size={12} class="opacity-50" />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-64 p-2 bg-card border-border shadow-xl">
|
||||
<div class="space-y-1">
|
||||
<For each={store.templates}>
|
||||
{(template) => (
|
||||
<button
|
||||
class="w-full flex items-center gap-3 p-2 rounded-lg hover:bg-muted transition-colors text-left group"
|
||||
onClick={() => applyTemplate(template)}
|
||||
>
|
||||
<div class="w-8 h-8 rounded-lg bg-primary/10 flex items-center justify-center text-primary shrink-0 group-hover:bg-primary group-hover:text-primary-foreground transition-all">
|
||||
<Copy size={14} />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm font-bold truncate tracking-tight">{template.name}</p>
|
||||
<p class="text-[0.5625rem] uppercase font-black text-muted-foreground opacity-60 tracking-widest">
|
||||
P:{template.priority} / U:{template.urgency}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</Show>
|
||||
|
||||
<Button size="sm" onClick={parseAndAdd} disabled={!input() || !priority() || !urgency()}>
|
||||
Create Task
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
import { type Component, createSignal } from "solid-js";
|
||||
import { NotepadView } from "./NotepadView";
|
||||
|
||||
export const EmbedNotesView: Component = () => {
|
||||
const [selectedNoteId, setSelectedNoteId] = createSignal<string | null>(null);
|
||||
|
||||
return (
|
||||
<div class="h-full w-full">
|
||||
<NotepadView
|
||||
selectedNoteId={selectedNoteId()}
|
||||
setSelectedNoteId={setSelectedNoteId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,287 +0,0 @@
|
||||
import { type Component, createSignal, onMount, For, Show } from "solid-js";
|
||||
import { addTask, calculateDateFromUrgency, calculateUrgencyFromDate, store, type TaskTemplate } from "@/store";
|
||||
import { Search, Clock, ArrowUpCircle, 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 "@/components/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("@/components/TaskEditor").then(m => ({ default: m.TaskEditor })));
|
||||
|
||||
export const EmbedQuickAddView: Component = () => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
const [input, setInput] = createSignal("");
|
||||
const [priority, setPriority] = createSignal<string>("");
|
||||
const [urgency, setUrgency] = createSignal<string>("");
|
||||
const [tags, setTags] = createSignal<string[]>([]);
|
||||
const [description, setDescription] = createSignal("");
|
||||
const [editorInstance, setEditorInstance] = createSignal<any>(null);
|
||||
const [size, setSize] = createSignal<string>("3");
|
||||
const [dateString, setDateString] = createSignal<string>("");
|
||||
|
||||
let inputRef: HTMLInputElement | undefined;
|
||||
|
||||
onMount(() => {
|
||||
setTimeout(() => inputRef?.focus(), 100);
|
||||
});
|
||||
|
||||
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);
|
||||
const dateObj = new Date(newDateIso);
|
||||
const localIso = new Date(dateObj.getTime() - (dateObj.getTimezoneOffset() * 60000)).toISOString().slice(0, 16);
|
||||
setDateString(localIso);
|
||||
}
|
||||
};
|
||||
|
||||
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 handleCreateTask = async () => {
|
||||
let text = input();
|
||||
if (!text || !priority() || !urgency()) return;
|
||||
|
||||
let finalPriority = parseInt(priority());
|
||||
let finalUrgency = parseInt(urgency());
|
||||
const finalTags = [...tags()];
|
||||
|
||||
let finalDueDate: Date | null = null;
|
||||
if (dateString()) {
|
||||
finalDueDate = new Date(dateString());
|
||||
} else {
|
||||
const dateStr = calculateDateFromUrgency(finalUrgency);
|
||||
finalDueDate = new Date(dateStr);
|
||||
}
|
||||
|
||||
await addTask(text.trim(), {
|
||||
priority: finalPriority,
|
||||
dueDate: finalDueDate || undefined,
|
||||
tags: finalTags,
|
||||
content: description(),
|
||||
size: parseInt(size())
|
||||
});
|
||||
|
||||
toast.success("Task created successfully");
|
||||
|
||||
// Reset
|
||||
setInput("");
|
||||
setPriority("5");
|
||||
setUrgency("5");
|
||||
setTags([]);
|
||||
setDescription("");
|
||||
setSize("3");
|
||||
setDateString("");
|
||||
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 || "");
|
||||
|
||||
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);
|
||||
|
||||
const instance = editorInstance();
|
||||
if (instance) instance.commands.setContent(template.content || "");
|
||||
toast.info(`Applied template: ${template.name}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="w-full max-w-xl mx-auto bg-card border border-border rounded-xl shadow-lg overflow-hidden flex flex-col">
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<div class="flex items-center px-4 py-3 border-b border-border sticky top-0 bg-card z-10">
|
||||
<Search class="text-muted-foreground mr-3" size={20} />
|
||||
<TextField class="flex-1">
|
||||
<TextFieldInput
|
||||
ref={inputRef}
|
||||
value={input()}
|
||||
onInput={(e: any) => setInput(e.currentTarget.value)}
|
||||
onKeyDown={(e: any) => e.key === "Enter" && handleCreateTask()}
|
||||
placeholder="Task title..."
|
||||
class="border-none shadow-none focus-visible:ring-0 text-lg"
|
||||
/>
|
||||
</TextField>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 p-4 gap-4 border-b border-border bg-muted/5">
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-[0.625rem] font-bold uppercase tracking-wider text-muted-foreground ml-1">Priority</label>
|
||||
<Select<any>
|
||||
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) => <SelectItem item={props.item}>{props.item.rawValue.label}</SelectItem>}
|
||||
>
|
||||
<SelectTrigger class="w-full bg-background border-none shadow-sm h-10 px-3">
|
||||
<div class="flex items-center gap-2 truncate">
|
||||
<ArrowUpCircle size={14} class="text-primary shrink-0" />
|
||||
<span class="text-sm font-medium truncate">{PRIORITY_OPTIONS.find(o => o.value === priority())?.label || priority()}</span>
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent />
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-[0.625rem] 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">
|
||||
<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="space-y-1.5">
|
||||
<label class="text-[0.625rem] font-bold uppercase tracking-wider text-muted-foreground ml-1">Urgency</label>
|
||||
<Select<any>
|
||||
value={urgency()}
|
||||
onChange={(val) => onUrgencySimpleChange(val)}
|
||||
options={URGENCY_OPTIONS}
|
||||
optionValue="value"
|
||||
optionTextValue="label"
|
||||
placeholder="Urgency"
|
||||
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">
|
||||
<div class="flex items-center gap-2 truncate">
|
||||
<Clock size={14} class="text-primary shrink-0" />
|
||||
<span class="text-sm font-medium truncate">{URGENCY_OPTIONS.find(o => o.value === urgency())?.label || urgency()}</span>
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent />
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row items-stretch sm:items-center px-4 py-3 gap-4 bg-muted/5 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 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;
|
||||
})(),
|
||||
"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))}
|
||||
>
|
||||
{tag}
|
||||
</Badge>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<div class="w-full sm:w-auto sm:min-w-[200px] space-y-1.5">
|
||||
<label class="text-[0.625rem] font-bold uppercase tracking-wider text-muted-foreground ml-1">Due Date</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={dateString()}
|
||||
onInput={onDateInputChange}
|
||||
class="w-full h-10 px-3 bg-background border border-border/50 rounded-md text-sm shadow-sm focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-3 bg-card min-h-[120px]">
|
||||
<Suspense fallback={<div class="h-20 animate-pulse bg-muted/20 rounded-lg" />}>
|
||||
<TaskEditor
|
||||
content={description()}
|
||||
onUpdate={setDescription}
|
||||
onEditorReady={setEditorInstance}
|
||||
class="min-h-[100px] prose-sm"
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 bg-muted/10 flex justify-between items-center border-t border-border shrink-0">
|
||||
<Show when={(store.templates || []).length > 0}>
|
||||
<Popover>
|
||||
<PopoverTrigger as={Button} variant="ghost" size="sm" class="h-9 px-3 gap-2">
|
||||
<Copy size={14} />
|
||||
<span class="text-[0.625rem] font-bold uppercase tracking-wider">Templates</span>
|
||||
<ChevronDown size={12} class="opacity-50" />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-64 p-2 bg-card border-border shadow-xl">
|
||||
<div class="space-y-1">
|
||||
<For each={store.templates}>
|
||||
{(template) => (
|
||||
<button
|
||||
class="w-full flex items-center gap-3 p-2 rounded-lg hover:bg-muted transition-colors text-left"
|
||||
onClick={() => applyTemplate(template)}
|
||||
>
|
||||
<div class="w-8 h-8 rounded-lg bg-primary/10 flex items-center justify-center text-primary shrink-0">
|
||||
<Copy size={14} />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm font-bold truncate">{template.name}</p>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</Show>
|
||||
<Button size="sm" onClick={handleCreateTask} disabled={!input()}>
|
||||
Create Task
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user