# Reason glyph (Notchset): prompt.md (v2.0.0)

- id: `reason-glyph` · version 2.0.0 · component · free
- category: Data
- build: one set of files for every shadcn style
- install: `npx shadcn@latest add https://notchset.dev/r/reason-glyph.json`
- npm dependencies: none
- registry dependencies: utils, https://notchset.dev/r/notchset-foundation.json
- docs: https://notchset.dev/components/reason-glyph
- The install command carries everything this item needs (files, CSS, tokens, npm and registry dependencies). Prefer it to copying source by hand.

Five small data glyphs, each drawing the reason for a judging word: a value against its usual band (arc), a span against a window, readings in or out of their bands (marks), two-point alignment and a run of days. Fills a tile's band, or sits inline before its word in a list row; never without its word..

## Build it
- Stack: React 18.3 or 19, TypeScript, Tailwind CSS v4 utility classes on the foundation's roles, a components.json (imports use its aliases).
- Packages: none beyond React.
- Files: `components/ui/notchset/reason-glyph.tsx`.
- Registry dependencies, installed with it automatically: shadcn `utils` (cn), `notchset-foundation`.
- Builds: one set of files, the same for every shadcn style.
- Exports to keep: `ReasonGlyph`, `ReasonWord`, `arcAt`, `glyphMoved`, and every exported type.
- CSS: the install adds the notchset foundation (its roles, keyframes and motion switch, reading your shadcn tokens for colour) to your global stylesheet once. Nothing to import by hand.
- Re-running `add` (or `--overwrite`) re-applies those rules: put overrides in your own CSS, never in the installed rules.
- Colours: every part reads the foundation's roles (`--ns-canvas`, `--ns-ink`, `--ns-line`, `--ns-signal` …), which read your shadcn tokens (`--background`, `--foreground`, `--border`, `--destructive` …), plus the optional `--success` and `--warning`. Retheme on `:root` or any container; never add Tailwind colour classes inside the component.
- This item reads `--ns-panel`, `--ns-signal`.
- Retune it (on any ancestor) with the foundation's roles and hooks: `--ns-bad`, `--ns-bad-ink`, `--ns-band`, `--ns-band-edge`, `--ns-band-grade`, `--ns-band-optimal`, `--ns-beat`, `--ns-bezel`, `--ns-canvas`, `--ns-cat-1`, `--ns-cat-2`, `--ns-cat-3`, `--ns-cat-4`, `--ns-cat-5`, `--ns-cat-6`, `--ns-check`, `--ns-chip`, `--ns-control`, `--ns-control-pad`, `--ns-control-sm`, `--ns-deep`, `--ns-deep-ink`, `--ns-deep-ink-2`, `--ns-deep-line`, `--ns-dialog`, `--ns-dialog-lg`, `--ns-dialog-sm`, `--ns-ease`, `--ns-ease-in`, `--ns-ease-panel`, `--ns-ease-pop`, `--ns-field`, `--ns-flood-arc`, `--ns-flood-aurora`, `--ns-flood-behind-1`, `--ns-flood-behind-2`, `--ns-flood-bloom`, `--ns-flood-deep`, `--ns-flood-dial-bloom`, `--ns-flood-glass`, `--ns-flood-grain`, `--ns-flood-image`, `--ns-flood-ink`, `--ns-flood-ink-low`, `--ns-flood-light`, `--ns-flood-mid`, `--ns-flood-pale`, `--ns-flood-pane`, `--ns-flood-pane-edge`, `--ns-flood-pill-ink`, `--ns-flood-ribbon`, `--ns-flood-ribbon-core`, `--ns-flood-shadow`, `--ns-flood-sheen`, `--ns-flood-wash`, `--ns-font-body`, `--ns-font-display`, `--ns-font-mono`, `--ns-font-script`, `--ns-glass`, `--ns-glass-blur`, `--ns-glass-shadow`, `--ns-glow`, `--ns-grain`, `--ns-gutter`, `--ns-header`, `--ns-heat`, `--ns-hover`, `--ns-ink`, `--ns-ink-2`, `--ns-ink-3`, `--ns-ink-4`, `--ns-leading`, `--ns-lift`, `--ns-light-1`, `--ns-light-2`, `--ns-light-3`, `--ns-light-spread`, `--ns-light-strength`, `--ns-line`, `--ns-line-soft`, `--ns-lit-edge`, `--ns-menu-dy`, `--ns-menu-row`, `--ns-mesh`, `--ns-mesh-frost`, `--ns-ok`, `--ns-ok-ink`, `--ns-orb-body`, `--ns-orb-cool`, `--ns-orb-core`, `--ns-orb-dawn`, `--ns-orb-deep`, `--ns-orb-edge`, `--ns-orb-grain`, `--ns-orb-ground`, `--ns-orb-halo`, `--ns-orb-inner`, `--ns-orb-key`, `--ns-orb-lift`, `--ns-orb-limb`, `--ns-orb-limb-from`, `--ns-orb-rim`, `--ns-orb-skin-hi`, `--ns-orb-skin-lo`, `--ns-orb-skin-mid`, `--ns-orb-warm`, `--ns-panel`, `--ns-panel-w`, `--ns-prism-edge`, `--ns-prism-glint`, `--ns-prism-sheen`, `--ns-prism-tint`, `--ns-pulse`, `--ns-radius`, `--ns-radius-card`, `--ns-radius-full`, `--ns-radius-lg`, `--ns-radius-sm`, `--ns-radius-xl`, `--ns-raised`, `--ns-range`, `--ns-row`, `--ns-row-compact`, `--ns-segment`, `--ns-selected`, `--ns-series`, `--ns-series-core`, `--ns-series-lilac`, `--ns-series-tint`, `--ns-shadow-dialog`, `--ns-shadow-float`, `--ns-shadow-panel`, `--ns-sheet`, `--ns-sheet-bottom`, `--ns-side`, `--ns-signal`, `--ns-sleep-awake`, `--ns-sleep-deep`, `--ns-sleep-light`, `--ns-sleep-rem`, `--ns-space-1`, `--ns-space-2`, `--ns-space-3`, `--ns-space-4`, `--ns-space-5`, `--ns-space-6`, `--ns-space-8`, `--ns-space-section`, `--ns-stack-on`, `--ns-standing-critical`, `--ns-standing-in`, `--ns-standing-out`, `--ns-state-doing`, `--ns-state-done`, `--ns-state-review`, `--ns-state-todo`, `--ns-switch-h`, `--ns-switch-w`, `--ns-text-body`, `--ns-text-control`, `--ns-text-display`, `--ns-text-figure`, `--ns-text-headline`, `--ns-text-hero`, `--ns-text-lede`, `--ns-text-meta`, `--ns-text-page`, `--ns-text-record`, `--ns-text-small`, `--ns-text-strong`, `--ns-text-sub`, `--ns-text-title`, `--ns-tile-shadow`, `--ns-time-born`, `--ns-time-color`, `--ns-time-dialog`, `--ns-time-menu`, `--ns-time-page`, `--ns-time-panel`, `--ns-time-toast`, `--ns-tint`, `--ns-tint-ink`, `--ns-toast-bottom`, `--ns-toast-left`, `--ns-touch`, `--ns-tracking-display`, `--ns-tracking-headline`, `--ns-warn`, `--ns-warn-ink`, `--ns-wash-clay`, `--ns-wash-dusk`, `--ns-wash-graphite`, `--ns-wash-ink`, `--ns-wash-sage`, `--ns-wash-sand`, `--ns-weight-light`, `--ns-weight-medium`, `--ns-weight-regular`, `--ns-weight-strong`, `--ns-well`.
- Never add Tailwind colour classes inside the component.

```tsx
import { ReasonGlyph, ReasonWord, type Glyph } from "@/components/ui/notchset/reason-glyph";
```

## Parts
- A single export that takes props.

## Match the original
- Read `components/ui/notchset/reason-glyph.tsx` as the reference implementation before changing or recreating anything, and match it: sizes, colours per theme, motion timings, copy and behaviour.
- If you deviate (a prop you can't honour, a style you changed, a dependency you swapped), say so in your reply, part by part.
- Keep the accessibility contract, the keyboard map and the motion levels listed below.

## Use it when

### Not when

## Mistakes
- Recreating the component instead of installing it

## Usage

```tsx
"use client";
import { ReasonGlyph, ReasonWord, type Glyph } from "@/components/ui/notchset/reason-glyph";

/* the five glyphs, each with the word it explains; then the inline pair as list rows use it */
const SET: { word: string; glyph: Glyph }[] = [
  { word: "Above usual", glyph: { kind: "arc", value: 61, low: 47, high: 55, min: 30, max: 75 } },
  { word: "Shorter than usual", glyph: { kind: "span", usual: [0.2, 0.8], span: [0.3, 0.7] } },
  { word: "Worth watching", glyph: { kind: "marks", marks: [{ label: "Temperature", at: 1.5 }, { label: "Resting heart rate", at: 1.3 }, { label: "HRV", at: 0.4 }] } },
  { word: "Drifting 40 min", glyph: { kind: "align", offset: 0.6 } },
  { word: "3 days in a row", glyph: { kind: "days", days: [false, false, null, false, true, true, true] } },
];

export default function ReasonGlyphDemo() {
  return (
    <div className="flex w-full max-w-[640px] flex-col gap-8">
      <div className="grid grid-cols-5 gap-4 max-[600px]:grid-cols-2">
        {SET.map((s) => (
          <figure key={s.word} className="m-0 flex flex-col gap-2">
            <span className="block h-9">
              <ReasonGlyph glyph={s.glyph} />
            </span>
            <figcaption className="text-[length:var(--ns-text-small)] text-[color-mix(in_oklab,var(--ns-signal)_var(--ns-tint-ink),var(--ns-ink))]">{s.word}</figcaption>
          </figure>
        ))}
      </div>
      <ul className="m-0 flex list-none flex-col p-0">
        {[
          ["Vitamin D", "Due", { kind: "span", usual: [0, 1], span: [0, 1.2] }],
          ["Bedtime", "Drifting", { kind: "align", offset: 0.55 }],
          ["LDL", "Out of range", { kind: "arc", value: 128, low: 0, high: 99, min: 40, max: 160 }],
        ].map(([name, word, glyph]) => (
          <li key={name as string} className="flex min-h-11 items-center justify-between border-0 border-b-[0.5px] border-[var(--ns-line)] text-[length:var(--ns-text-small)]">
            <span className="text-[var(--ns-ink)]">{name as string}</span>
            <ReasonWord word={word as string} glyph={glyph as Glyph} className="text-[var(--ns-ink-2)]" />
          </li>
        ))}
      </ul>
    </div>
  );
}
```

## Props

| Prop | Type | Default | What it does |
|---|---|---|---|
| `glyph` | `Glyph` |  | Required. |
| `small` | `boolean` |  |  |

Full docs: https://notchset.dev/components/reason-glyph

## Customising
- Colours: every part reads the foundation's roles (`--ns-canvas`, `--ns-ink`, `--ns-line`, `--ns-signal` …), which read your shadcn tokens (`--background`, `--foreground`, `--border`, `--destructive` …), plus the optional `--success` and `--warning`. Retheme on `:root` or any container; never add Tailwind colour classes inside the component.
- Dark mode follows the `.dark` class or `data-theme="dark"` on an ancestor (the shadcn and next-themes conventions).
- Update later: Commit or stash local changes first (or copy the files), so the update can be undone. Read https://notchset.dev/r/changelog.json for the new versions: a major version, an entry marked breaking or a merge says what to change. Then re-run the install with `--overwrite`, on a branch. It replaces the component's files, the shared helper files they import, and the item's CSS rules in the global stylesheet, so review the whole diff (components, shared files, CSS), re-apply your own edits, and run the app's typecheck, tests and build.

## Accessibility contract (preserve when editing)

## Install

```bash
npx shadcn@latest add https://notchset.dev/r/reason-glyph.json
```

Or add the `@notchset` registry to components.json and run `npx shadcn@latest add @notchset/reason-glyph`. Every components.json style gets the same files.

## Continue the workflow

- [Baseline meter](https://notchset.dev/components/baseline-meter)
- [Draw history](https://notchset.dev/components/draw-history)
- [Glance tile](https://notchset.dev/components/glance-tile)

## Source

### components/ui/notchset/reason-glyph.tsx

```tsx
// Notchset reason-glyph 2.0.0 · https://notchset.dev/changelog
"use client";
import { cn } from "@/lib/utils";

/*
 * Reason glyph: a small drawing of the reason for a judging word, never decoration. A word that judges ("Due",
 * "Drifting", "Out of range", "Above usual") gets a glyph that shows why; a glyph never appears without its word.
 * Five shapes, all in the Signature's family, at the weight of a status line:
 *   arc    a shallow arc with the usual band drawn heavier and a dot where the reading sits (a value against usual)
 *   span   a faint window with a solid span over it (last night's sleep against the usual window; a draw's age
 *          against the time it stays fresh)
 *   marks  a few readings, each a band with a dot inside it (usual) or outside it (moved)
 *   align  two dots on a line, the usual (a ring) and the latest (filled), that meet when they align
 *   days   a run of days as dots, the ones that count filled (a streak of watch days, a week of ticks)
 * `ReasonGlyph` fills its box (a tile's visual band). `ReasonWord` is the inline pair for a list row: the glyph at
 * 36 × 14 and its word after it, so the word is always there; the pair reads as the word alone to a screen reader.
 *
 *   <ReasonGlyph glyph={{ kind: "arc", value: 61, low: 47, high: 55, min: 30, max: 75 }} />
 *   <ReasonWord word="Due" glyph={{ kind: "span", usual: [0, 1], span: [0, 1.1] }} />
 */
type Glyph =
  /** where a reading sits on a scale min..max, against its usual band low..high */
  | { kind: "arc"; value: number; low: number; high: number; min: number; max: number }
  /** a 0–1 axis: the window (the usual, or how long a draw stays fresh) and the span over it; past 1 runs out */
  | { kind: "span"; usual: readonly [number, number]; span: readonly [number, number] }
  /** small readings: `at` is where each sits against its band (−1 and 1 are the band's edges); outside is moved */
  | { kind: "marks"; marks: readonly { label: string; at: number }[] }
  /** two dots on a line: the usual (a ring, centred) and the latest (filled), `offset` −1 to 1 apart */
  | { kind: "align"; offset: number }
  /** a run of days, oldest first: true counts (filled), false doesn't, null is no data (a faint ring) */
  | { kind: "days"; days: readonly (boolean | null)[] };

const clamp = (v: number, lo = 0, hi = 1) => Math.max(lo, Math.min(hi, v));
const soft = (p: number) => `color-mix(in oklab, var(--ns-signal) ${p}%, transparent)`;
/** the arc's points on a 200 × 34 box, 0..1 left to right */
const arcPt = (t: number): [number, number] => [4 + t * 192, 30 - Math.sin(t * Math.PI) * 22];

/** where an arc's reading sits, 0..1 along it (exported for tests) */
function arcAt(g: Extract<Glyph, { kind: "arc" }>) {
  return clamp((g.value - g.min) / (g.max - g.min || 1));
}

/** whether a glyph shows something moved (outside its band, out of its window, apart) */
function glyphMoved(g: Glyph): boolean {
  if (g.kind === "arc") return g.value < g.low || g.value > g.high;
  if (g.kind === "span") return g.span[0] < g.usual[0] || g.span[1] > g.usual[1];
  if (g.kind === "marks") return g.marks.some((m) => Math.abs(m.at) > 1);
  if (g.kind === "align") return Math.abs(g.offset) > 0.25;
  return g.days.some(Boolean);
}

function ReasonGlyph({ glyph: g, small, className }: { glyph: Glyph; small?: boolean | undefined; className?: string | undefined }) {
  const box = cn("relative block h-full w-full", className);
  const moved = glyphMoved(g) || undefined;
  if (g.kind === "arc") {
    const seg = (a: number, b: number) =>
      Array.from({ length: 25 }, (_, i) => arcPt(a + ((b - a) * i) / 24))
        .map((p, i) => `${i ? "L" : "M"}${p[0].toFixed(1)} ${p[1].toFixed(1)}`)
        .join("");
    const f = (x: number) => clamp((x - g.min) / (g.max - g.min || 1));
    const [x, y] = arcPt(arcAt(g));
    return (
      <span aria-hidden data-slot="reason-glyph" data-kind="arc" data-moved={moved} className={box}>
        <svg viewBox="0 0 200 34" preserveAspectRatio="none" className="absolute inset-0 h-full w-full overflow-visible">
          <path d={seg(0, 1)} fill="none" stroke={soft(30)} strokeWidth={1} vectorEffect="non-scaling-stroke" />
          <path d={seg(f(g.low), f(g.high))} fill="none" stroke={soft(70)} strokeWidth={small ? 2 : 3} strokeLinecap="round" vectorEffect="non-scaling-stroke" />
        </svg>
        <span className={cn("absolute -translate-x-1/2 -translate-y-1/2 rounded-full bg-[var(--ns-signal)] shadow-[0_0_0_1.5px_var(--ns-panel)]", small ? "size-[6px]" : "size-[9px]")} style={{ left: `${(x / 200) * 100}%`, top: `${(y / 34) * 100}%` }} />
      </span>
    );
  }
  if (g.kind === "span") {
    /* the axis runs 0..1 inside the box; a span past 1 (a draw past its fresh time) runs to the edge, solid */
    const [u0, u1] = [clamp(g.usual[0]), clamp(g.usual[1])];
    const [s0, s1] = [clamp(g.span[0]), clamp(g.span[1])];
    return (
      <span aria-hidden data-slot="reason-glyph" data-kind="span" data-moved={moved} className={box}>
        <span className="absolute inset-x-0 top-1/2 h-0 border-0 border-t-[0.5px] border-[color-mix(in_oklab,var(--ns-signal)_35%,transparent)]" />
        <span className={cn("absolute top-1/2 -translate-y-1/2 rounded-full bg-[color-mix(in_oklab,var(--ns-signal)_16%,transparent)]", small ? "h-2" : "h-2.5")} style={{ left: `${u0 * 100}%`, width: `${(u1 - u0) * 100}%` }} />
        <span className={cn("absolute top-1/2 -translate-y-1/2 rounded-full bg-[var(--ns-signal)]", small ? "h-1" : "h-1.5")} style={{ left: `${s0 * 100}%`, width: `${(s1 - s0) * 100}%` }} />
      </span>
    );
  }
  if (g.kind === "marks") {
    return (
      <span aria-hidden data-slot="reason-glyph" data-kind="marks" data-moved={moved} className={cn("flex h-full w-full items-center justify-around", className)}>
        {g.marks.map((m) => {
          const out = Math.abs(m.at) > 1;
          return (
            <span key={m.label} data-moved={out || undefined} className={cn("relative h-full", small ? "w-2" : "w-3")}>
              {/* the band: the middle half of the mark's height is the usual; the dot is the reading */}
              <span className="absolute inset-x-0 top-1/4 bottom-1/4 rounded-full bg-[color-mix(in_oklab,var(--ns-signal)_14%,transparent)]" />
              <span className={cn("absolute left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full", small ? "size-[5px]" : "size-[7px]")} style={{ top: `${50 - clamp(m.at, -1.9, 1.9) * 25}%`, background: out ? "var(--ns-signal)" : soft(88) }} />
            </span>
          );
        })}
      </span>
    );
  }
  if (g.kind === "align") {
    const o = clamp(g.offset, -1, 1);
    return (
      <span aria-hidden data-slot="reason-glyph" data-kind="align" data-moved={moved} className={box}>
        <span className="absolute inset-x-0 top-1/2 h-0 border-0 border-t-[0.5px] border-[color-mix(in_oklab,var(--ns-signal)_35%,transparent)]" />
        {/* the usual: a ring at the centre; the latest: a filled dot, moved by the offset */}
        <span className={cn("absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full border-[1.5px] border-[var(--ns-signal)] bg-[color-mix(in_oklab,var(--ns-signal)_5%,var(--ns-panel))]", small ? "size-[8px]" : "size-[11px]")} />
        <span className={cn("absolute top-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-[var(--ns-signal)]", small ? "size-[5px]" : "size-[7px]")} style={{ left: `${50 + o * 40}%` }} />
      </span>
    );
  }
  return (
    <span aria-hidden data-slot="reason-glyph" data-kind="days" data-moved={moved} className={cn("flex h-full w-full items-center justify-between", className)}>
      {g.days.map((d, i) => (
        <span key={i} data-on={d || undefined} className={cn("block shrink-0 rounded-full", small ? "size-[4px]" : "size-[6px]")} style={{ background: d ? "var(--ns-signal)" : d === false ? soft(22) : "transparent", boxShadow: d == null ? `inset 0 0 0 1px ${soft(28)}` : undefined }} />
      ))}
    </span>
  );
}

/** A judging word with its glyph before it, for a list row; the word is what a screen reader hears. */
function ReasonWord({ word, glyph, className }: { word: string; glyph: Glyph; className?: string | undefined }) {
  return (
    <span data-slot="reason-word" className={cn("inline-flex items-center gap-2 whitespace-nowrap", className)}>
      <span className="inline-block h-3.5 w-9 shrink-0">
        <ReasonGlyph glyph={glyph} small />
      </span>
      <span>{word}</span>
    </span>
  );
}

export { ReasonGlyph, ReasonWord, arcAt, glyphMoved, type Glyph };
```

The foundation (the tokens listed above, keyframes and motion levels) installs once with the first component; its CSS is public at https://notchset.dev/r/notchset-foundation.json.
