Adds first version of the app. Local storage only.

This commit is contained in:
2026-09-01 14:44:49 -05:00
commit 9710f1ee5a
44 changed files with 5865 additions and 0 deletions
+93
View File
@@ -0,0 +1,93 @@
import { useMemo, useState } from 'react'
import { allDeviceCategories } from '../../domain/project'
import { useProjectStore } from '../../state/projectStore'
const NEW_CATEGORY_VALUE = '__new__'
/** A device-category <select> with a built-in "+ New category…" option that
* switches to a small inline text field, creates the category, and selects
* it — so adding a category never requires leaving the form you're in. */
export default function CategorySelect({
value,
onChange,
className = 'mt-1 w-full rounded border border-slate-300 px-2 py-1 text-sm',
}: {
value: string
onChange: (categoryId: string) => void
className?: string
}) {
const project = useProjectStore((s) => s.project)
const addCustomDeviceCategory = useProjectStore((s) => s.addCustomDeviceCategory)
const [creating, setCreating] = useState(false)
const [draftName, setDraftName] = useState('')
const categories = useMemo(
() => [...allDeviceCategories(project)].sort((a, b) => a.name.localeCompare(b.name)),
[project],
)
const commitNewCategory = () => {
const trimmed = draftName.trim()
if (trimmed) {
onChange(addCustomDeviceCategory(trimmed))
}
setCreating(false)
setDraftName('')
}
if (creating) {
return (
<div className="mt-1 flex gap-1.5">
<input
autoFocus
value={draftName}
onChange={(e) => setDraftName(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault()
commitNewCategory()
} else if (e.key === 'Escape') {
setCreating(false)
setDraftName('')
}
}}
placeholder="New category name"
className="w-full min-w-0 flex-1 rounded border border-slate-300 px-2 py-1 text-sm"
/>
<button
onClick={commitNewCategory}
className="shrink-0 rounded bg-indigo-600 px-2 text-xs font-medium text-white hover:bg-indigo-500"
>
Add
</button>
<button
onClick={() => {
setCreating(false)
setDraftName('')
}}
className="shrink-0 rounded px-2 text-xs text-slate-500 hover:bg-slate-100"
>
Cancel
</button>
</div>
)
}
return (
<select
value={value}
onChange={(e) => {
if (e.target.value === NEW_CATEGORY_VALUE) setCreating(true)
else onChange(e.target.value)
}}
className={className}
>
{categories.map((c) => (
<option key={c.id} value={c.id}>
{c.name}
</option>
))}
<option value={NEW_CATEGORY_VALUE}>+ New category…</option>
</select>
)
}