tag colors and tag list tracking fix

This commit is contained in:
2026-02-02 15:10:29 -06:00
parent 3212402788
commit e36ae92441
12 changed files with 548 additions and 208 deletions
+49 -10
View File
@@ -1,13 +1,16 @@
import { type Component, createSignal, For, Show } from "solid-js";
import { Search, X, Hash, ArrowUpCircle, Clock } from "lucide-solid";
import { Search, X, Hash, ArrowUpCircle, Clock, Minus } from "lucide-solid";
import { store, setFilter, clearFilter } from "@/store";
import { Button } from "./ui/button";
import { cn } from "@/lib/utils";
import { Badge } from "./ui/badge";
import { Select, SelectContent, SelectItem, SelectTrigger } from "./ui/select";
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
import { useTheme } from "./ThemeProvider";
import { getThemeAdjustedColor } from "@/lib/colors";
export const FilterBar: Component = () => {
const { resolvedTheme } = useTheme();
const [isOpen, setIsOpen] = createSignal(false);
const hasActiveFilters = () => {
@@ -15,7 +18,7 @@ export const FilterBar: Component = () => {
return f.query !== "" || f.tags.length > 0 || f.priorityMin !== 1 || f.priorityMax !== 10 || f.urgencyMin !== 1 || f.urgencyMax !== 10;
};
const allTags = () => Object.keys(store.tagDefinitions || {}).sort();
const allTags = () => store.tagDefinitions.map(d => d.name).sort();
return (
<div class="fixed top-4 right-5 sm:right-6 z-[60]">
@@ -162,27 +165,63 @@ export const FilterBar: Component = () => {
<div class="space-y-2.5">
<label class="text-[10px] font-black uppercase tracking-widest text-muted-foreground ml-1 flex items-center justify-between">
Filter by Tags
<span class="text-[9px] lowercase font-medium opacity-50 tracking-normal">Click to remove</span>
<span class="text-[9px] lowercase font-medium opacity-50 tracking-normal text-right">
Click tag name to toggle exclude<br />
Click X to remove
</span>
</label>
<div class="flex flex-wrap gap-2 min-h-[44px] p-2 bg-muted/10 border border-dashed border-border/60 rounded-xl">
<For each={store.filter.tags}>
{(tag) => (
<Badge
variant="secondary"
class="h-7 px-2.5 rounded-lg text-xs gap-2 cursor-pointer transition-all hover:bg-destructive/10 hover:text-destructive hover:border-destructive/30 border-border/40"
onClick={() => setFilter({ tags: store.filter.tags.filter(t => t !== tag) })}
class={cn(
"h-7 px-2.5 rounded-lg text-xs gap-2 transition-all border shadow-sm",
tag.excluded
? "bg-red-500/10 text-red-600 dark:text-red-400 border-red-500/20 hover:bg-red-500/20"
: "bg-indigo-500/10 text-indigo-600 dark:text-indigo-300 border-indigo-500/20 hover:bg-indigo-500/20"
)}
style={!tag.excluded ? (() => {
const def = store.tagDefinitions.find(d => d.name === tag.name);
const color = getThemeAdjustedColor(def?.color, def?.theme, resolvedTheme());
return color ? {
"background-color": `${color}15`,
"color": color,
"border-color": `${color}30`
} : {};
})() : {}}
>
<Hash size={12} class="opacity-50" />
{tag}
<X size={10} />
<div
class="flex items-center gap-2 cursor-pointer h-full"
onClick={() => {
const newTags = store.filter.tags.map(t =>
t.name === tag.name ? { ...t, excluded: !t.excluded } : t
);
setFilter({ tags: newTags });
}}
>
<Show when={tag.excluded} fallback={<Hash size={12} class="opacity-70" />}>
<Minus size={12} class="opacity-100" />
</Show>
<span class={cn(tag.excluded && "line-through opacity-70")}>{tag.name}</span>
</div>
<button
class="p-0.5 hover:bg-foreground/10 rounded transition-colors"
onClick={(e) => {
e.stopPropagation();
setFilter({ tags: store.filter.tags.filter(t => t.name !== tag.name) });
}}
>
<X size={10} />
</button>
</Badge>
)}
</For>
<Select
value=""
onChange={(val) => {
if (val && !store.filter.tags.includes(val)) {
setFilter({ tags: [...store.filter.tags, val] });
if (val && !store.filter.tags.some(t => t.name === val)) {
setFilter({ tags: [...store.filter.tags, { name: val, excluded: false }] });
}
}}
options={allTags()}
+1 -1
View File
@@ -58,7 +58,7 @@ export const Sidebar: Component<{
>
<div class={cn("p-4 flex items-center justify-between", !props.isLocked && "pl-14")}>
<h1 class={cn(
"text-xl font-bold tracking-tighter text-primary px-2 transition-opacity",
"text-xl font-bold tracking-tighter text-foreground px-2 transition-opacity",
(props.isLocked || props.isPeeking)
? "opacity-100 duration-500 delay-[50ms]"
: "opacity-0 duration-116 delay-0"
+28 -12
View File
@@ -1,12 +1,13 @@
import { type Component, createSignal, createEffect, onCleanup, onMount, For, Show } from "solid-js";
import { addTask, calculateDateFromUrgency, calculateUrgencyFromDate, store, upsertMultipleTagDefinitions, type TaskTemplate } from "@/store";
import { addTask, calculateDateFromUrgency, calculateUrgencyFromDate, store, upsertTagDefinition, type TaskTemplate } from "@/store";
import { Search, Command, Clock, ArrowUpCircle, Plus, Copy, ChevronDown } from "lucide-solid";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
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";
@@ -15,6 +16,7 @@ import { PRIORITY_OPTIONS, URGENCY_OPTIONS } from "@/lib/constants";
const TaskEditor = lazy(() => import("./TaskEditor").then(m => ({ default: m.TaskEditor })));
export const QuickEntry: Component = () => {
const { resolvedTheme } = useTheme();
// Diff tracking for reactive tags
let lastParsedTags: string[] = [];
@@ -156,8 +158,6 @@ export const QuickEntry: Component = () => {
const segments = text.split(/\/t\s*/);
let title = segments[0];
const tagsToUpsert: Record<string, number> = {};
for (let i = 1; i < segments.length; i++) {
const seg = segments[i];
const firstColon = seg.indexOf(':');
@@ -185,18 +185,13 @@ export const QuickEntry: Component = () => {
finalTags.push(tagName);
}
if (tagValue !== undefined) {
tagsToUpsert[tagName] = tagValue;
} else if (store.tagDefinitions?.[tagName] === undefined) {
tagsToUpsert[tagName] = 5;
// update global definition
await upsertTagDefinition(tagName, tagValue, undefined, resolvedTheme());
}
}
title += " " + remainingText;
}
if (Object.keys(tagsToUpsert).length > 0) {
upsertMultipleTagDefinitions(tagsToUpsert);
}
let finalDueDate: Date | null = null;
if (dateString()) {
finalDueDate = new Date(dateString());
@@ -365,9 +360,30 @@ export const QuickEntry: Component = () => {
<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={cn("w-1.5 h-1.5 rounded-full transition-colors group-hover/tag:bg-destructive", (store.tagDefinitions?.[tag] ?? 5) > 5 ? "bg-green-500" : (store.tagDefinitions?.[tag] ?? 5) < 5 ? "bg-red-500" : "bg-gray-400")} />
<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>
)}
+8 -6
View File
@@ -1,5 +1,6 @@
import { type Component, createSignal, For, createEffect } from "solid-js";
import { store, upsertTagDefinition } from "@/store";
import { useTheme } from "./ThemeProvider";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Button } from "@/components/ui/button";
import { Hash, Plus } from "lucide-solid";
@@ -10,20 +11,21 @@ interface TagPickerProps {
}
export const TagPicker: Component<TagPickerProps> = (props) => {
const { resolvedTheme } = useTheme();
const [open, setOpen] = createSignal(false);
const [selectedTagName, setSelectedTagName] = createSignal("");
const [valueScore, setValueScore] = createSignal(5);
// Filter tags for the "Name" part
const allTagNames = () => Object.keys(store.tagDefinitions || {}).sort();
const allTagNames = () => store.tagDefinitions.map(d => d.name).sort();
// When a tag name is selected, update the value score to match its current definition
createEffect(() => {
const name = selectedTagName().trim();
if (name) {
const existingVal = store.tagDefinitions?.[name];
if (existingVal !== undefined) {
setValueScore(existingVal);
const existing = store.tagDefinitions.find(d => d.name === name);
if (existing) {
setValueScore(existing.value);
}
}
});
@@ -33,7 +35,7 @@ export const TagPicker: Component<TagPickerProps> = (props) => {
if (!name) return;
// update global definition
upsertTagDefinition(name, valueScore());
upsertTagDefinition(name, valueScore(), undefined, resolvedTheme());
// Toggle in current task selection if not already there
const currentSelected = props.selectedTags;
@@ -87,7 +89,7 @@ export const TagPicker: Component<TagPickerProps> = (props) => {
<Button size="sm" class="w-full mt-1" onClick={handleApply}>
<Plus size={14} class="mr-2" />
{store.tagDefinitions?.[selectedTagName().trim()] !== undefined ? "Update & Add" : "Create & Add"}
{store.tagDefinitions.find(d => d.name === selectedTagName().trim()) ? "Update & Add" : "Create & Add"}
</Button>
</PopoverContent>
</Popover>
+29 -2
View File
@@ -4,8 +4,11 @@ import { cn } from "@/lib/utils";
import { CheckCircle2, Circle, Clock, ArrowUpCircle, Copy } from "lucide-solid";
import { Badge } from "@/components/ui/badge";
import { For } from "solid-js";
import { useTheme } from "./ThemeProvider";
import { getThemeAdjustedColor } from "@/lib/colors";
export const TaskCard: Component<{ task: Task }> = (props) => {
const { resolvedTheme } = useTheme();
const urgencyLevel = createMemo(() => calculateUrgencyFromDate(props.task.dueDate));
const urgencyColor = () => {
@@ -59,8 +62,32 @@ export const TaskCard: Component<{ task: Task }> = (props) => {
<div class="flex flex-wrap items-center gap-1 min-w-0">
<For each={props.task.tags}>
{(tag) => (
<Badge variant="secondary" class="h-4 px-1.5 text-[9px] gap-1 bg-muted/50 border-border/50 shrink-0">
<div class={cn("w-1 h-1 rounded-full", (store.tagDefinitions?.[tag] ?? 5) > 5 ? "bg-green-500" : (store.tagDefinitions?.[tag] ?? 5) < 5 ? "bg-red-500" : "bg-gray-400")} />
<Badge
variant="secondary"
class="h-4 px-1.5 text-[9px] gap-1 bg-muted/50 border-border/50 shrink-0"
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";
})()
}}
>
<div
class="w-1 h-1 rounded-full"
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>
)}
+26 -1
View File
@@ -12,6 +12,8 @@ import { store } from "@/store";
import { cn } from "@/lib/utils";
import { toast } from "solid-sonner";
import { lazy, Suspense } from "solid-js";
import { useTheme } from "./ThemeProvider";
import { getThemeAdjustedColor } from "@/lib/colors";
const TaskEditor = lazy(() => import("./TaskEditor").then(m => ({ default: m.TaskEditor })));
@@ -22,6 +24,7 @@ interface TaskDetailProps {
}
export const TaskDetail: Component<TaskDetailProps> = (props) => {
const { resolvedTheme } = useTheme();
// Local state for immediate feedback, synced with props
const [title, setTitle] = createSignal(props.task.title);
const [editorInstance, setEditorInstance] = createSignal<any>(null);
@@ -247,10 +250,32 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
<Badge
variant="secondary"
class="h-7 px-2 text-xs gap-1.5 bg-muted/30 border-border/50 hover:bg-destructive/10 hover:border-destructive/30 cursor-pointer group/tag transition-all whitespace-nowrap shrink-0"
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={() => updateTags((props.task.tags || []).filter(t => t !== tag))}
>
<div class={cn("w-1.5 h-1.5 rounded-full transition-colors group-hover/tag:bg-destructive", (store.tagDefinitions?.[tag] ?? 5) > 5 ? "bg-green-500" : (store.tagDefinitions?.[tag] ?? 5) < 5 ? "bg-red-500" : "bg-gray-400")} />
<div
class="w-1.5 h-1.5 rounded-full"
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}
<X size={12} class="opacity-0 group-hover/tag:opacity-50 transition-opacity" />
</Badge>
)}
</For>
+9 -4
View File
@@ -5,12 +5,14 @@ type Theme = "dark" | "light" | "system";
interface ThemeProviderState {
theme: () => Theme;
setTheme: (theme: Theme) => void;
resolvedTheme: () => "light" | "dark";
}
const ThemeProviderContext = createContext<ThemeProviderState>();
export const ThemeProvider: ParentComponent = (props) => {
const [theme, setTheme] = createSignal<Theme>("system");
const [resolvedTheme, setResolvedTheme] = createSignal<"light" | "dark">("light");
// Load from local storage on mount
const storedTheme = localStorage.getItem("tasgrid-theme") as Theme | null;
@@ -23,19 +25,22 @@ export const ThemeProvider: ParentComponent = (props) => {
root.classList.remove("light", "dark");
const currentTheme = theme();
let resolved: "light" | "dark";
if (currentTheme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
root.classList.add(systemTheme);
resolved = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
root.classList.add(resolved);
} else {
root.classList.add(currentTheme);
resolved = currentTheme === "dark" ? "dark" : "light";
root.classList.add(resolved);
}
setResolvedTheme(resolved);
localStorage.setItem("tasgrid-theme", currentTheme);
});
return (
<ThemeProviderContext.Provider value={{ theme, setTheme }}>
<ThemeProviderContext.Provider value={{ theme, setTheme, resolvedTheme }}>
{props.children}
</ThemeProviderContext.Provider>
);