Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
682caea
Add Browse Skills nav button + TextMorph loading transition for /skills
hien-p Aug 14, 2026
aade3f5
Merge origin/main into feat/skills-browse-cta-and-loading
hien-p Aug 19, 2026
b1c918f
Redesign /skills as a dashboard with a code-editor-style file browser
hien-p Aug 19, 2026
d325c85
Fix markdown list parsing, sync editor modes, color the file tree
hien-p Aug 23, 2026
af956a1
Render code through a shared CodeBlock, link Source out to GitHub
hien-p Aug 23, 2026
020ee40
Add PixelLiquidBg fluid background behind the skills dashboard
hien-p Aug 23, 2026
b90f377
Make the dashboard cards glass so the background reads through them
hien-p Aug 23, 2026
7546025
Add a light/dark toggle and open the glass up
hien-p Aug 23, 2026
f97f266
Build the Visualize interactions page from the skill reference graph
hien-p Aug 23, 2026
30043db
Render eval.json as a spec, and drive the glass from Liquefy theming
hien-p Aug 23, 2026
24b2dec
Stop the icon rail's tooltips being painted over by the content panel
hien-p Aug 24, 2026
a67280c
Read skills/ once into a normalized catalog with a build-time snapshot
hien-p Aug 28, 2026
47879aa
Rebuild /skills as a searchable catalog beside a skill detail surface
hien-p Aug 28, 2026
83169bc
Give each skill a file workspace with local drafts that survive navig…
hien-p Aug 28, 2026
e034371
Tune the shell's layering, responsiveness and accessibility
hien-p Aug 28, 2026
f5c3b9d
Resolve the skill id in Prompt Inputs the same way every other surfac…
hien-p Aug 28, 2026
9bc333c
Meet WCAG AA on the skills surfaces in both themes
hien-p Aug 28, 2026
0d003af
Make graph state readable in both themes, and stop routing to a 404
hien-p Aug 29, 2026
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
18 changes: 15 additions & 3 deletions app/SkillPageClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { MaskedHeading } from "@/components/motion/masked-heading";
import { DecryptReveal } from "@/components/motion/decrypt-reveal";
import BendingMarquee from "@/components/motion/bending-marquee";
import { ParticleScroll } from "@/components/motion/particle-scroll";
import { SkillsTransitionLink } from "@/components/motion/skills-transition-link";
import { ScrambledInstallCommand, type PkgManager } from "@/components/motion/scrambled-install-command";
import { type RealSkill, CATEGORY_LABELS } from "@/lib/skill-types";
import {
Expand Down Expand Up @@ -382,6 +383,17 @@ function Nav({
<span>{repoMeta.stars !== null ? formatStarCount(repoMeta.stars) : "—"}</span>
</a>

<SkillsTransitionLink
href="/skills"
className={`hidden items-center gap-1.5 rounded border px-2.5 py-1.5 text-[13px] font-medium transition-colors sm:flex ${
isDark
? "border-[var(--border-subtle)] bg-[var(--bg-surface)] text-[var(--text-secondary)] hover:border-[var(--border-strong)] hover:text-[var(--text-primary)]"
: "border-[#E2E8F0] bg-[#F1F5F9] text-[#475569] hover:border-[#CBD5E1] hover:text-[#0F172A]"
}`}
>
Browse Skills
</SkillsTransitionLink>

<Link
href="/quickstart"
onClick={(e) => navigateToSection(e, "quickstart")}
Expand Down Expand Up @@ -1233,13 +1245,13 @@ function SkillCatalog({
</div>
<div className="lg:col-start-8 lg:col-span-5">
<p className="glass-text-secondary text-[15px] leading-[1.6] text-[var(--text-secondary)]">
Every skill is read live from skills/&lt;id&gt;/SKILL.md — name, description, and version straight from the repo.
Every skill is read straight from skills/&lt;id&gt;/SKILL.md when the site is built — name, description, and version out of the repo, not a hand-kept copy.
</p>
<div className="mt-2 flex items-center gap-3 font-mono text-[13px]">
<span className="glass-text-secondary text-[var(--text-tertiary)]">08.0 Skill Catalog →</span>
<Link href="/skills" className="text-[#82AAFF] hover:underline">
<SkillsTransitionLink href="/skills" className="text-[#82AAFF] hover:underline">
Browse every file →
</Link>
</SkillsTransitionLink>
</div>
</div>
</div>
Expand Down
122 changes: 122 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,125 @@
-webkit-font-smoothing: antialiased;
}


/* Glass surfaces: card backgrounds that let the animated background show
through. Defined once here so the whole dashboard is tuned from one place
— raise the percentage to calm the background down, lower it to let more
through. They resolve against whichever --bg-* the active theme sets, so a
single definition covers light and dark. Tailwind v3 cannot apply an
opacity modifier to a var colour, which is why these exist at all rather
than a `/72` suffix at each call site. */
:root {
/* Fallbacks for anything rendered outside a LiquefyProvider. */
--glass-surface: color-mix(in srgb, var(--bg-surface) 38%, transparent);
--glass-frame: color-mix(in srgb, var(--bg-frame) 30%, transparent);
--glass-elevated: color-mix(in srgb, var(--bg-elevated) 40%, transparent);
--glass-line: var(--border-subtle);
--glass-blur: 12px;
}

/* Declared on the provider rather than :root on purpose: a custom property is
substituted at computed-value time on the element that declares it, and
--lq-* only exists from .lq-provider down. Declaring these at :root
resolved --lq-glass-soft to its fallback before any card could inherit it,
so Liquefy's theme and tint had no effect on the surfaces. */
.lq-provider {
--glass-surface: color-mix(in srgb, var(--bg-surface) 38%, var(--lq-glass-soft));
--glass-frame: color-mix(in srgb, var(--bg-frame) 30%, var(--lq-glass-soft));
--glass-elevated: color-mix(in srgb, var(--bg-elevated) 40%, var(--lq-glass-soft));
--glass-line: var(--lq-line, var(--border-subtle));
--glass-blur: var(--lq-blur, 12px);
}

/* The /skills shell reads long prose and code, so its panels sit much closer
to opaque than the marketing pages' glass does. Declared after .lq-provider
so it wins on source order at equal specificity, and scoped to the shell so
the rest of the site keeps the translucent look. */
.skills-shell {
--glass-surface: color-mix(in srgb, var(--bg-surface) 88%, transparent);
--glass-frame: color-mix(in srgb, var(--bg-frame) 90%, transparent);
--glass-elevated: color-mix(in srgb, var(--bg-elevated) 88%, transparent);
}

/* The workspace is the one surface people read and type into for minutes at a
time, so it opts out of translucency entirely: nested translucent panels
over a moving background is what makes long reading tiring. Declared as
token overrides so every nested surface inherits it without each component
having to know it is inside a workspace. */
.skills-workspace {
--glass-surface: var(--bg-surface);
--glass-frame: var(--bg-frame);
--glass-elevated: var(--bg-elevated);
}

/* Measured, not guessed: on this shell's card surfaces the shared dark
--text-tertiary comes out at 3.67:1, under the 4.5:1 WCAG AA asks for at
these sizes, and here it carries real information — trigger phrases,
categories, the repository snapshot. Brightened locally to 4.76:1, which
keeps the hierarchy intact (primary 14.5, secondary 6.8, tertiary 4.8) and
leaves the rest of the site's text alone, the way .intro-panel-text
already does for the marketing sections. */
.skills-shell {
/* #82AAFF is the site's accent and reads well on dark, but as *text* on a
light surface it measures 1.96:1 — unreadable, and it was carrying the
card's primary action. Light mode gets a darker blue of the same family;
tints and filled buttons keep the original hue, since those are
backgrounds rather than text. */
--accent: #2F63C7;
--text-tertiary: #5C6675;
}

.skills-shell {
/* The graph's colours are meaning, not decoration: a node's fill says
whether it is the skill you pinned, and an edge's stroke says which
direction the reference runs. WCAG asks meaningful non-text graphics for
~3:1, and the dark palette's #82AAFF/#F472B6 sit near 1.9:1 on a light
card. Semantic tokens per theme, so the graph reads in both. */
--skill-node: #2F63C7;
--skill-node-active: #B4126B;
--skill-edge: #94A3B8;
--skill-edge-out: #2F63C7;
--skill-edge-in: #B4126B;
}

.dark .skills-shell {
--accent: #82AAFF;
--text-tertiary: #7C8593;
--text-disabled: #6E7684;
--skill-node: #82AAFF;
--skill-node-active: #F472B6;
--skill-edge: var(--border-strong);
--skill-edge-out: #82AAFF;
--skill-edge-in: #F472B6;
}

/* Scrollbars: the browser default renders as a bright white bar against the
dark surfaces, which reads as a UI element rather than chrome. Tint it to
the existing border token so it recedes, in both themes. */
@layer base {
* {
scrollbar-width: thin;
scrollbar-color: var(--border-strong) transparent;
}

::-webkit-scrollbar {
width: 10px;
height: 10px;
}

::-webkit-scrollbar-track,
::-webkit-scrollbar-corner {
background: transparent;
}

::-webkit-scrollbar-thumb {
background-color: var(--border-strong);
border: 3px solid transparent;
background-clip: content-box;
border-radius: 9999px;
}

::-webkit-scrollbar-thumb:hover {
background-color: var(--text-disabled);
}
}
73 changes: 73 additions & 0 deletions app/skills/[skillId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import type { Metadata } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";
import { ArrowLeft, ChevronRight } from "lucide-react";
import { getSkillCatalog, getSkillDocument } from "@/lib/skill-catalog";
import { getRepoSnapshot } from "@/lib/repo-snapshot";
import { SkillDetail } from "@/components/skills/skill-detail";
import { MarkdownPreview } from "@/components/skills/markdown-preview";
import { RepoSnapshotChip } from "@/components/skills/repo-snapshot-chip";
import { RememberSkill } from "@/components/skills/remember-skill";

export const dynamic = "force-static";
export const dynamicParams = false;

export function generateStaticParams() {
return getSkillCatalog().map((skill) => ({ skillId: skill.id }));
}

export async function generateMetadata({
params,
}: {
params: Promise<{ skillId: string }>;
}): Promise<Metadata> {
const { skillId } = await params;
const skill = getSkillCatalog().find((s) => s.id === skillId);
if (!skill) return {};
return {
title: `${skill.handle} · AI DevKit Skills`,
description: skill.summary || skill.description,
};
}

export default async function SkillDetailPage({ params }: { params: Promise<{ skillId: string }> }) {
const { skillId } = await params;
const skills = getSkillCatalog();
const skill = skills.find((s) => s.id === skillId);
if (!skill) notFound();

const handles = Object.fromEntries(skills.map((s) => [s.id, s.handle]));
const document = getSkillDocument(skill.id);

return (
<div className="min-h-0 flex-1 overflow-y-auto">
<RememberSkill id={skill.id} />

<div className="mx-auto flex max-w-[880px] flex-col gap-6 pb-10">
<div className="flex flex-wrap items-center justify-between gap-3">
<nav aria-label="Breadcrumb" className="flex items-center gap-1.5 text-[12.5px] text-[var(--text-tertiary)]">
<Link
href="/skills"
className="inline-flex items-center gap-1.5 transition-colors hover:text-[var(--text-primary)]"
>
<ArrowLeft size={13} strokeWidth={1.75} />
All skills
</Link>
<ChevronRight size={13} strokeWidth={1.75} aria-hidden="true" className="text-[var(--border-strong)]" />
<span className="font-mono text-[var(--text-secondary)]">{skill.id}</span>
</nav>
<RepoSnapshotChip snapshot={getRepoSnapshot()} />
</div>

<SkillDetail
skill={skill}
handles={handles}
variant="page"
documentSlot={
document ? <MarkdownPreview content={document} variant="document" showFrontmatter={false} /> : null
}
/>
</div>
</div>
);
}
90 changes: 90 additions & 0 deletions app/skills/[skillId]/workspace/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import type { Metadata } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";
import { ArrowLeft, ChevronRight, ExternalLink } from "lucide-react";
import { getSkillCatalog } from "@/lib/skill-catalog";
import { getSkillTreeNodes } from "@/lib/skills-tree";
import { REPO_SKILLS_TREE } from "@/lib/repo-links";
import SkillTreeBrowser from "@/components/skills/skill-tree-browser";
import { RememberSkill } from "@/components/skills/remember-skill";

export const dynamic = "force-static";
export const dynamicParams = false;

export function generateStaticParams() {
return getSkillCatalog().map((skill) => ({ skillId: skill.id }));
}

export async function generateMetadata({
params,
}: {
params: Promise<{ skillId: string }>;
}): Promise<Metadata> {
const { skillId } = await params;
const skill = getSkillCatalog().find((s) => s.id === skillId);
if (!skill) return {};
return {
title: `${skill.handle} workspace · AI DevKit Skills`,
description: `Read and mark up every file in ${skill.handle} — SKILL.md, references, and scripts.`,
};
}

export default async function SkillWorkspacePage({ params }: { params: Promise<{ skillId: string }> }) {
const { skillId } = await params;
const skill = getSkillCatalog().find((s) => s.id === skillId);
const tree = getSkillTreeNodes(skillId);
if (!skill || !tree) notFound();

return (
<div className="flex h-full min-h-0 flex-col gap-4">
<div className="flex shrink-0 flex-wrap items-start justify-between gap-3">
<div className="min-w-0">
<nav aria-label="Breadcrumb" className="flex items-center gap-1.5 text-[12px] text-[var(--text-tertiary)]">
<Link href="/skills" className="transition-colors hover:text-[var(--text-primary)]">
All skills
</Link>
<ChevronRight size={12} strokeWidth={1.75} aria-hidden="true" className="text-[var(--border-strong)]" />
<Link href={`/skills/${skill.id}`} className="font-mono transition-colors hover:text-[var(--text-primary)]">
{skill.id}
</Link>
<ChevronRight size={12} strokeWidth={1.75} aria-hidden="true" className="text-[var(--border-strong)]" />
<span className="text-[var(--text-secondary)]">Workspace</span>
</nav>
<div className="mt-1 flex flex-wrap items-baseline gap-x-3 gap-y-1">
<h1 className="font-mono text-[16px] font-semibold text-[#82AAFF]">{skill.handle}</h1>
<p className="max-w-[70ch] text-[13px] text-[var(--text-secondary)]">
{skill.summary || skill.description}
</p>
</div>
<p className="mt-1 text-[12px] text-[var(--text-tertiary)]">
{skill.files.length} files · read, mark up locally, copy out. Edits stay in this browser.
</p>
</div>

<div className="flex flex-wrap items-center gap-2">
<Link
href={`/skills/${skill.id}`}
className="inline-flex h-9 items-center gap-1.5 rounded-lg border border-[var(--border-subtle)] px-3 text-[12.5px] text-[var(--text-secondary)] transition-colors hover:border-[var(--border-strong)] hover:text-[var(--text-primary)]"
>
<ArrowLeft size={13} strokeWidth={1.75} />
Skill overview
</Link>
<a
href={`${REPO_SKILLS_TREE}/${skill.id}`}
target="_blank"
rel="noreferrer"
className="inline-flex h-9 items-center gap-1.5 rounded-lg border border-[var(--border-subtle)] px-3 text-[12.5px] text-[var(--text-secondary)] transition-colors hover:border-[var(--border-strong)] hover:text-[var(--text-primary)]"
>
<ExternalLink size={13} strokeWidth={1.75} />
GitHub
</a>
</div>
</div>

<div className="min-h-0 flex-1">
<RememberSkill id={skill.id} />
<SkillTreeBrowser tree={tree} rootLabel={skill.id} />
</div>
</div>
);
}
Loading
Loading