From 66bbb0b3c72c8e4fe732d0baef84cb0e00329104 Mon Sep 17 00:00:00 2001 From: grim <75869731+ripgrim@users.noreply.github.com> Date: Fri, 3 Jul 2026 17:57:52 -0700 Subject: [PATCH 1/5] fix: real default border color + dither-kit theme toggle - Tailwind v4 has no --default-border-color mechanism; the bare border utility fell back to currentColor (white on dark). Base layer rule now points default borders at the border token. - /dither-kit gets its theme toggle back: page-scoped .dither-light token overrides + a dark wrapper class for dark: variants, persisted in localStorage. The app itself stays dark-only. --- apps/web/src/routes/dither-kit.tsx | 37 ++++++++++++++++++++++++++++-- apps/web/src/styles.css | 35 +++++++++++++++++++++++++--- 2 files changed, 67 insertions(+), 5 deletions(-) diff --git a/apps/web/src/routes/dither-kit.tsx b/apps/web/src/routes/dither-kit.tsx index b2aaa3e..e2ae4d9 100644 --- a/apps/web/src/routes/dither-kit.tsx +++ b/apps/web/src/routes/dither-kit.tsx @@ -2,8 +2,10 @@ import { createFileRoute } from "@tanstack/react-router" import { CheckIcon, CopyIcon, + MoonIcon, RefreshCcwIcon, SlidersHorizontalIcon, + SunIcon, XIcon, } from "lucide-react" import { Fragment, type ReactNode, useEffect, useRef, useState } from "react" @@ -177,6 +179,24 @@ function useSettled(value: string | number, ms: number, onSettle: () => void) { }, [value, ms]) } +/** Page-scoped theme. The app is dark-only, so instead of a global theme the + * page swaps the `.dither-light` token overrides (and the `dark` class that + * drives `dark:` variants) on its own wrapper. Persisted per visitor. */ +function usePageTheme() { + const [light, setLight] = useState( + () => + typeof window !== "undefined" && + window.localStorage.getItem("dither-kit-theme") === "light" + ) + const toggle = () => { + setLight((v) => { + window.localStorage.setItem("dither-kit-theme", v ? "dark" : "light") + return !v + }) + } + return { light, toggle } +} + function useCopy() { const [copied, setCopied] = useState(false) const copy = (text: string) => { @@ -658,6 +678,7 @@ function Showcase({ /* ------------------------------------------------------------------ page */ function DitherKitDocs() { + const { light, toggle: toggleTheme } = usePageTheme() const [tweaks, setTweaks] = useState(DEFAULT_TWEAKS) const [panelOpen, setPanelOpen] = useState(false) const [pm, setPm] = useState("npm") @@ -694,8 +715,8 @@ function DitherKitDocs() { // stay in view while you turn the knobs.
@@ -711,6 +732,18 @@ function DitherKitDocs() { five chart types on one tiny canvas engine, no recharts

+

diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index f63c8fc..f583f12 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -27,9 +27,6 @@ --font-sans: "Geist", system-ui, sans-serif; --font-display: "Playfair Display", Georgia, serif; - /* Default border color for Tailwind's `border` utility */ - --default-border-color: #27272a; - /* Tripwire color palette (from prototype) */ --color-tw-bg: #0d0d0f; --color-tw-surface: #17171a; @@ -92,6 +89,38 @@ --font-pixel-geist: "Geist Pixel", ui-monospace, monospace; } +/* Default border color for Tailwind's `border` utility. v4 defaults to + currentColor (which reads as white on our dark theme); pointing it at the + border token restores the intended hairlines everywhere. */ +@layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-border); + } +} + +/* Light palette for the dither-kit docs page. The app itself is dark-only, so + the page's theme toggle swaps these shadcn-alias tokens on its own wrapper + instead of a global theme. */ +.dither-light { + --color-background: oklch(1 0 0); + --color-foreground: oklch(0.141 0.005 285.823); + --color-card: oklch(0.985 0 0); + --color-card-foreground: oklch(0.141 0.005 285.823); + --color-popover: oklch(1 0 0); + --color-popover-foreground: oklch(0.141 0.005 285.823); + --color-border: oklch(0.92 0.004 286.32); + --color-muted: oklch(0.967 0.001 286.375); + --color-muted-foreground: oklch(0.552 0.016 285.938); + --color-primary: oklch(0.21 0.006 285.885); + --color-primary-foreground: oklch(0.985 0 0); + --color-accent: oklch(0.967 0.001 286.375); + --color-accent-foreground: oklch(0.21 0.006 285.885); +} + body { @apply m-0 bg-tw-bg text-tw-text-primary antialiased; font-family: "Geist", system-ui, sans-serif; From 84b88fa3f777594d73f1f265f9cdef033b8165b2 Mon Sep 17 00:00:00 2001 From: grim <75869731+ripgrim@users.noreply.github.com> Date: Fri, 3 Jul 2026 18:04:20 -0700 Subject: [PATCH 2/5] feat: swap the dither-kit tweak panel for DialKit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hand-rolled sidebar (Field/Select/Range/TweakSidebar) is replaced by DialKit's floating control panel (https://joshpuckett.me/dialkit) — bloom folder (preset + custom sliders), per-series variant selects, stacking toggle, pie radius + entrance sliders, and a replay-all action. productionEnabled so visitors on tripwire.sh get the panel too; the settle-replay behavior is unchanged (any tweak replays after 300ms). --- apps/web/package.json | 1 + apps/web/src/routes/dither-kit.tsx | 348 +++++------------------------ pnpm-lock.yaml | 37 ++- 3 files changed, 99 insertions(+), 287 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index aa8e4eb..ac805cb 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -72,6 +72,7 @@ "clsx": "^2.1.1", "d3-scale": "^4.0.2", "d3-shape": "^3.2.0", + "dialkit": "^1.3.0", "drizzle-kit": "^0.31.9", "drizzle-orm": "^0.45.1", "evlog": "^2.17.0", diff --git a/apps/web/src/routes/dither-kit.tsx b/apps/web/src/routes/dither-kit.tsx index e2ae4d9..46f9dce 100644 --- a/apps/web/src/routes/dither-kit.tsx +++ b/apps/web/src/routes/dither-kit.tsx @@ -1,12 +1,12 @@ import { createFileRoute } from "@tanstack/react-router" +import { DialRoot, useDialKit } from "dialkit" +import "dialkit/styles.css" import { CheckIcon, CopyIcon, MoonIcon, RefreshCcwIcon, - SlidersHorizontalIcon, SunIcon, - XIcon, } from "lucide-react" import { Fragment, type ReactNode, useEffect, useRef, useState } from "react" import { @@ -108,19 +108,6 @@ type Tweaks = { duration: number // entrance ms } -const DEFAULT_TWEAKS: Tweaks = { - bloomPreset: "aura", - blur: 24, - brightness: 2.9, - opacity: 0.1, - saturate: 3, - primaryVariant: "gradient", - secondaryVariant: "hatched", - stacked: true, - donutRadius: 0.5, - duration: 900, -} - function bloomOf(t: Tweaks): BloomInput { return t.bloomPreset === "custom" ? { @@ -359,255 +346,6 @@ function ReplayButton({ onClick }: { onClick: () => void }) { ) } -/* --------------------------------------------------------- tweak sidebar */ - -function Field({ label, children }: { label: string; children: ReactNode }) { - return ( - - ) -} - -function Select({ - value, - options, - onChange, - ariaLabel, -}: { - value: T - options: readonly T[] - onChange: (v: T) => void - ariaLabel: string -}) { - return ( - - ) -} - -function Range({ - value, - min, - max, - step, - onChange, - ariaLabel, -}: { - value: number - min: number - max: number - step: number - onChange: (v: number) => void - ariaLabel: string -}) { - return ( -

- onChange(Number(e.target.value))} - className="w-full accent-foreground" - /> - - {value} - -
- ) -} - -function TweakSidebar({ - open, - onClose, - tweaks, - setTweaks, - onReplayAll, -}: { - open: boolean - onClose: () => void - tweaks: Tweaks - setTweaks: (t: Tweaks) => void - onReplayAll: () => void -}) { - const set = (key: K, value: Tweaks[K]) => - setTweaks({ ...tweaks, [key]: value }) - - // Docked, not modal — the page stays visible and interactive alongside, so - // you can tweak, watch the charts respond, and tweak again. - return ( -