added templates and fixed UX for virtual keyboard opening when opening a task
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { type Component, createSignal, createEffect, onCleanup, onMount, For } from "solid-js";
|
import { type Component, createSignal, createEffect, onCleanup, onMount, For, Show } from "solid-js";
|
||||||
import { Search, Command, Clock, ArrowUpCircle, Plus } from "lucide-solid";
|
import { addTask, calculateDateFromUrgency, calculateUrgencyFromDate, store, upsertMultipleTagDefinitions, type TaskTemplate } from "@/store";
|
||||||
import { addTask, calculateDateFromUrgency, calculateUrgencyFromDate, store, upsertMultipleTagDefinitions } from "@/store";
|
import { Search, Command, Clock, ArrowUpCircle, Plus, Copy, ChevronDown } from "lucide-solid";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -8,6 +8,8 @@ import { TextField, TextFieldInput } from "@/components/ui/textfield";
|
|||||||
import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select";
|
||||||
import { TagPicker } from "@/components/TagPicker";
|
import { TagPicker } from "@/components/TagPicker";
|
||||||
import { lazy, Suspense } from "solid-js";
|
import { lazy, Suspense } from "solid-js";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
|
import { toast } from "solid-sonner";
|
||||||
|
|
||||||
const TaskEditor = lazy(() => import("./TaskEditor").then(m => ({ default: m.TaskEditor })));
|
const TaskEditor = lazy(() => import("./TaskEditor").then(m => ({ default: m.TaskEditor })));
|
||||||
|
|
||||||
@@ -219,6 +221,29 @@ export const QuickEntry: Component = () => {
|
|||||||
if (instance) instance.commands.setContent("");
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
@@ -352,12 +377,52 @@ export const QuickEntry: Component = () => {
|
|||||||
<span class="hidden sm:inline">K to focus • Enter to Add</span>
|
<span class="hidden sm:inline">K to focus • Enter to Add</span>
|
||||||
<span class="sm:hidden">Enter to Add</span>
|
<span class="sm:hidden">Enter to Add</span>
|
||||||
</div>
|
</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-[10px] 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-[9px] 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()}>
|
<Button size="sm" onClick={parseAndAdd} disabled={!input() || !priority() || !urgency()}>
|
||||||
Create Task
|
Create Task
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { type Component, createEffect, createSignal, For } from "solid-js";
|
import { type Component, createEffect, createSignal, For } from "solid-js";
|
||||||
import { Sheet, SheetContent } from "@/components/ui/sheet";
|
import { Sheet, SheetContent } from "@/components/ui/sheet";
|
||||||
import { type Task, removeTask, restoreTask, updateTask } from "@/store";
|
import { type Task, removeTask, restoreTask, updateTask, saveTaskAsTemplate } from "@/store";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select";
|
||||||
import { ArrowUpCircle, Clock, Calendar, Type, Trash2, X } from "lucide-solid";
|
import { ArrowUpCircle, Clock, Calendar, Type, Trash2, X, Copy } from "lucide-solid";
|
||||||
import { calculateDateFromUrgency, calculateUrgencyFromDate } from "@/store";
|
import { calculateDateFromUrgency, calculateUrgencyFromDate } from "@/store";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
import { TagPicker } from "./TagPicker";
|
import { TagPicker } from "./TagPicker";
|
||||||
@@ -81,7 +81,16 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={props.isOpen} onOpenChange={(open) => !open && props.onClose()}>
|
<Sheet open={props.isOpen} onOpenChange={(open) => !open && props.onClose()}>
|
||||||
<SheetContent hideCloseButton class="w-full sm:max-w-2xl px-0 sm:px-0 flex flex-col h-full bg-background border-l border-border shadow-2xl">
|
<SheetContent
|
||||||
|
hideCloseButton
|
||||||
|
onOpenAutoFocus={(e) => {
|
||||||
|
// Prevent auto-focus on mobile to avoid keyboard popup
|
||||||
|
if (window.innerWidth < 768) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
class="w-full sm:max-w-2xl px-0 sm:px-0 flex flex-col h-full bg-background border-l border-border shadow-2xl"
|
||||||
|
>
|
||||||
{/* Header Area */}
|
{/* Header Area */}
|
||||||
<div class="px-6 pt-6 pb-2 shrink-0">
|
<div class="px-6 pt-6 pb-2 shrink-0">
|
||||||
<textarea
|
<textarea
|
||||||
@@ -263,6 +272,24 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
|||||||
<span class="text-xs font-bold uppercase tracking-wider">Delete</span>
|
<span class="text-xs font-bold uppercase tracking-wider">Delete</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
{/* Save as Template (Desktop + Mobile) */}
|
||||||
|
<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"
|
||||||
|
onClick={async () => {
|
||||||
|
const name = prompt("Enter a name for this template:");
|
||||||
|
if (name) {
|
||||||
|
await saveTaskAsTemplate(props.task.id, name);
|
||||||
|
toast.success(`Template "${name}" created`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Copy size={16} />
|
||||||
|
<span class="text-xs font-bold uppercase tracking-wider">Template</span>
|
||||||
|
</Button>
|
||||||
|
|
||||||
{/* Close button (Mobile only) */}
|
{/* Close button (Mobile only) */}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -274,6 +301,7 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
|||||||
<span class="text-xs font-bold uppercase tracking-wider">Close</span>
|
<span class="text-xs font-bold uppercase tracking-wider">Close</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -27,6 +27,16 @@ export interface Task {
|
|||||||
updated: string; // ISO string from PB
|
updated: string; // ISO string from PB
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TaskTemplate {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
title: string;
|
||||||
|
priority: number;
|
||||||
|
urgency: number;
|
||||||
|
tags: string[];
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Filter {
|
export interface Filter {
|
||||||
query: string;
|
query: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
@@ -43,6 +53,7 @@ interface TaskStore {
|
|||||||
matrixScaleDays: number;
|
matrixScaleDays: number;
|
||||||
prefId?: string; // ID of the user_preferences record
|
prefId?: string; // ID of the user_preferences record
|
||||||
tagDefinitions?: Record<string, number>;
|
tagDefinitions?: Record<string, number>;
|
||||||
|
templates?: TaskTemplate[];
|
||||||
filter: Filter;
|
filter: Filter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,6 +64,7 @@ export const [store, setStore] = createStore<TaskStore>({
|
|||||||
uWeight: 1.0,
|
uWeight: 1.0,
|
||||||
matrixScaleDays: 30,
|
matrixScaleDays: 30,
|
||||||
tagDefinitions: {}, // Map<Name, Value 0-10>
|
tagDefinitions: {}, // Map<Name, Value 0-10>
|
||||||
|
templates: [],
|
||||||
filter: {
|
filter: {
|
||||||
query: "",
|
query: "",
|
||||||
tags: [],
|
tags: [],
|
||||||
@@ -190,6 +202,7 @@ export const initStore = async () => {
|
|||||||
uWeight: prefs.uWeight || 1.0,
|
uWeight: prefs.uWeight || 1.0,
|
||||||
matrixScaleDays: prefs.matrixScaleDays || 30,
|
matrixScaleDays: prefs.matrixScaleDays || 30,
|
||||||
tagDefinitions: prefs.tagDefinitions || {},
|
tagDefinitions: prefs.tagDefinitions || {},
|
||||||
|
templates: prefs.templates || [],
|
||||||
prefId: userId // The ID to update is the User ID
|
prefId: userId // The ID to update is the User ID
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -583,6 +596,63 @@ export const renameTagDefinition = async (oldName: string, newName: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// -- Templates --
|
||||||
|
|
||||||
|
const syncPreferences = async () => {
|
||||||
|
const userId = pb.authStore.model?.id;
|
||||||
|
if (!userId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const prefs = {
|
||||||
|
pWeight: store.pWeight,
|
||||||
|
uWeight: store.uWeight,
|
||||||
|
matrixScaleDays: store.matrixScaleDays,
|
||||||
|
tagDefinitions: store.tagDefinitions,
|
||||||
|
templates: store.templates
|
||||||
|
};
|
||||||
|
|
||||||
|
await pb.collection('users').update(userId, {
|
||||||
|
Taskgrid_pref: prefs
|
||||||
|
}, { requestKey: null });
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e.isAbort) return;
|
||||||
|
console.error("Failed to sync preferences", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addTemplate = async (template: Omit<TaskTemplate, "id">) => {
|
||||||
|
const newTemplate = { ...template, id: crypto.randomUUID() };
|
||||||
|
setStore("templates", (prev) => [...(prev || []), newTemplate]);
|
||||||
|
await syncPreferences();
|
||||||
|
return newTemplate;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateTemplate = async (id: string, updates: Partial<TaskTemplate>) => {
|
||||||
|
setStore("templates", (t) => t!.id === id, updates);
|
||||||
|
await syncPreferences();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeTemplate = async (id: string) => {
|
||||||
|
setStore("templates", (t) => t.filter(tmpl => tmpl.id !== id));
|
||||||
|
await syncPreferences();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const saveTaskAsTemplate = async (taskId: string, name: string) => {
|
||||||
|
const task = store.tasks.find(t => t.id === taskId);
|
||||||
|
if (!task) return;
|
||||||
|
|
||||||
|
const template: Omit<TaskTemplate, "id"> = {
|
||||||
|
name,
|
||||||
|
title: task.title,
|
||||||
|
priority: task.priority,
|
||||||
|
urgency: calculateUrgencyFromDate(task.dueDate),
|
||||||
|
tags: [...(task.tags || [])],
|
||||||
|
content: task.content || ""
|
||||||
|
};
|
||||||
|
|
||||||
|
return await addTemplate(template);
|
||||||
|
};
|
||||||
|
|
||||||
export const setFilter = (update: Partial<Filter>) => {
|
export const setFilter = (update: Partial<Filter>) => {
|
||||||
setStore("filter", (f) => ({ ...f, ...update }));
|
setStore("filter", (f) => ({ ...f, ...update }));
|
||||||
};
|
};
|
||||||
|
|||||||
+243
-13
@@ -1,16 +1,22 @@
|
|||||||
import { type Component, For } from "solid-js";
|
import { type Component, For, Show, createSignal, lazy, Suspense } from "solid-js";
|
||||||
import { ThemeToggle } from "../components/ThemeToggle";
|
import { ThemeToggle } from "../components/ThemeToggle";
|
||||||
import { store, restoreTask, deleteTaskPermanently, setMatrixScaleDays } from "@/store";
|
import { store, restoreTask, deleteTaskPermanently, setMatrixScaleDays, addTemplate, removeTemplate, updateTemplate, upsertTagDefinition, removeTagDefinition, renameTagDefinition } from "@/store";
|
||||||
import { Trash2, Undo2, ArrowLeftRight, Hash } from "lucide-solid";
|
import { Trash2, Undo2, ArrowLeftRight, Hash, Copy, Plus, ChevronDown, ChevronRight, Type } from "lucide-solid";
|
||||||
|
import { TagPicker } from "@/components/TagPicker";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { toast } from "solid-sonner";
|
import { toast } from "solid-sonner";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select";
|
||||||
import { pb } from "@/lib/pocketbase";
|
import { pb } from "@/lib/pocketbase";
|
||||||
import { upsertTagDefinition, removeTagDefinition, renameTagDefinition } from "@/store";
|
|
||||||
import { createSignal } from "solid-js";
|
const TaskEditor = lazy(() => import("../components/TaskEditor").then(m => ({ default: m.TaskEditor })));
|
||||||
|
|
||||||
export const SettingsView: Component = () => {
|
export const SettingsView: Component = () => {
|
||||||
|
const [isTagsOpen, setIsTagsOpen] = createSignal(false);
|
||||||
|
const [isTemplatesOpen, setIsTemplatesOpen] = createSignal(false);
|
||||||
|
const [isTrashOpen, setIsTrashOpen] = createSignal(false);
|
||||||
|
const [expandedTemplateId, setExpandedTemplateId] = createSignal<string | null>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="w-full max-w-2xl mx-auto py-4 sm:py-10 px-4 sm:px-0 space-y-8 sm:space-y-12 animate-in fade-in slide-in-from-bottom-2 duration-500 min-w-0 overflow-hidden text-balance">
|
<div class="w-full max-w-2xl mx-auto py-4 sm:py-10 px-4 sm:px-0 space-y-8 sm:space-y-12 animate-in fade-in slide-in-from-bottom-2 duration-500 min-w-0 overflow-hidden text-balance">
|
||||||
<header class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
<header class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||||
@@ -31,6 +37,7 @@ export const SettingsView: Component = () => {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="grid gap-6">
|
<div class="grid gap-6">
|
||||||
|
{/* Appearance */}
|
||||||
<section class="p-4 sm:p-5 rounded-2xl border border-border bg-card shadow-sm">
|
<section class="p-4 sm:p-5 rounded-2xl border border-border bg-card shadow-sm">
|
||||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||||
<div class="space-y-0.5">
|
<div class="space-y-0.5">
|
||||||
@@ -70,7 +77,12 @@ export const SettingsView: Component = () => {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="p-4 sm:p-5 rounded-2xl border border-border bg-card shadow-sm space-y-4">
|
{/* Global Tag Manager */}
|
||||||
|
<section class="p-4 sm:p-5 rounded-2xl border border-border bg-card shadow-sm space-y-4 overflow-hidden">
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between cursor-pointer group"
|
||||||
|
onClick={() => setIsTagsOpen(!isTagsOpen())}
|
||||||
|
>
|
||||||
<div class="space-y-0.5">
|
<div class="space-y-0.5">
|
||||||
<h3 class="text-base font-semibold flex items-center gap-2">
|
<h3 class="text-base font-semibold flex items-center gap-2">
|
||||||
<Hash size={16} />
|
<Hash size={16} />
|
||||||
@@ -78,8 +90,13 @@ export const SettingsView: Component = () => {
|
|||||||
</h3>
|
</h3>
|
||||||
<p class="text-sm text-muted-foreground">Manage your global tag definitions and values.</p>
|
<p class="text-sm text-muted-foreground">Manage your global tag definitions and values.</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="w-8 h-8 rounded-full flex items-center justify-center bg-muted/50 group-hover:bg-muted transition-colors">
|
||||||
|
{isTagsOpen() ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<Show when={isTagsOpen()}>
|
||||||
|
<div class="space-y-2 pt-2 animate-in fade-in slide-in-from-top-2 duration-300">
|
||||||
<For each={Object.entries(store.tagDefinitions || {}).sort((a, b) => a[0].localeCompare(b[0]))} fallback={
|
<For each={Object.entries(store.tagDefinitions || {}).sort((a, b) => a[0].localeCompare(b[0]))} fallback={
|
||||||
<div class="text-center py-8 text-muted-foreground text-sm italic border border-dashed border-border rounded-xl">
|
<div class="text-center py-8 text-muted-foreground text-sm italic border border-dashed border-border rounded-xl">
|
||||||
No specific tag definitions found.
|
No specific tag definitions found.
|
||||||
@@ -97,13 +114,13 @@ export const SettingsView: Component = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between p-3 rounded-xl bg-muted/30 border border-border/50 gap-3 sm:gap-4">
|
<div class="flex flex-col sm:flex-row sm:items-center justify-between p-3 rounded-xl bg-muted/30 border border-border/50 gap-3 sm:gap-4 group">
|
||||||
<div class="flex items-center gap-3 min-w-0 flex-1">
|
<div class="flex items-center gap-3 min-w-0 flex-1">
|
||||||
<div class={cn("w-2 h-2 rounded-full shrink-0", val > 5 ? "bg-green-500" : val < 5 ? "bg-red-500" : "bg-gray-400")} />
|
<div class={cn("w-2 h-2 rounded-full shrink-0", val > 5 ? "bg-green-500" : val < 5 ? "bg-red-500" : "bg-gray-400")} />
|
||||||
|
|
||||||
{isEditingName() ? (
|
{isEditingName() ? (
|
||||||
<input
|
<input
|
||||||
class="flex h-7 min-w-0 w-full rounded-md border border-input bg-background px-2 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
class="flex h-7 min-w-0 w-full rounded-md border border-input bg-background px-2 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||||
value={tempName()}
|
value={tempName()}
|
||||||
onInput={(e) => setTempName(e.currentTarget.value)}
|
onInput={(e) => setTempName(e.currentTarget.value)}
|
||||||
onBlur={handleRename}
|
onBlur={handleRename}
|
||||||
@@ -153,9 +170,218 @@ export const SettingsView: Component = () => {
|
|||||||
}}
|
}}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
|
</Show>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="p-4 sm:p-5 rounded-2xl border border-border bg-card shadow-sm space-y-4 min-w-0 overflow-hidden">
|
{/* Task Template Manager */}
|
||||||
|
<section class="p-4 sm:p-5 rounded-2xl border border-border bg-card shadow-sm space-y-4 overflow-hidden">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div
|
||||||
|
class="flex flex-1 items-center justify-between cursor-pointer group mr-4"
|
||||||
|
onClick={() => setIsTemplatesOpen(!isTemplatesOpen())}
|
||||||
|
>
|
||||||
|
<div class="space-y-0.5">
|
||||||
|
<h3 class="text-base font-semibold flex items-center gap-2">
|
||||||
|
<Copy size={16} />
|
||||||
|
Task Templates
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-muted-foreground">Save frequent task configurations for quick reuse.</p>
|
||||||
|
</div>
|
||||||
|
<div class="w-8 h-8 rounded-full flex items-center justify-center bg-muted/50 group-hover:bg-muted transition-colors">
|
||||||
|
{isTemplatesOpen() ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
class="h-9 gap-2 rounded-xl font-bold uppercase tracking-wider text-[10px]"
|
||||||
|
onClick={() => {
|
||||||
|
addTemplate({
|
||||||
|
name: "New Template",
|
||||||
|
title: "",
|
||||||
|
priority: 5,
|
||||||
|
urgency: 5,
|
||||||
|
tags: [],
|
||||||
|
content: ""
|
||||||
|
});
|
||||||
|
toast.success("New template created");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus size={14} />
|
||||||
|
New
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={isTemplatesOpen()}>
|
||||||
|
<div class="grid gap-3 pt-2 animate-in fade-in slide-in-from-top-2 duration-300">
|
||||||
|
<For each={store.templates || []} fallback={
|
||||||
|
<div class="text-center py-8 text-muted-foreground text-sm italic border border-dashed border-border rounded-xl">
|
||||||
|
No templates created yet.
|
||||||
|
</div>
|
||||||
|
}>
|
||||||
|
{(template) => {
|
||||||
|
const [isEditingName, setIsEditingName] = createSignal(false);
|
||||||
|
const [editName, setEditName] = createSignal(template.name);
|
||||||
|
const isExpanded = () => expandedTemplateId() === template.id;
|
||||||
|
|
||||||
|
const handleSaveName = () => {
|
||||||
|
if (editName() && editName() !== template.name) {
|
||||||
|
updateTemplate(template.id, { name: editName() });
|
||||||
|
}
|
||||||
|
setIsEditingName(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class={cn(
|
||||||
|
"flex flex-col rounded-2xl border transition-all overflow-hidden shadow-sm",
|
||||||
|
isExpanded() ? "bg-card border-primary/50 ring-1 ring-primary/20" : "bg-muted/20 border-border/40 hover:bg-muted/40"
|
||||||
|
)}>
|
||||||
|
<div class="flex items-center justify-between gap-3 p-3.5">
|
||||||
|
<div class="flex items-center gap-3 min-w-0 flex-1">
|
||||||
|
<div
|
||||||
|
class={cn(
|
||||||
|
"w-8 h-8 rounded-xl flex items-center justify-center shrink-0 transition-colors cursor-pointer",
|
||||||
|
isExpanded() ? "bg-primary text-primary-foreground" : "bg-primary/10 text-primary"
|
||||||
|
)}
|
||||||
|
onClick={() => setExpandedTemplateId(isExpanded() ? null : template.id)}
|
||||||
|
>
|
||||||
|
<Copy size={16} />
|
||||||
|
</div>
|
||||||
|
{isEditingName() ? (
|
||||||
|
<input
|
||||||
|
class="flex h-8 min-w-0 w-full rounded-lg border border-input bg-background px-3 py-1 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||||
|
value={editName()}
|
||||||
|
onInput={(e) => setEditName(e.currentTarget.value)}
|
||||||
|
onBlur={handleSaveName}
|
||||||
|
onKeyDown={(e) => e.key === "Enter" && handleSaveName()}
|
||||||
|
autofocus
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div class="min-w-0 cursor-pointer flex-1" onClick={() => setExpandedTemplateId(isExpanded() ? null : template.id)}>
|
||||||
|
<p class="font-bold text-sm tracking-tight truncate">{template.name}</p>
|
||||||
|
<p class="text-[10px] text-muted-foreground uppercase font-black tracking-widest opacity-60">
|
||||||
|
P:{template.priority} / U:{template.urgency} / {template.tags?.length || 0} Tags
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1 shrink-0">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-8 w-8 hover:bg-primary/10 hover:text-primary rounded-xl"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsEditingName(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Type size={14} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-8 w-8 hover:bg-red-500/10 hover:text-red-600 rounded-xl"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (confirm(`Delete template "${template.name}"?`)) {
|
||||||
|
removeTemplate(template.id);
|
||||||
|
toast.error("Template removed");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 size={14} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={isExpanded()}>
|
||||||
|
<div class="border-t border-border/50 p-4 space-y-4 bg-background/50 animate-in fade-in slide-in-from-top-2 duration-300">
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<label class="text-[10px] font-bold uppercase tracking-wider text-muted-foreground ml-1">Title Placeholder</label>
|
||||||
|
<input
|
||||||
|
class="flex h-9 w-full rounded-lg border border-input bg-background px-3 py-1 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||||
|
value={template.title}
|
||||||
|
onInput={(e) => updateTemplate(template.id, { title: e.currentTarget.value })}
|
||||||
|
placeholder="Default task title..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<div class="flex-1 space-y-1.5">
|
||||||
|
<label class="text-[10px] font-bold uppercase tracking-wider text-muted-foreground ml-1">Priority</label>
|
||||||
|
<select
|
||||||
|
class="flex h-9 w-full rounded-lg border border-input bg-background px-3 py-1 text-sm shadow-sm outline-none focus:ring-1 focus:ring-primary appearance-none px-2"
|
||||||
|
value={template.priority}
|
||||||
|
onChange={(e) => updateTemplate(template.id, { priority: parseInt(e.currentTarget.value) })}
|
||||||
|
>
|
||||||
|
<For each={["10", "9", "8", "7", "6", "5", "4", "3", "2", "1"]}>
|
||||||
|
{(v) => <option value={v}>{v}</option>}
|
||||||
|
</For>
|
||||||
|
</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</label>
|
||||||
|
<select
|
||||||
|
class="flex h-9 w-full rounded-lg border border-input bg-background px-3 py-1 text-sm shadow-sm outline-none focus:ring-1 focus:ring-primary appearance-none px-2"
|
||||||
|
value={template.urgency}
|
||||||
|
onChange={(e) => updateTemplate(template.id, { urgency: parseInt(e.currentTarget.value) })}
|
||||||
|
>
|
||||||
|
<For each={["10", "9", "8", "7", "6", "5", "4", "3", "2", "1"]}>
|
||||||
|
{(v) => <option value={v}>{v}</option>}
|
||||||
|
</For>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<label class="text-[10px] font-bold uppercase tracking-wider text-muted-foreground ml-1">Tags</label>
|
||||||
|
<div class="flex flex-wrap gap-2 p-2 rounded-xl bg-muted/30 border border-border/40">
|
||||||
|
<TagPicker
|
||||||
|
selectedTags={template.tags || []}
|
||||||
|
onTagsChange={(tags) => updateTemplate(template.id, { tags })}
|
||||||
|
/>
|
||||||
|
<For each={template.tags || []}>
|
||||||
|
{(tag) => (
|
||||||
|
<button
|
||||||
|
class="flex items-center gap-1.5 px-2 py-1 rounded-lg bg-background border border-border/50 text-xs hover:bg-destructive/10 hover:border-destructive/30 group/tag transition-all"
|
||||||
|
onClick={() => updateTemplate(template.id, { tags: (template.tags || []).filter(t => t !== tag) })}
|
||||||
|
>
|
||||||
|
<div class={cn("w-1.5 h-1.5 rounded-full", (store.tagDefinitions?.[tag] ?? 5) > 5 ? "bg-green-500" : (store.tagDefinitions?.[tag] ?? 5) < 5 ? "bg-red-500" : "bg-gray-400")} />
|
||||||
|
{tag}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-1.5">
|
||||||
|
<label class="text-[10px] font-bold uppercase tracking-wider text-muted-foreground ml-1">Description (Content)</label>
|
||||||
|
<div class="min-h-[120px] rounded-xl border border-border/40 bg-background/50 p-2 overflow-hidden prose-sm">
|
||||||
|
<Suspense fallback={<div class="h-20 animate-pulse bg-muted/20 rounded-lg" />}>
|
||||||
|
<TaskEditor
|
||||||
|
content={template.content}
|
||||||
|
onUpdate={(html) => updateTemplate(template.id, { content: html })}
|
||||||
|
class="min-h-[100px]"
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Trash Manager */}
|
||||||
|
<section class="p-4 sm:p-5 rounded-2xl border border-border bg-card shadow-sm space-y-4 overflow-hidden">
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between cursor-pointer group"
|
||||||
|
onClick={() => setIsTrashOpen(!isTrashOpen())}
|
||||||
|
>
|
||||||
<div class="space-y-0.5 min-w-0">
|
<div class="space-y-0.5 min-w-0">
|
||||||
<h3 class="text-base font-semibold flex items-center gap-2">
|
<h3 class="text-base font-semibold flex items-center gap-2">
|
||||||
<Trash2 size={16} />
|
<Trash2 size={16} />
|
||||||
@@ -163,8 +389,13 @@ export const SettingsView: Component = () => {
|
|||||||
</h3>
|
</h3>
|
||||||
<p class="text-sm text-muted-foreground truncate">Items are permanently deleted after 7 days.</p>
|
<p class="text-sm text-muted-foreground truncate">Items are permanently deleted after 7 days.</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="w-8 h-8 rounded-full flex items-center justify-center bg-muted/50 group-hover:bg-muted transition-colors">
|
||||||
|
{isTrashOpen() ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<Show when={isTrashOpen()}>
|
||||||
|
<div class="space-y-2 pt-2 animate-in fade-in slide-in-from-top-2 duration-300">
|
||||||
<For each={store.tasks.filter(t => t.deletedAt)} fallback={
|
<For each={store.tasks.filter(t => t.deletedAt)} fallback={
|
||||||
<div class="text-center py-8 text-muted-foreground text-sm italic border border-dashed border-border rounded-xl">
|
<div class="text-center py-8 text-muted-foreground text-sm italic border border-dashed border-border rounded-xl">
|
||||||
Trash is empty.
|
Trash is empty.
|
||||||
@@ -210,9 +441,8 @@ export const SettingsView: Component = () => {
|
|||||||
}}
|
}}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
|
</Show>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user