Bootstrap ProjectReactRuntime package

This commit is contained in:
Quixos Todo bootstrap
2026-09-05 10:18:09 -07:00
commit 9bbed0910e
20 changed files with 1452 additions and 0 deletions
+185
View File
@@ -0,0 +1,185 @@
import { useState } from "react";
import {
h,
invokeCapability,
useLiveField,
type LiveFieldProp,
type ObjectRef,
type ReactComponentImplementationProps,
} from "@quixos/web-studio-react-runtime";
import {
TodoComponent,
type TodoComponentProps,
type TodoReactComponentAction,
} from "./gen/quixos.todo.TodoReactComponent.react.js";
import type { ProjectReactComponentRenderProps } from "./gen/quixos.todo.ProjectReactComponent.react.js";
type TaskRef = TodoComponentProps["forObject"];
type ProjectCaminoProps = {
project_id: ObjectRef<"atom:project">;
name: LiveFieldProp<string>;
tasks: TaskRef[];
};
type ProjectComponentProps = ReactComponentImplementationProps<
ProjectCaminoProps,
ProjectReactComponentRenderProps,
Record<string, never>
>;
const colors = {
ink: "#28362d",
muted: "#72786f",
cream: "#fffdf5",
paper: "#f6f0df",
line: "#d9d0ba",
moss: "#5f7f63",
honey: "#d99b3d",
};
export default function ProjectComponent({ camino, render }: ProjectComponentProps) {
const [name, setName] = useLiveField(camino.name);
const [selectedTask, setSelectedTask] = useState<TaskRef | null>(null);
const [newTaskTitle, setNewTaskTitle] = useState("");
const [addingTask, setAddingTask] = useState(false);
const [addTaskError, setAddTaskError] = useState("");
const handleTaskAction = (task: TaskRef, action: TodoReactComponentAction) => {
if (action.type === "SELECT") setSelectedTask(task);
};
const addTask = async () => {
if (addingTask || !newTaskTitle.trim()) return;
setAddingTask(true);
setAddTaskError("");
try {
await invokeCapability(
camino.project_id,
"interface:project-actions@1",
"operation:project-actions:add-task",
newTaskTitle,
);
setNewTaskTitle("");
} catch (error) {
setAddTaskError(error instanceof Error ? error.message : String(error));
} finally {
setAddingTask(false);
}
};
return (
<section style={{
display: "grid",
gridTemplateRows: "auto auto minmax(0, 1fr)",
gap: "12px",
width: "100%",
height: "100%",
minHeight: 0,
padding: render.compact ? "11px" : "14px",
color: colors.ink,
fontFamily: "Avenir Next, Nunito, ui-sans-serif, system-ui, sans-serif",
}}>
<header style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: "12px" }}>
<div style={{ display: "grid", gap: "2px", minWidth: 0, flex: 1 }}>
<input
aria-label="Project name"
value={name}
onInput={(event) => void setName(event.currentTarget.value)}
style={{
minWidth: 0,
width: "100%",
border: "none",
borderBottom: `1px solid ${colors.line}`,
background: "transparent",
color: colors.ink,
fontFamily: "Georgia, ui-serif, serif",
fontSize: render.compact ? "17px" : "22px",
fontWeight: 700,
padding: "3px 0 5px",
}}
/>
<span style={{ color: colors.muted, fontSize: "11px" }}>A little clearing for useful things.</span>
</div>
<span style={{
flex: "0 0 auto",
border: `1px solid ${colors.line}`,
borderRadius: "8px",
background: colors.paper,
color: colors.moss,
fontSize: "11px",
fontWeight: 800,
padding: "5px 8px",
}}>
{camino.tasks.length} {camino.tasks.length === 1 ? "task" : "tasks"}
</span>
</header>
<form
onSubmit={(event) => {
event.preventDefault();
void addTask();
}}
style={{ display: "grid", gridTemplateColumns: "minmax(0, 1fr) auto", gap: "8px" }}
>
<input
aria-label="New task title"
value={newTaskTitle}
onInput={(event) => setNewTaskTitle(event.currentTarget.value)}
placeholder="What wants doing?"
disabled={addingTask}
style={{ minWidth: 0, border: `1px solid ${colors.line}`, borderRadius: "9px", background: "#fff", padding: "8px 10px" }}
/>
<button
type="submit"
disabled={addingTask || newTaskTitle.trim().length === 0}
style={{
border: `1px solid ${colors.moss}`,
borderRadius: "9px",
background: colors.moss,
color: "#fffdf5",
cursor: "pointer",
fontSize: "12px",
fontWeight: 750,
padding: "8px 12px",
}}
>
{addingTask ? "Planting…" : "Add task"}
</button>
{addTaskError ? <div role="alert" style={{ gridColumn: "1 / -1", color: "#9f4741", fontSize: "11px" }}>{addTaskError}</div> : null}
</form>
<div style={{
display: "grid",
alignContent: "start",
gap: "9px",
minHeight: 0,
overflow: "auto",
paddingRight: "3px",
}}>
{camino.tasks.length === 0 ? (
<div style={{
display: "grid",
placeItems: "center",
minHeight: render.compact ? "80px" : "140px",
border: `1px dashed ${colors.line}`,
borderRadius: "12px",
background: colors.paper,
color: colors.muted,
fontSize: "12px",
textAlign: "center",
}}>
Your project is quiet. Add the first task when it arrives.
</div>
) : camino.tasks.map((task) => (
<TodoComponent
key={task}
forObject={task}
selected={selectedTask === task}
compact={render.compact ?? true}
onAction={(action) => handleTaskAction(task, action)}
fallback={<div style={{ color: colors.muted, fontSize: "11px" }}>Waking task…</div>}
/>
))}
</div>
</section>
);
}