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
@@ -0,0 +1,26 @@
// @generated by camino-codegen-ts-runtime. Do not edit.
import {
createWebStudioComponent,
type ReactComponentHostProps,
type ObjectRef,
} from "@quixos/web-studio-react-runtime";
export type ProjectReactComponentRenderProps = {
"compact"?: boolean;
};
export type Empty = {
};
export type ProjectComponentProps = {
forObject: ObjectRef<"atom:project">;
} & ProjectReactComponentRenderProps & ReactComponentHostProps<Record<string, never>>;
export const ProjectComponent = createWebStudioComponent<
ObjectRef<"atom:project">,
ProjectReactComponentRenderProps,
Record<string, never>
>({
expectedAtomId: "atom:project",
});
@@ -0,0 +1,29 @@
// @generated by camino-codegen-ts-runtime. Do not edit.
import {
createWebStudioComponent,
type ReactComponentHostProps,
type ObjectRef,
} from "@quixos/web-studio-react-runtime";
export type TodoReactComponentRenderProps = {
"selected"?: boolean;
"compact"?: boolean;
};
export type TodoReactComponentAction = {
"type"?: TodoReactComponentActionType;
};
export type TodoReactComponentActionType = "TODO_REACT_COMPONENT_ACTION_TYPE_UNSPECIFIED" | "SELECT";
export type TodoComponentProps = {
forObject: ObjectRef<"atom:task">;
} & TodoReactComponentRenderProps & ReactComponentHostProps<TodoReactComponentAction>;
export const TodoComponent = createWebStudioComponent<
ObjectRef<"atom:task">,
TodoReactComponentRenderProps,
TodoReactComponentAction
>({
expectedAtomId: "atom:task",
});
+54
View File
@@ -0,0 +1,54 @@
declare module "@quixos/web-studio-react-runtime" {
export type ObjectRef<AtomId extends string> = string & {
readonly $quixosAtom: AtomId;
};
export type LiveFieldProp<T> = {
value: T;
source: {
objectId: string;
slotId: string;
valueType?: string;
storagePolicy?: string;
revision?: string | number | bigint;
crdtSnapshot?: {
type: string;
encoding: string;
payload: string;
};
};
};
export type ReactComponentHostProps<Action> = {
onAction?: (action: Action) => void;
fallback?: unknown;
className?: string;
style?: Record<string, string | number | undefined>;
onError?: (error: Error) => void;
};
export type ReactComponentImplementationProps<CaminoProps, RenderProps, Action> = {
camino: CaminoProps;
render: RenderProps;
dispatch: (action: Action) => void;
};
export const createWebStudioComponent: <
ForObject extends ObjectRef<string>, RenderProps extends object, Action,
>(config: { expectedAtomId: string }) => (props: {
forObject: ForObject;
} & RenderProps & ReactComponentHostProps<Action>) => any;
export const invokeCapability: <Result = unknown>(
objectId: string,
interfaceRevisionId: string,
operationId: string,
value?: unknown,
) => Promise<Result>;
export const h: (...args: any[]) => any;
export const useLiveField: <T>(
field: LiveFieldProp<T>,
options?: {
reconcileRegister?: (state: {
confirmed: T;
optimistic: T;
pending: boolean;
}) => T;
},
) => [T, (value: T) => void | Promise<void>];
}
+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>
);
}
+26
View File
@@ -0,0 +1,26 @@
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { derived, type RuntimeContext } from "@quixos/camino-package-runtime";
const dirname = path.dirname(fileURLToPath(import.meta.url));
const componentSourcePath = path.join(dirname, "project-component.mjs");
export const source = async () => await fs.readFile(componentSourcePath, "utf8");
export const createComponent = (context: RuntimeContext) => context.objectId;
export const props = derived(async (context: RuntimeContext) => {
const projects = await context.edge("port:project-react:subject").resolve();
if (projects.length !== 1) {
throw new Error(`Project component ${context.objectId} has ${projects.length} subjects`);
}
return {
project_id: projects[0],
component_id: context.objectId,
name: await context.state<string>("port:project-react:name").live(),
tasks: await context.edge("port:project-react:tasks").resolve(),
};
});
export const stopWatch = () => null;
+13
View File
@@ -0,0 +1,13 @@
import { servePackageRuntime } from "@quixos/camino-package-runtime";
import { createComponent, props, source, stopWatch } from "./project-react.impl.js";
servePackageRuntime({
packageRevisionId: "package:project-react-runtime@1",
exports: {
"export:project-react-runtime:create-component": createComponent,
"export:project-react-runtime:source-get": source,
"export:project-react-runtime:props-get": props,
"export:project-react-runtime:props-watch": props,
"export:project-react-runtime:props-watch-stop": stopWatch,
},
});