initial commit

This commit is contained in:
Hunter
2026-11-01 13:23:44 -05:00
commit e03ea4f770
53 changed files with 10802 additions and 0 deletions

57
components/TagInput.tsx Normal file
View File

@@ -0,0 +1,57 @@
"use client";
import { useId, useState } from "react";
import { normalizeTags } from "@/lib/validate";
type Props = { value: string[]; onChange: (tags: string[]) => void; suggestions: string[] };
/** Chip-style tag entry: Enter or comma adds, Backspace on empty removes the last. */
export default function TagInput({ value, onChange, suggestions }: Props) {
const [draft, setDraft] = useState("");
const listId = useId();
function commit(text: string) {
if (!text.trim()) return;
onChange(normalizeTags([...value, ...text.split(",")]));
setDraft("");
}
return (
<div className="tag-input">
{value.map((tag) => (
<span key={tag} className="chip">
{tag}
<button type="button" aria-label={`remove ${tag}`} onClick={() => onChange(value.filter((t) => t !== tag))}>
×
</button>
</span>
))}
<input
list={listId}
value={draft}
placeholder={value.length ? "" : "add tags…"}
onChange={(e) => {
const v = e.target.value;
if (v.endsWith(",")) commit(v);
else setDraft(v);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
commit(draft);
} else if (e.key === "Backspace" && !draft && value.length) {
onChange(value.slice(0, -1));
}
}}
onBlur={() => commit(draft)}
/>
<datalist id={listId}>
{suggestions
.filter((s) => !value.includes(s))
.map((s) => (
<option key={s} value={s} />
))}
</datalist>
</div>
);
}