Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
281 changes: 180 additions & 101 deletions app/changelog/page.tsx

Large diffs are not rendered by default.

66 changes: 66 additions & 0 deletions components/changelog/ArcadeBackground.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"use client";

// Persistent version of the Arcade pixel engine — same field/grain/moire
// texture ArcadeSplash uses for the 5s intro reveal, but running
// continuously as the page background instead of fading out. Uses the dark
// colorway variant (see arcade/params.ts) so it recedes behind real content
// instead of reading as a bright logo.

import { useEffect, useRef } from "react";
import { cn } from "@/lib/utils";
import { Arcade } from "./arcade/engine";
import { COLORWAYS } from "./arcade/params";

const DARK_COLORWAY = COLORWAYS.findIndex((c) => c.name === "Cyanide Dark");

export interface ArcadeBackgroundProps {
className?: string;
word?: string;
/** Index into arcade/params.ts COLORWAYS. Defaults to the dark variant. */
colorway?: number;
/** Quantization edge — same param ArcadeSplash exposes. */
threshold?: number;
/** Surface texture (moire/grain/dust) strength, 0..1. */
texture?: number;
opacity?: number;
}

export function ArcadeBackground({
className,
word = "changelog",
colorway = DARK_COLORWAY,
threshold = 0.588,
texture = 0.25,
opacity = 1,
}: ArcadeBackgroundProps) {
const hostRef = useRef<HTMLDivElement>(null);

useEffect(() => {
const host = hostRef.current;
if (!host) return;

const engine = new Arcade(host);
if (!engine.ok) return;
engine.setParams({ word, colorway, threshold, texture });
engine.start();

// The engine only reads the host's size at construction and at start()
// — no internal ResizeObserver, so a first read that races ahead of
// layout (or an actual window resize, since a 5s splash never lives
// long enough for that to matter but a persistent background does)
// would otherwise stick forever. A rAF re-read catches the common case
// fast (the host is 0×0 on the very first synchronous read, every load);
// the ResizeObserver is the ongoing fallback for anything after that.
const raf = requestAnimationFrame(() => engine.resize());
const ro = new ResizeObserver(() => engine.resize());
ro.observe(host);

return () => {
cancelAnimationFrame(raf);
ro.disconnect();
engine.destroy();
};
}, [word, colorway, threshold, texture]);

return <div ref={hostRef} className={cn("relative", className)} style={{ opacity }} />;
}
56 changes: 56 additions & 0 deletions components/changelog/ArcadeSplash.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"use client";

import { useEffect, useRef, useState } from "react";
import { Arcade } from "./arcade/engine";

const TOTAL_MS = 5000;
const FADE_MS = 600;
const HOLD_MS = TOTAL_MS - FADE_MS;

/** Full-viewport intro: the arcade-pixel word holds, then fades to reveal the page. */
export function ArcadeSplash() {
const hostRef = useRef<HTMLDivElement>(null);
const [mounted, setMounted] = useState(false);
const [visible, setVisible] = useState(true);
const [fading, setFading] = useState(false);

useEffect(() => {
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
setVisible(false);
return;
}
setMounted(true);

const fadeTimer = setTimeout(() => setFading(true), HOLD_MS);
const removeTimer = setTimeout(() => setVisible(false), TOTAL_MS);
return () => {
clearTimeout(fadeTimer);
clearTimeout(removeTimer);
};
}, []);

useEffect(() => {
const host = hostRef.current;
if (!mounted || !host) return;

const engine = new Arcade(host);
if (engine.ok) engine.start();

return () => engine.destroy();
}, [mounted]);

if (!mounted || !visible) return null;

return (
<div
className="fixed inset-0 z-50 transition-opacity"
style={{
opacity: fading ? 0 : 1,
transitionDuration: `${FADE_MS}ms`,
pointerEvents: fading ? "none" : "auto",
}}
>
<div ref={hostRef} className="relative h-full w-full" />
</div>
);
}
72 changes: 72 additions & 0 deletions components/changelog/InstallBlock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"use client";

// The "here's how to actually get it" block for the launch entry — typical
// uses + a copyable install command, same shape as Superset's own changelog
// install callout. Commands come straight from this repo's own
// INSTALLATION.md (skills.sh + the Claude Code plugin trial), not copied
// from Superset's.

import { useState } from "react";
import { Copy, Check } from "lucide-react";

const USES = [
"Bootstrap /docs once, then capture requirements, design, and decisions as you build",
"Hand a ticket straight to delivery — intake, spec, implement, review, ship, end to end",
"Vendor and sync skills with upstream without losing your repo's own adaptations",
];

const INSTALL_CMD = "npx skills add CommandOSSLabs/ai-devkit";
const PLUGIN_CMD = "claude plugin add CommandOSSLabs/ai-devkit";

function CommandLine({ text }: { text: string }) {
const [copied, setCopied] = useState(false);
const [cmd, ...args] = text.split(" ");

const onCopy = async () => {
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
};

return (
<div className="flex items-center justify-between gap-3 rounded-lg border border-[var(--border-subtle)] bg-[var(--bg-base)] px-4 py-3">
<code className="font-mono text-[14px]">
<span className="text-[var(--syntax-func)]">{cmd}</span>{" "}
<span className="text-[var(--syntax-string)]">{args.join(" ")}</span>
</code>
<button
type="button"
onClick={onCopy}
aria-label="Copy command"
className="shrink-0 rounded p-1 text-[var(--text-tertiary)] transition-colors hover:text-[var(--text-primary)]"
>
{copied ? <Check size={14} /> : <Copy size={14} />}
</button>
</div>
);
}

export function InstallBlock() {
return (
<div className="mt-5 space-y-4 text-[15px] text-[var(--text-secondary)]">
<div>
<p>Typical uses:</p>
<ul className="mt-2 flex list-disc flex-col gap-2 pl-5 leading-relaxed marker:text-[var(--text-disabled)]">
{USES.map((use) => (
<li key={use}>{use}</li>
))}
</ul>
</div>

<CommandLine text={INSTALL_CMD} />

<p className="text-[14px]">
Claude Code users can also try it zero-setup:{" "}
<code className="rounded bg-[var(--bg-base)] px-1.5 py-0.5 font-mono text-[13px] text-[var(--text-primary)]">
{PLUGIN_CMD}
</code>
.
</p>
</div>
);
}
Loading
Loading