added tags and tag manager
This commit is contained in:
+128
-28
@@ -1,15 +1,22 @@
|
||||
import { type Component, createSignal, createEffect, onCleanup, onMount } from "solid-js";
|
||||
import { type Component, createSignal, createEffect, onCleanup, onMount, For } from "solid-js";
|
||||
import { Search, Command, Clock, ArrowUpCircle, Plus } from "lucide-solid";
|
||||
import { addTask, calculateDateFromUrgency, calculateUrgencyFromDate } from "@/store";
|
||||
import { addTask, calculateDateFromUrgency, calculateUrgencyFromDate, store, upsertTagDefinition } from "@/store";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { TextField, TextFieldInput } from "@/components/ui/textfield";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select";
|
||||
import { TagPicker } from "@/components/TagPicker";
|
||||
|
||||
export const QuickEntry: Component = () => {
|
||||
// Diff tracking for reactive tags
|
||||
let lastParsedTags: string[] = [];
|
||||
|
||||
const [isOpen, setIsOpen] = createSignal(false);
|
||||
const [input, setInput] = createSignal("");
|
||||
const [priority, setPriority] = createSignal<string>("5");
|
||||
const [urgency, setUrgency] = createSignal<string>("5");
|
||||
const [tags, setTags] = createSignal<string[]>([]);
|
||||
|
||||
// Reactive shorthand parsing
|
||||
createEffect(() => {
|
||||
@@ -28,6 +35,41 @@ export const QuickEntry: Component = () => {
|
||||
onUrgencySimpleChange(val);
|
||||
}
|
||||
}
|
||||
|
||||
// Tag Shortcut Parsing: /t [tag name]:[optional value]
|
||||
// This is reactive. To avoid "prefix storm", only add if terminated by : or /
|
||||
// Tag Shortcut Parsing: /t [tag name]:[optional value]
|
||||
// Regex: /t, name (no : or /), lookahead for terminator (: or /), optional value group
|
||||
const tagRegex = /\/t\s*([^:/]+)(?=(?:[:/]))(?:[:](\d+))?/g;
|
||||
|
||||
const newParsedTags: string[] = [];
|
||||
let match;
|
||||
|
||||
while ((match = tagRegex.exec(text)) !== null) {
|
||||
const tagName = match[1].trim();
|
||||
if (tagName) {
|
||||
newParsedTags.push(tagName);
|
||||
if (match[2]) {
|
||||
const val = Math.min(10, Math.max(0, parseInt(match[2])));
|
||||
upsertTagDefinition(tagName, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const currentTags = [...tags()];
|
||||
// Determine changes
|
||||
const tagsToRemove = lastParsedTags.filter(t => !newParsedTags.includes(t));
|
||||
const tagsToAdd = newParsedTags.filter(t => !currentTags.includes(t));
|
||||
|
||||
// Apply changes
|
||||
let nextTags = currentTags.filter(t => !tagsToRemove.includes(t));
|
||||
nextTags = [...nextTags, ...tagsToAdd];
|
||||
|
||||
if (JSON.stringify(nextTags) !== JSON.stringify(currentTags)) {
|
||||
setTags(nextTags);
|
||||
}
|
||||
|
||||
lastParsedTags = newParsedTags;
|
||||
});
|
||||
|
||||
// Explicit date string for the input (YYYY-MM-DDTHH:mm)
|
||||
@@ -81,56 +123,98 @@ export const QuickEntry: Component = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const parseAndAdd = () => {
|
||||
const parseAndAdd = async () => {
|
||||
const text = input();
|
||||
if (!text || !priority() || !urgency()) return;
|
||||
|
||||
let finalPriority = parseInt(priority());
|
||||
let finalUrgency = parseInt(urgency());
|
||||
const finalTags = [...tags()];
|
||||
let title = text;
|
||||
|
||||
// Parse shorthand overrides if present
|
||||
// 1. Handle Priority / Urgency (Simple regexes as they don't have spaces)
|
||||
const pMatch = text.match(/\/p(\d+)/);
|
||||
if (pMatch) finalPriority = Math.min(10, Math.max(1, parseInt(pMatch[1])));
|
||||
if (pMatch) {
|
||||
finalPriority = Math.min(10, Math.max(1, parseInt(pMatch[1])));
|
||||
title = title.replace(pMatch[0], "");
|
||||
}
|
||||
|
||||
// Check for urgency override
|
||||
const uMatchNumeric = text.match(/\/u(\d+)/);
|
||||
if (uMatchNumeric) {
|
||||
finalUrgency = Math.min(10, Math.max(1, parseInt(uMatchNumeric[1])));
|
||||
title = title.replace(uMatchNumeric[0], "");
|
||||
}
|
||||
|
||||
// 2. Handle Tags: /t [name]:[value]
|
||||
// We look for /t, then name, then optional :value.
|
||||
// We split by /t to find all segments.
|
||||
const segments = text.split(/\/t\s*/);
|
||||
// segments[0] is the part before first /t
|
||||
title = segments[0];
|
||||
|
||||
for (let i = 1; i < segments.length; i++) {
|
||||
const seg = segments[i];
|
||||
// Name ends at first : or / (but / is handled by split) or end of string
|
||||
const firstColon = seg.indexOf(':');
|
||||
let tagName = "";
|
||||
let tagValue: number | undefined;
|
||||
let remainingText = "";
|
||||
|
||||
if (firstColon === -1) {
|
||||
// No colon in this segment. Segment might be "Tag Name Title" or "Tag Name /t"
|
||||
// But the user said / or : are endpoints.
|
||||
// So if no colon, the WHOLE segment is the tag name?
|
||||
// Wait, if I type "/t Tag Title", the user said / and : are endpoints.
|
||||
// So if neither is there, the tag continues to end of string?
|
||||
// Actually, let's treat the FIRST SPACE after some non-space text as a fallback?
|
||||
// No, user specifically said: "It should allow spaces in this particular parse... use / or : as end points."
|
||||
// So if I type "/t Tag Name", it IS the tag name.
|
||||
tagName = seg.trim();
|
||||
} else {
|
||||
tagName = seg.slice(0, firstColon).trim();
|
||||
const afterColon = seg.slice(firstColon + 1);
|
||||
const valueMatch = afterColon.match(/^\s*(\d+)/);
|
||||
|
||||
if (valueMatch) {
|
||||
tagValue = Math.min(10, Math.max(0, parseInt(valueMatch[1])));
|
||||
remainingText = afterColon.slice(valueMatch[0].length);
|
||||
} else {
|
||||
// Colon was just a separator for title
|
||||
remainingText = afterColon;
|
||||
}
|
||||
}
|
||||
|
||||
if (tagName) {
|
||||
if (!finalTags.includes(tagName)) {
|
||||
finalTags.push(tagName);
|
||||
}
|
||||
if (tagValue !== undefined) {
|
||||
upsertTagDefinition(tagName, tagValue);
|
||||
}
|
||||
}
|
||||
title += " " + remainingText;
|
||||
}
|
||||
|
||||
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 u = finalUrgency;
|
||||
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, {
|
||||
addTask(title.replace(/\s+/g, " ").trim(), {
|
||||
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: []
|
||||
dueDate: finalDueDate || undefined,
|
||||
tags: finalTags
|
||||
});
|
||||
|
||||
setInput("");
|
||||
setPriority("5");
|
||||
setUrgency("5");
|
||||
setTags([]);
|
||||
lastParsedTags = [];
|
||||
setDateString("");
|
||||
setIsOpen(false);
|
||||
};
|
||||
@@ -172,7 +256,7 @@ export const QuickEntry: Component = () => {
|
||||
<Select
|
||||
value={priority()}
|
||||
onChange={(val) => val && setPriority(val)}
|
||||
options={["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]}
|
||||
options={["10", "9", "8", "7", "6", "5", "4", "3", "2", "1"]}
|
||||
placeholder="Priority"
|
||||
itemComponent={(props) => (
|
||||
<SelectItem item={props.item}>
|
||||
@@ -235,6 +319,22 @@ export const QuickEntry: Component = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-4 pb-4 bg-muted/10 border-b border-border flex flex-wrap gap-2 items-center">
|
||||
<TagPicker selectedTags={tags()} onTagsChange={setTags} />
|
||||
<For each={tags()}>
|
||||
{(tag) => (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
class="h-7 px-2 text-xs gap-1 bg-background border border-border/50 cursor-pointer hover:bg-destructive/10 hover:border-destructive/30 group/tag transition-all"
|
||||
onClick={() => setTags(tags().filter(t => t !== tag))}
|
||||
>
|
||||
<div class={cn("w-1.5 h-1.5 rounded-full transition-colors group-hover/tag:bg-destructive", (store.tagDefinitions?.[tag] ?? 5) > 5 ? "bg-green-500" : (store.tagDefinitions?.[tag] ?? 5) < 5 ? "bg-red-500" : "bg-gray-400")} />
|
||||
{tag}
|
||||
</Badge>
|
||||
)}
|
||||
</For>
|
||||
</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} />
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { type Component, createSignal, For, createEffect } from "solid-js";
|
||||
import { store, upsertTagDefinition } from "@/store";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Hash, Plus } from "lucide-solid";
|
||||
|
||||
interface TagPickerProps {
|
||||
selectedTags: string[];
|
||||
onTagsChange: (tags: string[]) => void;
|
||||
}
|
||||
|
||||
export const TagPicker: Component<TagPickerProps> = (props) => {
|
||||
const [open, setOpen] = createSignal(false);
|
||||
const [selectedTagName, setSelectedTagName] = createSignal("");
|
||||
const [valueScore, setValueScore] = createSignal(5);
|
||||
|
||||
// Filter tags for the "Name" part
|
||||
const allTagNames = () => Object.keys(store.tagDefinitions || {}).sort();
|
||||
|
||||
// When a tag name is selected, update the value score to match its current definition
|
||||
createEffect(() => {
|
||||
const name = selectedTagName().trim();
|
||||
if (name) {
|
||||
const existingVal = store.tagDefinitions?.[name];
|
||||
if (existingVal !== undefined) {
|
||||
setValueScore(existingVal);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const handleApply = () => {
|
||||
const name = selectedTagName().trim();
|
||||
if (!name) return;
|
||||
|
||||
// update global definition
|
||||
upsertTagDefinition(name, valueScore());
|
||||
|
||||
// Toggle in current task selection if not already there
|
||||
const currentSelected = props.selectedTags;
|
||||
if (!currentSelected.includes(name)) {
|
||||
props.onTagsChange([...currentSelected, name]);
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
setSelectedTagName("");
|
||||
setValueScore(5);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover open={open()} onOpenChange={setOpen}>
|
||||
<PopoverTrigger as={Button} variant="ghost" size="sm" class="h-8 border-dashed flex gap-2 items-center text-muted-foreground hover:text-foreground">
|
||||
<Hash size={14} />
|
||||
<span>Add Tag</span>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="p-3 w-[260px] 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>
|
||||
<div class="relative">
|
||||
<input
|
||||
list="existing-tags"
|
||||
value={selectedTagName()}
|
||||
onInput={(e) => setSelectedTagName(e.currentTarget.value)}
|
||||
placeholder="Select or type name..."
|
||||
class="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"
|
||||
/>
|
||||
<datalist id="existing-tags">
|
||||
<For each={allTagNames()}>
|
||||
{(tag) => <option value={tag} />}
|
||||
</For>
|
||||
</datalist>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">Value (Influence)</label>
|
||||
<select
|
||||
value={valueScore()}
|
||||
onInput={(e) => setValueScore(parseInt(e.currentTarget.value))}
|
||||
class="flex h-9 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm ring-offset-background focus:outline-none focus:ring-1 focus:ring-ring appearance-none"
|
||||
>
|
||||
<For each={["10", "9", "8", "7", "6", "5", "4", "3", "2", "1", "0"]}>
|
||||
{(val) => <option value={val}>{val}</option>}
|
||||
</For>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<Button size="sm" class="w-full mt-1" onClick={handleApply}>
|
||||
<Plus size={14} class="mr-2" />
|
||||
{store.tagDefinitions?.[selectedTagName().trim()] !== undefined ? "Update & Add" : "Create & Add"}
|
||||
</Button>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,9 @@
|
||||
import { type Component, createMemo } from "solid-js";
|
||||
import { type Task, toggleTask, calculateUrgencyFromDate, setActiveTaskId } from "@/store";
|
||||
import { type Task, toggleTask, calculateUrgencyFromDate, setActiveTaskId, store } from "@/store";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CheckCircle2, Circle, Clock, ArrowUpCircle } from "lucide-solid";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { For } from "solid-js";
|
||||
|
||||
export const TaskCard: Component<{ task: Task }> = (props) => {
|
||||
const urgencyLevel = createMemo(() => calculateUrgencyFromDate(props.task.dueDate));
|
||||
@@ -52,6 +54,20 @@ export const TaskCard: Component<{ task: Task }> = (props) => {
|
||||
<span>P{props.task.priority}</span>
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
{props.task.tags && props.task.tags.length > 0 && (
|
||||
<div class="flex items-center gap-1">
|
||||
<For each={props.task.tags}>
|
||||
{(tag) => (
|
||||
<Badge variant="secondary" class="h-5 px-1.5 text-[10px] gap-1 bg-muted/50 border-border/50">
|
||||
<div class={cn("w-1.5 h-1.5 rounded-full", (store.tagDefinitions?.[tag] ?? 5) > 5 ? "bg-green-500" : (store.tagDefinitions?.[tag] ?? 5) < 5 ? "bg-red-500" : "bg-gray-400")} />
|
||||
{tag}
|
||||
</Badge>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Due Date + Urgency Slide-out */}
|
||||
<div class="relative group/date flex items-center h-6 cursor-help">
|
||||
{/* Due Date - Static Layer on Top */}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { type Component, createEffect, createSignal } from "solid-js";
|
||||
import { type Component, createEffect, createSignal, For } from "solid-js";
|
||||
import { Sheet, SheetContent } from "@/components/ui/sheet";
|
||||
import { type Task, removeTask, restoreTask, updateTask } from "@/store";
|
||||
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 { TagPicker } from "./TagPicker";
|
||||
import { Badge } from "./ui/badge";
|
||||
import { store } from "@/store";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { toast } from "solid-sonner";
|
||||
import { lazy, Suspense } from "solid-js";
|
||||
|
||||
@@ -61,6 +65,10 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const updateTags = (tags: string[]) => {
|
||||
updateTask(props.task.id, { tags });
|
||||
};
|
||||
|
||||
const onDateInputChange = (e: Event) => {
|
||||
const val = (e.currentTarget as HTMLInputElement).value;
|
||||
if (val) {
|
||||
@@ -106,7 +114,7 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
||||
<Select
|
||||
value={props.task.priority.toString()}
|
||||
onChange={(val: string | null) => val && updatePriority(val)}
|
||||
options={["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]}
|
||||
options={["10", "9", "8", "7", "6", "5", "4", "3", "2", "1"]}
|
||||
placeholder="Priority"
|
||||
itemComponent={(props: any) => <SelectItem item={props.item}>{props.item.rawValue}</SelectItem>}
|
||||
>
|
||||
@@ -138,6 +146,7 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
||||
</SelectTrigger>
|
||||
<SelectContent />
|
||||
</Select>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Commands Shortcut */}
|
||||
@@ -202,6 +211,31 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tags Row */}
|
||||
<div class="flex items-center gap-2 mt-4 overflow-x-auto no-scrollbar pb-1">
|
||||
<span class="text-[10px] uppercase font-bold tracking-wider text-muted-foreground/70 shrink-0">Tags</span>
|
||||
<div class="flex items-center gap-1.5 min-w-0">
|
||||
<TagPicker
|
||||
selectedTags={props.task.tags || []}
|
||||
onTagsChange={updateTags}
|
||||
/>
|
||||
<div class="flex items-center gap-1.5 flex-nowrap overflow-x-auto no-scrollbar">
|
||||
<For each={props.task.tags || []}>
|
||||
{(tag) => (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
class="h-7 px-2 text-xs gap-1 bg-muted/40 border-border/50 hover:bg-destructive/10 hover:border-destructive/30 cursor-pointer group/tag transition-all whitespace-nowrap"
|
||||
onClick={() => updateTags((props.task.tags || []).filter(t => t !== tag))}
|
||||
>
|
||||
<div class={cn("w-1.5 h-1.5 rounded-full transition-colors group-hover/tag:bg-destructive", (store.tagDefinitions?.[tag] ?? 5) > 5 ? "bg-green-500" : (store.tagDefinitions?.[tag] ?? 5) < 5 ? "bg-red-500" : "bg-gray-400")} />
|
||||
{tag}
|
||||
</Badge>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="h-px w-full bg-border mt-4" />
|
||||
</div>
|
||||
|
||||
@@ -216,6 +250,6 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
||||
</Suspense>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</Sheet >
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { splitProps, type Component, type ComponentProps } from "solid-js"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface BadgeProps
|
||||
extends ComponentProps<"div">,
|
||||
VariantProps<typeof badgeVariants> { }
|
||||
|
||||
const Badge: Component<BadgeProps> = (props) => {
|
||||
const [local, others] = splitProps(props, ["class", "variant"])
|
||||
return (
|
||||
<div class={cn(badgeVariants({ variant: local.variant }), local.class)} {...others} />
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
@@ -0,0 +1,90 @@
|
||||
import { type Component, type ComponentProps, splitProps } from "solid-js"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Search } from "lucide-solid"
|
||||
|
||||
const Command: Component<ComponentProps<"div">> = (props) => {
|
||||
const [local, others] = splitProps(props, ["class"])
|
||||
return (
|
||||
<div
|
||||
class={cn(
|
||||
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
||||
local.class
|
||||
)}
|
||||
{...others}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const CommandInput: Component<ComponentProps<"input"> & { onValueChange?: (value: string) => void }> = (props) => {
|
||||
const [local, others] = splitProps(props, ["class", "onValueChange"])
|
||||
return (
|
||||
<div class="flex items-center border-b px-3">
|
||||
<Search class="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<input
|
||||
class={cn(
|
||||
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||
local.class
|
||||
)}
|
||||
onInput={(e) => local.onValueChange?.(e.currentTarget.value)}
|
||||
{...others}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const CommandList: Component<ComponentProps<"div">> = (props) => {
|
||||
const [local, others] = splitProps(props, ["class"])
|
||||
return (
|
||||
<div
|
||||
class={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", local.class)}
|
||||
{...others}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const CommandEmpty: Component<ComponentProps<"div">> = (props) => {
|
||||
return <div class="py-6 text-center text-sm" {...props} />
|
||||
}
|
||||
|
||||
const CommandGroup: Component<ComponentProps<"div"> & { heading?: string }> = (props) => {
|
||||
const [local, others] = splitProps(props, ["class", "heading"])
|
||||
return (
|
||||
<div
|
||||
class={cn(
|
||||
"overflow-hidden p-1 text-foreground",
|
||||
local.class
|
||||
)}
|
||||
{...others}
|
||||
>
|
||||
{local.heading && (
|
||||
<div class="px-2 py-1.5 text-xs font-medium text-muted-foreground">
|
||||
{local.heading}
|
||||
</div>
|
||||
)}
|
||||
{others.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const CommandItem: Component<ComponentProps<"div"> & { onSelect?: () => void, value?: string }> = (props) => {
|
||||
const [local, others] = splitProps(props, ["class", "onSelect", "value"])
|
||||
return (
|
||||
<div
|
||||
class={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 cursor-pointer",
|
||||
local.class
|
||||
)}
|
||||
onClick={local.onSelect}
|
||||
{...others}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
}
|
||||
@@ -11,7 +11,7 @@ const PopoverContent = (props: import("solid-js").ComponentProps<typeof PopoverP
|
||||
<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",
|
||||
"z-[200] 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}
|
||||
|
||||
Reference in New Issue
Block a user