# Glucose day (Notchset): prompt.md (v2.0.0)

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

Every 5 minutes as one luminous line over the target band, a quiet wash with its edges written at the left. Each meal is a grey dot where it was eaten, its peak a series dot with the rise above; a dose is a grey diamond along the top. Gaps stay open; a running day stops at now. The table carries every hour..

## 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/glucose-day.tsx`.
- Registry dependencies, installed with it automatically: shadcn `utils` (cn), `notchset-foundation`, `chart`.
- Builds: one set of files, the same for every shadcn style.
- Exports to keep: `GlucoseDay`, 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-band-grade`, `--ns-canvas`, `--ns-font-mono`, `--ns-ink-2`, `--ns-ink-3`, `--ns-line`, `--ns-line-soft`, `--ns-series`, `--ns-series-core`, `--ns-text-meta`.
- 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 { GlucoseDay } from "@/components/ui/notchset/glucose-day";
```

## Parts
- A single export that takes props.

## Match the original
- Read `components/ui/notchset/glucose-day.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

## Built from
- chart (`chart`), installed with it: reuse it, don't rebuild it

## Mistakes
- Recreating the component instead of installing it

## Usage

```tsx
"use client";
import { GlucoseDay } from "@/components/ui/notchset/glucose-day";

/* a sensor's day: near 85 overnight with a dawn rise, then breakfast, lunch and dinner, each rising and settling */
const meals = [
  { at: 92, label: "Breakfast", a: 18 },
  { at: 153, label: "Lunch", a: 44 },
  { at: 228, label: "Dinner", a: 34 },
];
const values = Array.from({ length: 288 }, (_, i) => {
  const dawn = i >= 48 && i <= 84 ? (7 * (i - 48)) / 36 : i > 84 && i < 108 ? 7 * (1 - (i - 84) / 24) : 0;
  const rise = meals.reduce((s, m) => {
    const x = (i - m.at) / 7;
    return x > 0 ? s + m.a * x * Math.exp(1 - x) : s;
  }, 0);
  return Math.round(84 + dawn + rise + Math.sin(i / 3) * 1.5);
});

export default function GlucoseDayDemo() {
  return (
    <div className="w-full max-w-[720px]">
      <GlucoseDay
        label="Glucose, yesterday"
        values={values}
        band={{ low: 70, high: 140 }}
        format={(v) => `${Math.round(v)} mg/dL`}
        meals={meals.map((m) => ({ at: m.at, label: m.label, peak: { at: m.at + 7, value: values[m.at + 7]!, rise: `+${values[m.at + 7]! - values[m.at]!}` } }))}
        doses={[{ at: 96, label: "Semaglutide 0.5 mg" }]}
      />
    </div>
  );
}
```

## Props

| Prop | Type | Default | What it does |
|---|---|---|---|
| `values` | `(number \| null)[]` |  | one value per step from midnight (288 for 5 minutes), null where nothing was read |
| `band` | `{ low: number; high: number }` |  | the target band, in the values' unit |
| `format` | `(v: number) => string` | `(v) => String(Math.round(v * 10) / 10)` |  |
| `meals` | `GlucoseDayMeal[]` | `[]` | meals at their slot, each with its peak (slot, value, the rise in words) and when it came back |
| `doses` | `{ at: number; label: string }[]` | `[]` | doses at their slot, named as logged |
| `step` | `number` | `5` | minutes per value |
| `label` | `string` |  | Required. |
| `height` | `number` | `240` |  |

Full docs: https://notchset.dev/components/glucose-day

## 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/glucose-day.json
```

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

## Continue the workflow

- [Day timeline](https://notchset.dev/components/day-timeline)
- [Ghost trend](https://notchset.dev/components/ghost-trend)
- [Goal cumulative](https://notchset.dev/components/goal-cumulative)

## Source

### components/ui/notchset/glucose-day.tsx

```tsx
// Notchset glucose-day 2.0.0 · https://notchset.dev/changelog
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
import { ChartHalo, ChartTable, ChartTableToggle, InkLine, useChartWidth } from "@/components/ui/notchset/chart";

const useIsomorphicLayoutEffect = typeof window === "undefined" ? React.useEffect : React.useLayoutEffect;

/*
 * Glucose day: a sensor's day, every 5 minutes, as one luminous line over the target band (a quiet wash, its edges
 * written at the left). Each meal is a grey dot on the line at the time it was eaten, its peak a series dot with the
 * meal and its rise written above ("Lunch +46", or "+46" alone where it would run into the tag before it); a dose is a small grey diamond at the top with its name. Gaps where the sensor lost
 * signal stay open; a day still running stops at now, haloed. The table carries every hour and every meal.
 *
 *   <GlucoseDay label="Glucose, today" values={readings} band={{ low: 70, high: 140 }} format={(v) => `${Math.round(v)} mg/dL`}
 *     meals={[{ at: 98, label: "Breakfast", peak: { at: 104, value: 105, rise: "+19" } }]} doses={[{ at: 96, label: "Semaglutide 0.5 mg" }]} />
 */
type GlucoseDayMeal = { at: number; label: string; peak?: { at: number; value: number; rise: string } | undefined; back?: string | undefined };
type GlucoseDayProps = {
  /** one value per step from midnight (288 for 5 minutes), null where nothing was read */
  values: (number | null)[];
  /** the target band, in the values' unit */
  band: { low: number; high: number };
  format?: ((v: number) => string) | undefined;
  /** meals at their slot, each with its peak (slot, value, the rise in words) and when it came back */
  meals?: GlucoseDayMeal[] | undefined;
  /** doses at their slot, named as logged */
  doses?: { at: number; label: string }[] | undefined;
  /** minutes per value */
  step?: number | undefined;
  label: string;
  height?: number | undefined;
  className?: string | undefined;
};

const clock = (slot: number, step: number) => {
  const m = slot * step;
  return `${String(Math.floor(m / 60) % 24).padStart(2, "0")}:${String(m % 60).padStart(2, "0")}`;
};

function GlucoseDay({ values, band, format = (v) => String(Math.round(v * 10) / 10), meals = [], doses = [], step = 5, label, height = 240, className }: GlucoseDayProps) {
  const [box, W] = useChartWidth<HTMLDivElement>(720);
  const [table, setTable] = React.useState(false);
  const n = Math.round((24 * 60) / step);
  const known = values.flatMap((v, i) => (v == null ? [] : [i]));
  const vs = known.map((i) => values[i]!);
  const lo = Math.min(band.low - (band.high - band.low) * 0.3, ...vs) * 0.98;
  const hi = Math.max(band.high + (band.high - band.low) * 0.15, ...vs) * 1.02;
  const T = doses.length ? 30 : 14,
    B = height - 28,
    L = 34;
  const X = (i: number) => L + (i / n) * (W - L - 6);
  const Y = (v: number) => B - ((v - lo) / (hi - lo)) * (B - T);
  let d = "";
  let pen = false;
  values.forEach((v, i) => {
    if (v == null) return void (pen = false);
    d += `${pen ? "L" : "M"}${X(i).toFixed(1)} ${Y(v).toFixed(1)}`;
    pen = true;
  });
  const lastI = known.at(-1) ?? -1;
  const at = (i: number) => values[i] ?? values[known.find((k) => k >= i) ?? lastI] ?? null;
  const tir = vs.length ? Math.round((vs.filter((v) => v >= band.low && v <= band.high).length / vs.length) * 100) : null;
  const summary = vs.length
    ? `${label}: ${vs.length} readings, ${tir}% inside the target ${format(band.low)} to ${format(band.high)}, lowest ${format(Math.min(...vs))}, highest ${format(Math.max(...vs))}${meals.length ? `; ${meals.map((m) => `${m.label} at ${clock(m.at, step)}${m.peak ? `, peak ${format(m.peak.value)} (${m.peak.rise}) at ${clock(m.peak.at, step)}` : ""}`).join("; ")}` : ""}${doses.length ? `; ${doses.map((x) => `${x.label} at ${clock(x.at, step)}`).join("; ")}` : ""}.`
    : `${label}: no readings.`;
  const hours = Array.from({ length: 24 }, (_, h) => {
    const i = Math.round((h * 60) / step);
    const v = values.slice(i, i + Math.round(60 / step)).filter((x): x is number => x != null);
    return [`${String(h).padStart(2, "0")}:00`, v.length ? format(v.reduce((a, b) => a + b, 0) / v.length) : "—", [...meals.filter((m) => Math.floor((m.at * step) / 60) === h).map((m) => m.label), ...doses.filter((x) => Math.floor((x.at * step) / 60) === h).map((x) => x.label)].join(", ") || ""];
  });
  const clampX = (x: number) => Math.min(W - 48, Math.max(L + 24, x));
  /* peak tags that would run into the one before them (two meals close together on a phone) say their rise alone
     ("+26"); the meal is still its grey dot, and the summary and the table name it. Widths come from the tags' mono
     advance, measured once it's drawn. */
  const tags = React.useRef<HTMLDivElement>(null);
  const [advance, setAdvance] = React.useState(7.2);
  const measure = React.useCallback(() => {
    const s = tags.current?.querySelector<HTMLElement>("[data-peak]");
    const a = s && s.textContent ? (s.getBoundingClientRect().width - 8) / s.textContent.length : 0;
    if (a > 0) setAdvance((was) => (Math.abs(a - was) > 0.05 ? a : was));
  }, []);
  useIsomorphicLayoutEffect(measure);
  /* the mono face can land after the first paint, and it sets the advance */
  React.useEffect(() => {
    document.fonts?.addEventListener?.("loadingdone", measure);
    return () => document.fonts?.removeEventListener?.("loadingdone", measure);
  }, [measure]);
  const riseOnly = new Set<number>();
  let prev: { right: number; top: number } | null = null;
  for (const m of [...meals].filter((x) => x.peak).sort((a, b) => a.peak!.at - b.peak!.at)) {
    const cx = clampX(X(m.peak!.at));
    const top = Y(m.peak!.value) - 20;
    const width = (text: string) => text.length * advance + 8;
    let w = width(`${m.label} ${m.peak!.rise}`);
    if (prev && Math.abs(top - prev.top) < 18 && cx - w / 2 < prev.right + 4) {
      riseOnly.add(m.at);
      w = width(m.peak!.rise);
    }
    prev = { right: cx + w / 2, top };
  }
  return (
    <div data-slot="glucose-day" className={cn("flex min-w-0 flex-col gap-2", className)}>
      <div ref={box} role="img" aria-label={summary} className="relative min-w-0" style={{ height }}>
        <svg width={W} height={height} className="block overflow-visible" aria-hidden>
          <rect x={L} width={W - L} y={Y(band.high)} height={Math.max(1, Y(band.low) - Y(band.high))} rx={6} fill="var(--ns-band-grade)" />
          <line x1={L} x2={W} y1={B + 0.5} y2={B + 0.5} stroke="var(--ns-line-soft)" strokeWidth={1} />
          {meals.map((m) => {
            const v = at(m.at);
            return v == null ? null : <line key={`h${m.at}`} x1={X(m.at)} x2={X(m.at)} y1={Y(v) + 5} y2={B} stroke="var(--ns-ink-3)" strokeWidth={1} strokeDasharray="1 3" opacity={0.6} />;
          })}
          <InkLine d={d} width={2.25} core="var(--ns-series-core)" />
          {meals.map((m) => {
            const v = at(m.at);
            return (
              <g key={`m${m.at}`}>
                {v != null && <circle cx={X(m.at)} cy={Y(v)} r={3.5} fill="var(--ns-ink-2)" stroke="var(--ns-canvas)" strokeWidth={2} />}
                {m.peak && <circle cx={X(m.peak.at)} cy={Y(m.peak.value)} r={3.5} fill="var(--ns-series)" stroke="var(--ns-canvas)" strokeWidth={2} />}
              </g>
            );
          })}
          {doses.map((x) => (
            <rect key={`d${x.at}`} x={X(x.at) - 4} y={8} width={8} height={8} transform={`rotate(45 ${X(x.at)} 12)`} fill="none" stroke="var(--ns-ink-2)" strokeWidth={1.5} />
          ))}
        </svg>
        {lastI >= 0 && lastI < n - 1 && <ChartHalo x={X(lastI)} y={Y(values[lastI]!)} />}
        <div ref={tags} aria-hidden className="font-[family-name:var(--ns-font-mono)] text-[length:var(--ns-text-meta)] leading-none text-[var(--ns-ink-3)]">
          {[band.high, band.low].map((v) => (
            <span key={v} className="absolute left-0 -translate-y-1/2 tabular-nums" style={{ top: Y(v) }}>
              {v}
            </span>
          ))}
          {meals.map(
            (m) =>
              m.peak && (
                <span key={`p${m.at}`} data-peak="" className="absolute -translate-x-1/2 rounded-[4px] bg-[var(--ns-canvas)] px-1 whitespace-nowrap text-[var(--ns-ink-2)]" style={{ left: clampX(X(m.peak.at)), top: Y(m.peak.value) - 20 }}>
                  {riseOnly.has(m.at) ? m.peak.rise : `${m.label} ${m.peak.rise}`}
                </span>
              ),
          )}
          {doses.map((x) => (
            <span key={`dl${x.at}`} className="absolute whitespace-nowrap text-[var(--ns-ink-2)]" style={{ left: Math.min(W - 140, X(x.at) + 9), top: 6 }}>
              {x.label}
            </span>
          ))}
          {[0, 6, 12, 18].map((h) => (
              <span key={h} className={cn("absolute", h ? "-translate-x-1/2" : "")} style={{ left: X((h * 60) / step), top: B + 8 }}>
                {`${String(h).padStart(2, "0")}:00`}
              </span>
            ))}
        </div>
      </div>
      <div className="flex flex-wrap items-center gap-x-4 gap-y-1 font-[family-name:var(--ns-font-mono)] text-[length:var(--ns-text-meta)] text-[var(--ns-ink-3)]">
        <span className="flex items-center gap-1.5">
          <i aria-hidden className="block h-2 w-3 rounded-[2px] bg-[var(--ns-band-grade)] shadow-[inset_0_0_0_0.5px_var(--ns-line)]" />
          Target {band.low}–{band.high}
        </span>
        {meals.length > 0 && (
          <span className="flex items-center gap-1.5">
            <i aria-hidden className="block size-[7px] rounded-full bg-[var(--ns-ink-2)]" />
            Meal
            <i aria-hidden className="ml-1.5 block size-[7px] rounded-full bg-[var(--ns-series)]" />
            Its peak
          </span>
        )}
        {doses.length > 0 && (
          <span className="flex items-center gap-1.5">
            <i aria-hidden className="block size-[7px] rotate-45 border-[1.5px] border-[var(--ns-ink-2)]" />
            Dose
          </span>
        )}
        <ChartTableToggle open={table} onToggle={() => setTable(!table)} className="ml-auto" />
      </div>
      <ChartTable open={table} caption={label} head={["Hour", "Average", "Logged"]} rows={hours} />
    </div>
  );
}

export { GlucoseDay, type GlucoseDayMeal, type GlucoseDayProps };
```

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.
