Web InnoventixFreeCode

Empty First Run

Original · free

first-run get started empty state

byWeb InnoventixReact + Tailwind
emptyfirstrunstates
Open

Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.

npx shadcn@latest add https://webinnoventix.com/r/empty-first-run.json
empty-first-run.tsx
"use client";

import {
  useCallback,
  useEffect,
  useId,
  useMemo,
  useRef,
  useState,
  type KeyboardEvent as ReactKeyboardEvent,
  type ReactNode,
} from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";

type StepId = "connect" | "invite" | "threshold" | "cli";

type Step = {
  id: StepId;
  title: string;
  summary: string;
  detail: string;
  minutes: number;
  action: string;
  hint: string;
};

const CLI_COMMAND = "npm i -g @meridian/cli && meridian login";

const STEPS: Step[] = [
  {
    id: "connect",
    title: "Connect your first service",
    summary: "Point Meridian at a health endpoint and we start polling it.",
    detail:
      "Paste any HTTPS URL that returns a 2xx when your service is healthy. Meridian polls it every 30 seconds from four regions and records latency, status code, and TLS expiry. Nothing else needs to change in your app.",
    minutes: 2,
    action: "Add a service",
    hint: "Most teams start with their production API root.",
  },
  {
    id: "invite",
    title: "Invite your on-call rotation",
    summary: "Alerts are useless if they reach an empty room.",
    detail:
      "Add teammates by email and assign a rotation order. The first responder gets paged, and if there is no acknowledgement within five minutes, the page escalates to the next person on the list.",
    minutes: 1,
    action: "Invite teammates",
    hint: "Two people is the minimum we recommend for escalation.",
  },
  {
    id: "threshold",
    title: "Set an alert threshold",
    summary: "Decide what counts as broken before it breaks.",
    detail:
      "Choose the p95 latency and error rate that should trigger a page. Meridian defaults to 800 ms p95 over three consecutive checks, which is quiet enough to sleep through and loud enough to matter.",
    minutes: 3,
    action: "Configure alerts",
    hint: "You can tune this per service later without losing history.",
  },
  {
    id: "cli",
    title: "Install the CLI",
    summary: "Ship deploy markers so incidents line up with releases.",
    detail:
      "The CLI writes a deploy marker onto your latency charts, so a spike at 14:02 is immediately traceable to the release at 14:01. Add the command to your deploy pipeline once and forget it.",
    minutes: 2,
    action: "View install docs",
    hint: "Works with GitHub Actions, GitLab CI, and plain shell scripts.",
  },
];

function CheckIcon({ className }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={3}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      className={className}
    >
      <path d="M5 12.5l4.5 4.5L19 7" />
    </svg>
  );
}

function ChevronIcon({ className }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      className={className}
    >
      <path d="M6 9l6 6 6-6" />
    </svg>
  );
}

function ArrowIcon({ className }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      className={className}
    >
      <path d="M5 12h13" />
      <path d="M12 5l7 7-7 7" />
    </svg>
  );
}

export default function EmptyFirstRun() {
  const reduce = useReducedMotion();
  const headingId = useId();
  const listId = useId();
  const statusId = useId();

  const [done, setDone] = useState<StepId[]>([]);
  const [open, setOpen] = useState<StepId | null>("connect");
  const [copied, setCopied] = useState(false);

  const headerRefs = useRef<Array<HTMLButtonElement | null>>([]);
  const copyTimer = useRef<number | null>(null);

  useEffect(() => {
    return () => {
      if (copyTimer.current !== null) window.clearTimeout(copyTimer.current);
    };
  }, []);

  const isDone = useCallback((id: StepId) => done.includes(id), [done]);

  const toggleDone = useCallback((id: StepId) => {
    setDone((prev) =>
      prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id],
    );
  }, []);

  const completed = done.length;
  const total = STEPS.length;
  const allDone = completed === total;
  const percent = Math.round((completed / total) * 100);

  const nextStep = useMemo<Step | null>(
    () => STEPS.find((s) => !done.includes(s.id)) ?? null,
    [done],
  );

  const minutesLeft = useMemo(
    () =>
      STEPS.filter((s) => !done.includes(s.id)).reduce(
        (sum, s) => sum + s.minutes,
        0,
      ),
    [done],
  );

  const copyCommand = useCallback(async () => {
    try {
      await navigator.clipboard.writeText(CLI_COMMAND);
      setCopied(true);
      if (copyTimer.current !== null) window.clearTimeout(copyTimer.current);
      copyTimer.current = window.setTimeout(() => setCopied(false), 1800);
    } catch {
      setCopied(false);
    }
  }, []);

  function onHeaderKeyDown(
    e: ReactKeyboardEvent<HTMLButtonElement>,
    i: number,
  ) {
    let next = -1;
    if (e.key === "ArrowDown") next = (i + 1) % STEPS.length;
    else if (e.key === "ArrowUp") next = (i - 1 + STEPS.length) % STEPS.length;
    else if (e.key === "Home") next = 0;
    else if (e.key === "End") next = STEPS.length - 1;
    else return;
    e.preventDefault();
    headerRefs.current[next]?.focus();
  }

  const RADIUS = 34;
  const CIRC = 2 * Math.PI * RADIUS;
  const offset = CIRC * (1 - completed / total);

  return (
    <section
      aria-labelledby={headingId}
      className="relative w-full overflow-hidden bg-slate-50 px-4 py-16 text-slate-900 sm:px-6 sm:py-24 dark:bg-slate-950 dark:text-slate-100"
    >
      <style>{`
        @keyframes efr-drift {
          0%, 100% { transform: translate3d(0, 0, 0); }
          50% { transform: translate3d(0, -10px, 0); }
        }
        @keyframes efr-pulse-ring {
          0% { transform: scale(0.9); opacity: 0.65; }
          70% { transform: scale(1.5); opacity: 0; }
          100% { transform: scale(1.5); opacity: 0; }
        }
        @keyframes efr-sweep {
          0% { transform: translateX(-110%); }
          100% { transform: translateX(320%); }
        }
        @keyframes efr-pop {
          0% { transform: scale(0.4); opacity: 0; }
          60% { transform: scale(1.12); opacity: 1; }
          100% { transform: scale(1); opacity: 1; }
        }
        @keyframes efr-spark {
          0% { transform: translateY(0) scale(0.6); opacity: 0; }
          25% { opacity: 1; }
          100% { transform: translateY(-46px) scale(1); opacity: 0; }
        }
        .efr-drift { animation: efr-drift 7s ease-in-out infinite; }
        .efr-pulse-ring { animation: efr-pulse-ring 2.4s ease-out infinite; }
        .efr-sweep { animation: efr-sweep 2.8s ease-in-out infinite; }
        .efr-pop { animation: efr-pop 420ms cubic-bezier(0.22, 1, 0.36, 1) both; }
        .efr-spark { animation: efr-spark 1.6s ease-out infinite; }
        @media (prefers-reduced-motion: reduce) {
          .efr-drift,
          .efr-pulse-ring,
          .efr-sweep,
          .efr-pop,
          .efr-spark {
            animation: none !important;
          }
        }
      `}</style>

      <div
        aria-hidden="true"
        className="pointer-events-none absolute -top-24 left-1/2 h-72 w-[42rem] max-w-full -translate-x-1/2 rounded-full bg-indigo-500/10 blur-3xl dark:bg-indigo-500/15"
      />

      <div className="relative mx-auto max-w-6xl">
        {/* Header */}
        <div className="flex flex-col gap-6 sm:flex-row sm:items-end sm:justify-between">
          <div className="max-w-xl">
            <span className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white px-3 py-1 text-xs font-medium text-slate-600 shadow-sm dark:border-slate-800 dark:bg-slate-900 dark:text-slate-300">
              <span className="relative flex h-2 w-2">
                <span
                  aria-hidden="true"
                  className="efr-pulse-ring absolute inline-flex h-full w-full rounded-full bg-emerald-500"
                />
                <span
                  aria-hidden="true"
                  className="relative inline-flex h-2 w-2 rounded-full bg-emerald-500"
                />
              </span>
              Workspace created &middot; nothing monitored yet
            </span>

            <h2
              id={headingId}
              className="mt-5 text-3xl font-semibold tracking-tight text-slate-900 sm:text-4xl dark:text-white"
            >
              Welcome to Meridian, Priya.
            </h2>
            <p className="mt-3 text-base leading-relaxed text-slate-600 dark:text-slate-400">
              Your workspace is empty on purpose &mdash; we don&apos;t guess at
              your infrastructure. Four short steps and you&apos;ll have live
              latency charts, a working escalation path, and deploy markers on
              every release.
            </p>
          </div>

          {/* Progress ring */}
          <div className="flex shrink-0 items-center gap-4">
            <div className="relative h-24 w-24">
              <svg viewBox="0 0 80 80" className="h-24 w-24 -rotate-90">
                <circle
                  cx="40"
                  cy="40"
                  r={RADIUS}
                  fill="none"
                  strokeWidth="7"
                  className="stroke-slate-200 dark:stroke-slate-800"
                />
                <motion.circle
                  cx="40"
                  cy="40"
                  r={RADIUS}
                  fill="none"
                  strokeWidth="7"
                  strokeLinecap="round"
                  strokeDasharray={CIRC}
                  className={
                    allDone
                      ? "stroke-emerald-500"
                      : "stroke-indigo-500 dark:stroke-indigo-400"
                  }
                  initial={false}
                  animate={{ strokeDashoffset: offset }}
                  transition={
                    reduce
                      ? { duration: 0 }
                      : { duration: 0.55, ease: [0.22, 1, 0.36, 1] }
                  }
                />
              </svg>
              <div className="absolute inset-0 flex flex-col items-center justify-center">
                <span className="text-lg font-semibold tabular-nums text-slate-900 dark:text-white">
                  {percent}%
                </span>
                <span className="text-[10px] font-medium uppercase tracking-wider text-slate-400 dark:text-slate-500">
                  setup
                </span>
              </div>
            </div>
            <div className="text-sm">
              <p className="font-medium text-slate-900 dark:text-white">
                {completed} of {total} done
              </p>
              <p className="mt-0.5 text-slate-500 dark:text-slate-400">
                {allDone
                  ? "You're fully set up."
                  : `About ${minutesLeft} min left`}
              </p>
            </div>
          </div>
        </div>

        <p id={statusId} role="status" aria-live="polite" className="sr-only">
          {allDone
            ? "Setup complete. All four steps are done."
            : `${completed} of ${total} setup steps complete. Next up: ${
                nextStep ? nextStep.title : ""
              }.`}
        </p>

        <div className="mt-12 grid gap-8 lg:grid-cols-[minmax(0,1fr)_minmax(0,0.85fr)] lg:gap-12">
          {/* Checklist */}
          <div>
            <div className="flex items-baseline justify-between gap-4">
              <h3 className="text-sm font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">
                Get started
              </h3>
              {completed > 0 ? (
                <button
                  type="button"
                  onClick={() => setDone([])}
                  className="rounded text-xs font-medium text-slate-500 underline-offset-4 transition hover:text-indigo-600 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:text-slate-400 dark:hover:text-indigo-400 dark:focus-visible:ring-offset-slate-950"
                >
                  Reset checklist
                </button>
              ) : null}
            </div>

            <ul id={listId} className="mt-4 space-y-3">
              {STEPS.map((step, i) => {
                const stepDone = isDone(step.id);
                const expanded = open === step.id;
                const isNext = !stepDone && nextStep?.id === step.id;

                return (
                  <li
                    key={step.id}
                    className={[
                      "rounded-2xl border bg-white shadow-sm transition-colors dark:bg-slate-900",
                      stepDone
                        ? "border-emerald-200 dark:border-emerald-500/30"
                        : isNext
                          ? "border-indigo-300 dark:border-indigo-500/50"
                          : "border-slate-200 dark:border-slate-800",
                    ].join(" ")}
                  >
                    <div className="flex items-start gap-3 p-4">
                      <button
                        type="button"
                        role="checkbox"
                        aria-checked={stepDone}
                        aria-label={`Mark "${step.title}" as ${
                          stepDone ? "not done" : "done"
                        }`}
                        onClick={() => toggleDone(step.id)}
                        className={[
                          "mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full border-2 transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900",
                          stepDone
                            ? "border-emerald-500 bg-emerald-500 text-white"
                            : "border-slate-300 bg-white text-transparent hover:border-indigo-500 dark:border-slate-600 dark:bg-slate-900",
                        ].join(" ")}
                      >
                        {stepDone ? (
                          <CheckIcon className="efr-pop h-3.5 w-3.5" />
                        ) : (
                          <span className="text-xs font-semibold text-slate-400 dark:text-slate-500">
                            {i + 1}
                          </span>
                        )}
                      </button>

                      <button
                        type="button"
                        ref={(el) => {
                          headerRefs.current[i] = el;
                        }}
                        aria-expanded={expanded}
                        aria-controls={`${listId}-panel-${step.id}`}
                        id={`${listId}-header-${step.id}`}
                        onClick={() => setOpen(expanded ? null : step.id)}
                        onKeyDown={(e) => onHeaderKeyDown(e, i)}
                        className="flex min-w-0 flex-1 items-start gap-3 rounded-lg text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900"
                      >
                        <span className="min-w-0 flex-1">
                          <span className="flex flex-wrap items-center gap-2">
                            <span
                              className={[
                                "text-[15px] font-semibold",
                                stepDone
                                  ? "text-slate-400 line-through decoration-slate-300 dark:text-slate-500 dark:decoration-slate-600"
                                  : "text-slate-900 dark:text-white",
                              ].join(" ")}
                            >
                              {step.title}
                            </span>
                            {isNext ? (
                              <span className="rounded-md bg-indigo-100 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-300">
                                Next up
                              </span>
                            ) : null}
                            <span className="text-xs text-slate-400 dark:text-slate-500">
                              {step.minutes} min
                            </span>
                          </span>
                          <span className="mt-1 block text-sm text-slate-600 dark:text-slate-400">
                            {step.summary}
                          </span>
                        </span>
                        <ChevronIcon
                          className={[
                            "mt-1 h-4 w-4 shrink-0 text-slate-400 transition-transform duration-200 dark:text-slate-500",
                            expanded ? "rotate-180" : "",
                          ].join(" ")}
                        />
                      </button>
                    </div>

                    <AnimatePresence initial={false}>
                      {expanded ? (
                        <motion.div
                          key="panel"
                          id={`${listId}-panel-${step.id}`}
                          role="region"
                          aria-labelledby={`${listId}-header-${step.id}`}
                          initial={reduce ? false : { height: 0, opacity: 0 }}
                          animate={{ height: "auto", opacity: 1 }}
                          exit={
                            reduce
                              ? { opacity: 0 }
                              : { height: 0, opacity: 0 }
                          }
                          transition={
                            reduce
                              ? { duration: 0 }
                              : { duration: 0.26, ease: [0.22, 1, 0.36, 1] }
                          }
                          className="overflow-hidden"
                        >
                          <div className="border-t border-slate-100 px-4 pb-4 pl-[3.25rem] pt-4 dark:border-slate-800">
                            <p className="text-sm leading-relaxed text-slate-600 dark:text-slate-400">
                              {step.detail}
                            </p>

                            {step.id === "cli" ? (
                              <div className="mt-3 flex items-center gap-2 rounded-xl border border-slate-200 bg-slate-50 p-2 dark:border-slate-800 dark:bg-slate-950">
                                <code className="min-w-0 flex-1 overflow-x-auto whitespace-nowrap px-2 py-1 font-mono text-xs text-slate-700 dark:text-slate-300">
                                  {CLI_COMMAND}
                                </code>
                                <button
                                  type="button"
                                  onClick={copyCommand}
                                  className="shrink-0 rounded-lg border border-slate-300 bg-white px-2.5 py-1.5 text-xs font-semibold text-slate-700 transition hover:border-indigo-400 hover:text-indigo-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:border-indigo-500 dark:hover:text-indigo-300 dark:focus-visible:ring-offset-slate-950"
                                >
                                  {copied ? "Copied" : "Copy"}
                                </button>
                              </div>
                            ) : null}

                            <div className="mt-4 flex flex-wrap items-center gap-3">
                              <button
                                type="button"
                                onClick={() => toggleDone(step.id)}
                                className="inline-flex items-center gap-2 rounded-xl bg-indigo-600 px-4 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-indigo-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white active:scale-[0.98] dark:focus-visible:ring-offset-slate-900"
                              >
                                {stepDone ? "Marked done" : step.action}
                                <ArrowIcon className="h-4 w-4" />
                              </button>
                              <span className="text-xs text-slate-500 dark:text-slate-400">
                                {step.hint}
                              </span>
                            </div>
                          </div>
                        </motion.div>
                      ) : null}
                    </AnimatePresence>
                  </li>
                );
              })}
            </ul>

            <div className="mt-5 flex flex-wrap items-center gap-x-4 gap-y-2 text-xs text-slate-500 dark:text-slate-400">
              <span>
                Use{" "}
                <kbd className="rounded border border-slate-300 bg-white px-1 py-0.5 font-sans text-[10px] font-medium text-slate-600 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300">
                  &uarr;
                </kbd>{" "}
                <kbd className="rounded border border-slate-300 bg-white px-1 py-0.5 font-sans text-[10px] font-medium text-slate-600 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300">
                  &darr;
                </kbd>{" "}
                to move between steps.
              </span>
              <a
                href="#meridian-docs"
                className="rounded font-medium text-slate-600 underline decoration-dotted underline-offset-4 transition hover:text-indigo-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:text-slate-300 dark:hover:text-indigo-400 dark:focus-visible:ring-offset-slate-950"
              >
                Skip setup and explore an example workspace
              </a>
            </div>
          </div>

          {/* Live preview canvas */}
          <div className="lg:sticky lg:top-8 lg:self-start">
            <div className="relative overflow-hidden rounded-2xl border border-slate-200 bg-white p-5 shadow-sm dark:border-slate-800 dark:bg-slate-900">
              <div className="flex items-center justify-between">
                <p className="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">
                  Your dashboard
                </p>
                <span
                  className={[
                    "rounded-md px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide transition-colors",
                    allDone
                      ? "bg-emerald-100 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300"
                      : "bg-slate-100 text-slate-500 dark:bg-slate-800 dark:text-slate-400",
                  ].join(" ")}
                >
                  {allDone ? "Live" : "Preview"}
                </span>
              </div>

              <div className="mt-4 space-y-3">
                <PreviewRow active={isDone("connect")} reduce={reduce}>
                  <div className="flex items-center gap-3">
                    <span className="h-2 w-2 shrink-0 rounded-full bg-emerald-500" />
                    <span className="min-w-0 flex-1 truncate text-sm font-medium text-slate-900 dark:text-white">
                      api.meridian.dev/health
                    </span>
                    <span className="shrink-0 text-xs tabular-nums text-slate-500 dark:text-slate-400">
                      142 ms
                    </span>
                  </div>
                </PreviewRow>

                <PreviewRow active={isDone("invite")} reduce={reduce}>
                  <div className="flex items-center gap-3">
                    <span className="flex -space-x-1.5">
                      {["PR", "DK", "AM"].map((initials) => (
                        <span
                          key={initials}
                          className="flex h-6 w-6 items-center justify-center rounded-full border-2 border-white bg-indigo-100 text-[9px] font-semibold text-indigo-700 dark:border-slate-900 dark:bg-indigo-500/20 dark:text-indigo-300"
                        >
                          {initials}
                        </span>
                      ))}
                    </span>
                    <span className="text-sm text-slate-600 dark:text-slate-400">
                      On-call rotation &middot; 3 responders
                    </span>
                  </div>
                </PreviewRow>

                <PreviewRow active={isDone("threshold")} reduce={reduce}>
                  <div>
                    <div className="flex items-center justify-between">
                      <span className="text-sm text-slate-600 dark:text-slate-400">
                        Alert at p95
                      </span>
                      <span className="text-sm font-semibold tabular-nums text-slate-900 dark:text-white">
                        800 ms
                      </span>
                    </div>
                    <div className="mt-2 h-1.5 w-full overflow-hidden rounded-full bg-slate-100 dark:bg-slate-800">
                      <div className="h-full w-[38%] rounded-full bg-gradient-to-r from-emerald-500 to-amber-400" />
                    </div>
                  </div>
                </PreviewRow>

                <PreviewRow active={isDone("cli")} reduce={reduce}>
                  <div className="flex items-center gap-3">
                    <span className="rounded bg-violet-100 px-1.5 py-0.5 font-mono text-[10px] font-semibold text-violet-700 dark:bg-violet-500/15 dark:text-violet-300">
                      v2.14.0
                    </span>
                    <span className="text-sm text-slate-600 dark:text-slate-400">
                      Deploy marker &middot; 2 minutes ago
                    </span>
                  </div>
                </PreviewRow>
              </div>

              {/* Empty-canvas placeholder */}
              {completed === 0 ? (
                <div className="relative mt-4 overflow-hidden rounded-xl border border-dashed border-slate-300 bg-slate-50/70 px-4 py-8 text-center dark:border-slate-700 dark:bg-slate-950/40">
                  <span
                    aria-hidden="true"
                    className="efr-sweep pointer-events-none absolute inset-y-0 left-0 w-16 bg-gradient-to-r from-transparent via-white/70 to-transparent dark:via-white/5"
                  />
                  <svg
                    viewBox="0 0 96 60"
                    role="img"
                    aria-label="Empty dashboard placeholder"
                    className="efr-drift mx-auto h-14 w-auto"
                  >
                    <g
                      className="text-slate-300 dark:text-slate-700"
                      stroke="currentColor"
                      fill="none"
                      strokeWidth={2.5}
                      strokeLinecap="round"
                    >
                      <rect
                        x="4"
                        y="6"
                        width="88"
                        height="48"
                        rx="8"
                        strokeDasharray="6 8"
                      />
                    </g>
                    <polyline
                      points="16,42 32,34 46,38 60,22 76,28"
                      fill="none"
                      strokeWidth={3}
                      strokeLinecap="round"
                      strokeLinejoin="round"
                      className="stroke-indigo-400/60 dark:stroke-indigo-400/40"
                    />
                  </svg>
                  <p className="relative mt-3 text-xs text-slate-500 dark:text-slate-400">
                    No data yet. Complete a step and this panel fills in.
                  </p>
                </div>
              ) : null}

              {/* Completion celebration */}
              <AnimatePresence initial={false}>
                {allDone ? (
                  <motion.div
                    key="celebrate"
                    initial={reduce ? false : { opacity: 0, y: 12 }}
                    animate={{ opacity: 1, y: 0 }}
                    exit={reduce ? { opacity: 0 } : { opacity: 0, y: 8 }}
                    transition={
                      reduce
                        ? { duration: 0 }
                        : { duration: 0.32, ease: [0.22, 1, 0.36, 1] }
                    }
                    className="relative mt-5 overflow-hidden rounded-xl border border-emerald-200 bg-emerald-50 p-4 dark:border-emerald-500/30 dark:bg-emerald-500/10"
                  >
                    <span aria-hidden="true" className="pointer-events-none absolute inset-0">
                      {[12, 30, 50, 68, 86].map((left, i) => (
                        <span
                          key={left}
                          className="efr-spark absolute bottom-2 h-1.5 w-1.5 rounded-full bg-emerald-400"
                          style={{
                            left: `${left}%`,
                            animationDelay: `${i * 0.22}s`,
                          }}
                        />
                      ))}
                    </span>
                    <p className="relative text-sm font-semibold text-emerald-900 dark:text-emerald-200">
                      Monitoring is live.
                    </p>
                    <p className="relative mt-1 text-sm text-emerald-800/90 dark:text-emerald-200/80">
                      Meridian is polling from four regions and your rotation is
                      armed. First report lands in your inbox on Monday.
                    </p>
                    <a
                      href="#meridian-dashboard"
                      className="relative mt-3 inline-flex items-center gap-2 rounded-lg bg-emerald-600 px-3.5 py-2 text-xs font-semibold text-white transition hover:bg-emerald-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2 focus-visible:ring-offset-emerald-50 dark:focus-visible:ring-offset-slate-900"
                    >
                      Open the dashboard
                      <ArrowIcon className="h-3.5 w-3.5" />
                    </a>
                  </motion.div>
                ) : null}
              </AnimatePresence>
            </div>

            <p className="mt-4 px-1 text-xs leading-relaxed text-slate-500 dark:text-slate-400">
              Everything here is reversible. Delete a service and its history
              goes with it &mdash; no support ticket, no retention trap.
            </p>
          </div>
        </div>
      </div>
    </section>
  );
}

function PreviewRow({
  active,
  reduce,
  children,
}: {
  active: boolean;
  reduce: boolean | null;
  children: ReactNode;
}) {
  return (
    <motion.div
      initial={false}
      animate={{ opacity: active ? 1 : 0.35 }}
      transition={reduce ? { duration: 0 } : { duration: 0.3 }}
      className={[
        "rounded-xl border p-3 transition-colors",
        active
          ? "border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-800/50"
          : "border-dashed border-slate-200 bg-slate-50 grayscale dark:border-slate-800 dark:bg-slate-950/40",
      ].join(" ")}
    >
      {children}
    </motion.div>
  );
}

Dependencies

motion

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

Similar components

Browse all →