Bevel

Engine telegraph

A ship's engine order telegraph: you set an order, the other side answers by moving its own pointer to match. Both show on one dial, so a pending order is visible at a glance. For autonomy levels, deploy stages and any setting that takes effect only once acknowledged.

Agent autonomy

Set how much the agent may do alone. It answers once its current step is done.

StopPlanAskActAuto

Confirmed: ask.

Installation

Add with the CLI

  1. Once per project, add the Bevel registry to components.json.

    components.json
    {
      "registries": {
        "@bevel": "https://bevel-ui.vercel.app/r/{name}.json"
      }
    }
  2. Add Engine telegraph. This brings in the Bevel foundation, installs @base-ui/react and cn, and merges the styles into your global CSS.

    Terminal
    npx shadcn@latest add @bevel/engine-telegraph
Install manually
  1. Install the dependencies.

    Terminal
    pnpm add @base-ui/react cn
  2. Add the Bevel foundation, if you haven't already. Light, depth, materials, motion and shape tokens, plus the utilities components use to apply them.

    styles/bevel.css
    /*
     * Bevel foundation. Every component builds on these tokens and utilities;
     * component stylesheets only add their own palettes and shapes.
     *
     * Layers:
     *   1. Light     one source, above and slightly in front of the screen.
     *   2. Depth     named elevations built from the light: recessed, flush,
     *                raised, pressed, object.
     *   3. Material  a surface recipe (leather, plastic, metal) that reads
     *                --bevel-material and --bevel-material-edge.
     *   4. Motion    two curves, a spring, and durations named by purpose.
     *   5. Shape     radii, stitching, and the type scale for amounts.
     */
    
    :root {
      /* 1. Light */
      --bevel-highlight: oklch(1 0 0 / 0.22);
      --bevel-highlight-strong: oklch(1 0 0 / 0.35);
      --bevel-shade: oklch(0 0 0 / 0.32);
      --bevel-shade-strong: oklch(0 0 0 / 0.5);
      --bevel-cast: oklch(0.25 0.03 50 / 0.22);
      /* Stamped into a surface: shaded on the upper inner edge, lit below. */
      --bevel-deboss: 0 -1px 0 oklch(0 0 0 / 0.45), 0 1px 0 oklch(1 0 0 / 0.12);
    
      /* 3. Material defaults; components and variants override these. */
      --bevel-material: oklch(0.47 0.085 52);
      --bevel-material-edge: oklch(0.3 0.06 44);
      --bevel-material-recess: oklch(0.2 0.008 260); /* neutral dark; materials set their own */
      --bevel-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.7' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .7 0 0 0 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E");
      --bevel-grain-size: 180px;
    
      /* 2. Depth */
      --bevel-depth-recessed:
        inset 0 1px 2px var(--bevel-shade-strong),
        0 1px 0 var(--bevel-highlight);
      --bevel-depth-flush:
        inset 0 1px 0 var(--bevel-highlight),
        inset 0 -1px 0 var(--bevel-shade);
      --bevel-depth-raised:
        inset 0 1px 0 var(--bevel-highlight),
        0 1px 0 var(--bevel-material-edge),
        0 2px 0 var(--bevel-material-edge),
        0 3px 5px var(--bevel-shade);
      --bevel-depth-pressed:
        inset 0 1px 2px var(--bevel-shade),
        0 1px 0 var(--bevel-highlight);
      /* A whole object resting on the page. */
      --bevel-depth-object:
        inset 0 1px 0 var(--bevel-highlight),
        inset 0 -1px 0 var(--bevel-shade),
        0 0 0 1px var(--bevel-material-edge),
        0 1px 1px var(--bevel-cast),
        0 6px 12px -2px var(--bevel-cast),
        0 18px 32px -8px var(--bevel-cast);
      /* The shadow under a loose object, for filter: drop-shadow(). */
      --bevel-depth-cast-sm: 0 4px 6px var(--bevel-cast);
      /* A thin object (a card) lying on another surface. */
      --bevel-depth-sheet:
        inset 0 1px 0 var(--bevel-highlight-strong),
        inset 0 0 0 1px oklch(0 0 0 / 0.12),
        0 -1px 2px oklch(0 0 0 / 0.18);
    
      /* 4. Motion */
      --bevel-ease-out: cubic-bezier(0.23, 1, 0.32, 1);
      --bevel-ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
      /* Underdamped spring, ~8% overshoot. Pure CSS; pair with --bevel-duration-move or longer. */
      --bevel-ease-spring: linear(0, 0.06, 0.202, 0.38, 0.561, 0.723, 0.856, 0.955, 1.023, 1.062, 1.081, 1.083, 1.075, 1.062, 1.047, 1.032, 1.019, 1.009, 1.002, 0.997, 0.994, 0.993, 0.993, 0.994, 1);
      --bevel-duration-press: 100ms; /* a control giving under a finger */
      --bevel-duration-small: 200ms; /* hover lifts, tabs, small state changes */
      --bevel-duration-move: 300ms; /* objects moving: cards, drawers */
      --bevel-duration-ambient: 520ms; /* whole-scene changes: lighting */
      --bevel-stagger: 30ms;
      --bevel-lift: 3px; /* how far an object rises on hover */
      --bevel-perspective: 30rem; /* viewing distance for parts that turn in 3D (covers, flaps) */
      /* Unchosen items in a group sit back in shadow. */
      --bevel-dim: 0.82;
    
      /* 5. Shape */
      --bevel-radius-xs: 0.25rem;
      --bevel-radius-sm: 0.5rem;
      --bevel-radius-md: 0.625rem;
      --bevel-radius-lg: 1rem;
      --bevel-radius-xl: 1.5rem;
      --bevel-stitch-width: 1.5px;
      --bevel-stitch-inset: 0.375rem;
      /* Amounts: the figure, its secondary parts (currency, cents), labels, fine print. */
      --bevel-amount: 2.25rem;
      --bevel-amount-minor: 1.25rem;
      --bevel-label: 0.75rem;
      --bevel-fine: 0.6875rem;
      --bevel-tracking-label: 0.08em; /* stamped labels */
      --bevel-tracking-print: 0.06em; /* printed marks on cards */
    
      /* Paper and ink */
      --bevel-paper: oklch(0.975 0.008 85);
      --bevel-paper-foreground: oklch(0.28 0.02 260);
      --bevel-paper-muted: oklch(0.48 0.015 260);
      --bevel-paper-rule: oklch(0.78 0.04 240 / 0.45);
      --bevel-paper-margin: oklch(0.68 0.14 25 / 0.45);
      --bevel-fibre: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='f'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9 .35' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .18 0 0 0 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23f)'/%3E%3C/svg%3E");
      /* Ink soaks unevenly: mostly solid with pale specks. Used as a mask. */
      --bevel-ink-texture: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='i'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -2.4 0 0 0 2.1'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23i)'/%3E%3C/svg%3E");
      --bevel-ink-red: oklch(0.52 0.19 27);
      --bevel-ink-blue: oklch(0.44 0.14 262);
      --bevel-ink-green: oklch(0.5 0.12 155);
      --bevel-ink-black: oklch(0.3 0.01 260);
      --bevel-ink-amber: oklch(0.55 0.13 65);
    
      /* Brass and ivory: instrument hardware and dial faces */
      --bevel-brass: oklch(0.76 0.11 82);
      --bevel-brass-edge: oklch(0.52 0.08 70);
      --bevel-ivory: oklch(0.95 0.025 90);
      --bevel-ivory-foreground: oklch(0.25 0.015 60);
      --bevel-needle: oklch(0.55 0.2 27);
    
      /* Desk stationery: legal pads, pens, pencils, exhibit stickers */
      --bevel-pad: oklch(0.95 0.075 98); /* yellow legal pad */
      --bevel-pad-foreground: oklch(0.27 0.03 255);
      --bevel-pad-rule: oklch(0.72 0.06 235 / 0.45);
      --bevel-pad-margin: oklch(0.62 0.16 25 / 0.55);
      --bevel-pen-red: oklch(0.52 0.2 27); /* correcting pen */
      --bevel-pen-blue: oklch(0.42 0.15 262); /* writing pen */
      --bevel-pencil: oklch(0.55 0.01 260); /* not written in ink yet */
      --bevel-sticker: oklch(0.9 0.12 95); /* exhibit sticker */
      --bevel-sticker-foreground: oklch(0.26 0.03 70);
      /* Handwriting. Bring a marker font (e.g. Permanent Marker) for the full effect; these are fallbacks. */
      --bevel-font-marker: "Permanent Marker", "Marker Felt", "Chalkboard SE", ui-rounded, "Arial Rounded MT Bold", system-ui, sans-serif;
    }
    
    .dark {
      --bevel-highlight: oklch(1 0 0 / 0.14);
      /* Paper under dim light, not inverted: it's still paper. */
      --bevel-paper: oklch(0.88 0.008 85);
      --bevel-ivory: oklch(0.86 0.022 90);
      --bevel-pad: oklch(0.86 0.07 98);
      --bevel-cast: oklch(0 0 0 / 0.55);
    }
    
    /* Depth */
    @utility bevel-recessed {
      background-color: var(--bevel-material-recess);
      box-shadow: var(--bevel-depth-recessed);
    }
    @utility bevel-flush {
      box-shadow: var(--bevel-depth-flush);
    }
    @utility bevel-raised {
      background-color: color-mix(in oklch, var(--bevel-material), white 7%);
      background-image: linear-gradient(to bottom, var(--bevel-highlight), transparent 60%);
      box-shadow: var(--bevel-depth-raised);
    }
    @utility bevel-pressed {
      background-color: color-mix(in oklch, var(--bevel-material), black 4%);
      background-image: none;
      box-shadow: var(--bevel-depth-pressed);
    }
    @utility bevel-object {
      box-shadow: var(--bevel-depth-object);
    }
    
    /* Materials */
    @utility bevel-leather {
      background-color: var(--bevel-material);
      background-image:
        var(--bevel-grain),
        linear-gradient(to bottom, var(--bevel-highlight), transparent 28%, transparent 72%, var(--bevel-shade));
      background-size: var(--bevel-grain-size) var(--bevel-grain-size), 100% 100%;
      background-blend-mode: soft-light, normal;
    }
    @utility bevel-plastic {
      background-color: var(--bevel-material);
      background-image:
        linear-gradient(115deg, var(--bevel-highlight-strong), transparent 40%),
        linear-gradient(to bottom, transparent 25%, oklch(0 0 0 / 0.45));
      box-shadow: var(--bevel-depth-sheet);
    }
    @utility bevel-metal {
      background-color: var(--bevel-material);
      background-image:
        linear-gradient(to bottom, var(--bevel-highlight-strong), transparent 45%, var(--bevel-shade) 100%),
        repeating-linear-gradient(90deg, oklch(1 0 0 / 0.06) 0 1px, transparent 1px 3px);
      box-shadow:
        inset 0 0 0 1px oklch(0 0 0 / 0.25),
        inset 0 1px 0 var(--bevel-highlight-strong);
    }
    
    /* Details */
    @utility bevel-deboss {
      text-shadow: var(--bevel-deboss);
    }
    @utility bevel-stitch {
      border-color: var(--bevel-stitch, oklch(0.88 0.05 82 / 0.8));
      border-style: dashed;
      border-width: var(--bevel-stitch-width);
      filter: drop-shadow(0 1px 0 var(--bevel-shade));
    }
    /* A lit fill sitting in a recess (meters, progress). Colour from --bevel-fill. */
    @utility bevel-fill {
      background-color: var(--bevel-fill);
      background-image: linear-gradient(to bottom, oklch(1 0 0 / 0.55), transparent 55%, oklch(0 0 0 / 0.18));
      box-shadow: 0 0 6px -1px var(--bevel-fill);
    }
    /* A small indicator light. Colour from --bevel-led. */
    @utility bevel-led {
      background-color: var(--bevel-led);
      background-image: radial-gradient(circle at 35% 30%, oklch(1 0 0 / 0.85), transparent 45%);
      box-shadow:
        0 0 6px var(--bevel-led),
        inset 0 -1px 1px oklch(0 0 0 / 0.35),
        0 1px 0 var(--bevel-highlight);
    }
    
    /* Paper: a sheet resting on the surface, with fibre in it. */
    @utility bevel-paper {
      background-color: var(--bevel-paper);
      background-image: var(--bevel-fibre);
      background-size: 240px 240px;
      color: var(--bevel-paper-foreground);
      box-shadow:
        0 1px 1px var(--bevel-cast),
        0 6px 14px -4px var(--bevel-cast);
    }
    /* Ink on paper: multiplies into the surface and soaks unevenly. Colour from --bevel-ink. */
    @utility bevel-ink {
      color: var(--bevel-ink, var(--bevel-ink-red));
      border-color: currentColor;
      mix-blend-mode: multiply;
      mask-image: var(--bevel-ink-texture);
      mask-size: 160px 160px;
    }
    /* Glass over an instrument window: one reflection from the light above. Put on an overlay element. */
    @utility bevel-glass {
      pointer-events: none;
      background-image: linear-gradient(165deg, var(--bevel-highlight-strong) 0%, transparent 38%, transparent 70%, var(--bevel-shade) 100%);
    }
    
    /* Material families: one per screen. Set on the screen's container. */
    [data-bevel-family="leather"] {
      --bevel-cast: oklch(0.25 0.03 50 / 0.22);
    }
    [data-bevel-family="paper"] {
      --bevel-cast: oklch(0.3 0.01 250 / 0.16);
    }
    .dark :is([data-bevel-family="leather"], [data-bevel-family="paper"]) {
      --bevel-cast: oklch(0 0 0 / 0.55);
    }
    
    /*
     * Ruled paper: rules every --bevel-rule-height and a margin line at
     * --bevel-rule-margin. Set --bevel-sheet for the paper colour. The ruling
     * scrolls with the content.
     */
    @utility bevel-ruled {
      background-color: var(--bevel-sheet, var(--bevel-paper));
      background-image:
        linear-gradient(
          to right,
          transparent calc(var(--bevel-rule-margin) - 1px),
          var(--bevel-sheet-margin, var(--bevel-paper-margin)) calc(var(--bevel-rule-margin) - 1px) calc(var(--bevel-rule-margin) + 1px),
          transparent calc(var(--bevel-rule-margin) + 1px)
        ),
        repeating-linear-gradient(
          to bottom,
          transparent 0 calc(var(--bevel-rule-height) - 1px),
          var(--bevel-sheet-rule, var(--bevel-paper-rule)) calc(var(--bevel-rule-height) - 1px) var(--bevel-rule-height)
        ),
        var(--bevel-fibre);
      background-size: 100% 100%, 100% 100%, 240px 240px;
      background-attachment: local;
      color: var(--bevel-paper-foreground);
      box-shadow:
        0 1px 1px var(--bevel-cast),
        0 6px 14px -4px var(--bevel-cast);
    }
  3. Copy engine-telegraph.tsx into components/ui.

    components/engine-telegraph.tsx
    "use client"
    
    import * as React from "react"
    import { Radio } from "@base-ui/react/radio"
    import { RadioGroup } from "@base-ui/react/radio-group"
    import { cn } from "cn"
    
    type Order = { value: string; label: string }
    
    type EngineTelegraphContextValue = {
      orders: Order[]
      register: (order: Order) => () => void
      value: string
      confirmed: string
      statusId: string
    }
    
    const EngineTelegraphContext = React.createContext<EngineTelegraphContextValue | null>(null)
    
    function useEngineTelegraph() {
      const context = React.useContext(EngineTelegraphContext)
      if (!context) throw new Error("EngineTelegraph parts must be inside <EngineTelegraph>")
      return context
    }
    
    /* Orders spread over the top of the dial, from -70° to 70°. */
    const SPREAD = 140
    
    function angleOf(orders: Order[], value: string) {
      const i = orders.findIndex((o) => o.value === value)
      if (i === -1 || orders.length < 2) return 0
      return -SPREAD / 2 + (SPREAD * i) / (orders.length - 1)
    }
    
    type EngineTelegraphProps = Omit<React.ComponentProps<"div">, "onChange"> & {
      /** The order you've set. */
      value: string
      onValueChange?: (value: string) => void
      /** The order the other side has acknowledged. Differs from value while an order is pending. */
      confirmed: string
    }
    
    /**
     * An engine order telegraph. You set an order; the other side (an agent,
     * a service, a teammate) answers by confirming it. Both pointers show on
     * one dial, so a pending order is visible at a glance.
     */
    function EngineTelegraph({ className, value, onValueChange, confirmed, children, ...props }: EngineTelegraphProps) {
      const [orders, setOrders] = React.useState<Order[]>([])
      const statusId = React.useId()
      const register = React.useCallback((order: Order) => {
        // Orders keep the order they mounted in, which is their order on the dial.
        setOrders((all) =>
          all.some((o) => o.value === order.value)
            ? all.map((o) => (o.value === order.value ? order : o))
            : [...all, order]
        )
        return () => setOrders((all) => all.filter((o) => o.value !== order.value))
      }, [])
      const context = React.useMemo(
        () => ({ orders, register, value, confirmed, statusId }),
        [orders, register, value, confirmed, statusId]
      )
      return (
        <EngineTelegraphContext value={context}>
          <div
            role="group"
            data-slot="engine-telegraph"
            data-pending={value !== confirmed ? "" : undefined}
            className={cn("flex w-full max-w-sm flex-col items-center gap-4", className)}
            {...props}
          >
            <RadioGroup
              value={value}
              onValueChange={(next) => onValueChange?.(next as string)}
              aria-describedby={statusId}
              className="contents"
            >
              {children}
            </RadioGroup>
          </div>
        </EngineTelegraphContext>
      )
    }
    
    /** The dial: your handle and the answering pointer, over the orders. */
    function EngineTelegraphDial({ className, ...props }: React.ComponentProps<"div">) {
      const { orders, value, confirmed } = useEngineTelegraph()
      return (
        <div
          aria-hidden
          data-slot="engine-telegraph-dial"
          className={cn("bevel-metal w-full rounded-t-full p-2 pb-0 shadow-(--bevel-depth-object)", className)}
          {...props}
        >
          <div className="relative overflow-hidden rounded-t-full bg-(--bevel-ivory) text-(--bevel-ivory-foreground) shadow-(--bevel-depth-recessed)">
            <svg viewBox="0 0 100 60" className="block w-full">
              {orders.map((order) => {
                const angle = angleOf(orders, order.value)
                return (
                  <g key={order.value} transform={`rotate(${angle} 50 56)`}>
                    <line x1="50" y1="6" x2="50" y2="12" stroke="currentColor" strokeWidth="0.8" />
                    <text x="50" y="19" textAnchor="middle" fontSize="4.6" fontWeight="700" fill="currentColor">
                      {order.label.toUpperCase()}
                    </text>
                  </g>
                )
              })}
              {/* The answer pointer: short and red, moved by the other side. */}
              <g
                className="origin-(--engine-telegraph-pivot) transition-[rotate] duration-(--bevel-duration-ambient) ease-(--bevel-ease-spring) motion-reduce:transition-none"
                style={{ rotate: `${angleOf(orders, confirmed)}deg` }}
              >
                <path d="M48.6 56 L50 26 L51.4 56 Z" fill="var(--engine-telegraph-answer)" />
              </g>
              {/* Your handle: long and dark, moved by you. */}
              <g
                className="origin-(--engine-telegraph-pivot) transition-[rotate] duration-(--bevel-duration-move) ease-(--bevel-ease-out) motion-reduce:transition-none"
                style={{ rotate: `${angleOf(orders, value)}deg` }}
              >
                <path d="M49.2 56 L50 22 L50.8 56 Z" fill="currentColor" />
                <rect x="47.5" y="21" width="5" height="3" rx="1" fill="currentColor" />
              </g>
              <circle cx="50" cy="56" r="4" fill="var(--bevel-brass)" stroke="var(--bevel-brass-edge)" strokeWidth="0.6" />
            </svg>
            <span className="bevel-glass absolute inset-0" />
          </div>
        </div>
      )
    }
    
    /** The orders you can give: a row of plates. */
    function EngineTelegraphOrders({ className, ...props }: React.ComponentProps<"div">) {
      return <div data-slot="engine-telegraph-orders" className={cn("flex flex-wrap justify-center gap-2", className)} {...props} />
    }
    
    type EngineTelegraphOrderProps = Omit<Radio.Root.Props, "value" | "children"> & {
      value: string
      /** Shown on the plate and the dial. Keep it to a word. */
      children: string
    }
    
    /** One order. A radio: arrow keys move between orders. */
    function EngineTelegraphOrder({ className, value, children, ...props }: EngineTelegraphOrderProps) {
      const { register, confirmed } = useEngineTelegraph()
      React.useEffect(() => register({ value, label: children }), [value, children, register])
      return (
        <Radio.Root
          value={value}
          data-slot="engine-telegraph-order"
          data-confirmed={confirmed === value ? "" : undefined}
          className={cn(
            "bevel-raised flex h-9 min-w-14 items-center justify-center rounded-(--bevel-radius-sm) px-3 text-xs font-bold tracking-(--bevel-tracking-label) text-(--bevel-ivory-foreground) uppercase outline-none select-none",
            "transition-[translate,box-shadow] duration-(--bevel-duration-press) ease-(--bevel-ease-out) motion-reduce:transition-none",
            "data-checked:translate-y-0.5 data-checked:bevel-pressed",
            "focus-visible:ring-3 focus-visible:ring-ring/60 data-disabled:cursor-not-allowed data-disabled:opacity-50",
            className
          )}
          {...props}
        >
          {children}
        </Radio.Root>
      )
    }
    
    /** Whether the other side has answered, in words. Announced when that changes. */
    function EngineTelegraphStatus({ className, ...props }: React.ComponentProps<"p">) {
      const { orders, value, confirmed, statusId } = useEngineTelegraph()
      const label = (v: string) => orders.find((o) => o.value === v)?.label ?? v
      return (
        <p
          id={statusId}
          role="status"
          data-slot="engine-telegraph-status"
          className={cn("flex items-center gap-2 text-sm text-muted-foreground", className)}
          {...props}
        >
          <span aria-hidden className="bevel-led size-2 rounded-full" />
          {value === confirmed
            ? `Confirmed: ${label(confirmed)}.`
            : `Ordered: ${label(value)}. Waiting for confirmation (now at ${label(confirmed)}).`}
        </p>
      )
    }
    
    export { EngineTelegraph, EngineTelegraphDial, EngineTelegraphOrders, EngineTelegraphOrder, EngineTelegraphStatus }
    export type { EngineTelegraphProps, EngineTelegraphOrderProps }
  4. Copy engine-telegraph.css into your styles.

    styles/engine-telegraph.css
    /*
     * Engine telegraph: a ship's order dial. The bridge sets the handle; the
     * engine room answers by moving its own pointer to match. Until the two
     * agree, the order is pending. Builds on bevel.css (brass, ivory, metal).
     */
    
    :root {
      --engine-telegraph-answer: oklch(0.55 0.2 27); /* the engine room's pointer */
      --engine-telegraph-pending: oklch(0.78 0.16 75);
      --engine-telegraph-agreed: oklch(0.72 0.16 150);
      --engine-telegraph-pivot: 50px 56px; /* the pointers' hub, in dial units */
    }
    
    [data-slot="engine-telegraph"] {
      --bevel-material: var(--bevel-brass);
      --bevel-material-edge: var(--bevel-brass-edge);
      --bevel-led: var(--engine-telegraph-agreed);
    }
    [data-slot="engine-telegraph"][data-pending] {
      --bevel-led: var(--engine-telegraph-pending);
    }
  5. Import the stylesheets after Tailwind in your global CSS, foundation first.

    globals.css
    @import "./bevel.css";
    @import "./engine-telegraph.css";

Anatomy

Each part is its own component. Leave out the ones you don't need.

Anatomy
<EngineTelegraph aria-label="Agent autonomy" value={value} onValueChange={setValue} confirmed={confirmed}>
  <EngineTelegraphDial />
  <EngineTelegraphOrders>
    <EngineTelegraphOrder value="ask">Ask</EngineTelegraphOrder>
    <EngineTelegraphOrder value="act">Act</EngineTelegraphOrder>
  </EngineTelegraphOrders>
  <EngineTelegraphStatus />
</EngineTelegraph>

Parts

PartRendersWhat it does
EngineTelegraphdiv (group) + RadioGroupvalue and onValueChange for your order; confirmed for the other side's answer. Give it an aria-label.
EngineTelegraphDialdiv (decorative)Your handle (dark, long) and the answer pointer (red, short) over the orders, in the order they're listed.
EngineTelegraphOrdersdivLays out the order plates.
EngineTelegraphOrderRadioOne order; its text is its label on the dial too. Arrow keys move between orders.
EngineTelegraphStatusp (status)“Ordered: Act. Waiting for confirmation”, or “Confirmed: Act.” Describes the group.