chore(repo): initialize planarchy workspace

This commit is contained in:
2026-03-14 14:31:09 +01:00
commit dd55d0e78b
769 changed files with 166461 additions and 0 deletions
+164
View File
@@ -0,0 +1,164 @@
"use client";
import { useRef, useState } from "react";
import type { RoleWithResourceCount } from "@planarchy/shared";
import { trpc } from "~/lib/trpc/client.js";
import { useFocusTrap } from "~/hooks/useFocusTrap.js";
const PRESET_COLORS = [
"#6366f1", "#8b5cf6", "#ec4899", "#ef4444", "#f97316",
"#eab308", "#22c55e", "#14b8a6", "#06b6d4", "#3b82f6",
];
interface RoleModalProps {
role?: RoleWithResourceCount | null;
onClose: () => void;
onSuccess: () => void;
}
export function RoleModal({ role, onClose, onSuccess }: RoleModalProps) {
const isEditing = Boolean(role);
const [name, setName] = useState(role?.name ?? "");
const [description, setDescription] = useState(role?.description ?? "");
const [color, setColor] = useState(role?.color ?? PRESET_COLORS[0]!);
const [serverError, setServerError] = useState<string | null>(null);
const panelRef = useRef<HTMLDivElement>(null);
useFocusTrap(panelRef, true);
const utils = trpc.useUtils();
const createMutation = trpc.role.create.useMutation({
onSuccess: async () => {
await utils.role.list.invalidate();
onSuccess();
},
onError: (err) => setServerError(err.message),
});
const updateMutation = trpc.role.update.useMutation({
onSuccess: async () => {
await utils.role.list.invalidate();
onSuccess();
},
onError: (err) => setServerError(err.message),
});
const isPending = createMutation.isPending || updateMutation.isPending;
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setServerError(null);
if (!name.trim()) {
setServerError("Name is required.");
return;
}
const payload = {
name: name.trim(),
description: description.trim() || undefined,
color: color || undefined,
};
if (isEditing && role) {
updateMutation.mutate({ id: role.id, data: payload });
} else {
createMutation.mutate(payload);
}
}
const inputClass = "w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-500 text-sm";
const labelClass = "block text-sm font-medium text-gray-700 mb-1";
return (
<div
className="fixed inset-0 bg-black/50 z-50 flex items-start justify-center overflow-y-auto py-8"
onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
>
<div
ref={panelRef}
className="bg-white rounded-xl shadow-2xl w-full max-w-md mx-4"
onKeyDown={(e) => { if (e.key === "Escape") onClose(); }}
>
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200">
<h2 className="text-lg font-semibold text-gray-900">
{isEditing ? "Edit Role" : "New Role"}
</h2>
<button type="button" onClick={onClose} className="text-gray-400 hover:text-gray-600 text-xl leading-none">&times;</button>
</div>
<form onSubmit={handleSubmit} className="px-6 py-5 space-y-4">
<div>
<label className={labelClass}>Name <span className="text-red-500">*</span></label>
<input
type="text"
value={name}
onChange={(e) => { setName(e.target.value); setServerError(null); }}
placeholder="e.g. 3D Artist"
className={inputClass}
maxLength={100}
required
/>
</div>
<div>
<label className={labelClass}>Description</label>
<input
type="text"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Optional description"
className={inputClass}
maxLength={500}
/>
</div>
<div>
<label className={labelClass}>Color</label>
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full border-2 border-gray-200 flex-shrink-0" style={{ backgroundColor: color }} />
<div className="flex flex-wrap gap-2 flex-1">
{PRESET_COLORS.map((c) => (
<button
key={c}
type="button"
onClick={() => setColor(c)}
className="w-6 h-6 rounded-full border-2 transition-transform hover:scale-110"
style={{
backgroundColor: c,
borderColor: color === c ? "#1f2937" : "transparent",
}}
/>
))}
</div>
<input
type="color"
value={color}
onChange={(e) => setColor(e.target.value)}
className="w-8 h-8 rounded cursor-pointer border border-gray-300"
title="Custom color"
/>
</div>
</div>
{serverError && (
<div className="rounded-lg bg-red-50 border border-red-200 px-4 py-3 text-sm text-red-700">
{serverError}
</div>
)}
<div className="flex items-center justify-end gap-3 pt-2">
<button type="button" onClick={onClose} disabled={isPending} className="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900 disabled:opacity-50">
Cancel
</button>
<button type="submit" disabled={isPending} className="px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 text-sm font-medium disabled:opacity-50">
{isPending ? "Saving…" : "Save"}
</button>
</div>
</form>
</div>
</div>
);
}