From c89d06dd7cad00a9357ba45316505f1dd22a3af4 Mon Sep 17 00:00:00 2001
From: BryanFRD
Date: Thu, 20 Aug 2026 22:01:33 +0200
Subject: [PATCH 1/2] feat: slide the segmented control background to the
active option
---
.../tracker/ImplementationTracker.tsx | 158 +++++++-----------
src/components/tracker/SegmentedControl.tsx | 110 ++++++++++++
2 files changed, 174 insertions(+), 94 deletions(-)
create mode 100644 src/components/tracker/SegmentedControl.tsx
diff --git a/src/components/tracker/ImplementationTracker.tsx b/src/components/tracker/ImplementationTracker.tsx
index 02bdb3b..66dc469 100644
--- a/src/components/tracker/ImplementationTracker.tsx
+++ b/src/components/tracker/ImplementationTracker.tsx
@@ -1,5 +1,6 @@
import { useState, useEffect, useMemo } from "react";
import { Search, ChevronDown, Blocks, Sword, Filter, PawPrint } from "lucide-react";
+import SegmentedControl from "./SegmentedControl";
interface ClassGroup {
implemented: boolean;
@@ -123,23 +124,16 @@ export default function ImplementationTracker() {
-
- setProgressMetric("surface")}
- >
- Surface area
-
- setProgressMetric("classes")}
- >
- Behaviors
-
-
+
@@ -185,21 +179,40 @@ export default function ImplementationTracker() {
{/* Controls */}
- {/* Tabs */}
-
-
setTab("blocks")}>
-
- Blocks
-
-
setTab("items")}>
-
- Items
-
-
setTab("entities")}>
-
- Entities
-
-
+
+
+ Blocks
+ >
+ ),
+ },
+ {
+ value: "items",
+ content: (
+ <>
+
+ Items
+ >
+ ),
+ },
+ {
+ value: "entities",
+ content: (
+ <>
+
+ Entities
+ >
+ ),
+ },
+ ]}
+ />
{/* Search */}
@@ -213,22 +226,25 @@ export default function ImplementationTracker() {
/>
- {/* Status filter */}
-
- setStatusFilter("all")}>
-
- All
-
- setStatusFilter("complete")}>
- Complete
-
- setStatusFilter("partial")}>
- Partial
-
- setStatusFilter("unimplemented")}>
- Todo
-
-
+
+
+ All
+ >
+ ),
+ },
+ { value: "complete", content: "Complete" },
+ { value: "partial", content: "Partial" },
+ { value: "unimplemented", content: "Todo" },
+ ]}
+ />
{/* Results count */}
@@ -364,50 +380,4 @@ function StatCard({ label, value, color = "default" }: { label: string; value: n
);
}
-function TabButton({
- active,
- onClick,
- children,
-}: {
- active: boolean;
- onClick: () => void;
- children: React.ReactNode;
-}) {
- return (
-
- );
-}
-function MetricButton({
- active,
- onClick,
- children,
-}: {
- active: boolean;
- onClick: () => void;
- children: React.ReactNode;
-}) {
- return (
-
- );
-}
diff --git a/src/components/tracker/SegmentedControl.tsx b/src/components/tracker/SegmentedControl.tsx
new file mode 100644
index 0000000..37e20d2
--- /dev/null
+++ b/src/components/tracker/SegmentedControl.tsx
@@ -0,0 +1,110 @@
+import { useLayoutEffect, useRef, useState, type ReactNode } from "react";
+
+export interface Segment {
+ value: T;
+ content: ReactNode;
+}
+
+interface Props {
+ label: string;
+ segments: Segment[];
+ value: T;
+ onChange: (value: T) => void;
+ size?: "md" | "sm";
+}
+
+const SIZES = {
+ md: {
+ wrapper: "p-1 rounded-xl",
+ button: "gap-1.5 px-3 py-1.5 text-sm rounded-lg",
+ indicator: "rounded-lg",
+ },
+ sm: {
+ wrapper: "p-0.5 rounded-lg",
+ button: "gap-1 px-2.5 py-1 text-xs rounded-md",
+ indicator: "rounded-md",
+ },
+};
+
+export default function SegmentedControl({
+ label,
+ segments,
+ value,
+ onChange,
+ size = "md",
+}: Props) {
+ const buttons = useRef(new Map());
+ const wrapper = useRef(null);
+ const [indicator, setIndicator] = useState<{ left: number; width: number } | null>(null);
+ const styles = SIZES[size];
+
+ // global.css sets an unlayered `transition` on div/span/button, which outranks any
+ // utility class, so the sliding transition has to be declared inline.
+ const reducedMotion =
+ typeof window !== "undefined" &&
+ window.matchMedia("(prefers-reduced-motion: reduce)").matches;
+
+ // The indicator is positioned from the active button's box, so it keeps up with
+ // label changes, font loading and container resizes rather than a fixed width.
+ useLayoutEffect(() => {
+ const measure = () => {
+ const active = buttons.current.get(value);
+ if (!active) return;
+ setIndicator({ left: active.offsetLeft, width: active.offsetWidth });
+ };
+ measure();
+
+ const observer = new ResizeObserver(measure);
+ if (wrapper.current) observer.observe(wrapper.current);
+ for (const button of buttons.current.values()) observer.observe(button);
+ return () => observer.disconnect();
+ }, [value, segments]);
+
+ return (
+
+ {indicator && (
+
+ )}
+
+ {segments.map((segment) => (
+
+ ))}
+
+ );
+}
From b735c173ab14d21b1a67981917422739445086b4 Mon Sep 17 00:00:00 2001
From: BryanFRD
Date: Fri, 21 Aug 2026 09:44:30 +0200
Subject: [PATCH 2/2] feat: overshoot the segmented control indicator on its
way to the active option
---
src/components/tracker/SegmentedControl.tsx | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/src/components/tracker/SegmentedControl.tsx b/src/components/tracker/SegmentedControl.tsx
index 37e20d2..d41feed 100644
--- a/src/components/tracker/SegmentedControl.tsx
+++ b/src/components/tracker/SegmentedControl.tsx
@@ -65,7 +65,7 @@ export default function SegmentedControl({
ref={wrapper}
role="tablist"
aria-label={label}
- className={`relative flex shrink-0 bg-teal-100 dark:bg-white/5 border border-teal-200/40 dark:border-white/10 ${styles.wrapper}`}
+ className={`relative flex shrink-0 overflow-hidden bg-teal-100 dark:bg-white/5 border border-teal-200/40 dark:border-white/10 ${styles.wrapper}`}
>
{indicator && (
({
transform: `translateX(${indicator.left}px)`,
width: `${indicator.width}px`,
transitionProperty: "transform, width",
- transitionDuration: reducedMotion ? "0ms" : "280ms",
- transitionTimingFunction: "cubic-bezier(0.22, 1, 0.36, 1)",
+ // The slide overshoots slightly then settles; the width stays on a plain
+ // ease-out so the pill does not visibly stretch past its target.
+ transitionDuration: reducedMotion ? "0ms, 0ms" : "420ms, 260ms",
+ transitionTimingFunction: reducedMotion
+ ? "linear, linear"
+ : "cubic-bezier(0.34, 1.35, 0.5, 1), cubic-bezier(0.22, 1, 0.36, 1)",
}}
/>
)}