223 lines
7.4 KiB
TypeScript
223 lines
7.4 KiB
TypeScript
import {
|
|
h,
|
|
useLiveField,
|
|
type LiveFieldProp,
|
|
type ReactComponentImplementationProps,
|
|
} from "@quixos/web-studio-react-runtime";
|
|
import type {
|
|
TodoReactComponentAction,
|
|
TodoReactComponentRenderProps,
|
|
} from "./gen/quixos.todo.TodoReactComponent.react.js";
|
|
|
|
type TodoReactComponentCaminoProps = {
|
|
task_id: string;
|
|
title: LiveFieldProp<string>;
|
|
status: LiveFieldProp<string>;
|
|
priority: LiveFieldProp<string>;
|
|
notes: LiveFieldProp<string>;
|
|
due_date: LiveFieldProp<string>;
|
|
energy: LiveFieldProp<string>;
|
|
display_label: string;
|
|
};
|
|
|
|
declare global {
|
|
namespace JSX {
|
|
interface IntrinsicElements {
|
|
div: Record<string, unknown>;
|
|
button: Record<string, unknown>;
|
|
input: Record<string, unknown>;
|
|
label: Record<string, unknown>;
|
|
option: Record<string, unknown>;
|
|
select: Record<string, unknown>;
|
|
span: Record<string, unknown>;
|
|
strong: Record<string, unknown>;
|
|
textarea: Record<string, unknown>;
|
|
}
|
|
}
|
|
}
|
|
|
|
const palette = {
|
|
ink: "#28362d",
|
|
muted: "#6f776e",
|
|
cream: "#fffdf5",
|
|
line: "#d9d0ba",
|
|
moss: "#5f7f63",
|
|
honey: "#d99b3d",
|
|
berry: "#a9544f",
|
|
};
|
|
|
|
const statusTone = (status: string) => status === "DONE"
|
|
? palette.moss
|
|
: status === "ARCHIVED" ? palette.muted : "#52749a";
|
|
|
|
const fieldLabelStyle = {
|
|
display: "grid",
|
|
gap: "4px",
|
|
color: palette.muted,
|
|
fontSize: "10px",
|
|
fontWeight: 750,
|
|
letterSpacing: "0.08em",
|
|
textTransform: "uppercase" as const,
|
|
};
|
|
|
|
const controlStyle = {
|
|
minWidth: 0,
|
|
width: "100%",
|
|
border: `1px solid ${palette.line}`,
|
|
borderRadius: "7px",
|
|
background: "#fff",
|
|
color: palette.ink,
|
|
fontSize: "11px",
|
|
padding: "6px 8px",
|
|
};
|
|
|
|
type TodoComponentImplementationProps = ReactComponentImplementationProps<
|
|
TodoReactComponentCaminoProps,
|
|
TodoReactComponentRenderProps,
|
|
TodoReactComponentAction
|
|
>;
|
|
|
|
export default function TodoComponent({
|
|
camino: props,
|
|
render,
|
|
dispatch,
|
|
}: TodoComponentImplementationProps) {
|
|
const [title, setTitle] = useLiveField(props.title);
|
|
const [status, setStatus] = useLiveField(props.status);
|
|
const [priority, setPriority] = useLiveField(props.priority);
|
|
const [notes, setNotes] = useLiveField(props.notes);
|
|
const [dueDate, setDueDate] = useLiveField(props.due_date);
|
|
const [energy, setEnergy] = useLiveField(props.energy);
|
|
const done = status === "DONE";
|
|
|
|
return (
|
|
<div style={{
|
|
display: "grid",
|
|
gap: render.compact ? "9px" : "13px",
|
|
minWidth: 0,
|
|
height: "100%",
|
|
padding: render.compact ? "11px" : "15px",
|
|
border: render.selected ? `2px solid ${palette.honey}` : `1px solid ${palette.line}`,
|
|
borderRadius: "12px",
|
|
background: palette.cream,
|
|
boxShadow: render.selected ? "0 0 0 3px rgba(217,155,61,.14)" : "none",
|
|
color: palette.ink,
|
|
fontFamily: "Avenir Next, Nunito, ui-sans-serif, system-ui, sans-serif",
|
|
overflow: "auto",
|
|
}}>
|
|
<div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: "10px" }}>
|
|
<button
|
|
type="button"
|
|
aria-label={done ? "Reopen task" : "Complete task"}
|
|
onClick={() => void setStatus(done ? "OPEN" : "DONE")}
|
|
style={{
|
|
flex: "0 0 auto",
|
|
width: "26px",
|
|
height: "26px",
|
|
border: `1px solid ${done ? palette.moss : palette.line}`,
|
|
borderRadius: "8px",
|
|
background: done ? palette.moss : "#fff",
|
|
color: "#fff",
|
|
fontWeight: 900,
|
|
cursor: "pointer",
|
|
}}
|
|
>
|
|
{done ? "✓" : ""}
|
|
</button>
|
|
<div style={{ display: "grid", flex: 1, gap: "3px", minWidth: 0 }}>
|
|
<strong style={{
|
|
overflow: "hidden",
|
|
fontSize: render.compact ? "14px" : "17px",
|
|
lineHeight: 1.25,
|
|
textDecoration: done ? "line-through" : "none",
|
|
textOverflow: "ellipsis",
|
|
whiteSpace: "nowrap",
|
|
}}>
|
|
{title || "Untitled task"}
|
|
</strong>
|
|
<span style={{ color: statusTone(status), fontSize: "10px", fontWeight: 800, letterSpacing: ".08em" }}>
|
|
{status || "OPEN"} · {priority || "NORMAL"}
|
|
</span>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => dispatch({ type: "SELECT" })}
|
|
style={{
|
|
border: "none",
|
|
background: "transparent",
|
|
color: palette.muted,
|
|
cursor: "pointer",
|
|
fontSize: "12px",
|
|
}}
|
|
>
|
|
{render.selected ? "Pinned" : "Open"}
|
|
</button>
|
|
</div>
|
|
|
|
{!render.compact ? (
|
|
<div style={{ display: "contents" }}>
|
|
<label style={fieldLabelStyle}>
|
|
Title
|
|
<input
|
|
value={title}
|
|
onInput={(event: { currentTarget: HTMLInputElement }) => void setTitle(event.currentTarget.value)}
|
|
style={controlStyle}
|
|
/>
|
|
</label>
|
|
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(112px, 1fr))", gap: "8px" }}>
|
|
<label style={fieldLabelStyle}>
|
|
Status
|
|
<select style={controlStyle} value={status} onChange={(event: { currentTarget: HTMLSelectElement }) => void setStatus(event.currentTarget.value)}>
|
|
<option value="OPEN">Open</option>
|
|
<option value="DONE">Done</option>
|
|
<option value="ARCHIVED">Archived</option>
|
|
</select>
|
|
</label>
|
|
<label style={fieldLabelStyle}>
|
|
Priority
|
|
<select style={controlStyle} value={priority} onChange={(event: { currentTarget: HTMLSelectElement }) => void setPriority(event.currentTarget.value)}>
|
|
<option value="LOW">Low</option>
|
|
<option value="NORMAL">Normal</option>
|
|
<option value="HIGH">High</option>
|
|
</select>
|
|
</label>
|
|
<label style={fieldLabelStyle}>
|
|
Energy
|
|
<select style={controlStyle} value={energy} onChange={(event: { currentTarget: HTMLSelectElement }) => void setEnergy(event.currentTarget.value)}>
|
|
<option value="LOW">Low</option>
|
|
<option value="MEDIUM">Medium</option>
|
|
<option value="HIGH">High</option>
|
|
</select>
|
|
</label>
|
|
<label style={fieldLabelStyle}>
|
|
Due
|
|
<input style={controlStyle} type="date" value={dueDate} onInput={(event: { currentTarget: HTMLInputElement }) => void setDueDate(event.currentTarget.value)} />
|
|
</label>
|
|
</div>
|
|
<label style={fieldLabelStyle}>
|
|
Notes
|
|
<textarea
|
|
aria-label="Notes"
|
|
value={notes}
|
|
onInput={(event: { currentTarget: HTMLTextAreaElement }) => void setNotes(event.currentTarget.value)}
|
|
rows={4}
|
|
placeholder="Leave a trail for your future self…"
|
|
style={{ ...controlStyle, minHeight: "78px", resize: "vertical" }}
|
|
/>
|
|
</label>
|
|
</div>
|
|
) : (
|
|
<div style={{ display: "flex", flexWrap: "wrap", gap: "6px", color: palette.muted, fontSize: "11px" }}>
|
|
{dueDate ? <span>Due {dueDate}</span> : <span>No due date</span>}
|
|
<span>·</span>
|
|
<span>{energy || "MEDIUM"} energy</span>
|
|
</div>
|
|
)}
|
|
|
|
<div style={{ color: palette.muted, fontSize: "10px", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
|
{props.task_id}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|