diff --git a/src/views/SettingsView.tsx b/src/views/SettingsView.tsx
index e60c5ac..64b1007 100644
--- a/src/views/SettingsView.tsx
+++ b/src/views/SettingsView.tsx
@@ -1,6 +1,6 @@
import { type Component, For, Show, createSignal, lazy, Suspense } from "solid-js";
import { ThemeToggle } from "../components/ThemeToggle";
-import { store, restoreTask, deleteTaskPermanently, setMatrixScaleDays, addTemplate, removeTemplate, updateTemplate, upsertTagDefinition, removeTagDefinition, renameTagDefinition, addShareRule, removeShareRule, updateShareRule, createBucket, updateBucket, deleteBucket, toggleBucketSubscription } from "@/store";
+import { store, restoreTask, deleteTaskPermanently, addTemplate, removeTemplate, updateTemplate, upsertTagDefinition, removeTagDefinition, renameTagDefinition, addShareRule, removeShareRule, updateShareRule, createBucket, updateBucket, deleteBucket, toggleBucketSubscription } from "@/store";
import { useTheme } from "@/components/ThemeProvider";
import { Trash2, Undo2, ArrowLeftRight, Hash, Copy, Plus, ChevronDown, ChevronRight, Type, Share2, Users, Tag, Upload, Box, Edit2, Archive } from "lucide-solid";
import { TagPicker } from "@/components/TagPicker";
@@ -62,286 +62,193 @@ export const SettingsView: Component = () => {
{pb.authStore.model?.email}
- { pb.authStore.clear(); location.reload(); }}
- >
- Sign Out
-
+
+
+
+
+
{ pb.authStore.clear(); location.reload(); }}
+ >
+ Sign Out
+
+
-
- {/* Appearance */}
-
-
-
-
Appearance
-
Select your preferred color scheme.
-
-
-
-
-
-
+
- {/* Sharing */}
-
- setIsSharingOpen(!isSharingOpen())}
- >
-
-
-
- Task Sharing
-
-
Share all tasks or tasks with specific tags with others.
-
-
- {isSharingOpen() ? : }
-
-
-
-
- {/* Add new share rule */}
-
-
-
setShareType('all')}
- >
-
- All Tasks
-
-
setShareType('tag')}
- >
-
- By Tag
-
+ {/* Sharing Category */}
+
+
Sharing
+
+ {/* Sharing */}
+
+ setIsSharingOpen(!isSharingOpen())}
+ >
+
+
+
+ Task Sharing
+
+
Share all tasks or tasks with specific tags with others.
-
- {/* Share Mode Selection */}
-
-
setShareMode('ADD_USER')}
- >
-
- Collaborate
-
-
setShareMode('SEND_TASK')}
- >
-
- Handoff
-
+
+ {isSharingOpen() ? : }
-
-
- {
- if (val) {
- setShareTag(val);
- const tagDef = store.tagDefinitions.find(t => t.name === val);
- const isBucket = tagDef?.isBucket || store.buckets.some(b => b.name === val);
- const isUser = tagDef?.isUser;
-
- if (isBucket) {
- setShareMode('SEND_TASK');
- } else if (isUser) {
- setShareMode('ADD_USER');
- }
- }
- }}
- options={store.tagDefinitions.map(t => t.name)}
- placeholder="Select a tag"
- itemComponent={(props: any) => {props.item.rawValue} }
- >
-
- {shareTag() || "Select a tag..."}
-
-
-
-
-
-
-
- value={allUsers().find(u => u.id === shareUserId()) || null}
- onChange={(user) => setShareUserId(user?.id || "")}
- options={availableUsers()}
- optionValue="id"
- optionTextValue="name"
- placeholder={usersLoading() ? "Loading..." : "Select user..."}
- itemComponent={(itemProps: any) => (
- {itemProps.item.rawValue.name}
- )}
- >
-
- >
- {(state) => state.selectedOption()?.name || "Select user..."}
-
-
-
-
-
{
- if (shareUserId()) {
- await addShareRule(shareType(), shareUserId(), shareType() === 'tag' ? shareTag() : undefined, shareMode());
- setShareUserId("");
- }
- }}
- >
-
- Share
-
-
-
- {shareMode() === 'ADD_USER'
- ? "User will be added to the task's shared list."
- : "Task owner will change to the recipient immediately when tagged."}
-
-
- {/* Active share rules */}
- {/* Active share rules - Outgoing */}
-
-
-
- Sharing With Others
-
-
{
- const tagDef = r.type === 'tag' ? store.tagDefinitions.find(t => t.name === r.tagName) : null;
- const isBucketTag = r.type === 'tag' && store.buckets.some(b => b.name === r.tagName);
- const isSystem = tagDef?.isUser || tagDef?.isBucket || isBucketTag;
- const isOwner = r.ownerId === pb.authStore.model?.id;
- return !isSystem && isOwner;
- })} fallback={
-
- You aren't sharing with anyone yet.
-
- }>
- {(rule) => (
-
-
-
- {rule.type === 'all' ? : }
-
-
-
To: {getUserName(rule.targetUserId)}
-
- {rule.type === 'all' ? 'All Tasks' : `Tag: ${rule.tagName}`}
- •
- updateShareRule(rule.id, {
- share_mode: rule.share_mode === 'SEND_TASK' ? 'ADD_USER' : 'SEND_TASK'
- })}
- >
- {rule.share_mode === 'SEND_TASK' ? 'HANDOFF' : 'COLLAB'}
-
-
-
-
+
+
+ {/* Add new share rule */}
+
+
removeShareRule(rule.id)}
+ variant={shareType() === 'all' ? 'default' : 'outline'}
+ size="sm"
+ class="h-8 text-xs gap-1"
+ onClick={() => setShareType('all')}
>
-
+
+ All Tasks
+
+ setShareType('tag')}
+ >
+
+ By Tag
- )}
-
-
- {/* Active share rules - Incoming */}
-
-
-
- Shared With You
-
-
{
- const tagDef = r.type === 'tag' ? store.tagDefinitions.find(t => t.name === r.tagName) : null;
- const isBucketTag = r.type === 'tag' && store.buckets.some(b => b.name === r.tagName);
- const isSystem = tagDef?.isUser || tagDef?.isBucket || isBucketTag;
- const isTarget = r.targetUserId === pb.authStore.model?.id;
- return !isSystem && isTarget;
- })} fallback={
-
- No one has shared with you yet.
-
- }>
- {(rule) => (
-
-
-
- {rule.type === 'all' ? : }
-
-
-
From: {getUserName(rule.ownerId)}
-
- {rule.type === 'all' ? 'All Tasks' : `Tag: ${rule.tagName}`}
- •
- {rule.share_mode === 'SEND_TASK' ? 'HANDOFF' : 'COLLAB'}
-
-
-
- {/* No delete button for incoming rules unless we add leave logic later */}
+ {/* Share Mode Selection */}
+
+
setShareMode('ADD_USER')}
+ >
+
+ Collaborate
+
+
setShareMode('SEND_TASK')}
+ >
+
+ Handoff
+
- )}
-
-
- {/* Active share rules - System */}
-
-
-
- System Rules (Auto-Generated)
-
-
-
{
- const tagDef = r.type === 'tag' ? store.tagDefinitions.find(t => t.name === r.tagName) : null;
- const isBucketTag = r.type === 'tag' && store.buckets.some(b => b.name === r.tagName);
- return tagDef?.isUser || tagDef?.isBucket || isBucketTag;
- })}>
- {(rule) => {
- const tagDef = store.tagDefinitions.find(t => t.name === rule.tagName);
- return (
-
+
+ {/* Active share rules */}
+ {/* Active share rules - Outgoing */}
+
+
+
+ Sharing With Others
+
+
{
+ const tagDef = r.type === 'tag' ? store.tagDefinitions.find(t => t.name === r.tagName) : null;
+ const isBucketTag = r.type === 'tag' && store.buckets.some(b => b.name === r.tagName);
+ const isSystem = tagDef?.isUser || tagDef?.isBucket || isBucketTag;
+ const isOwner = r.ownerId === pb.authStore.model?.id;
+ return !isSystem && isOwner;
+ })} fallback={
+
+ You aren't sharing with anyone yet.
+
+ }>
+ {(rule) => (
+
-
+
+ {rule.type === 'all' ? : }
+
-
- {/* If it's a user tag, show the person's name. If it's a bucket, show the bucket name (tagName). */}
- {tagDef?.isUser ? getUserName(rule.targetUserId) : rule.tagName}
- {tagDef?.isUser ? : }
-
-
- Auto-shared via {tagDef?.isUser ? 'User' : 'Bucket'} Tag
+
To: {getUserName(rule.targetUserId)}
+
+ {rule.type === 'all' ? 'All Tasks' : `Tag: ${rule.tagName}`}
•
{
+
removeShareRule(rule.id)}
+ >
+
+
+
+ )}
+
+
+
+ {/* Active share rules - Incoming */}
+
+
+
+ Shared With You
+
+
{
+ const tagDef = r.type === 'tag' ? store.tagDefinitions.find(t => t.name === r.tagName) : null;
+ const isBucketTag = r.type === 'tag' && store.buckets.some(b => b.name === r.tagName);
+ const isSystem = tagDef?.isUser || tagDef?.isBucket || isBucketTag;
+ const isTarget = r.targetUserId === pb.authStore.model?.id;
+ return !isSystem && isTarget;
+ })} fallback={
+
+ No one has shared with you yet.
+
+ }>
+ {(rule) => (
+
+
+
+ {rule.type === 'all' ? : }
+
+
+
From: {getUserName(rule.ownerId)}
+
+ {rule.type === 'all' ? 'All Tasks' : `Tag: ${rule.tagName}`}
+ •
+ {rule.share_mode === 'SEND_TASK' ? 'HANDOFF' : 'COLLAB'}
+
+
+
+ {/* No delete button for incoming rules unless we add leave logic later */}
+
+ )}
+
+
+
+ {/* Active share rules - System */}
+
+
+
+
+
+ System Rules (Auto-Generated)
+
+
+
{
+ const tagDef = r.type === 'tag' ? store.tagDefinitions.find(t => t.name === r.tagName) : null;
+ const isBucketTag = r.type === 'tag' && store.buckets.some(b => b.name === r.tagName);
+ return tagDef?.isUser || tagDef?.isBucket || isBucketTag;
+ })}>
+ {(rule) => {
+ const tagDef = store.tagDefinitions.find(t => t.name === rule.tagName);
+ return (
+
+
+
+
+
+ {/* If it's a user tag, show the person's name. If it's a bucket, show the bucket name (tagName). */}
+ {tagDef?.isUser ? getUserName(rule.targetUserId) : rule.tagName}
+ {tagDef?.isUser ? : }
+
+
+ Auto-shared via {tagDef?.isUser ? 'User' : 'Bucket'} Tag
+ •
+ updateShareRule(rule.id, {
+ share_mode: rule.share_mode === 'SEND_TASK' ? 'ADD_USER' : 'SEND_TASK'
+ })}
+ >
+ {rule.share_mode === 'SEND_TASK' ? 'HANDOFF' : 'COLLAB'}
+
+
+
+
+
+ );
+ }}
+
+
+
+
+
+
+
+
+ {/* Buckets Manager */}
+
+ setIsBucketsOpen(!isBucketsOpen())}
+ >
+
+
+
+ Shared Global Buckets
+
+
Manage public task buckets and your subscriptions.
+
+
+ {isBucketsOpen() ? : }
+
+
+
+
+
+ {/* Create Bucket */}
+
+
+ {/* List Buckets */}
+
+
+ No buckets found.
+
+ }>
+ {(bucket) => {
+ const isSubscribed = () => store.subscribedBuckets.includes(bucket.id);
+ const [isEditing, setIsEditing] = createSignal(false);
+ const [editName, setEditName] = createSignal(bucket.name);
+
+ return (
+
+
+
+
+
+ {bucket.name}
+ setIsEditing(true)} class="opacity-0 group-hover/bucket:opacity-50 hover:opacity-100 transition-opacity">
+
+
+
+ }>
+
setEditName(e.currentTarget.value)}
+ onBlur={() => {
+ if (editName() !== bucket.name) updateBucket(bucket.id, { name: editName() });
+ setIsEditing(false);
+ }}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ if (editName() !== bucket.name) updateBucket(bucket.id, { name: editName() });
+ setIsEditing(false);
+ }
+ }}
+ class="h-6 w-full max-w-[150px] rounded border border-input bg-transparent px-1 text-sm"
+ autofocus
+ />
+
+
{bucket.description || "No description"}
+
+
+
+
+
toggleBucketSubscription(bucket.id)}
+ >
+ {isSubscribed() ? (
+ <>
+
+ Pinned
+ >
+ ) : (
+ <>
+
+ Pin
+ >
+ )}
+
+
+
{
+ if (confirm(`Delete the bucket "${bucket.name}"? This cannot be undone and will affect all users.`)) {
+ deleteBucket(bucket.id);
+ }
+ }}
+ >
+
+
+
+
+ );
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Tasks Category */}
+
+
Tasks
+
+ {/* Task Template Manager */}
+
+
+
setIsTemplatesOpen(!isTemplatesOpen())}
+ >
+
+
+
+ Task Templates
+
+
Save frequent task configurations for quick reuse.
+
+
+ {isTemplatesOpen() ? : }
+
+
+
{
+ addTemplate({
+ name: "New Template",
+ title: "",
+ priority: 5,
+ urgency: 5,
+ tags: [],
+ content: ""
+ });
+ toast.success("New template created");
+ }}
+ >
+
+ New
+
+
+
+
+
+
+ No templates created yet.
+
+ }>
+ {(template) => {
+ const [isEditingName, setIsEditingName] = createSignal(false);
+ const [editName, setEditName] = createSignal(template.name);
+ const [editTitle, setEditTitle] = createSignal(template.title);
+
+ const isExpanded = () => expandedTemplateId() === template.id;
+
+ const handleSaveName = () => {
+ if (editName() && editName() !== template.name) {
+ updateTemplate(template.id, { name: editName() });
+ }
+ setIsEditingName(false);
+ };
+
+ const handleSaveTitle = () => {
+ if (editTitle() !== template.title) {
+ updateTemplate(template.id, { title: editTitle() });
+ }
+ };
+
+ return (
+
+
+
+
setExpandedTemplateId(isExpanded() ? null : template.id)}
+ >
+
+
+ {isEditingName() ? (
+
setEditName(e.currentTarget.value)}
+ onBlur={handleSaveName}
+ onKeyDown={(e) => e.key === "Enter" && handleSaveName()}
+ autofocus
+ />
+ ) : (
+
setExpandedTemplateId(isExpanded() ? null : template.id)}>
+
{template.name}
+
+ P:{template.priority} / U:{template.urgency} / {template.tags?.length || 0} Tags
+
+
+ )}
+
+
+ {
+ e.stopPropagation();
+ setIsEditingName(true);
+ }}
+ >
+
+
+ {
+ e.stopPropagation();
+ if (confirm(`Delete template "${template.name}" ? `)) {
+ removeTemplate(template.id);
+ toast.error("Template removed");
+ }
+ }}
+ >
+
+
+
+
+
+
+
+
+
+ Title Placeholder
+ setEditTitle(e.currentTarget.value)}
+ onBlur={handleSaveTitle}
+ placeholder="Default task title..."
+ />
+
+
+
+ Priority
+ updateTemplate(template.id, { priority: parseInt(e.currentTarget.value) })}
+ >
+
+ {(opt) => {opt.label} }
+
+
+
+
+ Urgency
+ updateTemplate(template.id, { urgency: parseInt(e.currentTarget.value) })}
+ >
+
+ {(opt) => {opt.label} }
+
+
+
+
+
+
+
+
Tags
+
+
updateTemplate(template.id, { tags })}
+ />
+
+ {(tag) => (
+ updateTemplate(template.id, { tags: (template.tags || []).filter(t => t !== tag) })}
+ >
+ d.name === tag)?.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}
+
+ )}
+
+
+
+
+
+
Description (Content)
+
+ }>
+ {
+ // For the rich editor, we can use a slightly debounced store update
+ // to avoid losing focus if the parent re-renders,
+ // although Tiptap usually handles its own state well.
+ // But to be safe, let's just update the store.
+ updateTemplate(template.id, { content: html });
+ }}
+ class="min-h-[100px]"
+ />
+
+
+
+
+
);
}}
-
-
-
-
+
+
-
- {/* Buckets Manager */}
-
- setIsBucketsOpen(!isBucketsOpen())}
- >
-
-
-
- Shared Global Buckets
-
-
Manage public task buckets and your subscriptions.
-
-
- {isBucketsOpen() ? : }
-
-
-
-
-
- {/* Create Bucket */}
-
-
- {/* List Buckets */}
-
-
- No buckets found.
-
- }>
- {(bucket) => {
- const isSubscribed = () => store.subscribedBuckets.includes(bucket.id);
- const [isEditing, setIsEditing] = createSignal(false);
- const [editName, setEditName] = createSignal(bucket.name);
-
- return (
-
-
-
-
-
- {bucket.name}
- setIsEditing(true)} class="opacity-0 group-hover/bucket:opacity-50 hover:opacity-100 transition-opacity">
-
-
-
- }>
-
setEditName(e.currentTarget.value)}
- onBlur={() => {
- if (editName() !== bucket.name) updateBucket(bucket.id, { name: editName() });
- setIsEditing(false);
- }}
- onKeyDown={(e) => {
- if (e.key === "Enter") {
- if (editName() !== bucket.name) updateBucket(bucket.id, { name: editName() });
- setIsEditing(false);
- }
- }}
- class="h-6 w-full max-w-[150px] rounded border border-input bg-transparent px-1 text-sm"
- autofocus
- />
-
-
{bucket.description || "No description"}
-
-
-
-
-
toggleBucketSubscription(bucket.id)}
- >
- {isSubscribed() ? (
- <>
-
- Pinned
- >
- ) : (
- <>
-
- Pin
- >
- )}
-
-
-
{
- if (confirm(`Delete the bucket "${bucket.name}"? This cannot be undone and will affect all users.`)) {
- deleteBucket(bucket.id);
- }
- }}
- >
-
-
-
-
- );
- }}
-
-
-
-
-
-
- {/* Matrix Configuration */}
-
-
-
-
-
- Matrix Horizon
-
-
Adjust the time scale for your strategy grid.
-
-
-
val && setMatrixScaleDays(parseInt(val))}
- options={["7", "14", "30", "60", "90"]}
- placeholder="Select days"
- itemComponent={(props) => {props.item.rawValue} days }
+ {/* Import Tasks */}
+
+ setIsImportOpen(!isImportOpen())}
>
-
- {store.matrixScaleDays} days
-
-
-
-
-
-
-
- {/* Global Tag Manager */}
-
- setIsTagsOpen(!isTagsOpen())}
- >
-
-
-
- Global Tags
-
-
Manage your global tag definitions and values.
-
-
- {isTagsOpen() ? : }
-
-
-
-
-
-
-
!t.isUser && !store.buckets.some(b => b.name === t.name)).sort((a, b) => a.name.localeCompare(b.name))} fallback={
-
- No custom tags found.
-
- }>
- {(tag) => {
- const [isEditingName, setIsEditingName] = createSignal(false);
- const [tempName, setTempName] = createSignal(tag.name);
-
- const handleRename = () => {
- if (tempName() && tempName() !== tag.name) {
- renameTagDefinition(tag.name, tempName());
- }
- setIsEditingName(false);
- };
-
- return (
-
-
- upsertTagDefinition(tag.name, tag.value, e.currentTarget.value, resolvedTheme())}
- class="w-4 h-4 rounded-full border-none p-0 bg-transparent cursor-pointer shrink-0 overflow-hidden"
- title="Tag accent color"
- />
-
- {isEditingName() ? (
- setTempName(e.currentTarget.value)}
- onBlur={handleRename}
- onKeyDown={(e) => e.key === "Enter" && handleRename()}
- autofocus
- />
- ) : (
- setIsEditingName(true)}
- title="Click to rename"
- >
- {tag.name}
-
- )}
-
-
-
-
- Val
- upsertTagDefinition(tag.name, parseInt(e.currentTarget.value), tag.color, resolvedTheme())}
- class="flex h-8 w-16 items-center justify-between rounded-md border border-input bg-background px-2 py-1 text-xs shadow-sm ring-offset-background focus:outline-none focus:ring-1 focus:ring-ring appearance-none"
- >
-
- {(v) => {v} }
-
-
-
-
-
{
- if (confirm(`Delete tag definition for "${tag.name}" ? `)) {
- removeTagDefinition(tag.name);
- }
- }}
- >
-
-
-
-
- );
- }}
-
+
+
+
+ Import Tasks
+
+
Paste a list of tasks from another app to import them.
+
+
+ {isImportOpen() ? : }
+
- {/* User/System Tags Expansion */}
-
-
-
-
-
- System Tags (Users & Buckets)
-
-
-
t.isUser || store.buckets.some(b => b.name === t.name)).sort((a, b) => a.name.localeCompare(b.name))}>
- {(tag) => (
-
-
- upsertTagDefinition(tag.name, tag.value, e.currentTarget.value, resolvedTheme())}
- class="w-4 h-4 rounded-full border-none p-0 bg-transparent cursor-pointer shrink-0 overflow-hidden"
- title="Tag accent color"
- />
-
- {tag.name}
-
-
-
+
+
+ }>
+
+
+
+
+
-
-
-
Val
-
upsertTagDefinition(tag.name, parseInt(e.currentTarget.value), tag.color, resolvedTheme())}
- class="flex h-7 w-14 items-center justify-between rounded-md border border-input bg-background/50 px-2 py-1 text-xs shadow-sm ring-offset-background focus:outline-none focus:ring-1 focus:ring-ring appearance-none"
+ {/* Trash Manager */}
+
+ setIsTrashOpen(!isTrashOpen())}
+ >
+
+
+
+ Trash
+
+
Items are permanently deleted after 7 days.
+
+
+ {isTrashOpen() ? : }
+
+
+
+
+
+
t.deletedAt)} fallback={
+
+ Trash is empty.
+
+ }>
+ {(task) => {
+ const daysLeft = Math.ceil(((task.deletedAt || 0) + (7 * 24 * 60 * 60 * 1000) - Date.now()) / (1000 * 60 * 60 * 24));
+ return (
+
+
+
{task.title || "Untitled"}
+
Expires in {Math.max(0, daysLeft)} days
+
+
+ {
+ restoreTask(task.id);
+ toast.success("Task restored");
+ }}
>
-
- {(v) => {v} }
-
-
+
+ Recover
+
+ {
+ if (confirm("Permanently delete this task? This cannot be undone.")) {
+ deleteTaskPermanently(task.id);
+ toast.error("Task permanently deleted");
+ }
+ }}
+ >
+
+
-
- )}
+ );
+ }}
-
-
-
-
-
- {/* Task Template Manager */}
-
-
-
setIsTemplatesOpen(!isTemplatesOpen())}
- >
-
-
-
- Task Templates
-
-
Save frequent task configurations for quick reuse.
-
-
- {isTemplatesOpen() ? : }
-
-
-
{
- addTemplate({
- name: "New Template",
- title: "",
- priority: 5,
- urgency: 5,
- tags: [],
- content: ""
- });
- toast.success("New template created");
- }}
- >
-
- New
-
+
+
+
-
-
-
- No templates created yet.
+ {/* Organization Category */}
+
+
Organization
+
+ {/* Global Tag Manager */}
+
+ setIsTagsOpen(!isTagsOpen())}
+ >
+
+
+
+ Global Tags
+
+
Manage your global tag definitions and values.
- }>
- {(template) => {
- const [isEditingName, setIsEditingName] = createSignal(false);
- const [editName, setEditName] = createSignal(template.name);
- const [editTitle, setEditTitle] = createSignal(template.title);
+
+ {isTagsOpen() ? : }
+
+
- const isExpanded = () => expandedTemplateId() === template.id;
-
- const handleSaveName = () => {
- if (editName() && editName() !== template.name) {
- updateTemplate(template.id, { name: editName() });
- }
- setIsEditingName(false);
- };
-
- const handleSaveTitle = () => {
- if (editTitle() !== template.title) {
- updateTemplate(template.id, { title: editTitle() });
- }
- };
-
- return (
-
-
-
-
setExpandedTemplateId(isExpanded() ? null : template.id)}
- >
-
-
- {isEditingName() ? (
-
setEditName(e.currentTarget.value)}
- onBlur={handleSaveName}
- onKeyDown={(e) => e.key === "Enter" && handleSaveName()}
- autofocus
- />
- ) : (
-
setExpandedTemplateId(isExpanded() ? null : template.id)}>
-
{template.name}
-
- P:{template.priority} / U:{template.urgency} / {template.tags?.length || 0} Tags
-
-
- )}
-
-
- {
- e.stopPropagation();
- setIsEditingName(true);
- }}
- >
-
-
- {
- e.stopPropagation();
- if (confirm(`Delete template "${template.name}" ? `)) {
- removeTemplate(template.id);
- toast.error("Template removed");
- }
- }}
- >
-
-
-
+
+
+
+
!t.isUser && !store.buckets.some(b => b.name === t.name)).sort((a, b) => a.name.localeCompare(b.name))} fallback={
+
+ No custom tags found.
+ }>
+ {(tag) => {
+ const [isEditingName, setIsEditingName] = createSignal(false);
+ const [tempName, setTempName] = createSignal(tag.name);
-
-
-
-
-
Title Placeholder
+ const handleRename = () => {
+ if (tempName() && tempName() !== tag.name) {
+ renameTagDefinition(tag.name, tempName());
+ }
+ setIsEditingName(false);
+ };
+
+ return (
+
+
setEditTitle(e.currentTarget.value)}
- onBlur={handleSaveTitle}
- placeholder="Default task title..."
+ type="color"
+ value={tag.color || "#6366f1"}
+ onInput={(e) => upsertTagDefinition(tag.name, tag.value, e.currentTarget.value, resolvedTheme())}
+ class="w-4 h-4 rounded-full border-none p-0 bg-transparent cursor-pointer shrink-0 overflow-hidden"
+ title="Tag accent color"
/>
-
-
-
- Priority
- updateTemplate(template.id, { priority: parseInt(e.currentTarget.value) })}
- >
-
- {(opt) => {opt.label} }
-
-
-
-
- Urgency
- updateTemplate(template.id, { urgency: parseInt(e.currentTarget.value) })}
- >
-
- {(opt) => {opt.label} }
-
-
-
-
-
-
-
Tags
-
-
updateTemplate(template.id, { tags })}
- />
-
- {(tag) => (
- updateTemplate(template.id, { tags: (template.tags || []).filter(t => t !== tag) })}
- >
- d.name === tag)?.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}
-
- )}
-
-
-
-
-
-
Description (Content)
-
- }>
- {
- // For the rich editor, we can use a slightly debounced store update
- // to avoid losing focus if the parent re-renders,
- // although Tiptap usually handles its own state well.
- // But to be safe, let's just update the store.
- updateTemplate(template.id, { content: html });
- }}
- class="min-h-[100px]"
+ {isEditingName() ? (
+ setTempName(e.currentTarget.value)}
+ onBlur={handleRename}
+ onKeyDown={(e) => e.key === "Enter" && handleRename()}
+ autofocus
/>
-
+ ) : (
+ setIsEditingName(true)}
+ title="Click to rename"
+ >
+ {tag.name}
+
+ )}
+
+
+
+
+ Val
+ upsertTagDefinition(tag.name, parseInt(e.currentTarget.value), tag.color, resolvedTheme())}
+ class="flex h-8 w-16 items-center justify-between rounded-md border border-input bg-background px-2 py-1 text-xs shadow-sm ring-offset-background focus:outline-none focus:ring-1 focus:ring-ring appearance-none"
+ >
+
+ {(v) => {v} }
+
+
+
+
+
{
+ if (confirm(`Delete tag definition for "${tag.name}" ? `)) {
+ removeTagDefinition(tag.name);
+ }
+ }}
+ >
+
+
-
-
+ );
+ }}
+
+
+
+ {/* User/System Tags Expansion */}
+
+
+
+
+
+ System Tags (Users & Buckets)
+
+
+
t.isUser || store.buckets.some(b => b.name === t.name)).sort((a, b) => a.name.localeCompare(b.name))}>
+ {(tag) => (
+
+
+ upsertTagDefinition(tag.name, tag.value, e.currentTarget.value, resolvedTheme())}
+ class="w-4 h-4 rounded-full border-none p-0 bg-transparent cursor-pointer shrink-0 overflow-hidden"
+ title="Tag accent color"
+ />
+
+ {tag.name}
+
+
+
+
+
+
+ Val
+ upsertTagDefinition(tag.name, parseInt(e.currentTarget.value), tag.color, resolvedTheme())}
+ class="flex h-7 w-14 items-center justify-between rounded-md border border-input bg-background/50 px-2 py-1 text-xs shadow-sm ring-offset-background focus:outline-none focus:ring-1 focus:ring-ring appearance-none"
+ >
+
+ {(v) => {v} }
+
+
+
+
+
+ )}
+
- );
- }}
-
-
-
-
-
- {/* Import Tasks */}
-
- setIsImportOpen(!isImportOpen())}
- >
-
-
-
- Import Tasks
-
-
Paste a list of tasks from another app to import them.
-
-
- {isImportOpen() ? : }
-
-
-
-
-
- }>
-
-
-
-
-
-
- {/* Trash Manager */}
-
- setIsTrashOpen(!isTrashOpen())}
- >
-
-
-
- Trash
-
-
Items are permanently deleted after 7 days.
-
-
- {isTrashOpen() ? : }
-
-
-
-
-
-
t.deletedAt)} fallback={
-
- Trash is empty.
+
- }>
- {(task) => {
- const daysLeft = Math.ceil(((task.deletedAt || 0) + (7 * 24 * 60 * 60 * 1000) - Date.now()) / (1000 * 60 * 60 * 24));
- return (
-
-
-
{task.title || "Untitled"}
-
Expires in {Math.max(0, daysLeft)} days
-
-
- {
- restoreTask(task.id);
- toast.success("Task restored");
- }}
- >
-
- Recover
-
- {
- if (confirm("Permanently delete this task? This cannot be undone.")) {
- deleteTaskPermanently(task.id);
- toast.error("Task permanently deleted");
- }
- }}
- >
-
-
-
-
- );
- }}
-
-
-
-
+
+
+
+
);