143 lines
8.7 KiB
TypeScript
143 lines
8.7 KiB
TypeScript
import { type Component, For, createMemo } from "solid-js";
|
|
import { store, calculateUrgencyScore, setActiveTaskId, setStore, matchesFilter } from "@/store";
|
|
import { cn } from "@/lib/utils";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select";
|
|
|
|
export const MatrixView: Component = () => {
|
|
const activeTasks = createMemo(() => store.tasks.filter(t => !t.completed && !t.deletedAt && matchesFilter(t)));
|
|
|
|
const scaleOptions = ["1", "7", "30", "60", "90"];
|
|
|
|
return (
|
|
<div class="h-[calc(100vh-12rem)] md:h-[calc(100vh-8rem)] flex flex-col">
|
|
<header class="mb-6">
|
|
<h2 class="text-3xl font-bold tracking-tight">Strategy Matrix</h2>
|
|
<p class="text-muted-foreground mt-1 text-lg">Urgency vs Priority mapping.</p>
|
|
</header>
|
|
|
|
<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-[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-[0.5rem] md:text-[0.5625rem]">Time:</span>
|
|
<Select
|
|
value={store.matrixScaleDays.toString()}
|
|
onChange={(val) => val && setStore("matrixScaleDays", parseInt(val))}
|
|
options={scaleOptions}
|
|
placeholder="Scale"
|
|
itemComponent={(props) => (
|
|
<SelectItem item={props.item}>
|
|
{props.item.rawValue} {props.item.rawValue === "1" ? "day" : "days"}
|
|
</SelectItem>
|
|
)}
|
|
>
|
|
<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]" />
|
|
</Select>
|
|
</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-[0.5rem] md:text-[0.625rem] font-bold uppercase tracking-widest text-muted-foreground/30 pointer-events-none">
|
|
← Low (Priority) High →
|
|
</div>
|
|
|
|
{/* Quadrant Lines */}
|
|
<div class="absolute inset-0 flex pointer-events-none">
|
|
<div class="flex-1 border-r border-muted/30" />
|
|
<div class="flex-1" />
|
|
</div>
|
|
<div class="absolute inset-0 flex flex-col pointer-events-none">
|
|
<div class="flex-1 border-b border-muted/30" />
|
|
<div class="flex-1" />
|
|
</div>
|
|
|
|
{/* Quadrant Labels */}
|
|
<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()}>
|
|
{(task) => {
|
|
const getPosition = () => {
|
|
// Continuous geometric urgency mapping
|
|
const now = new Date().getTime();
|
|
const due = new Date(task.dueDate).getTime();
|
|
const diffHours = Math.max(0.1, (due - now) / (1000 * 60 * 60));
|
|
|
|
// Level formula: H = 24 * base^(10-v) => v = 10 - logBase(H/24)
|
|
const base = Math.pow(Math.max(1, store.matrixScaleDays), 1 / 9);
|
|
const continuousLevel = 10 - (Math.log(diffHours / 24) / Math.log(base));
|
|
const clampedLevel = Math.min(10, Math.max(1, continuousLevel));
|
|
|
|
// Map 1-10 level to 10-90% X coordinate (10 on left, 1 on right)
|
|
const x = 100 - (((clampedLevel - 1) / 9) * 80 + 10);
|
|
|
|
// Y-axis: Priority (1 to 10)
|
|
// 10 = 10%, 1 = 90%
|
|
const y = 100 - (((task.priority - 1) / 9) * 80 + 10);
|
|
|
|
return { x, y };
|
|
};
|
|
|
|
const urgencyScore = calculateUrgencyScore(task.dueDate);
|
|
|
|
// Map task size (0-10) to HSL Hue (240 = Blue/Cool, 0 = Red/Warm)
|
|
// This creates a "true gradient" from small to large tasks.
|
|
const taskSize = task.size ?? 5;
|
|
const hue = (1 - (taskSize / 10)) * 240;
|
|
const dotColor = `hsl(${hue}, 85%, 55%)`;
|
|
|
|
return (
|
|
<div
|
|
class="absolute group cursor-pointer z-10"
|
|
onClick={() => setActiveTaskId(task.id)}
|
|
style={{
|
|
left: `${getPosition().x}%`,
|
|
top: `${getPosition().y}%`,
|
|
transform: 'translate(-50%, -50%)'
|
|
}}
|
|
>
|
|
<div
|
|
class="w-4 h-4 rounded-full border-2 border-background shadow-lg transition-all duration-300 group-hover:scale-150 group-hover:ring-4"
|
|
style={{
|
|
"background-color": dotColor,
|
|
"--tw-ring-color": `${dotColor}40` // 25% opacity for ring
|
|
}}
|
|
/>
|
|
|
|
{/* Tooltip */}
|
|
<div class={cn(
|
|
"absolute left-1/2 -translate-x-1/2 opacity-0 group-hover:opacity-100 transition-all pointer-events-none z-20 w-48",
|
|
getPosition().y < 25 ? "top-full mt-3" : "bottom-full mb-3"
|
|
)}>
|
|
<div class={cn(
|
|
"w-2 h-2 bg-popover border-l border-t border-border rotate-45 mx-auto",
|
|
getPosition().y < 25 ? "-mb-1 order-first" : "-mt-1 order-last"
|
|
)} />
|
|
<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-[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(
|
|
"w-2 h-2 bg-popover border-r border-b border-border rotate-45 mx-auto",
|
|
getPosition().y < 25 ? "hidden" : "-mt-1"
|
|
)} />
|
|
</div>
|
|
</div>
|
|
);
|
|
}}
|
|
</For>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|