Compare commits
7 Commits
01348ce229
...
09240e8f89
| Author | SHA1 | Date | |
|---|---|---|---|
| 09240e8f89 | |||
| 759083ea7d | |||
| 49cbe6abb2 | |||
| b8f5db030c | |||
| 3d6e29d2c0 | |||
| 9a3bd4cca3 | |||
| e5c7666a2d |
@@ -55,7 +55,7 @@ export const FilterBar: Component = () => {
|
||||
<button
|
||||
onClick={() => setFilter({ editedToday: !store.filter.editedToday })}
|
||||
class={cn(
|
||||
"flex items-center gap-1.5 px-2 py-0.5 rounded-full text-[9px] font-black uppercase tracking-tight transition-all border",
|
||||
"flex items-center gap-1.5 px-2 py-0.5 rounded-full text-[0.5625rem] font-black uppercase tracking-tight transition-all border",
|
||||
store.filter.editedToday
|
||||
? "bg-primary/10 text-primary border-primary/20"
|
||||
: "bg-muted/50 text-muted-foreground border-transparent hover:bg-muted"
|
||||
@@ -70,7 +70,7 @@ export const FilterBar: Component = () => {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-7 px-2 text-[10px] font-bold uppercase tracking-wider text-muted-foreground hover:text-destructive"
|
||||
class="h-7 px-2 text-[0.625rem] font-bold uppercase tracking-wider text-muted-foreground hover:text-destructive"
|
||||
onClick={clearFilter}
|
||||
>
|
||||
Clear
|
||||
@@ -109,7 +109,7 @@ export const FilterBar: Component = () => {
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
{/* Priority Range */}
|
||||
<div class="space-y-2.5">
|
||||
<label class="text-[10px] font-black uppercase tracking-widest text-muted-foreground ml-1">Priority Range</label>
|
||||
<label class="text-[0.625rem] font-black uppercase tracking-widest text-muted-foreground ml-1">Priority Range</label>
|
||||
<div class="flex items-center gap-2 bg-muted/20 p-1 rounded-xl border border-border/30">
|
||||
<Select
|
||||
value={store.filter.priorityMin.toString()}
|
||||
@@ -144,7 +144,7 @@ export const FilterBar: Component = () => {
|
||||
|
||||
{/* Urgency Range */}
|
||||
<div class="space-y-2.5">
|
||||
<label class="text-[10px] font-black uppercase tracking-widest text-muted-foreground ml-1">Urgency Range</label>
|
||||
<label class="text-[0.625rem] font-black uppercase tracking-widest text-muted-foreground ml-1">Urgency Range</label>
|
||||
<div class="flex items-center gap-2 bg-muted/20 p-1 rounded-xl border border-border/30">
|
||||
<Select
|
||||
value={store.filter.urgencyMin.toString()}
|
||||
@@ -180,9 +180,9 @@ export const FilterBar: Component = () => {
|
||||
|
||||
{/* Tags Multi-select */}
|
||||
<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">
|
||||
<label class="text-[0.625rem] 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 text-right">
|
||||
<span class="text-[0.5625rem] lowercase font-medium opacity-50 tracking-normal text-right">
|
||||
Click tag name to toggle exclude<br />
|
||||
Click X to remove
|
||||
</span>
|
||||
@@ -245,7 +245,7 @@ export const FilterBar: Component = () => {
|
||||
placeholder="Add tag..."
|
||||
itemComponent={(props) => <SelectItem item={props.item}>{props.item.rawValue}</SelectItem>}
|
||||
>
|
||||
<SelectTrigger class="h-7 px-3 bg-muted/40 border-none shadow-none focus:ring-0 text-[10px] font-bold uppercase tracking-wider rounded-lg hover:bg-muted/60 transition-colors">
|
||||
<SelectTrigger class="h-7 px-3 bg-muted/40 border-none shadow-none focus:ring-0 text-[0.625rem] font-bold uppercase tracking-wider rounded-lg hover:bg-muted/60 transition-colors">
|
||||
<span>+ Tag</span>
|
||||
</SelectTrigger>
|
||||
<SelectContent />
|
||||
@@ -257,14 +257,14 @@ export const FilterBar: Component = () => {
|
||||
{/* Filter Templates Section */}
|
||||
<div class="px-4 pb-4 pt-4 border-t border-border/50 space-y-4 bg-muted/5">
|
||||
<div class="flex items-center justify-between ml-1">
|
||||
<label class="text-[10px] font-black uppercase tracking-widest text-muted-foreground flex items-center gap-2">
|
||||
<label class="text-[0.625rem] font-black uppercase tracking-widest text-muted-foreground flex items-center gap-2">
|
||||
<Bookmark size={12} class="text-primary" />
|
||||
Saved Filters
|
||||
</label>
|
||||
<Show when={!isSaving() && hasActiveFilters()}>
|
||||
<button
|
||||
onClick={() => setIsSaving(true)}
|
||||
class="text-[9px] font-bold uppercase tracking-wider text-primary hover:underline flex items-center gap-1"
|
||||
class="text-[0.5625rem] font-bold uppercase tracking-wider text-primary hover:underline flex items-center gap-1"
|
||||
>
|
||||
<Save size={10} />
|
||||
Save Current
|
||||
@@ -318,7 +318,7 @@ export const FilterBar: Component = () => {
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
<For each={store.filterTemplates} fallback={
|
||||
<div class="col-span-2 py-4 text-center border border-dashed border-border/40 rounded-xl bg-muted/5">
|
||||
<p class="text-[10px] text-muted-foreground uppercase tracking-widest">No saved filters yet</p>
|
||||
<p class="text-[0.625rem] text-muted-foreground uppercase tracking-widest">No saved filters yet</p>
|
||||
</div>
|
||||
}>
|
||||
{(template) => (
|
||||
@@ -328,7 +328,7 @@ export const FilterBar: Component = () => {
|
||||
onClick={() => applyFilterTemplate(template.id)}
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="text-[11px] font-bold text-foreground truncate">{template.name}</p>
|
||||
<p class="text-[0.6875rem] font-bold text-foreground truncate">{template.name}</p>
|
||||
<div class="flex gap-1 opacity-60 shrink-0">
|
||||
<Show when={template.filter.query}>
|
||||
<div class="w-1 h-1 rounded-full bg-primary" />
|
||||
|
||||
@@ -137,7 +137,7 @@ export const ImportTool: Component = () => {
|
||||
<Show when={step() === "paste"}>
|
||||
<div class="space-y-3 animate-in fade-in duration-300">
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-[10px] font-bold uppercase tracking-wider text-muted-foreground ml-1">
|
||||
<label class="text-[0.625rem] font-bold uppercase tracking-wider text-muted-foreground ml-1">
|
||||
Paste your task list
|
||||
</label>
|
||||
<textarea
|
||||
@@ -154,7 +154,7 @@ export const ImportTool: Component = () => {
|
||||
</p>
|
||||
<Button
|
||||
size="sm"
|
||||
class="gap-2 rounded-xl font-bold uppercase tracking-wider text-[10px] h-9 px-4"
|
||||
class="gap-2 rounded-xl font-bold uppercase tracking-wider text-[0.625rem] h-9 px-4"
|
||||
disabled={!rawText().trim()}
|
||||
onClick={parseText}
|
||||
>
|
||||
@@ -170,13 +170,13 @@ export const ImportTool: Component = () => {
|
||||
{/* Bulk defaults */}
|
||||
<div class="p-3 rounded-xl bg-muted/30 border border-border/50 space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">
|
||||
<p class="text-[0.625rem] font-bold uppercase tracking-wider text-muted-foreground">
|
||||
Bulk Defaults
|
||||
</p>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-7 text-[10px] font-bold uppercase tracking-wider rounded-lg"
|
||||
class="h-7 text-[0.625rem] font-bold uppercase tracking-wider rounded-lg"
|
||||
onClick={applyBulkDefaults}
|
||||
>
|
||||
Apply to All
|
||||
@@ -185,7 +185,7 @@ export const ImportTool: Component = () => {
|
||||
|
||||
<div class="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
||||
<div class="space-y-1">
|
||||
<label class="text-[9px] font-bold uppercase tracking-wider text-muted-foreground/70 ml-0.5">Priority</label>
|
||||
<label class="text-[0.5625rem] font-bold uppercase tracking-wider text-muted-foreground/70 ml-0.5">Priority</label>
|
||||
<select
|
||||
value={bulkPriority()}
|
||||
onChange={(e) => setBulkPriority(e.currentTarget.value)}
|
||||
@@ -197,7 +197,7 @@ export const ImportTool: Component = () => {
|
||||
</select>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="text-[9px] font-bold uppercase tracking-wider text-muted-foreground/70 ml-0.5">Urgency</label>
|
||||
<label class="text-[0.5625rem] font-bold uppercase tracking-wider text-muted-foreground/70 ml-0.5">Urgency</label>
|
||||
<select
|
||||
value={bulkUrgency()}
|
||||
onChange={(e) => setBulkUrgency(e.currentTarget.value)}
|
||||
@@ -209,7 +209,7 @@ export const ImportTool: Component = () => {
|
||||
</select>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="text-[9px] font-bold uppercase tracking-wider text-muted-foreground/70 ml-0.5">Size</label>
|
||||
<label class="text-[0.5625rem] font-bold uppercase tracking-wider text-muted-foreground/70 ml-0.5">Size</label>
|
||||
<select
|
||||
value={bulkSize()}
|
||||
onChange={(e) => setBulkSize(e.currentTarget.value)}
|
||||
@@ -221,14 +221,14 @@ export const ImportTool: Component = () => {
|
||||
</select>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<label class="text-[9px] font-bold uppercase tracking-wider text-muted-foreground/70 ml-0.5">Tags</label>
|
||||
<label class="text-[0.5625rem] font-bold uppercase tracking-wider text-muted-foreground/70 ml-0.5">Tags</label>
|
||||
<div class="flex items-center gap-1 flex-wrap">
|
||||
<TagPicker selectedTags={bulkTags()} onTagsChange={setBulkTags} />
|
||||
<For each={bulkTags()}>
|
||||
{(tag) => (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
class="h-6 px-1.5 text-[10px] gap-1 cursor-pointer hover:bg-destructive/10"
|
||||
class="h-6 px-1.5 text-[0.625rem] gap-1 cursor-pointer hover:bg-destructive/10"
|
||||
onClick={() => setBulkTags(bulkTags().filter(t => t !== tag))}
|
||||
>
|
||||
{tag}
|
||||
@@ -250,7 +250,7 @@ export const ImportTool: Component = () => {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-7 text-[10px] font-bold uppercase tracking-wider rounded-lg text-muted-foreground"
|
||||
class="h-7 text-[0.625rem] font-bold uppercase tracking-wider rounded-lg text-muted-foreground"
|
||||
onClick={reset}
|
||||
>
|
||||
← Back
|
||||
@@ -284,7 +284,7 @@ export const ImportTool: Component = () => {
|
||||
<select
|
||||
value={task.priority}
|
||||
onChange={(e) => updateTask(task.id, "priority", e.currentTarget.value)}
|
||||
class="h-7 rounded-lg border border-input bg-background px-1.5 text-[11px] shadow-sm outline-none focus:ring-1 focus:ring-ring appearance-none"
|
||||
class="h-7 rounded-lg border border-input bg-background px-1.5 text-[0.6875rem] shadow-sm outline-none focus:ring-1 focus:ring-ring appearance-none"
|
||||
title="Priority"
|
||||
>
|
||||
<For each={PRIORITY_OPTIONS}>
|
||||
@@ -295,7 +295,7 @@ export const ImportTool: Component = () => {
|
||||
<select
|
||||
value={task.urgency}
|
||||
onChange={(e) => updateTask(task.id, "urgency", e.currentTarget.value)}
|
||||
class="h-7 rounded-lg border border-input bg-background px-1.5 text-[11px] shadow-sm outline-none focus:ring-1 focus:ring-ring appearance-none"
|
||||
class="h-7 rounded-lg border border-input bg-background px-1.5 text-[0.6875rem] shadow-sm outline-none focus:ring-1 focus:ring-ring appearance-none"
|
||||
title="Urgency"
|
||||
>
|
||||
<For each={URGENCY_OPTIONS}>
|
||||
@@ -306,7 +306,7 @@ export const ImportTool: Component = () => {
|
||||
<select
|
||||
value={task.size}
|
||||
onChange={(e) => updateTask(task.id, "size", e.currentTarget.value)}
|
||||
class="h-7 rounded-lg border border-input bg-background px-1.5 text-[11px] shadow-sm outline-none focus:ring-1 focus:ring-ring appearance-none"
|
||||
class="h-7 rounded-lg border border-input bg-background px-1.5 text-[0.6875rem] shadow-sm outline-none focus:ring-1 focus:ring-ring appearance-none"
|
||||
title="Size"
|
||||
>
|
||||
<For each={SIZE_OPTIONS}>
|
||||
@@ -323,7 +323,7 @@ export const ImportTool: Component = () => {
|
||||
{(tag) => (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
class="h-6 px-1.5 text-[10px] gap-0.5 cursor-pointer hover:bg-destructive/10"
|
||||
class="h-6 px-1.5 text-[0.625rem] gap-0.5 cursor-pointer hover:bg-destructive/10"
|
||||
onClick={() => updateTask(task.id, "tags", task.tags.filter(t => t !== tag))}
|
||||
>
|
||||
{tag}
|
||||
@@ -352,7 +352,7 @@ export const ImportTool: Component = () => {
|
||||
</Show>
|
||||
<Button
|
||||
size="sm"
|
||||
class="gap-2 rounded-xl font-bold uppercase tracking-wider text-[10px] h-9 px-5"
|
||||
class="gap-2 rounded-xl font-bold uppercase tracking-wider text-[0.625rem] h-9 px-5"
|
||||
disabled={tasks().length === 0 || importing()}
|
||||
onClick={handleImport}
|
||||
>
|
||||
|
||||
@@ -68,7 +68,7 @@ export const BottomNav: Component<{ currentView: string; setView: (v: string) =>
|
||||
)}
|
||||
>
|
||||
<group.icon size={20} />
|
||||
<span class="text-[10px] font-medium uppercase tracking-wider">{group.label}</span>
|
||||
<span class="text-[0.625rem] font-medium uppercase tracking-wider">{group.label}</span>
|
||||
</button>
|
||||
}>
|
||||
<Popover>
|
||||
@@ -82,7 +82,7 @@ export const BottomNav: Component<{ currentView: string; setView: (v: string) =>
|
||||
<group.icon size={20} />
|
||||
<ChevronDown size={10} class="absolute -right-2 -bottom-0.5 opacity-60" />
|
||||
</div>
|
||||
<span class="text-[10px] font-medium uppercase tracking-wider">{group.label}</span>
|
||||
<span class="text-[0.625rem] font-medium uppercase tracking-wider">{group.label}</span>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-40 p-1 bg-card border-border shadow-xl mb-2">
|
||||
<div class="space-y-0.5">
|
||||
@@ -233,9 +233,9 @@ export const ContextSwitcher: Component<{
|
||||
<div class="py-1">
|
||||
<div class="h-px bg-border/50" />
|
||||
</div>
|
||||
<div class="px-2 py-1 text-[10px] font-bold uppercase text-muted-foreground tracking-wider flex items-center gap-2">
|
||||
<div class="px-2 py-1 text-[0.625rem] font-bold uppercase text-muted-foreground tracking-wider flex items-center gap-2">
|
||||
<span>Buckets</span>
|
||||
<span class="ml-auto bg-muted px-1.5 rounded text-[9px]">{store.subscribedBuckets.length}</span>
|
||||
<span class="ml-auto bg-muted px-1.5 rounded text-[0.5625rem]">{store.subscribedBuckets.length}</span>
|
||||
</div>
|
||||
<For each={store.buckets.filter(b => store.subscribedBuckets.includes(b.id))}>
|
||||
{(bucket) => (
|
||||
@@ -262,9 +262,9 @@ export const ContextSwitcher: Component<{
|
||||
<div class="py-1">
|
||||
<div class="h-px bg-border/50" />
|
||||
</div>
|
||||
<div class="px-2 py-1 text-[10px] font-bold uppercase text-muted-foreground tracking-wider flex items-center gap-2">
|
||||
<div class="px-2 py-1 text-[0.625rem] font-bold uppercase text-muted-foreground tracking-wider flex items-center gap-2">
|
||||
<span>Oversight</span>
|
||||
<span class="ml-auto bg-muted px-1.5 rounded text-[9px]">{oversightUsers()?.length}</span>
|
||||
<span class="ml-auto bg-muted px-1.5 rounded text-[0.5625rem]">{oversightUsers()?.length}</span>
|
||||
</div>
|
||||
<For each={oversightUsers()}>
|
||||
{(user) => (
|
||||
@@ -339,15 +339,7 @@ export const Sidebar: Component<{
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
{/* Context Switcher - Only visible if there are oversight contexts */}
|
||||
<div class={cn("px-3 mb-2", !props.isLocked && !props.isPeeking && "hidden")}>
|
||||
<ContextSwitcher
|
||||
isLocked={props.isLocked}
|
||||
isPeeking={props.isPeeking}
|
||||
setIsPeeking={props.setIsPeeking}
|
||||
onOpenChange={setSwitcherOpen}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<nav class="flex-1 px-3 space-y-1 mt-2">
|
||||
<For each={desktopNavItems}>
|
||||
@@ -367,6 +359,20 @@ export const Sidebar: Component<{
|
||||
)}
|
||||
</For>
|
||||
</nav>
|
||||
|
||||
{/* Context Switcher - Only visible if there are oversight contexts */}
|
||||
<div class={cn(
|
||||
"px-3 mb-2 transition-opacity duration-200",
|
||||
(!props.isLocked && !props.isPeeking) ? "opacity-0 pointer-events-none" : "opacity-100 delay-100"
|
||||
)}>
|
||||
<ContextSwitcher
|
||||
isLocked={props.isLocked}
|
||||
isPeeking={props.isPeeking}
|
||||
setIsPeeking={props.setIsPeeking}
|
||||
onOpenChange={setSwitcherOpen}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="p-3 border-t border-border mt-auto">
|
||||
<button
|
||||
onClick={() => props.setView("settings")}
|
||||
|
||||
@@ -290,7 +290,7 @@ export const QuickEntry: Component = () => {
|
||||
{/* 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-[10px] font-bold uppercase tracking-wider text-muted-foreground ml-1">
|
||||
<label class="text-[0.625rem] font-bold uppercase tracking-wider text-muted-foreground ml-1">
|
||||
Priority <span class="text-destructive">*</span>
|
||||
</label>
|
||||
<Select<any>
|
||||
@@ -322,7 +322,7 @@ export const QuickEntry: Component = () => {
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-w-0 md:min-w-[140px] space-y-1.5">
|
||||
<label class="text-[10px] font-bold uppercase tracking-wider text-muted-foreground ml-1">Size</label>
|
||||
<label class="text-[0.625rem] font-bold uppercase tracking-wider text-muted-foreground ml-1">Size</label>
|
||||
<Select<any>
|
||||
value={size()}
|
||||
onChange={(val) => {
|
||||
@@ -352,7 +352,7 @@ export const QuickEntry: Component = () => {
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-w-0 md:min-w-[140px] space-y-1.5">
|
||||
<label class="text-[10px] font-bold uppercase tracking-wider text-muted-foreground ml-1">
|
||||
<label class="text-[0.625rem] font-bold uppercase tracking-wider text-muted-foreground ml-1">
|
||||
Urgency <span class="text-destructive">*</span>
|
||||
</label>
|
||||
<Select<any>
|
||||
@@ -421,7 +421,7 @@ export const QuickEntry: Component = () => {
|
||||
</div>
|
||||
|
||||
<div class="w-full md:w-auto md:min-w-[200px] space-y-1.5">
|
||||
<label class="text-[10px] font-bold uppercase tracking-wider text-muted-foreground ml-1">Due Date</label>
|
||||
<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"
|
||||
@@ -453,7 +453,7 @@ export const QuickEntry: Component = () => {
|
||||
</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-[10px] font-bold text-muted-foreground uppercase tracking-widest">
|
||||
<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>
|
||||
@@ -469,7 +469,7 @@ export const QuickEntry: Component = () => {
|
||||
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>
|
||||
<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">
|
||||
@@ -485,7 +485,7 @@ export const QuickEntry: Component = () => {
|
||||
</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 class="text-[0.5625rem] uppercase font-black text-muted-foreground opacity-60 tracking-widest">
|
||||
P:{template.priority} / U:{template.urgency}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -150,7 +150,7 @@ export const SlashMenu: Component<{
|
||||
</div>
|
||||
<div class="flex flex-col min-w-0 overflow-hidden">
|
||||
<span class="text-sm font-medium leading-none">{item.title}</span>
|
||||
<span class="text-[11px] text-muted-foreground truncate mt-1">{item.description}</span>
|
||||
<span class="text-[0.6875rem] text-muted-foreground truncate mt-1">{item.description}</span>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { type Component } from "solid-js";
|
||||
import { CheckCircle2 } from "lucide-solid";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface StatusCircleProps {
|
||||
status: number;
|
||||
size?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const StatusCircle: Component<StatusCircleProps> = (props) => {
|
||||
const size = () => props.size || 24;
|
||||
const radius = () => (size() / 2) - 2; // Subtract stroke width/padding
|
||||
const circumference = () => 2 * Math.PI * radius();
|
||||
|
||||
// Status 0: Empty Circle
|
||||
// Status 10: CheckCircle2 (Green)
|
||||
// Status 1-9: Progress Ring
|
||||
|
||||
const strokeDashoffset = () => {
|
||||
// Invert to fill clockwise?
|
||||
// Progress 1-9.
|
||||
// 1 = 10%, 9 = 90%
|
||||
const percentage = Math.max(0, Math.min(10, props.status)) / 10;
|
||||
return circumference() * (1 - percentage);
|
||||
};
|
||||
|
||||
return (
|
||||
<div class={cn("relative flex items-center justify-center", props.className)} style={{ width: `${size()}px`, height: `${size()}px` }}>
|
||||
{props.status >= 10 ? (
|
||||
<CheckCircle2 size={size()} class="text-green-500" />
|
||||
) : (
|
||||
// Progress Circle
|
||||
<div class="relative group">
|
||||
{/* Background Circle (Ghost) */}
|
||||
<svg width={size()} height={size()} class="rotate-[-90deg]">
|
||||
<circle
|
||||
cx={size() / 2}
|
||||
cy={size() / 2}
|
||||
r={radius()}
|
||||
fill="transparent"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
class="text-muted-foreground/20"
|
||||
/>
|
||||
{/* Progress Arc */}
|
||||
<circle
|
||||
cx={size() / 2}
|
||||
cy={size() / 2}
|
||||
r={radius()}
|
||||
fill="transparent"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-dasharray={circumference().toString()}
|
||||
stroke-dashoffset={strokeDashoffset()}
|
||||
stroke-linecap="round"
|
||||
class="text-primary transition-all duration-300"
|
||||
/>
|
||||
</svg>
|
||||
{/* Hover indicator (maybe show check on hover?)
|
||||
User didn't ask for generic hover, just visual update.
|
||||
*/}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -63,7 +63,7 @@ export const TagPicker: Component<TagPickerProps> = (props) => {
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="p-3 w-[min(calc(100vw-2rem),280px)] flex flex-col gap-3" align="start">
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">Tag Name</label>
|
||||
<label class="text-[0.625rem] font-bold uppercase tracking-wider text-muted-foreground">Tag Name</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
list="existing-tags"
|
||||
@@ -81,7 +81,7 @@ export const TagPicker: Component<TagPickerProps> = (props) => {
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">Value (Sorting Influence)</label>
|
||||
<label class="text-[0.625rem] font-bold uppercase tracking-wider text-muted-foreground">Value (Sorting Influence)</label>
|
||||
<select
|
||||
value={valueScore().toString()}
|
||||
onInput={(e) => setValueScore(parseInt(e.currentTarget.value))}
|
||||
|
||||
+41
-19
@@ -1,11 +1,13 @@
|
||||
import { type Component, createMemo } from "solid-js";
|
||||
import { type Task, toggleTask, calculateUrgencyFromDate, setActiveTaskId, store, copyTask } from "@/store";
|
||||
import { type Component, createMemo, For } from "solid-js";
|
||||
import { type Task, calculateUrgencyFromDate, setActiveTaskId, store, copyTask, updateTask } from "@/store";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CheckCircle2, Circle, Clock, ArrowUpCircle, Copy, Users2 } from "lucide-solid";
|
||||
import { Clock, ArrowUpCircle, Copy, Users2 } from "lucide-solid";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { For } from "solid-js";
|
||||
import { useTheme } from "./ThemeProvider";
|
||||
import { getThemeAdjustedColor } from "@/lib/colors";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select";
|
||||
import { STATUS_OPTIONS } from "@/lib/constants";
|
||||
import { StatusCircle } from "./StatusCircle";
|
||||
|
||||
export const TaskCard: Component<{ task: Task }> = (props) => {
|
||||
const { resolvedTheme } = useTheme();
|
||||
@@ -32,16 +34,36 @@ export const TaskCard: Component<{ task: Task }> = (props) => {
|
||||
)}
|
||||
onClick={() => setActiveTaskId(props.task.id)}
|
||||
>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); toggleTask(props.task.id); }}
|
||||
class="mr-3 sm:mr-4 text-muted-foreground hover:text-primary transition-colors shrink-0"
|
||||
>
|
||||
{props.task.completed ? (
|
||||
<CheckCircle2 class="text-green-500" size={22} />
|
||||
) : (
|
||||
<Circle size={22} />
|
||||
)}
|
||||
</button>
|
||||
{/* Status Dropdown */}
|
||||
<div class="mr-2 sm:mr-3 shrink-0" onClick={(e) => e.stopPropagation()}>
|
||||
<Select<any>
|
||||
value={(props.task.status ?? 0).toString()}
|
||||
onChange={(val: any) => {
|
||||
const value = typeof val === 'object' ? val?.value : val;
|
||||
if (value) {
|
||||
const s = parseInt(value);
|
||||
if (!isNaN(s)) updateTask(props.task.id, { status: s });
|
||||
}
|
||||
}}
|
||||
options={STATUS_OPTIONS}
|
||||
optionValue="value"
|
||||
optionTextValue="label"
|
||||
placeholder="Status"
|
||||
itemComponent={(props: any) => <SelectItem item={props.item}>{props.item.rawValue.label}</SelectItem>}
|
||||
>
|
||||
<SelectTrigger
|
||||
class="h-9 w-9 p-0 bg-transparent border-transparent hover:bg-muted/50 data-[expanded]:bg-muted/50 shadow-none focus:ring-0 shrink-0 overflow-hidden flex items-center justify-center [&>svg]:hidden"
|
||||
onDoubleClick={(e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
updateTask(props.task.id, { status: 10 });
|
||||
}}
|
||||
>
|
||||
<StatusCircle status={props.task.status ?? 0} size={26} />
|
||||
</SelectTrigger>
|
||||
<SelectContent />
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-w-0 flex flex-col">
|
||||
<h3 class={cn(
|
||||
@@ -51,7 +73,7 @@ export const TaskCard: Component<{ task: Task }> = (props) => {
|
||||
{props.task.title}
|
||||
</h3>
|
||||
{props.task.content && (
|
||||
<div class="text-[11px] sm:text-xs text-muted-foreground/60 line-clamp-1 mt-1 leading-tight whitespace-pre">
|
||||
<div class="text-[0.6875rem] sm:text-xs text-muted-foreground/60 line-clamp-1 mt-1 leading-tight whitespace-pre">
|
||||
{(() => {
|
||||
let html = props.task.content || "";
|
||||
// Replace block tags with newlines first to preserve structure
|
||||
@@ -75,7 +97,7 @@ export const TaskCard: Component<{ task: Task }> = (props) => {
|
||||
{/* Priority */}
|
||||
<div class="flex items-center gap-1.5 shrink-0">
|
||||
<ArrowUpCircle size={12} class="text-primary opacity-60" />
|
||||
<span class="text-[10px] font-medium">P{props.task.priority}</span>
|
||||
<span class="text-[0.625rem] font-medium">P{props.task.priority}</span>
|
||||
</div>
|
||||
|
||||
{/* Shared Indicator - shows for individual shares OR tag-based ShareRules */}
|
||||
@@ -110,7 +132,7 @@ export const TaskCard: Component<{ task: Task }> = (props) => {
|
||||
{(tag) => (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
class="h-4 px-1.5 text-[9px] gap-1 bg-muted/50 border-border/50 shrink-0"
|
||||
class="h-4 px-1.5 text-[0.5625rem] gap-1 bg-muted/50 border-border/50 shrink-0"
|
||||
style={{
|
||||
"background-color": (() => {
|
||||
const def = store.tagDefinitions.find(d => d.name === tag);
|
||||
@@ -144,7 +166,7 @@ export const TaskCard: Component<{ task: Task }> = (props) => {
|
||||
{/* Due Date + Urgency Slide-out */}
|
||||
<div class="relative group/date flex items-center h-5 cursor-help shrink-0 min-w-0">
|
||||
{/* Due Date - Static Layer on Top */}
|
||||
<div class="flex items-center space-x-1 text-[9px] font-bold text-muted-foreground uppercase tracking-wider bg-card relative z-20 pr-1">
|
||||
<div class="flex items-center space-x-1 text-[0.5625rem] font-bold text-muted-foreground uppercase tracking-wider bg-card relative z-20 pr-1">
|
||||
<Clock size={11} class="text-primary/60" />
|
||||
<span class="truncate">{formattedDate()}</span>
|
||||
</div>
|
||||
@@ -154,7 +176,7 @@ export const TaskCard: Component<{ task: Task }> = (props) => {
|
||||
"absolute left-4 top-0 bottom-0 flex items-center transition-all duration-300 ease-out z-10 opacity-0 group-hover/date:opacity-100 group-hover/date:left-full whitespace-nowrap",
|
||||
urgencyColor()
|
||||
)}>
|
||||
<span class="text-[9px] font-bold ml-2 transition-all duration-300 transform translate-x-[-8px] group-hover/date:translate-x-0 group-hover/date:blur-none blur-[2px]">
|
||||
<span class="text-[0.5625rem] font-bold ml-2 transition-all duration-300 transform translate-x-[-8px] group-hover/date:translate-x-0 group-hover/date:blur-none blur-[2px]">
|
||||
Urgency {urgencyLevel()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -4,11 +4,12 @@ import { type Task, removeTask, restoreTask, updateTask, saveTaskAsTemplate, sha
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { ArrowUpCircle, Clock, Calendar, Type, Trash2, X, Copy, MoreHorizontal, Gauge, Share2, UserMinus, GitBranch, Tag, Settings } from "lucide-solid";
|
||||
import { StatusCircle } from "./StatusCircle";
|
||||
import { calculateDateFromUrgency, calculateUrgencyFromDate } from "@/store";
|
||||
import { Button } from "./ui/button";
|
||||
import { TagPicker } from "./TagPicker";
|
||||
import { Badge } from "./ui/badge";
|
||||
import { PRIORITY_OPTIONS, URGENCY_OPTIONS, SIZE_OPTIONS } from "@/lib/constants";
|
||||
import { PRIORITY_OPTIONS, URGENCY_OPTIONS, SIZE_OPTIONS, STATUS_OPTIONS } from "@/lib/constants";
|
||||
import { store } from "@/store";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { toast } from "solid-sonner";
|
||||
@@ -72,6 +73,14 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const updateStatus = (val: any) => {
|
||||
const value = typeof val === 'object' ? val.value : val;
|
||||
const s = parseInt(value);
|
||||
if (!isNaN(s)) {
|
||||
updateTask(props.task.id, { status: s });
|
||||
}
|
||||
};
|
||||
|
||||
const updateUrgency = (val: any) => {
|
||||
const value = typeof val === 'object' ? val.value : val;
|
||||
const u = parseInt(value);
|
||||
@@ -119,32 +128,60 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
||||
>
|
||||
{/* Header Area */}
|
||||
<div class="px-6 pt-6 pb-2 shrink-0">
|
||||
<textarea
|
||||
id="task-detail-title"
|
||||
name="task-title"
|
||||
value={title()}
|
||||
onInput={(e) => {
|
||||
updateTitle(e.currentTarget.value);
|
||||
// Auto-resize
|
||||
e.currentTarget.style.height = 'auto';
|
||||
e.currentTarget.style.height = e.currentTarget.scrollHeight + 'px';
|
||||
}}
|
||||
ref={(el) => {
|
||||
setTimeout(() => {
|
||||
el.style.height = 'auto';
|
||||
el.style.height = el.scrollHeight + 'px';
|
||||
}, 0);
|
||||
}}
|
||||
rows={1}
|
||||
class="text-xl sm:text-2xl font-bold bg-transparent border-none focus:outline-none focus:ring-0 w-full placeholder:text-muted-foreground/50 resize-none overflow-hidden leading-tight"
|
||||
placeholder="Untitled Task"
|
||||
/>
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="shrink-0 mt-0.5">
|
||||
<Select<any>
|
||||
value={(props.task.status ?? 0).toString()}
|
||||
onChange={(val: string | null) => val && updateStatus(val)}
|
||||
options={STATUS_OPTIONS}
|
||||
optionValue="value"
|
||||
optionTextValue="label"
|
||||
placeholder="Status"
|
||||
itemComponent={(props: any) => <SelectItem item={props.item}>{props.item.rawValue.label}</SelectItem>}
|
||||
>
|
||||
<SelectTrigger
|
||||
class="h-9 w-9 p-0 bg-transparent border-transparent hover:bg-muted/50 data-[expanded]:bg-muted/50 px-0 shadow-none focus:ring-0 shrink-0 overflow-hidden flex items-center justify-center [&>svg]:hidden"
|
||||
onDoubleClick={(e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
updateStatus("10");
|
||||
}}
|
||||
>
|
||||
<StatusCircle status={props.task.status ?? 0} size={28} />
|
||||
</SelectTrigger>
|
||||
<SelectContent />
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
id="task-detail-title"
|
||||
name="task-title"
|
||||
value={title()}
|
||||
onInput={(e) => {
|
||||
updateTitle(e.currentTarget.value);
|
||||
// Auto-resize
|
||||
e.currentTarget.style.height = 'auto';
|
||||
e.currentTarget.style.height = e.currentTarget.scrollHeight + 'px';
|
||||
}}
|
||||
ref={(el) => {
|
||||
setTimeout(() => {
|
||||
el.style.height = 'auto';
|
||||
el.style.height = el.scrollHeight + 'px';
|
||||
}, 0);
|
||||
}}
|
||||
rows={1}
|
||||
class="text-xl sm:text-2xl font-bold bg-transparent border-none focus:outline-none focus:ring-0 w-full placeholder:text-muted-foreground/50 resize-none overflow-hidden leading-tight flex-1 min-w-0"
|
||||
placeholder="Untitled Task"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Properties Bar */}
|
||||
<div class="flex flex-wrap items-center gap-2 sm:gap-4 mt-2 text-sm">
|
||||
|
||||
|
||||
{/* Priority */}
|
||||
<div class="flex items-center gap-1 group shrink-0">
|
||||
<span class="text-[10px] uppercase font-bold tracking-wider text-muted-foreground/70 hidden sm:inline">Priority</span>
|
||||
<span class="text-[0.625rem] uppercase font-bold tracking-wider text-muted-foreground/70 hidden sm:inline">Priority</span>
|
||||
<Select<any>
|
||||
value={props.task.priority.toString()}
|
||||
onChange={(val: string | null) => val && updatePriority(val)}
|
||||
@@ -168,7 +205,7 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
||||
|
||||
{/* Urgency */}
|
||||
<div class="flex items-center gap-1 group shrink-0">
|
||||
<span class="text-[10px] uppercase font-bold tracking-wider text-muted-foreground/70 hidden sm:inline">Time</span>
|
||||
<span class="text-[0.625rem] uppercase font-bold tracking-wider text-muted-foreground/70 hidden sm:inline">Time</span>
|
||||
<Select<any>
|
||||
value={currentUrgency()}
|
||||
onChange={(val: string | null) => val && updateUrgency(val)}
|
||||
@@ -192,7 +229,7 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
||||
|
||||
{/* Size */}
|
||||
<div class="flex items-center gap-1 group shrink-0">
|
||||
<span class="text-[10px] uppercase font-bold tracking-wider text-muted-foreground/70 hidden sm:inline">Size</span>
|
||||
<span class="text-[0.625rem] uppercase font-bold tracking-wider text-muted-foreground/70 hidden sm:inline">Size</span>
|
||||
<Select<any>
|
||||
value={(props.task.size ?? 5).toString()}
|
||||
onChange={(val: any) => {
|
||||
@@ -225,7 +262,7 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-8 px-1 sm:px-2 text-[10px] font-bold uppercase tracking-wider hover:bg-muted/50 flex items-center gap-1 sm:gap-1.5 text-muted-foreground hover:text-foreground transition-colors"
|
||||
class="h-8 px-1 sm:px-2 text-[0.625rem] font-bold uppercase tracking-wider hover:bg-muted/50 flex items-center gap-1 sm:gap-1.5 text-muted-foreground hover:text-foreground transition-colors"
|
||||
onClick={() => {
|
||||
const instance = editorInstance();
|
||||
if (instance) {
|
||||
@@ -288,7 +325,7 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
||||
{/* Tags at the bottom */}
|
||||
<div class="mt-auto pt-6 border-t border-border/50">
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<span class="text-[10px] uppercase font-bold tracking-wider text-muted-foreground/60 shrink-0">Tags</span>
|
||||
<span class="text-[0.625rem] uppercase font-bold tracking-wider text-muted-foreground/60 shrink-0">Tags</span>
|
||||
<div class="flex flex-wrap items-center gap-1.5 min-w-0 flex-1">
|
||||
<TagPicker
|
||||
selectedTags={props.task.tags || []}
|
||||
@@ -367,7 +404,7 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
||||
<PopoverContent class="w-72 p-0 overflow-hidden" align="end">
|
||||
<div class="flex flex-col">
|
||||
<div class="p-2 border-b border-border/50">
|
||||
<div class="text-[10px] font-bold uppercase tracking-wider text-muted-foreground/70 px-2 py-1">Recurrence</div>
|
||||
<div class="text-[0.625rem] font-bold uppercase tracking-wider text-muted-foreground/70 px-2 py-1">Recurrence</div>
|
||||
<div class="flex flex-col gap-2 p-2 pt-0">
|
||||
<Select
|
||||
value={props.task.recurrence?.type || "none"}
|
||||
@@ -405,7 +442,7 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
||||
{(day, index) => (
|
||||
<button
|
||||
class={cn(
|
||||
"w-7 h-7 rounded-full text-[10px] font-bold flex items-center justify-center transition-colors shadow-sm",
|
||||
"w-7 h-7 rounded-full text-[0.625rem] font-bold flex items-center justify-center transition-colors shadow-sm",
|
||||
props.task.recurrence?.days?.includes(index())
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-muted text-muted-foreground hover:bg-muted/80"
|
||||
@@ -472,7 +509,7 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
||||
|
||||
{/* Sharing Section */}
|
||||
<div class="p-2 border-t border-border/50">
|
||||
<div class="text-[10px] font-bold uppercase tracking-wider text-muted-foreground/70 px-2 py-1 mb-1">Sharing</div>
|
||||
<div class="text-[0.625rem] font-bold uppercase tracking-wider text-muted-foreground/70 px-2 py-1 mb-1">Sharing</div>
|
||||
<UserSharePicker
|
||||
taskId={props.task.id}
|
||||
sharedWith={props.task.sharedWith || []}
|
||||
@@ -532,12 +569,12 @@ const MetadataItem: Component<{
|
||||
isToggled() ? "-translate-y-4" : "translate-y-0"
|
||||
)}>
|
||||
{/* Layer 1: Date */}
|
||||
<div class="h-4 flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground/80 whitespace-nowrap">
|
||||
<div class="h-4 flex items-center gap-1.5 text-[0.6875rem] font-medium text-muted-foreground/80 whitespace-nowrap">
|
||||
{props.icon}
|
||||
{props.date}
|
||||
</div>
|
||||
{/* Layer 2: Time */}
|
||||
<div class="h-4 flex items-center gap-2 text-[11px] font-bold text-foreground whitespace-nowrap">
|
||||
<div class="h-4 flex items-center gap-2 text-[0.6875rem] font-bold text-foreground whitespace-nowrap">
|
||||
<Clock size={11} class="text-primary/60 shrink-0" />
|
||||
<span>{props.time}</span>
|
||||
</div>
|
||||
@@ -669,7 +706,7 @@ const UserSharePicker: Component<{
|
||||
{rule.type === 'tag' ? <Tag size={10} /> : <Settings size={10} />}
|
||||
</div>
|
||||
<div class="flex flex-col min-w-0">
|
||||
<span class="text-[11px] font-medium leading-none truncate">
|
||||
<span class="text-[0.6875rem] font-medium leading-none truncate">
|
||||
{getUserName(rule.targetUserId)}
|
||||
</span>
|
||||
<span class="text-[9px] text-muted-foreground/50 mt-0.5 uppercase tracking-tighter">
|
||||
|
||||
@@ -54,3 +54,17 @@ export const SIZE_OPTIONS = [
|
||||
{ value: "1", label: "1 - Five minutes" },
|
||||
{ value: "0", label: "0 - Less than a minute" }
|
||||
];
|
||||
|
||||
export const STATUS_OPTIONS = [
|
||||
{ value: "10", label: "10 - Complete" },
|
||||
{ value: "9", label: "9" },
|
||||
{ value: "8", label: "8" },
|
||||
{ value: "7", label: "7" },
|
||||
{ value: "6", label: "6" },
|
||||
{ value: "5", label: "5" },
|
||||
{ value: "4", label: "4" },
|
||||
{ value: "3", label: "3" },
|
||||
{ value: "2", label: "2" },
|
||||
{ value: "1", label: "1" },
|
||||
{ value: "0", label: "0 - Not Yet Started" }
|
||||
];
|
||||
|
||||
+224
-50
@@ -23,9 +23,10 @@ export interface Task {
|
||||
startDate: string;
|
||||
dueDate: string;
|
||||
priority: number; // 1-10
|
||||
completed: boolean;
|
||||
status: number; // 0-10
|
||||
completed: boolean; // Deprecated, kept for compat/computed from status = 10
|
||||
tags: string[];
|
||||
ownerId: string; // Add ownerId to interface
|
||||
ownerId: string | null; // Add ownerId to interface
|
||||
content?: string; // HTML content from Tiptap
|
||||
deletedAt?: number; // Timestamp of soft delete
|
||||
created: string; // ISO string from PB
|
||||
@@ -46,7 +47,7 @@ export const checkRecurringTasks = () => {
|
||||
const todayStr = nowObj.toLocaleDateString('en-CA'); // YYYY-MM-DD in local time
|
||||
|
||||
tasks.forEach(task => {
|
||||
if (!task.completed || !task.recurrence) return;
|
||||
if (task.status !== 10 || !task.recurrence) return;
|
||||
|
||||
// If deleted, we don't recurse?
|
||||
if (task.deletedAt) return;
|
||||
@@ -101,6 +102,7 @@ export const checkRecurringTasks = () => {
|
||||
if (shouldReset) {
|
||||
console.log(`Resetting recurring task: ${task.title}`);
|
||||
updateTask(task.id, {
|
||||
status: 0,
|
||||
completed: false,
|
||||
recurrence: {
|
||||
...task.recurrence,
|
||||
@@ -128,6 +130,7 @@ export interface TagDefinition {
|
||||
color?: string;
|
||||
theme?: "light" | "dark";
|
||||
isUser?: boolean; // New flag to identify user-based tags
|
||||
isBucket?: boolean; // New flag to identify bucket-based tags
|
||||
}
|
||||
|
||||
export interface FilterTag {
|
||||
@@ -158,6 +161,7 @@ export interface ShareRule {
|
||||
targetUserId: string;
|
||||
type: 'tag' | 'all';
|
||||
tagName?: string;
|
||||
share_mode?: 'ADD_USER' | 'SEND_TASK'; // Default to ADD_USER if undefined
|
||||
}
|
||||
|
||||
export interface Bucket {
|
||||
@@ -368,6 +372,13 @@ export const getCombinedScore = (task: Task): number => {
|
||||
const sizeScore = ((task.size ?? 5) - 5) * -0.4;
|
||||
baseScore += sizeScore;
|
||||
|
||||
// Status adjustment: 0-9 -> 0.0-2.0 weight
|
||||
if (task.status < 10) {
|
||||
// Map 0-9 to 0-2 range
|
||||
const statusWeight = (task.status / 9) * 2.0;
|
||||
baseScore += statusWeight;
|
||||
}
|
||||
|
||||
return baseScore;
|
||||
};
|
||||
|
||||
@@ -385,9 +396,10 @@ const mapRecordToTask = (r: any): Task => {
|
||||
startDate: r.startDate,
|
||||
dueDate: r.dueDate,
|
||||
priority: r.priority,
|
||||
completed: r.completed,
|
||||
status: r.status ?? (r.completed ? 10 : 0),
|
||||
completed: r.status === 10 || r.completed,
|
||||
tags: tags,
|
||||
ownerId: r.user,
|
||||
ownerId: r.user || null,
|
||||
content: r.content,
|
||||
deletedAt: r.deletedAt ? new Date(r.deletedAt).getTime() : undefined,
|
||||
created: r.created,
|
||||
@@ -404,7 +416,8 @@ const mapRecordToShareRule = (r: any): ShareRule => {
|
||||
ownerId: r.ownerId,
|
||||
targetUserId: r.targetUserId,
|
||||
type: r.type as 'tag' | 'all',
|
||||
tagName: r.tagName
|
||||
tagName: r.tagName,
|
||||
share_mode: r.share_mode || 'ADD_USER'
|
||||
};
|
||||
};
|
||||
|
||||
@@ -707,15 +720,17 @@ export const initStore = async () => {
|
||||
name: r.name,
|
||||
value: r.value,
|
||||
color: r.color,
|
||||
theme: r.theme
|
||||
theme: r.theme,
|
||||
isUser: r.isUser || false,
|
||||
isBucket: r.isBucket || false
|
||||
}));
|
||||
setStore("tagDefinitions", reconcile(loadedTags));
|
||||
} catch (tagErr) {
|
||||
console.warn("Failed to load tags:", tagErr);
|
||||
}
|
||||
|
||||
// 1.8 Sync User Tags & Share Rules
|
||||
await syncUserTagsAndRules();
|
||||
// 1.8 Sync System Tags & Share Rules
|
||||
await syncSystemTagsAndRules();
|
||||
|
||||
// 2. Fetch Tasks & Templates + Shared Tasks in PARALLEL
|
||||
const currentUserId = pb.authStore.model?.id;
|
||||
@@ -955,6 +970,7 @@ export const createBucket = async (name: string, color: string, description?: st
|
||||
color,
|
||||
description
|
||||
});
|
||||
await syncSystemTagsAndRules();
|
||||
toast.success("Bucket created");
|
||||
} catch (err) {
|
||||
console.error("Failed to create bucket:", err);
|
||||
@@ -965,6 +981,9 @@ export const createBucket = async (name: string, color: string, description?: st
|
||||
export const updateBucket = async (id: string, data: { name?: string, color?: string, description?: string }) => {
|
||||
try {
|
||||
await pb.collection(BUCKETS_COLLECTION).update(id, data);
|
||||
if (data.name) {
|
||||
await syncSystemTagsAndRules();
|
||||
}
|
||||
toast.success("Bucket updated");
|
||||
} catch (err) {
|
||||
console.error("Failed to update bucket:", err);
|
||||
@@ -1004,6 +1023,7 @@ export const addTask = async (title: string, options?: { dueDate?: Date | string
|
||||
startDate,
|
||||
dueDate,
|
||||
priority,
|
||||
status: 0,
|
||||
completed: false,
|
||||
tags,
|
||||
ownerId: pb.authStore.model?.id || "",
|
||||
@@ -1023,6 +1043,7 @@ export const addTask = async (title: string, options?: { dueDate?: Date | string
|
||||
startDate,
|
||||
dueDate,
|
||||
priority,
|
||||
status: 0,
|
||||
completed: false,
|
||||
tags,
|
||||
content,
|
||||
@@ -1070,12 +1091,81 @@ export const copyTask = async (id: string) => {
|
||||
toast.success("Task duplicated");
|
||||
};
|
||||
|
||||
const checkForHandoffs = (task: Task, updates: Partial<Task>): Partial<Task> => {
|
||||
// Only check if tags are being modified
|
||||
if (!updates.tags) return updates;
|
||||
|
||||
const currentUserId = pb.authStore.model?.id;
|
||||
if (!currentUserId || task.ownerId !== currentUserId) return updates;
|
||||
|
||||
// Check for any tag that triggers a SEND_TASK rule
|
||||
for (const tag of updates.tags) {
|
||||
const rule = store.shareRules.find(r =>
|
||||
r.ownerId === currentUserId &&
|
||||
r.type === 'tag' &&
|
||||
r.tagName === tag &&
|
||||
r.share_mode === 'SEND_TASK'
|
||||
);
|
||||
|
||||
if (rule) {
|
||||
console.log(`Handoff triggered by tag "${tag}" -> Transferring to ${rule.targetUserId}`);
|
||||
|
||||
// Check if it's a bucket tag (target is self, but tag is a bucket)
|
||||
const isBucketRule = rule.targetUserId === currentUserId && store.tagDefinitions.find(t => t.name === tag)?.isBucket;
|
||||
|
||||
if (isBucketRule) {
|
||||
return {
|
||||
...updates,
|
||||
ownerId: null, // Unassign from current user (null is better for PB relations)
|
||||
// Remove current user from sharedWith to ensure it disappears from their view
|
||||
sharedWith: (task.sharedWith || []).filter(s => s.userId !== currentUserId)
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...updates,
|
||||
ownerId: rule.targetUserId,
|
||||
// Also clear sharedWith for the specific target user if they were already shared
|
||||
sharedWith: (task.sharedWith || []).filter(s => s.userId !== rule.targetUserId)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return updates;
|
||||
};
|
||||
|
||||
export const updateTask = async (id: string, updates: Partial<Task>) => {
|
||||
if (!pb.authStore.isValid) return;
|
||||
|
||||
// 0. Check for Hand-off Logic (Tag-based ownership transfer)
|
||||
const currentTask = store.tasks.find(t => t.id === id);
|
||||
let finalUpdates = { ...updates };
|
||||
|
||||
// Sync status and completed
|
||||
if (finalUpdates.status !== undefined) {
|
||||
finalUpdates.completed = finalUpdates.status === 10;
|
||||
} else if (finalUpdates.completed !== undefined) {
|
||||
// If only completed is passed, sync status
|
||||
// But if both pass, status takes precedence (above)
|
||||
// Wait, if I change completed to false, status should be 0? Or previous status?
|
||||
// Let's assume toggleTask handles that. updateTask is lower level.
|
||||
// But for safety:
|
||||
if (finalUpdates.completed) {
|
||||
finalUpdates.status = 10;
|
||||
} else if (currentTask && currentTask.status === 10) {
|
||||
// Uncompleting a completed task -> reset to 0
|
||||
finalUpdates.status = 0;
|
||||
}
|
||||
// If uncompleting a non-completed task (weird), keep status?
|
||||
}
|
||||
|
||||
if (currentTask) {
|
||||
finalUpdates = checkForHandoffs(currentTask, finalUpdates);
|
||||
}
|
||||
|
||||
// Optimistic update with timestamp to prevent stale realtime events
|
||||
const optimisticUpdates = {
|
||||
...updates,
|
||||
...finalUpdates,
|
||||
updated: new Date().toISOString()
|
||||
};
|
||||
setStore("tasks", (t) => t.id === id, optimisticUpdates);
|
||||
@@ -1083,15 +1173,21 @@ export const updateTask = async (id: string, updates: Partial<Task>) => {
|
||||
try {
|
||||
// Map Task fields to PB fields if needed (dates are ISO strings, so should matches)
|
||||
// Check for specific fields being updated
|
||||
const pbUpdates: any = { ...updates };
|
||||
if (updates.deletedAt !== undefined) {
|
||||
const pbUpdates: any = { ...finalUpdates };
|
||||
|
||||
if (finalUpdates.ownerId !== undefined) {
|
||||
pbUpdates.user = finalUpdates.ownerId || null; // Map ownerId to 'user' field in PB
|
||||
delete pbUpdates.ownerId;
|
||||
}
|
||||
|
||||
if (finalUpdates.deletedAt !== undefined) {
|
||||
// PB expects a date string or null for date fields usually,
|
||||
// but if we defined deletedAt as a 'date' field in PB.
|
||||
// If we defined it as a number in PB? User didn't specify.
|
||||
// Assuming it's a 'Date' field in PB based on previous conversation
|
||||
// "deletedAt | Date | No"
|
||||
if (updates.deletedAt) {
|
||||
pbUpdates.deletedAt = new Date(updates.deletedAt).toISOString();
|
||||
if (finalUpdates.deletedAt) {
|
||||
pbUpdates.deletedAt = new Date(finalUpdates.deletedAt).toISOString();
|
||||
} else {
|
||||
pbUpdates.deletedAt = null; // restore
|
||||
}
|
||||
@@ -1159,7 +1255,11 @@ export const setMatrixScaleDays = async (days: number) => {
|
||||
export const toggleTask = (id: string) => {
|
||||
const task = store.tasks.find(t => t.id === id);
|
||||
if (task) {
|
||||
updateTask(id, { completed: !task.completed });
|
||||
const newCompleted = !task.completed;
|
||||
updateTask(id, {
|
||||
completed: newCompleted,
|
||||
status: newCompleted ? 10 : 0
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1223,7 +1323,8 @@ export const upsertTagDefinition = async (name: string, value: number, color?: s
|
||||
name: record.name,
|
||||
value: record.value,
|
||||
color: record.color,
|
||||
theme: record.theme as "light" | "dark"
|
||||
theme: record.theme as "light" | "dark",
|
||||
isBucket: record.isBucket || false
|
||||
};
|
||||
setStore("tagDefinitions", (prev) => [...prev, newDef]);
|
||||
}
|
||||
@@ -1257,50 +1358,107 @@ export const removeTagDefinition = async (name: string) => {
|
||||
|
||||
|
||||
|
||||
export const syncUserTagsAndRules = async () => {
|
||||
export const syncSystemTagsAndRules = async () => {
|
||||
if (!pb.authStore.isValid) return;
|
||||
const currentUserId = pb.authStore.model?.id;
|
||||
|
||||
try {
|
||||
// 1. Fetch only verified users
|
||||
const usersPromise = pb.collection('users').getFullList({
|
||||
filter: 'verified = true',
|
||||
fields: 'id,name,email,verified',
|
||||
requestKey: null
|
||||
});
|
||||
// 1. Fetch data
|
||||
const [users, pbRules, tagDefinitions] = await Promise.all([
|
||||
pb.collection('users').getFullList({
|
||||
filter: 'verified = true',
|
||||
fields: 'id,name,email,verified',
|
||||
requestKey: null
|
||||
}),
|
||||
pb.collection(SHARE_RULES_COLLECTION).getFullList({
|
||||
filter: `ownerId = "${currentUserId}"`,
|
||||
requestKey: null
|
||||
}),
|
||||
pb.collection(TAGS_COLLECTION).getFullList({
|
||||
filter: `user = "${currentUserId}"`,
|
||||
requestKey: null
|
||||
})
|
||||
]);
|
||||
|
||||
// 2. Fetch existing rules DIRECTLY from PB to avoid store race conditions
|
||||
const rulesPromise = pb.collection(SHARE_RULES_COLLECTION).getFullList({
|
||||
filter: `ownerId = "${currentUserId}"`,
|
||||
requestKey: null
|
||||
});
|
||||
|
||||
const [users, pbRules] = await Promise.all([usersPromise, rulesPromise]);
|
||||
|
||||
// 2a. Map PB rules to check for duplicates
|
||||
const existingTags = store.tagDefinitions;
|
||||
const normalizedRules = pbRules.map(r => ({
|
||||
id: r.id,
|
||||
ownerId: r.ownerId,
|
||||
targetUserId: r.targetUserId,
|
||||
type: r.type,
|
||||
tagName: r.tagName
|
||||
}));
|
||||
|
||||
// --- Sync Bucket Tags ---
|
||||
for (const bucket of store.buckets) {
|
||||
const tagExists = tagDefinitions.find(t => t.name === bucket.name);
|
||||
if (!tagExists) {
|
||||
try {
|
||||
const record = await pb.collection(TAGS_COLLECTION).create({
|
||||
user: currentUserId,
|
||||
name: bucket.name,
|
||||
value: 5,
|
||||
color: bucket.color || "#64748b",
|
||||
theme: "dark",
|
||||
isBucket: true
|
||||
});
|
||||
setStore("tagDefinitions", prev => [...prev, {
|
||||
id: record.id,
|
||||
name: record.name,
|
||||
value: record.value,
|
||||
color: record.color,
|
||||
theme: record.theme,
|
||||
isBucket: true
|
||||
}]);
|
||||
} catch (e) {
|
||||
console.error(`Failed to create bucket tag ${bucket.name}`, e);
|
||||
}
|
||||
} else if (!tagExists.isBucket) {
|
||||
try {
|
||||
await pb.collection(TAGS_COLLECTION).update(tagExists.id, { isBucket: true });
|
||||
setStore("tagDefinitions", d => d.id === tagExists.id, { isBucket: true });
|
||||
} catch (e) {
|
||||
console.error(`Failed to flag tag ${bucket.name} as bucket`, e);
|
||||
}
|
||||
}
|
||||
// --- Sync Bucket System Rule ---
|
||||
// Ensure a ShareRule exists for this bucket so it appears in "System Rules"
|
||||
const bucketRuleExists = normalizedRules.some(r =>
|
||||
r.ownerId === currentUserId &&
|
||||
r.targetUserId === currentUserId &&
|
||||
r.type === 'tag' &&
|
||||
r.tagName === bucket.name
|
||||
);
|
||||
|
||||
if (!bucketRuleExists) {
|
||||
try {
|
||||
await pb.collection(SHARE_RULES_COLLECTION).create({
|
||||
ownerId: currentUserId,
|
||||
targetUserId: currentUserId,
|
||||
type: 'tag',
|
||||
tagName: bucket.name,
|
||||
share_mode: 'SEND_TASK'
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(`Failed to ensure system rule for bucket ${bucket.name}`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Sync User Tags ---
|
||||
for (const user of users) {
|
||||
const userName = user.name || user.email;
|
||||
if (!userName) continue;
|
||||
|
||||
// --- Tag Sync ---
|
||||
const tagExists = existingTags.find(t => t.name === userName);
|
||||
const tagExists = tagDefinitions.find(t => t.name === userName);
|
||||
if (!tagExists) {
|
||||
// ... create tag (logic unchanged) ...
|
||||
try {
|
||||
const record = await pb.collection(TAGS_COLLECTION).create({
|
||||
user: currentUserId,
|
||||
name: userName,
|
||||
value: 5,
|
||||
color: "#64748b",
|
||||
theme: "dark"
|
||||
theme: "dark",
|
||||
isUser: true
|
||||
});
|
||||
setStore("tagDefinitions", prev => [...prev, {
|
||||
id: record.id,
|
||||
@@ -1313,15 +1471,17 @@ export const syncUserTagsAndRules = async () => {
|
||||
} catch (e) {
|
||||
// console.error(`Failed to create user tag ${userName}`, e);
|
||||
}
|
||||
} else {
|
||||
if (!tagExists.isUser) {
|
||||
} else if (!tagExists.isUser) {
|
||||
try {
|
||||
await pb.collection(TAGS_COLLECTION).update(tagExists.id, { isUser: true });
|
||||
setStore("tagDefinitions", d => d.id === tagExists.id, { isUser: true });
|
||||
} catch (e) {
|
||||
console.error(`Failed to flag tag ${userName} as user`, e);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Share Rule Sync ---
|
||||
// --- User Share Rule Sync ---
|
||||
if (user.id !== currentUserId) {
|
||||
// Find ALL rules matching this criteria
|
||||
const matchingRules = normalizedRules.filter(r =>
|
||||
r.targetUserId === user.id &&
|
||||
r.type === 'tag' &&
|
||||
@@ -1329,16 +1489,12 @@ export const syncUserTagsAndRules = async () => {
|
||||
);
|
||||
|
||||
if (matchingRules.length === 0) {
|
||||
console.log(`Creating auto-share rule for ${userName}`);
|
||||
await addShareRule('tag', user.id, userName);
|
||||
await addShareRule('tag', user.id, userName, 'ADD_USER');
|
||||
} else if (matchingRules.length > 1) {
|
||||
// DUPLICATE DETECTED: Keep first, delete others
|
||||
console.warn(`Duplicate share rules found for ${userName}. Cleaning up...`);
|
||||
const [, ...remove] = matchingRules;
|
||||
for (const rule of remove) {
|
||||
try {
|
||||
await pb.collection(SHARE_RULES_COLLECTION).delete(rule.id);
|
||||
// Update local store immediately to reflect deletion
|
||||
setStore("shareRules", list => list.filter(r => r.id !== rule.id));
|
||||
} catch (e) {
|
||||
console.error("Failed to delete duplicate rule:", e);
|
||||
@@ -1349,7 +1505,7 @@ export const syncUserTagsAndRules = async () => {
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error("Failed to sync user tags/rules:", err);
|
||||
console.error("Failed to sync system tags/rules:", err);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1550,6 +1706,7 @@ export const splitTask = async (taskId: string, userId: string) => {
|
||||
tags: task.tags,
|
||||
startDate: task.startDate,
|
||||
dueDate: task.dueDate,
|
||||
status: task.status,
|
||||
completed: task.completed,
|
||||
deletedAt: null,
|
||||
sharedWith: [], // Start fresh with no shares
|
||||
@@ -1589,7 +1746,8 @@ export const loadShareRules = async () => {
|
||||
ownerId: r.ownerId,
|
||||
targetUserId: r.targetUserId,
|
||||
type: r.type as 'tag' | 'all',
|
||||
tagName: r.tagName
|
||||
tagName: r.tagName,
|
||||
share_mode: r.share_mode || 'ADD_USER'
|
||||
}));
|
||||
|
||||
setStore("shareRules", reconcile(rules));
|
||||
@@ -1598,7 +1756,7 @@ export const loadShareRules = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
export const addShareRule = async (type: 'tag' | 'all', targetUserId: string, tagName?: string) => {
|
||||
export const addShareRule = async (type: 'tag' | 'all', targetUserId: string, tagName?: string, share_mode: 'ADD_USER' | 'SEND_TASK' = 'ADD_USER') => {
|
||||
if (!pb.authStore.isValid) return;
|
||||
|
||||
try {
|
||||
@@ -1606,7 +1764,8 @@ export const addShareRule = async (type: 'tag' | 'all', targetUserId: string, ta
|
||||
ownerId: pb.authStore.model?.id,
|
||||
targetUserId,
|
||||
type,
|
||||
tagName: type === 'tag' ? tagName : null
|
||||
tagName: type === 'tag' ? tagName : null,
|
||||
share_mode: share_mode || 'ADD_USER'
|
||||
});
|
||||
// Realtime subscription will handle adding to store
|
||||
toast.success(`Share rule created`);
|
||||
@@ -1616,6 +1775,21 @@ export const addShareRule = async (type: 'tag' | 'all', targetUserId: string, ta
|
||||
}
|
||||
};
|
||||
|
||||
export const updateShareRule = async (ruleId: string, updates: Partial<ShareRule>) => {
|
||||
if (!pb.authStore.isValid) return;
|
||||
|
||||
try {
|
||||
await pb.collection(SHARE_RULES_COLLECTION).update(ruleId, updates);
|
||||
setStore("shareRules", (rules) =>
|
||||
rules.map(r => r.id === ruleId ? { ...r, ...updates } : r)
|
||||
);
|
||||
toast.success("Share rule updated");
|
||||
} catch (err) {
|
||||
console.error("Failed to update share rule:", err);
|
||||
toast.error("Failed to update share rule.");
|
||||
}
|
||||
};
|
||||
|
||||
export const removeShareRule = async (ruleId: string) => {
|
||||
if (!pb.authStore.isValid) return;
|
||||
|
||||
|
||||
+11
-11
@@ -17,10 +17,10 @@ export const MatrixView: Component = () => {
|
||||
|
||||
<div class="flex-1 relative border-2 border-dashed border-muted rounded-2xl md:rounded-3xl overflow-hidden bg-muted/20">
|
||||
{/* Axis Labels */}
|
||||
<div class="absolute bottom-4 left-1/2 -translate-x-1/2 text-[9px] md:text-[10px] font-bold uppercase tracking-widest text-muted-foreground/50 flex items-center gap-1 md:gap-2 z-30 w-[calc(100%-2rem)] justify-center">
|
||||
<div class="absolute bottom-4 left-1/2 -translate-x-1/2 text-[0.5625rem] md:text-[0.625rem] font-bold uppercase tracking-widest text-muted-foreground/50 flex items-center gap-1 md:gap-2 z-30 w-[calc(100%-2rem)] justify-center">
|
||||
<span class="truncate">← Soon</span>
|
||||
<div class="flex items-center gap-1 bg-background/50 backdrop-blur-sm px-1.5 md:px-2 py-0.5 rounded-full border border-border/50 group hover:bg-background/80 transition-colors cursor-pointer shrink-0">
|
||||
<span class="text-[8px] md:text-[9px]">Time:</span>
|
||||
<span class="text-[0.5rem] md:text-[0.5625rem]">Time:</span>
|
||||
<Select
|
||||
value={store.matrixScaleDays.toString()}
|
||||
onChange={(val) => val && setStore("matrixScaleDays", parseInt(val))}
|
||||
@@ -32,7 +32,7 @@ export const MatrixView: Component = () => {
|
||||
</SelectItem>
|
||||
)}
|
||||
>
|
||||
<SelectTrigger class="h-auto p-0 min-w-[40px] md:min-w-[50px] bg-transparent border-none shadow-none focus:ring-0 text-[8px] md:text-[10px] font-black text-foreground">
|
||||
<SelectTrigger class="h-auto p-0 min-w-[40px] md:min-w-[50px] bg-transparent border-none shadow-none focus:ring-0 text-[0.5rem] md:text-[0.625rem] font-black text-foreground">
|
||||
<span>{store.matrixScaleDays} {store.matrixScaleDays === 1 ? 'day' : 'days'}</span>
|
||||
</SelectTrigger>
|
||||
<SelectContent class="z-[150]" />
|
||||
@@ -40,7 +40,7 @@ export const MatrixView: Component = () => {
|
||||
</div>
|
||||
<span class="truncate">Later →</span>
|
||||
</div>
|
||||
<div class="absolute left-2 md:left-4 top-1/2 -translate-y-1/2 -rotate-90 text-[8px] md:text-[10px] font-bold uppercase tracking-widest text-muted-foreground/30 pointer-events-none">
|
||||
<div class="absolute left-2 md:left-4 top-1/2 -translate-y-1/2 -rotate-90 text-[0.5rem] md:text-[0.625rem] font-bold uppercase tracking-widest text-muted-foreground/30 pointer-events-none">
|
||||
← Low (Priority) High →
|
||||
</div>
|
||||
|
||||
@@ -55,10 +55,10 @@ export const MatrixView: Component = () => {
|
||||
</div>
|
||||
|
||||
{/* Quadrant Labels */}
|
||||
<div class="absolute top-4 left-4 text-[10px] font-bold text-muted-foreground/30 pointer-events-none">DO FIRST</div>
|
||||
<div class="absolute top-4 right-4 text-[10px] font-bold text-muted-foreground/30 pointer-events-none">SCHEDULE</div>
|
||||
<div class="absolute bottom-4 left-4 text-[10px] font-bold text-muted-foreground/30 pointer-events-none">DELEGATE</div>
|
||||
<div class="absolute bottom-4 right-4 text-[10px] font-bold text-muted-foreground/30 pointer-events-none">ELIMINATE</div>
|
||||
<div class="absolute top-4 left-4 text-[0.625rem] font-bold text-muted-foreground/30 pointer-events-none">DO FIRST</div>
|
||||
<div class="absolute top-4 right-4 text-[0.625rem] font-bold text-muted-foreground/30 pointer-events-none">SCHEDULE</div>
|
||||
<div class="absolute bottom-4 left-4 text-[0.625rem] font-bold text-muted-foreground/30 pointer-events-none">DELEGATE</div>
|
||||
<div class="absolute bottom-4 right-4 text-[0.625rem] font-bold text-muted-foreground/30 pointer-events-none">ELIMINATE</div>
|
||||
|
||||
{/* Task Pips */}
|
||||
<For each={activeTasks()}>
|
||||
@@ -122,9 +122,9 @@ export const MatrixView: Component = () => {
|
||||
<div class="bg-popover text-popover-foreground border border-border rounded-xl p-3 shadow-2xl scale-95 group-hover:scale-100 transition-transform">
|
||||
<p class="text-xs font-bold leading-tight">{task.title}</p>
|
||||
<div class="flex items-center justify-between mt-2 pt-2 border-t border-border">
|
||||
<span class="text-[9px] text-muted-foreground">U: {urgencyScore}</span>
|
||||
<span class="text-[9px] text-muted-foreground">S: {task.size ?? 5}</span>
|
||||
<span class="text-[9px] text-muted-foreground">P: {task.priority}</span>
|
||||
<span class="text-[0.5625rem] text-muted-foreground">U: {urgencyScore}</span>
|
||||
<span class="text-[0.5625rem] text-muted-foreground">S: {task.size ?? 5}</span>
|
||||
<span class="text-[0.5625rem] text-muted-foreground">P: {task.priority}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class={cn(
|
||||
|
||||
+115
-44
@@ -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, createBucket, updateBucket, deleteBucket, toggleBucketSubscription } from "@/store";
|
||||
import { store, restoreTask, deleteTaskPermanently, setMatrixScaleDays, 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";
|
||||
@@ -26,6 +26,7 @@ export const SettingsView: Component = () => {
|
||||
const [expandedTemplateId, setExpandedTemplateId] = createSignal<string | null>(null);
|
||||
const [shareUserId, setShareUserId] = createSignal("");
|
||||
const [shareType, setShareType] = createSignal<'all' | 'tag'>('all');
|
||||
const [shareMode, setShareMode] = createSignal<'ADD_USER' | 'SEND_TASK'>('ADD_USER');
|
||||
const [shareTag, setShareTag] = createSignal<string>("");
|
||||
const [allUsers, setAllUsers] = createSignal<Array<{ id: string; name: string }>>([]);
|
||||
const [usersLoading, setUsersLoading] = createSignal(false);
|
||||
@@ -57,14 +58,14 @@ export const SettingsView: Component = () => {
|
||||
<header class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div class="min-w-0 flex-1">
|
||||
<h1 class="text-2xl sm:text-4xl font-black tracking-tighter">Settings</h1>
|
||||
<p class="text-[10px] sm:text-xs text-muted-foreground truncate opacity-70 font-mono tracking-tight" title={pb.authStore.model?.email}>
|
||||
<p class="text-[0.625rem] sm:text-xs text-muted-foreground truncate opacity-70 font-mono tracking-tight" title={pb.authStore.model?.email}>
|
||||
{pb.authStore.model?.email}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="w-full sm:w-auto font-black uppercase tracking-widest text-[10px] h-10 sm:h-8 rounded-xl shadow-sm border-border/60"
|
||||
class="w-full sm:w-auto font-black uppercase tracking-widest text-[0.625rem] h-10 sm:h-8 rounded-xl shadow-sm border-border/60"
|
||||
onClick={() => { pb.authStore.clear(); location.reload(); }}
|
||||
>
|
||||
Sign Out
|
||||
@@ -128,10 +129,47 @@ export const SettingsView: Component = () => {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Share Mode Selection */}
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
variant={shareMode() === 'ADD_USER' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
class="h-7 text-[0.625rem] gap-1 px-2"
|
||||
title="Recipient gets edit access (Collaboration)"
|
||||
onClick={() => setShareMode('ADD_USER')}
|
||||
>
|
||||
<Users size={10} />
|
||||
Collaborate
|
||||
</Button>
|
||||
<Button
|
||||
variant={shareMode() === 'SEND_TASK' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
class="h-7 text-[0.625rem] gap-1 px-2"
|
||||
title="Task ownership is transferred to recipient (Handoff)"
|
||||
onClick={() => setShareMode('SEND_TASK')}
|
||||
>
|
||||
<ArrowLeftRight size={10} />
|
||||
Handoff
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Show when={shareType() === 'tag'}>
|
||||
<Select
|
||||
value={shareTag()}
|
||||
onChange={(val) => val && setShareTag(val)}
|
||||
onChange={(val) => {
|
||||
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) => <SelectItem item={props.item}>{props.item.rawValue}</SelectItem>}
|
||||
@@ -168,7 +206,7 @@ export const SettingsView: Component = () => {
|
||||
disabled={!shareUserId()}
|
||||
onClick={async () => {
|
||||
if (shareUserId()) {
|
||||
await addShareRule(shareType(), shareUserId(), shareType() === 'tag' ? shareTag() : undefined);
|
||||
await addShareRule(shareType(), shareUserId(), shareType() === 'tag' ? shareTag() : undefined, shareMode());
|
||||
setShareUserId("");
|
||||
}
|
||||
}}
|
||||
@@ -177,21 +215,29 @@ export const SettingsView: Component = () => {
|
||||
Share
|
||||
</Button>
|
||||
</div>
|
||||
<p class="text-[0.625rem] text-muted-foreground italic px-1 pt-1">
|
||||
{shareMode() === 'ADD_USER'
|
||||
? "User will be added to the task's shared list."
|
||||
: "Task owner will change to the recipient immediately when tagged."}
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Active share rules */}
|
||||
{/* Active share rules - Outgoing */}
|
||||
<div class="space-y-3">
|
||||
<h4 class="text-[10px] font-black uppercase tracking-widest text-muted-foreground pb-1 flex items-center gap-2">
|
||||
<h4 class="text-[0.625rem] font-black uppercase tracking-widest text-muted-foreground pb-1 flex items-center gap-2">
|
||||
<div class="w-1 h-1 rounded-full bg-primary" />
|
||||
Sharing With Others
|
||||
</h4>
|
||||
<For each={store.shareRules.filter(r => {
|
||||
const isSystem = r.type === 'tag' && store.tagDefinitions.find(t => t.name === r.tagName)?.isUser;
|
||||
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={
|
||||
<div class="text-center py-4 text-muted-foreground text-[10px] uppercase tracking-widest italic border border-dashed border-border/40 rounded-xl bg-muted/5">
|
||||
<div class="text-center py-4 text-muted-foreground text-[0.625rem] uppercase tracking-widest italic border border-dashed border-border/40 rounded-xl bg-muted/5">
|
||||
You aren't sharing with anyone yet.
|
||||
</div>
|
||||
}>
|
||||
@@ -203,9 +249,17 @@ export const SettingsView: Component = () => {
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="text-xs font-bold truncate">To: {getUserName(rule.targetUserId)}</p>
|
||||
<p class="text-[9px] text-muted-foreground uppercase font-black tracking-widest opacity-60 flex items-center gap-1">
|
||||
<p class="text-[0.5625rem] text-muted-foreground uppercase font-black tracking-widest opacity-60 flex items-center gap-1">
|
||||
{rule.type === 'all' ? 'All Tasks' : `Tag: ${rule.tagName}`}
|
||||
<ArrowLeftRight size={8} class="rotate-45" />
|
||||
<span class="mx-1">•</span>
|
||||
<button
|
||||
class="hover:text-primary transition-colors hover:underline"
|
||||
onClick={() => updateShareRule(rule.id, {
|
||||
share_mode: rule.share_mode === 'SEND_TASK' ? 'ADD_USER' : 'SEND_TASK'
|
||||
})}
|
||||
>
|
||||
{rule.share_mode === 'SEND_TASK' ? 'HANDOFF' : 'COLLAB'}
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -224,16 +278,18 @@ export const SettingsView: Component = () => {
|
||||
|
||||
{/* Active share rules - Incoming */}
|
||||
<div class="space-y-3 pt-4 border-t border-border/20">
|
||||
<h4 class="text-[10px] font-black uppercase tracking-widest text-muted-foreground pb-1 flex items-center gap-2">
|
||||
<h4 class="text-[0.625rem] font-black uppercase tracking-widest text-muted-foreground pb-1 flex items-center gap-2">
|
||||
<div class="w-1 h-1 rounded-full bg-indigo-500" />
|
||||
Shared With You
|
||||
</h4>
|
||||
<For each={store.shareRules.filter(r => {
|
||||
const isSystem = r.type === 'tag' && store.tagDefinitions.find(t => t.name === r.tagName)?.isUser;
|
||||
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={
|
||||
<div class="text-center py-4 text-muted-foreground text-[10px] uppercase tracking-widest italic border border-dashed border-border/40 rounded-xl bg-muted/5">
|
||||
<div class="text-center py-4 text-muted-foreground text-[0.625rem] uppercase tracking-widest italic border border-dashed border-border/40 rounded-xl bg-muted/5">
|
||||
No one has shared with you yet.
|
||||
</div>
|
||||
}>
|
||||
@@ -245,9 +301,10 @@ export const SettingsView: Component = () => {
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="text-xs font-bold truncate">From: {getUserName(rule.ownerId)}</p>
|
||||
<p class="text-[9px] text-muted-foreground uppercase font-black tracking-widest opacity-60 flex items-center gap-1">
|
||||
<p class="text-[0.5625rem] text-muted-foreground uppercase font-black tracking-widest opacity-60 flex items-center gap-1">
|
||||
{rule.type === 'all' ? 'All Tasks' : `Tag: ${rule.tagName}`}
|
||||
<ArrowLeftRight size={8} class="-rotate-45" />
|
||||
<span class="mx-1">•</span>
|
||||
{rule.share_mode === 'SEND_TASK' ? 'HANDOFF' : 'COLLAB'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -267,25 +324,39 @@ export const SettingsView: Component = () => {
|
||||
</summary>
|
||||
<div class="pt-2 pl-4 space-y-2 animate-in fade-in slide-in-from-top-1 duration-200">
|
||||
<For each={store.shareRules.filter(r => {
|
||||
const isSystem = r.type === 'tag' && store.tagDefinitions.find(t => t.name === r.tagName)?.isUser;
|
||||
return isSystem;
|
||||
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) => (
|
||||
<div class="flex items-center justify-between p-2 rounded-lg bg-muted/10 border border-border/20 gap-3 opacity-70">
|
||||
<div class="flex items-center gap-3 min-w-0 flex-1">
|
||||
<Tag size={14} class="text-muted-foreground shrink-0" />
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm font-medium truncate flex items-center gap-2">
|
||||
{getUserName(rule.targetUserId)}
|
||||
<Users size={10} class="text-muted-foreground" />
|
||||
</p>
|
||||
<p class="text-[10px] text-muted-foreground uppercase tracking-wider">
|
||||
Auto-shared via User Tag
|
||||
</p>
|
||||
{(rule) => {
|
||||
const tagDef = store.tagDefinitions.find(t => t.name === rule.tagName);
|
||||
return (
|
||||
<div class="flex items-center justify-between p-2 rounded-lg bg-muted/10 border border-border/20 gap-3 opacity-70">
|
||||
<div class="flex items-center gap-3 min-w-0 flex-1">
|
||||
<Tag size={14} class="text-muted-foreground shrink-0" />
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm font-medium truncate flex items-center gap-2">
|
||||
{/* 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 ? <Users size={10} class="text-muted-foreground" /> : <Archive size={10} class="text-muted-foreground" />}
|
||||
</p>
|
||||
<p class="text-[0.625rem] text-muted-foreground uppercase tracking-wider flex items-center gap-1">
|
||||
Auto-shared via {tagDef?.isUser ? 'User' : 'Bucket'} Tag
|
||||
<span class="mx-1">•</span>
|
||||
<button
|
||||
class="hover:text-primary transition-colors hover:underline"
|
||||
onClick={() => updateShareRule(rule.id, {
|
||||
share_mode: rule.share_mode === 'SEND_TASK' ? 'ADD_USER' : 'SEND_TASK'
|
||||
})}
|
||||
>
|
||||
{rule.share_mode === 'SEND_TASK' ? 'HANDOFF' : 'COLLAB'}
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
</details>
|
||||
@@ -316,7 +387,7 @@ export const SettingsView: Component = () => {
|
||||
<div class="space-y-4 pt-2 animate-in fade-in slide-in-from-top-2 duration-300">
|
||||
{/* Create Bucket */}
|
||||
<div class="p-3 rounded-xl bg-muted/30 border border-border/50 space-y-3">
|
||||
<h4 class="text-[10px] font-black uppercase tracking-widest text-muted-foreground">Create New Bucket</h4>
|
||||
<h4 class="text-[0.625rem] font-black uppercase tracking-widest text-muted-foreground">Create New Bucket</h4>
|
||||
<form
|
||||
class="flex gap-2"
|
||||
onSubmit={(e) => {
|
||||
@@ -403,7 +474,7 @@ export const SettingsView: Component = () => {
|
||||
autofocus
|
||||
/>
|
||||
</Show>
|
||||
<p class="text-[10px] text-muted-foreground truncate opacity-80">{bucket.description || "No description"}</p>
|
||||
<p class="text-[0.625rem] text-muted-foreground truncate opacity-80">{bucket.description || "No description"}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -549,7 +620,7 @@ export const SettingsView: Component = () => {
|
||||
|
||||
<div class="flex items-center grow sm:grow-0 justify-between sm:justify-end gap-3 sm:gap-4 shrink-0 border-t sm:border-t-0 pt-2 sm:pt-0 border-border/20">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-[10px] text-muted-foreground font-bold uppercase tracking-wider">Val</span>
|
||||
<span class="text-[0.625rem] text-muted-foreground font-bold uppercase tracking-wider">Val</span>
|
||||
<select
|
||||
value={String(tag.value)}
|
||||
onChange={(e) => upsertTagDefinition(tag.name, parseInt(e.currentTarget.value), tag.color, resolvedTheme())}
|
||||
@@ -608,7 +679,7 @@ export const SettingsView: Component = () => {
|
||||
|
||||
<div class="flex items-center grow sm:grow-0 justify-between sm:justify-end gap-3 sm:gap-4 shrink-0 border-t sm:border-t-0 pt-2 sm:pt-0 border-border/20">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-[10px] text-muted-foreground font-bold uppercase tracking-wider">Val</span>
|
||||
<span class="text-[0.625rem] text-muted-foreground font-bold uppercase tracking-wider">Val</span>
|
||||
<select
|
||||
value={String(tag.value)}
|
||||
onChange={(e) => upsertTagDefinition(tag.name, parseInt(e.currentTarget.value), tag.color, resolvedTheme())}
|
||||
@@ -649,7 +720,7 @@ export const SettingsView: Component = () => {
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
class="h-9 gap-2 rounded-xl font-bold uppercase tracking-wider text-[10px]"
|
||||
class="h-9 gap-2 rounded-xl font-bold uppercase tracking-wider text-[0.625rem]"
|
||||
onClick={() => {
|
||||
addTemplate({
|
||||
name: "New Template",
|
||||
@@ -722,7 +793,7 @@ export const SettingsView: Component = () => {
|
||||
) : (
|
||||
<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 class="text-[0.625rem] text-muted-foreground uppercase font-black tracking-widest opacity-60">
|
||||
P:{template.priority} / U:{template.urgency} / {template.tags?.length || 0} Tags
|
||||
</p>
|
||||
</div>
|
||||
@@ -761,7 +832,7 @@ export const SettingsView: Component = () => {
|
||||
<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>
|
||||
<label class="text-[0.625rem] 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={editTitle()}
|
||||
@@ -772,7 +843,7 @@ export const SettingsView: Component = () => {
|
||||
</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>
|
||||
<label class="text-[0.625rem] 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}
|
||||
@@ -784,7 +855,7 @@ export const SettingsView: Component = () => {
|
||||
</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>
|
||||
<label class="text-[0.625rem] 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}
|
||||
@@ -799,7 +870,7 @@ export const SettingsView: Component = () => {
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-[10px] font-bold uppercase tracking-wider text-muted-foreground ml-1">Tags</label>
|
||||
<label class="text-[0.625rem] 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 || []}
|
||||
@@ -826,7 +897,7 @@ export const SettingsView: Component = () => {
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-[10px] font-bold uppercase tracking-wider text-muted-foreground ml-1">Description (Content)</label>
|
||||
<label class="text-[0.625rem] 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
|
||||
@@ -911,13 +982,13 @@ export const SettingsView: Component = () => {
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between p-3.5 rounded-2xl bg-muted/20 border border-border/40 group hover:bg-muted/40 transition-all gap-3 min-w-0">
|
||||
<div class="min-w-0 flex-1 px-1">
|
||||
<p class="font-semibold truncate text-sm sm:text-base">{task.title || "Untitled"}</p>
|
||||
<p class="text-[11px] text-muted-foreground opacity-70">Expires in {Math.max(0, daysLeft)} days</p>
|
||||
<p class="text-[0.5rem] text-muted-foreground opacity-70">Expires in {Math.max(0, daysLeft)} days</p>
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-2 shrink-0 border-t sm:border-t-0 pt-3 sm:pt-0 border-border/10">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
class="h-9 px-4 text-[10px] font-bold uppercase tracking-widest hover:bg-green-500/10 hover:text-green-600 bg-background/50 border border-border/50 transition-all rounded-xl shadow-sm"
|
||||
class="h-9 px-4 text-[0.625rem] font-bold uppercase tracking-widest hover:bg-green-500/10 hover:text-green-600 bg-background/50 border border-border/50 transition-all rounded-xl shadow-sm"
|
||||
onClick={() => {
|
||||
restoreTask(task.id);
|
||||
toast.success("Task restored");
|
||||
|
||||
Reference in New Issue
Block a user