Bootstrap ProjectReactRuntime package
This commit is contained in:
@@ -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
@@ -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>];
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user