zeroUI

Objectives

A quest and objective tracker for React: a leaning header over a dark band, a slanted mark per objective, and a strike drawn through what is done.

Install

terminal
# once per project: where @zeroui items come frompnpm dlx shadcn@latest registry add @zeroui=https://zeroui.catforest.dev/r/{name}.jsonpnpm dlx shadcn@latest add @zeroui/objectives

Examples

A shift's objectives

Night shift

Done: Reach the rail yards
Current: Load the sealed crates2 / 5
To do: Cross the river before the gate shuts
To do: Hand over at the North gate
objectives-shift.tsx
import { useState } from "react";import { Button } from "@/components/zero/button";import {  Objective,  ObjectiveCount,  ObjectiveDescription,  type ObjectiveState,  Objectives,  ObjectivesHeader,  ObjectiveTitle,} from "@/components/zero/objectives";const steps = [  "Reach the rail yards",  "Load the sealed crates",  "Cross the river before the gate shuts",  "Hand over at the North gate",];export default function ObjectivesShift() {  const [step, setStep] = useState(1);  const [crates, setCrates] = useState(2);  const [failed, setFailed] = useState(false);  const state = (index: number): ObjectiveState =>    index < step      ? "done"      : index === step        ? failed          ? "failed"          : "active"        : "pending";  const advance = () => {    if (step === 1 && crates < 5) setCrates((n) => n + 1);    else setStep((s) => Math.min(steps.length, s + 1));  };  return (    <div className="grid justify-items-start gap-5">      <Objectives aria-label="Night shift">        <ObjectivesHeader>Night shift</ObjectivesHeader>        {steps.map((title, index) => (          <Objective key={title} state={state(index)}>            <ObjectiveTitle>{title}</ObjectiveTitle>            {index === 1 ? <ObjectiveCount>{crates} / 5</ObjectiveCount> : null}            {index === 2 && state(index) === "active" ? (              <ObjectiveDescription>                The gate shuts at 02:00.              </ObjectiveDescription>            ) : null}          </Objective>        ))}      </Objectives>      <div className="flex flex-wrap gap-3">        <Button size="sm" onClick={advance} disabled={failed}>          Next        </Button>        <Button          variant="danger"          size="sm"          onClick={() => setFailed(true)}          disabled={failed || step >= steps.length}        >          Fail it        </Button>        <Button          variant="ghost"          size="sm"          onClick={() => {            setStep(1);            setCrates(2);            setFailed(false);          }}        >          Start over        </Button>      </div>    </div>  );}

Behaviour

Each objective has a slanted mark: an empty frame while it waits, the accent frame while it is the one in hand, the filled accent with a check when done, red with a cross when it failed. A done or failed title is struck through, the line drawn across from the start. The band behind fades out to the right, so it can sit over a map or a scene.

The state is also said in words to screen readers ("Done:", "Current:"). ObjectiveCount sits at the line's end; ObjectiveDescription goes under the title.

Props

Objective

PropTypeDefaultDescription
state"pending" | "active" | "done" | "failed""pending"Its mark, and how its title is drawn.

Objectives, ObjectivesHeader, ObjectiveTitle, ObjectiveCount, ObjectiveDescription