initial commit 3??

This commit is contained in:
2026-01-30 14:10:11 -06:00
parent 34bbe8e98f
commit f2a75c954f
42 changed files with 3935 additions and 1 deletions
+62
View File
@@ -0,0 +1,62 @@
import { createSignal } from "solid-js";
import { pb } from "@/lib/pocketbase";
import { Button } from "@/components/ui/button";
import { toast } from "solid-sonner";
export const AuthCallback = (props: { onSuccess: () => void }) => {
const [email, setEmail] = createSignal("");
const [password, setPassword] = createSignal("");
const [loading, setLoading] = createSignal(false);
const handleLogin = async (e: Event) => {
e.preventDefault();
setLoading(true);
try {
await pb.collection("users").authWithPassword(email(), password());
toast.success("Welcome back!");
props.onSuccess();
} catch (err: any) {
console.error(err);
toast.error("Login failed. Please check your credentials.");
} finally {
setLoading(false);
}
};
return (
<div class="flex flex-col items-center justify-center min-h-screen bg-background p-4 animate-in fade-in duration-500">
<div class="w-full max-w-sm space-y-6">
<div class="text-center space-y-2">
<h1 class="text-2xl font-bold tracking-tight">Welcome to Tasgrid</h1>
<p class="text-sm text-muted-foreground">Sign in to sync your tasks across devices.</p>
</div>
<form onSubmit={handleLogin} class="space-y-4">
<div class="space-y-2">
<input
type="email"
placeholder="Email"
value={email()}
onInput={(e) => setEmail(e.currentTarget.value)}
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
required
/>
</div>
<div class="space-y-2">
<input
type="password"
placeholder="Password"
value={password()}
onInput={(e) => setPassword(e.currentTarget.value)}
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
required
/>
</div>
<Button type="submit" class="w-full" disabled={loading()}>
{loading() ? "Signing in..." : "Sign In"}
</Button>
</form>
</div>
</div>
);
};
+72
View File
@@ -0,0 +1,72 @@
import { type Component, type JSX, createSignal, Show } from "solid-js";
import { Sidebar, BottomNav } from "./Navigation";
import { QuickEntry } from "./QuickEntry";
import { TaskDetail } from "./TaskDetail";
import { store, activeTaskId, setActiveTaskId } from "@/store";
import { PanelLeftOpen } from "lucide-solid";
import { Button } from "./ui/button";
import { cn } from "@/lib/utils";
interface LayoutProps {
children: JSX.Element;
currentView: string;
setView: (v: string) => void;
}
export const Layout: Component<LayoutProps> = (props) => {
const [isSidebarLocked, setIsSidebarLocked] = createSignal(true);
const [isSidebarPeeking, setIsSidebarPeeking] = createSignal(false);
// Derived active task for the detail view
const activeTask = () => store.tasks.find(t => t.id === activeTaskId());
return (
<div class="flex h-screen w-full bg-background overflow-hidden relative font-sans antialiased text-foreground">
<Sidebar
currentView={props.currentView}
setView={props.setView}
isLocked={isSidebarLocked()}
setIsLocked={setIsSidebarLocked}
isPeeking={isSidebarPeeking()}
setIsPeeking={setIsSidebarPeeking}
/>
<div class={cn(
"flex-1 flex flex-col h-full transition-all duration-300 ease-in-out relative",
isSidebarLocked() ? "md:ml-48" : "ml-0"
)}>
{/* Expand Trigger Button (visible when not locked) */}
{!isSidebarLocked() && (
<div class="fixed top-4 left-4 z-[60] md:flex hidden">
<Button
variant="ghost"
size="icon"
onMouseEnter={() => setIsSidebarPeeking(true)}
onClick={() => setIsSidebarLocked(true)}
class="bg-background/80 backdrop-blur-sm border border-border hover:bg-muted shadow-sm"
>
<PanelLeftOpen size={18} />
</Button>
</div>
)}
<main class="flex-1 overflow-auto p-4 md:p-8 pt-4 pb-20 md:pb-8 scroll-smooth relative z-0">
{props.children}
</main>
{/* Mobile Bottom Nav */}
<BottomNav currentView={props.currentView} setView={props.setView} />
</div>
<QuickEntry />
<Show when={activeTask()}>
<TaskDetail
task={activeTask()!}
isOpen={!!activeTaskId()}
onClose={() => setActiveTaskId(null)}
/>
</Show>
</div>
);
};
+108
View File
@@ -0,0 +1,108 @@
import { type Component, For } from "solid-js";
import { LayoutDashboard, ListTodo, Settings, Clock, ArrowUpCircle, PanelLeftClose } from "lucide-solid";
import { cn } from "@/lib/utils";
import { Button } from "./ui/button";
interface NavItem {
icon: any;
label: string;
view: string;
}
const navItems: NavItem[] = [
{ icon: ListTodo, label: "Focus", view: "critical" },
{ icon: Clock, label: "Time", view: "urgency" },
{ icon: ArrowUpCircle, label: "Priority", view: "priority" },
{ icon: LayoutDashboard, label: "Matrix", view: "matrix" },
{ icon: Settings, label: "Settings", view: "settings" },
];
export const BottomNav: Component<{ currentView: string; setView: (v: string) => void }> = (props) => {
return (
<nav class="fixed bottom-0 left-0 right-0 bg-background border-t border-border flex justify-around items-center h-16 md:hidden z-50">
<For each={navItems}>
{(item) => (
<button
onClick={() => props.setView(item.view)}
class={cn(
"flex flex-col items-center justify-center w-full h-full space-y-1 transition-colors",
props.currentView === item.view ? "text-primary" : "text-muted-foreground"
)}
>
<item.icon size={20} />
<span class="text-[10px] font-medium uppercase tracking-wider">{item.label}</span>
</button>
)}
</For>
</nav>
);
};
export const Sidebar: Component<{
currentView: string;
setView: (v: string) => void;
isLocked: boolean;
setIsLocked: (v: boolean) => void;
isPeeking: boolean;
setIsPeeking: (v: boolean) => void;
}> = (props) => {
return (
<aside
onMouseEnter={() => !props.isLocked && props.setIsPeeking(true)}
onMouseLeave={() => !props.isLocked && props.setIsPeeking(false)}
class={cn(
"hidden md:flex flex-col border-r border-border bg-card h-screen fixed left-0 top-0 transition-all duration-300 ease-in-out z-[50]",
props.isLocked ? "w-48 translate-x-0" : "w-48 -translate-x-full",
!props.isLocked && props.isPeeking && "translate-x-0 shadow-2xl ring-1 ring-border"
)}
>
<div class={cn("p-4 flex items-center justify-between", !props.isLocked && "pl-14")}>
<h1 class="text-xl font-bold tracking-tighter text-primary px-2">Tasgrid</h1>
{/* Close Button - Only visible when Locked (to collapse) or Peeking (manual close) */}
<Button
variant="ghost"
size="icon"
class="h-8 w-8 text-muted-foreground hover:text-foreground"
onClick={() => {
props.setIsLocked(false);
props.setIsPeeking(false);
}}
>
<PanelLeftClose size={16} />
</Button>
</div>
<nav class="flex-1 px-3 space-y-1 mt-2">
<For each={navItems.filter(i => i.view !== "settings")}>
{(item) => (
<button
onClick={() => props.setView(item.view)}
class={cn(
"flex items-center space-x-3 w-full px-3 py-2.5 rounded-lg transition-all duration-200 group text-sm",
props.currentView === item.view
? "bg-primary text-primary-foreground shadow-sm"
: "text-muted-foreground hover:bg-muted hover:text-foreground"
)}
>
<item.icon size={16} class={cn("transition-transform group-hover:scale-110")} />
<span class="font-medium">{item.label}</span>
</button>
)}
</For>
</nav>
<div class="p-3 border-t border-border mt-auto">
<button
onClick={() => props.setView("settings")}
class={cn(
"flex items-center space-x-3 w-full px-3 py-2.5 rounded-lg transition-all duration-200 group text-sm",
props.currentView === "settings"
? "bg-primary text-primary-foreground shadow-sm"
: "text-muted-foreground hover:bg-muted hover:text-foreground"
)}
>
<Settings size={16} class={cn("transition-transform group-hover:scale-110")} />
<span class="font-medium">Settings</span>
</button>
</div>
</aside>
);
};
+251
View File
@@ -0,0 +1,251 @@
import { type Component, createSignal, createEffect, onCleanup, onMount } from "solid-js";
import { Search, Command, Clock, ArrowUpCircle, Plus } from "lucide-solid";
import { addTask, calculateDateFromUrgency, calculateUrgencyFromDate } from "@/store";
import { Button } from "@/components/ui/button";
import { TextField, TextFieldInput } from "@/components/ui/textfield";
import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select";
export const QuickEntry: Component = () => {
const [isOpen, setIsOpen] = createSignal(false);
const [input, setInput] = createSignal("");
const [priority, setPriority] = createSignal<string>("5");
const [urgency, setUrgency] = createSignal<string>("5");
// Reactive shorthand parsing
createEffect(() => {
const text = input();
const pMatch = text.match(/\/p(\d+)/);
if (pMatch) {
const val = Math.min(10, Math.max(1, parseInt(pMatch[1]))).toString();
setPriority(val);
}
const uMatch = text.match(/\/u(\d+)/);
if (uMatch) {
const val = Math.min(10, Math.max(1, parseInt(uMatch[1]))).toString();
if (val !== urgency()) {
onUrgencySimpleChange(val);
}
}
});
// Explicit date string for the input (YYYY-MM-DDTHH:mm)
const [dateString, setDateString] = createSignal<string>("");
let inputRef: HTMLInputElement | undefined;
createEffect(() => {
if (isOpen()) {
// Slight delay to ensure DOM is ready and animations don't interfere
setTimeout(() => inputRef?.focus(), 50);
}
});
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
e.preventDefault();
if (isOpen()) {
inputRef?.focus();
} else {
setIsOpen(true);
}
}
if (e.key === "Escape") {
setIsOpen(false);
}
};
onMount(() => window.addEventListener("keydown", handleKeyDown));
onCleanup(() => window.removeEventListener("keydown", handleKeyDown));
// When urgency changes via dropdown, update the date
const onUrgencySimpleChange = (val: string) => {
setUrgency(val);
const level = parseInt(val);
if (!isNaN(level)) {
const newDateIso = calculateDateFromUrgency(level);
// Format for datetime-local input: YYYY-MM-DDTHH:mm
const dateObj = new Date(newDateIso);
const localIso = new Date(dateObj.getTime() - (dateObj.getTimezoneOffset() * 60000)).toISOString().slice(0, 16);
setDateString(localIso);
}
};
// When date changes via input, update urgency level
const onDateInputChange = (e: Event) => {
const val = (e.target as HTMLInputElement).value;
setDateString(val);
if (val) {
const level = calculateUrgencyFromDate(new Date(val).toISOString());
setUrgency(level.toString());
}
};
const parseAndAdd = () => {
const text = input();
if (!text || !priority() || !urgency()) return;
let finalPriority = parseInt(priority());
let finalUrgency = parseInt(urgency());
// Parse shorthand overrides if present
const pMatch = text.match(/\/p(\d+)/);
if (pMatch) finalPriority = Math.min(10, Math.max(1, parseInt(pMatch[1])));
// Check for urgency override
const uMatchNumeric = text.match(/\/u(\d+)/);
if (uMatchNumeric) {
finalUrgency = Math.min(10, Math.max(1, parseInt(uMatchNumeric[1])));
}
let finalDueDate: Date | null = null;
if (dateString()) {
finalDueDate = new Date(dateString());
} else {
// If urgency is selected, calculate date
const u = parseInt(urgency());
if (!isNaN(u)) {
// Check if override happened
if (uMatchNumeric) {
const dateStr = calculateDateFromUrgency(finalUrgency);
finalDueDate = new Date(dateStr);
} else {
const dateStr = calculateDateFromUrgency(u);
finalDueDate = new Date(dateStr);
}
}
}
const title = text
.replace(/\/p\d+/, "")
.replace(/\/u\d+/, "")
.trim();
// Use the new signature: addTask(title, options)
addTask(title, {
priority: finalPriority,
dueDate: finalDueDate || undefined, // undefined will let store pick default if that was the intent, but here we probably want strictly calculated date
tags: []
});
setInput("");
setPriority("5");
setUrgency("5");
setDateString("");
setIsOpen(false);
};
return (
<>
<Button
size="icon"
onClick={() => setIsOpen(true)}
class="fixed bottom-20 left-[80%] -translate-x-1/2 md:bottom-10 md:right-10 md:left-auto md:translate-x-0 w-14 h-14 rounded-full shadow-2xl z-50 group hover:scale-110 transition-all duration-300"
>
<Plus size={28} class="group-hover:rotate-90 transition-transform duration-300" />
</Button>
{isOpen() && (
<div class="fixed inset-0 z-[100] flex items-start justify-center pt-[15vh] px-4">
<div class="absolute inset-0 bg-background/80 backdrop-blur-sm" onClick={() => setIsOpen(false)} />
<div class="relative w-full max-w-xl bg-card border border-border rounded-2xl shadow-2xl overflow-hidden animate-in fade-in zoom-in-95 duration-200">
<div class="flex items-center px-4 py-3 border-b border-border">
<Search class="text-muted-foreground mr-3" size={20} />
<TextField class="flex-1">
<TextFieldInput
ref={inputRef}
value={input()}
onInput={(e) => setInput(e.currentTarget.value)}
onKeyDown={(e) => e.key === "Enter" && parseAndAdd()}
placeholder="Task title... type /p1-10 for priority and /u1-10 for urgency"
class="border-none shadow-none focus-visible:ring-0 text-lg"
/>
</TextField>
</div>
<div class="flex flex-col md:flex-row items-stretch md:items-center p-4 gap-4 border-b border-border bg-muted/10">
<div class="flex-1 space-y-1.5">
<label class="text-[10px] font-bold uppercase tracking-wider text-muted-foreground ml-1">Priority</label>
<Select
value={priority()}
onChange={(val) => val && setPriority(val)}
options={["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]}
placeholder="Priority"
itemComponent={(props) => (
<SelectItem item={props.item}>
{props.item.rawValue}
</SelectItem>
)}
>
<SelectTrigger class="w-full bg-background border-none shadow-sm h-10 px-3">
<div class="flex items-center gap-2">
<ArrowUpCircle size={14} class="text-primary shrink-0" />
<span class="text-sm font-medium">{priority()}</span>
</div>
</SelectTrigger>
<SelectContent />
</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 (Time)</label>
<Select
value={urgency()}
onChange={(val) => {
if (val) onUrgencySimpleChange(val);
}}
options={["10", "9", "8", "7", "6", "5", "4", "3", "2", "1"]}
placeholder="Urgency"
itemComponent={(props) => (
<SelectItem item={props.item}>
{props.item.rawValue}
</SelectItem>
)}
>
<SelectTrigger class="w-full bg-background border-none shadow-sm h-10 px-3">
<div class="flex items-center gap-2">
<Clock size={14} class="text-primary shrink-0" />
<span class="text-sm font-medium">{urgency()}</span>
</div>
</SelectTrigger>
<SelectContent />
</Select>
</div>
<div class="flex-1 space-y-1.5">
<label class="text-[10px] font-bold uppercase tracking-wider text-muted-foreground ml-1">Due Date</label>
<div class="relative">
<input
type="datetime-local"
value={dateString()}
onInput={onDateInputChange}
onClick={(e) => (e.target as HTMLInputElement).showPicker?.()}
onKeyDown={(e) => {
if (e.key !== "Tab" && e.key !== "Escape") {
e.preventDefault();
}
}}
step="900"
class="w-full h-10 px-3 bg-background border-none rounded-md text-sm shadow-sm focus:outline-none focus:ring-1 focus:ring-ring z-[102] relative cursor-pointer"
/>
</div>
</div>
</div>
<div class="p-4 bg-muted/30 flex justify-between items-center">
<div class="flex items-center space-x-2 text-[10px] 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>
</div>
<Button size="sm" onClick={parseAndAdd} disabled={!input() || !priority() || !urgency()}>
Create Task
</Button>
</div>
</div>
</div>
)}
</>
);
};
+164
View File
@@ -0,0 +1,164 @@
import { type Component, createSignal, For, createEffect } from "solid-js";
import {
Heading1, Heading2, Heading3,
List, ListTodo, Type,
Code, Quote
} from "lucide-solid";
import { cn } from "@/lib/utils";
export interface CommandItem {
title: string;
description: string;
icon: any;
command: (props: { editor: any; range: any }) => void;
}
export const getSuggestionItems = ({ query }: { query: string }): CommandItem[] => {
return [
{
title: "Text",
description: "Just start typing with plain text.",
icon: Type,
command: ({ editor, range }: { editor: any, range: any }) => {
editor.chain().focus().deleteRange(range).setNode("paragraph").run();
},
},
{
title: "Heading 1",
description: "Big section heading.",
icon: Heading1,
command: ({ editor, range }: { editor: any, range: any }) => {
editor.chain().focus().deleteRange(range).setNode("heading", { level: 1 }).run();
},
},
{
title: "Heading 2",
description: "Medium section heading.",
icon: Heading2,
command: ({ editor, range }: { editor: any, range: any }) => {
editor.chain().focus().deleteRange(range).setNode("heading", { level: 2 }).run();
},
},
{
title: "Heading 3",
description: "Small section heading.",
icon: Heading3,
command: ({ editor, range }: { editor: any, range: any }) => {
editor.chain().focus().deleteRange(range).setNode("heading", { level: 3 }).run();
},
},
{
title: "Bullet List",
description: "Create a simple bulleted list.",
icon: List,
command: ({ editor, range }: { editor: any, range: any }) => {
editor.chain().focus().deleteRange(range).toggleBulletList().run();
},
},
{
title: "Task List",
description: "Track tasks with a checklist.",
icon: ListTodo,
command: ({ editor, range }: { editor: any, range: any }) => {
editor.chain().focus().deleteRange(range).toggleTaskList().run();
},
},
{
title: "Code Block",
description: "Capture a code snippet.",
icon: Code,
command: ({ editor, range }: { editor: any, range: any }) => {
editor.chain().focus().deleteRange(range).toggleCodeBlock().run();
},
},
{
title: "Blockquote",
description: "Capture a quote.",
icon: Quote,
command: ({ editor, range }: { editor: any, range: any }) => {
editor.chain().focus().deleteRange(range).toggleBlockquote().run();
},
},
].filter(item => item.title.toLowerCase().startsWith(query.toLowerCase()));
};
export const SlashMenu: Component<{
items: CommandItem[];
command: (item: CommandItem) => void;
editor: any;
ref: (handlers: { onKeyDown: (e: KeyboardEvent) => boolean }) => void;
}> = (props) => {
const [selectedIndex, setSelectedIndex] = createSignal(0);
// Auto-select the first item when the list changes (filtering)
createEffect(() => {
// We track props.items. When it changes, we reset.
props.items;
setSelectedIndex(0);
});
const selectItem = (index: number) => {
const item = props.items[index];
if (item) {
props.command(item);
}
};
// Expose handlers to the parent via the ref prop
props.ref({
onKeyDown: (e: KeyboardEvent) => {
if (e.key === "ArrowUp") {
setSelectedIndex((prev) => (prev + props.items.length - 1) % props.items.length);
return true;
}
if (e.key === "ArrowDown") {
setSelectedIndex((prev) => (prev + 1) % props.items.length);
return true;
}
if (e.key === "Enter") {
selectItem(selectedIndex());
return true;
}
return false;
}
});
return (
<div
class="z-[200] w-64 bg-popover border border-border rounded-lg shadow-xl overflow-hidden p-1 bg-background pointer-events-auto"
onMouseDown={(e) => e.stopPropagation()}
onWheel={(e) => e.stopPropagation()}
>
<div class="max-h-[300px] overflow-y-auto scrollbar-thin scrollbar-thumb-muted-foreground/20 overscroll-contain">
<For each={props.items}>
{(item, index) => (
<button
class={cn(
"flex items-center gap-3 w-full px-2 py-1.5 text-left rounded-md transition-colors outline-none",
index() === selectedIndex() ? "bg-accent text-accent-foreground" : "hover:bg-muted"
)}
onMouseDown={(e) => {
// Prevent editor focus loss and side-sheet closure
e.preventDefault();
e.stopPropagation();
selectItem(index());
}}
onMouseEnter={() => setSelectedIndex(index())}
>
<div class="flex items-center justify-center w-8 h-8 rounded border border-border bg-muted/50 text-muted-foreground shrink-0">
<item.icon size={16} />
</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>
</div>
</button>
)}
</For>
{props.items.length === 0 && (
<div class="p-2 text-xs text-muted-foreground text-center">No results found</div>
)}
</div>
</div>
);
};
+83
View File
@@ -0,0 +1,83 @@
import { type Component, createMemo } from "solid-js";
import { type Task, toggleTask, calculateUrgencyFromDate, setActiveTaskId } from "@/store";
import { cn } from "@/lib/utils";
import { CheckCircle2, Circle, Clock, ArrowUpCircle } from "lucide-solid";
export const TaskCard: Component<{ task: Task }> = (props) => {
const urgencyLevel = createMemo(() => calculateUrgencyFromDate(props.task.dueDate));
const urgencyColor = () => {
const u = urgencyLevel();
if (u >= 8) return "text-red-500";
if (u >= 6) return "text-orange-500";
if (u >= 4) return "text-blue-500";
return "text-muted-foreground";
};
const formattedDate = () => {
const date = new Date(props.task.dueDate);
return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
};
return (
<div
class={cn(
"group flex items-center p-4 bg-card border border-border rounded-2xl transition-all duration-300 hover:shadow-xl hover:shadow-primary/5 hover:-translate-y-0.5 cursor-pointer",
props.task.completed && "opacity-60"
)}
onClick={() => setActiveTaskId(props.task.id)}
>
<button
onClick={(e) => { e.stopPropagation(); toggleTask(props.task.id); }}
class="mr-4 text-muted-foreground hover:text-primary transition-colors shrink-0"
>
{props.task.completed ? (
<CheckCircle2 class="text-green-500" size={24} />
) : (
<Circle size={24} />
)}
</button>
<div class="flex-1 min-w-0">
<h3 class={cn(
"text-base font-semibold truncate transition-all",
props.task.completed && "line-through"
)}>
{props.task.title}
</h3>
<div class="flex items-center space-x-4 mt-1">
{/* Priority */}
<div class="flex items-center gap-1.5 min-w-[32px]">
<ArrowUpCircle size={12} class="text-primary opacity-60" />
<span>P{props.task.priority}</span>
</div>
{/* Due Date + Urgency Slide-out */}
<div class="relative group/date flex items-center h-6 cursor-help">
{/* Due Date - Static Layer on Top */}
<div class="flex items-center space-x-1.5 text-[10px] font-bold text-muted-foreground uppercase tracking-wider bg-card relative z-20 pr-1">
<Clock size={12} class="text-primary/60" />
<span>{formattedDate()}</span>
</div>
{/* Urgency Number - Slides out from behind with fade */}
<div class={cn(
"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-[10px] 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>
</div>
</div>
</div>
<div class="ml-4 opacity-0 group-hover:opacity-100 transition-opacity shrink-0">
<button class="p-2 hover:bg-muted rounded-full transition-colors">
<div class="w-1.5 h-1.5 bg-muted-foreground rounded-full" />
</button>
</div>
</div>
);
};
+213
View File
@@ -0,0 +1,213 @@
import { type Component, createEffect, createSignal } from "solid-js";
import { Sheet, SheetContent } from "@/components/ui/sheet";
import { type Task, removeTask, restoreTask, updateTask } from "@/store";
import { TaskEditor } from "@/components/TaskEditor";
import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select";
import { ArrowUpCircle, Clock, Calendar, Type, Trash2 } from "lucide-solid";
import { calculateDateFromUrgency, calculateUrgencyFromDate } from "@/store";
import { Button } from "./ui/button";
import { toast } from "solid-sonner";
interface TaskDetailProps {
task: Task;
isOpen: boolean;
onClose: () => void;
}
export const TaskDetail: Component<TaskDetailProps> = (props) => {
// Local state for immediate feedback, synced with props
const [title, setTitle] = createSignal(props.task.title);
const [editorInstance, setEditorInstance] = createSignal<any>(null);
// Sync input string for datetime-local
const [dateString, setDateString] = createSignal<string>("");
createEffect(() => {
setTitle(props.task.title);
// Format for datetime-local: yyyy-MM-ddThh:mm (local time)
const dateObj = new Date(props.task.dueDate);
const localIso = new Date(dateObj.getTime() - (dateObj.getTimezoneOffset() * 60000)).toISOString().slice(0, 16);
setDateString(localIso);
});
let debounceTimer: number | undefined;
const updateTaskContent = (html: string) => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
updateTask(props.task.id, { content: html });
}, 1000);
};
const updateTitle = (val: string) => {
setTitle(val);
// Debouncing could be added here, but direct update is fine for now if PB handles it well
updateTask(props.task.id, { title: val });
};
const updatePriority = (val: string) => {
const p = parseInt(val);
if (!isNaN(p)) {
updateTask(props.task.id, { priority: p });
}
};
const updateUrgency = (val: string) => {
const u = parseInt(val);
if (!isNaN(u)) {
const newDate = calculateDateFromUrgency(u);
updateTask(props.task.id, { dueDate: newDate });
}
};
const onDateInputChange = (e: Event) => {
const val = (e.currentTarget as HTMLInputElement).value;
if (val) {
const iso = new Date(val).toISOString();
updateTask(props.task.id, { dueDate: iso });
}
};
// Calculate current urgency level for display
const currentUrgency = () => calculateUrgencyFromDate(props.task.dueDate).toString();
return (
<Sheet open={props.isOpen} onOpenChange={(open) => !open && props.onClose()}>
<SheetContent class="w-full sm:max-w-2xl px-0 sm:px-0 flex flex-col h-full bg-background border-l border-border shadow-2xl">
{/* Header Area */}
<div class="px-6 pt-6 pb-2 shrink-0">
<textarea
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"
/>
{/* Properties Bar */}
<div class="flex flex-row items-center gap-1.5 sm:gap-4 mt-2 text-sm overflow-x-auto no-scrollbar flex-nowrap">
{/* 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>
<Select
value={props.task.priority.toString()}
onChange={(val) => val && updatePriority(val)}
options={["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]}
placeholder="Priority"
itemComponent={(props) => <SelectItem item={props.item}>{props.item.rawValue}</SelectItem>}
>
<SelectTrigger class="h-8 min-w-[40px] bg-transparent border-transparent hover:bg-muted/50 px-1 sm:px-2 shadow-none focus:ring-0 shrink-0">
<div class="flex items-center gap-1 sm:gap-1.5 text-foreground font-medium">
<ArrowUpCircle size={14} class="text-primary opacity-70 group-hover:opacity-100 transition-opacity" />
<span class="text-xs sm:text-sm">{props.task.priority}</span>
</div>
</SelectTrigger>
<SelectContent />
</Select>
</div>
{/* 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>
<Select
value={currentUrgency()}
onChange={(val) => val && updateUrgency(val)}
options={["10", "9", "8", "7", "6", "5", "4", "3", "2", "1"]}
placeholder="Urgency"
itemComponent={(props) => <SelectItem item={props.item}>{props.item.rawValue}</SelectItem>}
>
<SelectTrigger class="h-8 min-w-[40px] bg-transparent border-transparent hover:bg-muted/50 px-1 sm:px-2 shadow-none focus:ring-0 shrink-0">
<div class="flex items-center gap-1 sm:gap-1.5 text-foreground font-medium">
<Clock size={14} class="text-primary opacity-70 group-hover:opacity-100 transition-opacity" />
<span class="text-xs sm:text-sm">{currentUrgency()}</span>
</div>
</SelectTrigger>
<SelectContent />
</Select>
</div>
{/* Commands Shortcut */}
<div class="flex items-center gap-1 shrink-0">
<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"
onClick={() => {
const instance = editorInstance();
if (instance) {
instance.chain().focus().insertContent("/").run();
}
}}
>
<Type size={14} class="opacity-70" />
<span class="hidden sm:inline">Commands</span>
</Button>
</div>
{/* Date - Editable */}
<div class="flex items-center gap-1 text-muted-foreground group relative shrink-0 min-w-0 flex-1 sm:flex-initial">
<Calendar size={14} class="group-hover:text-primary transition-colors shrink-0" />
<input
type="datetime-local"
value={dateString()}
onInput={onDateInputChange}
onClick={(e) => (e.target as HTMLInputElement).showPicker?.()}
onKeyDown={(e) => {
if (e.key !== "Tab" && e.key !== "Escape") {
e.preventDefault();
}
}}
step="900"
class="bg-transparent border-none p-0 text-[11px] sm:text-xs font-medium focus:ring-0 focus:outline-none cursor-pointer hover:text-foreground appearance-none min-w-[125px] w-full"
title="Change Due Date"
/>
</div>
{/* Delete Button */}
<div class="flex items-center gap-1 shrink-0 ml-auto">
<Button
variant="ghost"
size="sm"
class="h-8 w-8 px-0 hover:bg-red-500/10 hover:text-red-500 text-muted-foreground transition-colors"
onClick={() => {
const taskId = props.task.id;
removeTask(taskId);
props.onClose();
toast.info("Task moved to trash", {
action: {
label: "Undo",
onClick: () => restoreTask(taskId)
}
});
}}
>
<Trash2 size={16} />
</Button>
</div>
</div>
<div class="h-px w-full bg-border mt-4" />
</div>
{/* Editor Content */}
<div class="flex-1 overflow-y-auto px-6 py-4 pb-20">
<TaskEditor
content={props.task.content}
onUpdate={updateTaskContent}
onEditorReady={setEditorInstance}
/>
</div>
</SheetContent>
</Sheet>
);
};
+99
View File
@@ -0,0 +1,99 @@
import { type Component, createEffect, untrack } from "solid-js";
import { createTiptapEditor } from "solid-tiptap";
import StarterKit from "@tiptap/starter-kit";
import Placeholder from "@tiptap/extension-placeholder";
import TaskList from "@tiptap/extension-task-list";
import TaskItem from "@tiptap/extension-task-item";
import { cn } from "@/lib/utils";
import { SlashCommands } from "@/lib/slash-command";
import { suggestion } from "@/lib/slash-renderer";
interface TaskEditorProps {
content?: string;
onUpdate: (html: string) => void;
onEditorReady?: (editor: any) => void;
editable?: boolean;
class?: string;
}
export const TaskEditor: Component<TaskEditorProps> = (props) => {
let editorRef: HTMLDivElement | undefined;
const editor = createTiptapEditor(() => ({
element: editorRef!,
extensions: [
StarterKit.configure({
heading: {
levels: [1, 2, 3],
},
}),
Placeholder.configure({
placeholder: "Type '/' for commands or start writing...",
emptyEditorClass: "is-editor-empty before:content-[attr(data-placeholder)] before:text-muted-foreground before:float-left before:pointer-events-none before:h-0",
}),
TaskList.configure({
HTMLAttributes: {
class: "not-prose pl-2",
},
}),
TaskItem.configure({
nested: true,
HTMLAttributes: {
class: "flex gap-2 items-start my-1",
},
}),
SlashCommands.configure({
suggestion,
}),
],
// Use untrack so the editor doesn't re-initialize when props.content changes
content: untrack(() => props.content) || "",
editable: props.editable ?? true,
onUpdate: ({ editor }) => {
props.onUpdate(editor.getHTML());
},
editorProps: {
attributes: {
class: cn(
"prose prose-sm dark:prose-invert max-w-none focus:outline-none min-h-[150px]",
"prose-headings:font-semibold prose-h1:text-2xl prose-h2:text-xl prose-h3:text-lg",
"prose-p:my-1 prose-headings:my-2 prose-ul:my-1 prose-ol:my-1",
"prose-pre:bg-muted prose-pre:rounded-lg prose-pre:p-4",
"prose-img:rounded-lg prose-img:border prose-img:border-border",
// Custom Task List Styling
"[&_ul[data-type='taskList']]:list-none [&_ul[data-type='taskList']]:p-0",
"[&_li[data-type='taskItem']]:flex [&_li[data-type='taskItem']]:items-start",
"[&_li[data-type='taskItem']>label]:mr-2 [&_li[data-type='taskItem']>label]:mt-0.5 [&_li[data-type='taskItem']>label]:select-none",
"[&_li[data-type='taskItem']>div]:flex-1",
props.class
),
},
},
}));
// Expose editor to parent
createEffect(() => {
const e = editor();
if (e && props.onEditorReady) {
props.onEditorReady(e);
}
});
// Sync content updates from outside if needed (but only if NOT focused to avoid jumps)
createEffect(() => {
const content = props.content;
const e = editor();
if (e && content !== undefined && !e.isFocused && content !== e.getHTML()) {
e.commands.setContent(content, { emitUpdate: false });
}
});
return (
<div
ref={editorRef}
class="w-full h-full cursor-text"
onClick={() => editor()?.chain().focus().run()}
/>
);
};
+48
View File
@@ -0,0 +1,48 @@
import { createSignal, createContext, useContext, createEffect, type ParentComponent } from "solid-js";
type Theme = "dark" | "light" | "system";
interface ThemeProviderState {
theme: () => Theme;
setTheme: (theme: Theme) => void;
}
const ThemeProviderContext = createContext<ThemeProviderState>();
export const ThemeProvider: ParentComponent = (props) => {
const [theme, setTheme] = createSignal<Theme>("system");
// Load from local storage on mount
const storedTheme = localStorage.getItem("tasgrid-theme") as Theme | null;
if (storedTheme) {
setTheme(storedTheme);
}
createEffect(() => {
const root = window.document.documentElement;
root.classList.remove("light", "dark");
const currentTheme = theme();
if (currentTheme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
root.classList.add(systemTheme);
} else {
root.classList.add(currentTheme);
}
localStorage.setItem("tasgrid-theme", currentTheme);
});
return (
<ThemeProviderContext.Provider value={{ theme, setTheme }}>
{props.children}
</ThemeProviderContext.Provider>
);
};
export const useTheme = () => {
const context = useContext(ThemeProviderContext);
if (!context) throw new Error("useTheme must be used within a ThemeProvider");
return context;
};
+21
View File
@@ -0,0 +1,21 @@
import { type Component } from "solid-js";
import { useTheme } from "./ThemeProvider";
import { Button } from "@/components/ui/button";
import { Moon, Sun } from "lucide-solid";
export const ThemeToggle: Component = () => {
const { theme, setTheme } = useTheme();
return (
<Button
variant="ghost"
size="icon"
onClick={() => setTheme(theme() === "dark" ? "light" : "dark")}
class="w-9 h-9 px-0"
>
<Sun class="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon class="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span class="sr-only">Toggle theme</span>
</Button>
);
};
+54
View File
@@ -0,0 +1,54 @@
import { splitProps, type JSX } from "solid-js"
import { Button as ButtonPrimitive } from "@kobalte/core/button"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline"
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9"
}
},
defaultVariants: {
variant: "default",
size: "default"
}
}
)
export interface ButtonProps extends VariantProps<typeof buttonVariants> {
class?: string
onClick?: (e: MouseEvent) => void
onMouseEnter?: (e: MouseEvent) => void
onMouseLeave?: (e: MouseEvent) => void
children?: JSX.Element
size?: "default" | "sm" | "lg" | "icon"
variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link"
type?: "button" | "submit" | "reset"
disabled?: boolean
}
const Button = (props: ButtonProps) => {
const [local, others] = splitProps(props, ["variant", "size", "class"])
return (
<ButtonPrimitive
class={cn(buttonVariants({ variant: local.variant, size: local.size }), local.class)}
{...others}
/>
)
}
export { Button, buttonVariants }
+23
View File
@@ -0,0 +1,23 @@
import { splitProps } from "solid-js"
import { Popover as PopoverPrimitive } from "@kobalte/core/popover"
import { cn } from "@/lib/utils"
const Popover = PopoverPrimitive
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverContent = (props: import("solid-js").ComponentProps<typeof PopoverPrimitive.Content>) => {
const [local, others] = splitProps(props, ["class"])
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
class={cn(
"z-[110] w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
local.class
)}
{...others}
/>
</PopoverPrimitive.Portal>
)
}
export { Popover, PopoverTrigger, PopoverContent }
+62
View File
@@ -0,0 +1,62 @@
import { splitProps, type ComponentProps } from "solid-js"
import { Select as SelectPrimitive } from "@kobalte/core/select"
import { cn } from "@/lib/utils"
import { Check, ChevronDown } from "lucide-solid"
const Select = SelectPrimitive
const SelectValue = SelectPrimitive.Value
const SelectTrigger = (props: ComponentProps<typeof SelectPrimitive.Trigger>) => {
const [local, others] = splitProps(props, ["class", "children"])
return (
<SelectPrimitive.Trigger
class={cn(
"flex h-9 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
local.class
)}
{...others}
>
{local.children}
<SelectPrimitive.Icon as={ChevronDown} class="h-4 w-4 opacity-50 transition-transform duration-200" />
</SelectPrimitive.Trigger>
)
}
const SelectContent = (props: ComponentProps<typeof SelectPrimitive.Content>) => {
const [local, others] = splitProps(props, ["class"])
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
class={cn(
"relative z-[200] min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md animate-in fade-in-80 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
local.class
)}
{...others}
>
<SelectPrimitive.Listbox class="p-1" />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
}
const SelectItem = (props: ComponentProps<typeof SelectPrimitive.Item>) => {
const [local, others] = splitProps(props, ["class", "children"])
return (
<SelectPrimitive.Item
class={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
local.class
)}
{...others}
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check class="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemLabel>{local.children}</SelectPrimitive.ItemLabel>
</SelectPrimitive.Item>
)
}
export { Select, SelectValue, SelectTrigger, SelectContent, SelectItem }
+98
View File
@@ -0,0 +1,98 @@
import { type Component, splitProps, type ComponentProps } from "solid-js";
import { Dialog as SheetPrimitive } from "@kobalte/core/dialog";
import { cn } from "@/lib/utils";
import { X } from "lucide-solid";
const Sheet = SheetPrimitive;
const SheetTrigger = SheetPrimitive.Trigger;
const SheetClose = SheetPrimitive.CloseButton;
const SheetOverlay: Component<ComponentProps<typeof SheetPrimitive.Overlay>> = (props) => {
const [local, others] = splitProps(props, ["class"]);
return (
<SheetPrimitive.Overlay
class={cn(
"fixed inset-0 z-[150] bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
local.class
)}
{...others}
/>
);
};
const SheetContent: Component<ComponentProps<typeof SheetPrimitive.Content>> = (props) => {
const [local, others] = splitProps(props, ["class", "children"]);
return (
<SheetPrimitive.Portal>
<SheetOverlay />
<SheetPrimitive.Content
class={cn(
"fixed z-[150] gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500 inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-xl",
local.class
)}
{...others}
>
{/* Close Button */}
<SheetPrimitive.CloseButton class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<X class="h-4 w-4" />
<span class="sr-only">Close</span>
</SheetPrimitive.CloseButton>
{local.children}
</SheetPrimitive.Content>
</SheetPrimitive.Portal>
);
};
const SheetHeader: Component<ComponentProps<"div">> = (props) => {
const [local, others] = splitProps(props, ["class"]);
return (
<div
class={cn("flex flex-col space-y-2 text-center sm:text-left", local.class)}
{...others}
/>
);
};
const SheetFooter: Component<ComponentProps<"div">> = (props) => {
const [local, others] = splitProps(props, ["class"]);
return (
<div
class={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
local.class
)}
{...others}
/>
);
};
const SheetTitle: Component<ComponentProps<typeof SheetPrimitive.Title>> = (props) => {
const [local, others] = splitProps(props, ["class"]);
return (
<SheetPrimitive.Title
class={cn("text-lg font-semibold text-foreground", local.class)}
{...others}
/>
);
};
const SheetDescription: Component<ComponentProps<typeof SheetPrimitive.Description>> = (props) => {
const [local, others] = splitProps(props, ["class"]);
return (
<SheetPrimitive.Description
class={cn("text-sm text-muted-foreground", local.class)}
{...others}
/>
);
};
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
};
+30
View File
@@ -0,0 +1,30 @@
import { Toaster as Sonner } from "solid-sonner"
import { useTheme } from "../ThemeProvider"
type ToasterProps = Parameters<typeof Sonner>[0]
const Toaster = (props: ToasterProps) => {
const { theme } = useTheme()
return (
<Sonner
theme={theme() as ToasterProps["theme"]}
class="toaster group"
position="top-center"
toastOptions={{
classes: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...props}
/>
)
}
export { Toaster }
+60
View File
@@ -0,0 +1,60 @@
import { splitProps, type ComponentProps } from "solid-js"
import { TextField as TextFieldPrimitive } from "@kobalte/core/text-field"
import { cn } from "@/lib/utils"
const TextField = TextFieldPrimitive
const TextFieldInput = (props: ComponentProps<"input">) => {
const [local, others] = splitProps(props, ["class", "type"])
return (
<TextFieldPrimitive.Input
type={local.type}
class={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
local.class
)}
{...(others as any)}
/>
)
}
const TextFieldLabel = (props: any) => {
const [local, others] = splitProps(props, ["class"])
return (
<TextFieldPrimitive.Label
class={cn(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
local.class
)}
{...others}
/>
)
}
const TextFieldDescription = (props: any) => {
const [local, others] = splitProps(props, ["class"])
return (
<TextFieldPrimitive.Description
class={cn("text-[0.8rem] text-muted-foreground", local.class)}
{...others}
/>
)
}
const TextFieldErrorMessage = (props: any) => {
const [local, others] = splitProps(props, ["class"])
return (
<TextFieldPrimitive.ErrorMessage
class={cn("text-[0.8rem] font-medium text-destructive", local.class)}
{...others}
/>
)
}
export {
TextField,
TextFieldInput,
TextFieldLabel,
TextFieldDescription,
TextFieldErrorMessage
}