tag colors and tag list tracking fix
This commit is contained in:
@@ -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()}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user