Site Updates
Working changes to the site around May 16, 2026.
- Slicing with /stack-pr
- floGPT
- Design tokens and UI primitives
- Flash-free panel persistence
- Bookshelf sections
- Content infrastructure
- People and places
- z-index cleanup
- Skills section
Slicing with /stack-pr
/stack-pr reads git status, git log main..HEAD, and git diff --stat in parallel, then proposes 2-4 semantic groups: self-contained, dependency-ordered, with a branch name and file list.
Once confirmed, it builds the stack:
- Checking out a new branch from the previous branch (or
main) - Staging only the files for that group โ never
git add -A - Running Prettier on staged files before committing
- Creating each PR targeting the previous branch, not
main
main โ PR1 (tokens) โ PR2 (flochat-polish) โ PR3 (bookshelf) โ PR4 (content)Each PR body includes a checklist of the full stack.
floGPT
See Building floGPT for the original context.
System prompt and personality. Expanded from facts into background, day-to-day, taste, and how the site works.
Bubble shrink-wrap. fit-content still leaves dead space on wrapped multi-line bubbles. The target is the narrowest width that preserves the same line count.
UserBubble uses prepareWithSegments + walkLineRanges from @chenglou/pretext to binary-search that width without touching the DOM:
const bubbleWidth = useMemo(() => {
if (typeof window === "undefined" || !text.trim()) return maxBubbleWidth
try {
const prepared = prepareWithSegments(text, CHAT_USER_FONT, {
whiteSpace: "pre-wrap",
})
// Count lines at max width
let lineCount = 0
walkLineRanges(prepared, textMaxWidth, () => {
lineCount++
})
// Binary-search the tightest width that keeps the same line count
let lo = 0,
hi = textMaxWidth
while (lo < hi - 1) {
const mid = Math.floor((lo + hi) / 2)
let count = 0
walkLineRanges(prepared, mid, () => {
count++
})
if (count === lineCount) hi = mid
else lo = mid
}
return Math.min(hi + BUBBLE_PADDING_H, maxBubbleWidth)
} catch {
return maxBubbleWidth
}
}, [text, maxBubbleWidth, textMaxWidth])Curated follow-ups. Replaced /api/chat/suggestions with a hand-curated prompt pool that shuffles on each response. USE_AI_SUGGESTIONS keeps the AI path available:
// flip to true to use AI-generated suggestions
const USE_AI_SUGGESTIONS = false
const FOLLOWUP_PROMPTS = [
"What's it like being a design engineer?",
"How did you get into tech?",
"What does your day-to-day look like?",
// ...
]
function pickPrompts(n = 2): string[] {
return [...FOLLOWUP_PROMPTS].sort(() => Math.random() - 0.5).slice(0, n)
}ASCII dandelion. Empty state flowers grow in with a clip-path reveal, then float. Resetting chat replays the animation.
@keyframes flower-grow {
from {
clip-path: inset(100% 0 0 0);
}
to {
clip-path: inset(0% 0 0 0);
}
}
@keyframes float {
0%,
100% {
transform: translateY(0px);
}
50% {
transform: translateY(-5px);
}
}Each flower chains grow, then float (delay + 1.4s).
\|// :
\|// ;
\|// ;
=\|| \
||/_ --
-\|| \
.. . ..
.. \ | / ..
-(:)-
.. / | \ ..
.. ! ..
|
|
|
|\ | /|
/ \ | / \
/_ / | \ _\
\ | /
\ | /
\|/Design tokens and UI primitives
globals.css adds three motion easing tokens, replacing scattered inline cubic-bezier(...) values.
button.tsx adds spring press animations on IconButton and OutlineButton using two transitions on the same property:
/* press: fast and sharp */
active:scale-[0.94] active:duration-[50ms] active:ease-out
/* release: slow and springy โ the default transition takes over on mouseup */
transition-transform ease-spring duration-400select.tsx moved from Radix to Base UI for more consistent focus behavior. Options now use explicit <label> spans.
Flash-free panel persistence
use-resizable.ts now reads stored width in the useState initializer instead of after paint in useEffect.
// before: useEffect fires after first paint โ flash
const [width, setWidth] = useState(defaultWidth)
useEffect(() => {
const stored = localStorage.getItem(storageKey)
if (stored) setWidth(Number(stored))
}, [])
// after: initializer runs synchronously before first render
const [width, setWidth] = useState(() => {
if (typeof window === "undefined") return defaultWidth
const stored = localStorage.getItem(storageKey)
return stored ? Number(stored) : defaultWidth
})The panel starts at the stored width and avoids the first-paint flash.
Bookshelf sections
Books are grouped into collapsible sections: currently reading, in progress, finished. BookSection owns local collapse state.
Content infrastructure
Code blocks. Added language labels and copy buttons. Copy/check icons swap with the icon-swap keyframe in globals.css.
writtenBy field. Notes can declare writtenBy in frontmatter to show an attribution badge. Draft system. isDraft: true hides content in production and keeps it visible in dev.
People and places
Added Emily Campbell, BT Norris, and Inflight to people I admire. Added websites with a sortable/filterable WebsitesList.
z-index cleanup
Replaced semantic z-index classes (z-above, z-raised, z-nav, z-panel) with explicit numeric Tailwind values to fix FloChat/sidebar layering conflicts.
Skills section
The /skills tab documents local Claude Code skills. Two content sources merge at build time.
content/skills/*.mdx โ site docs with title, slug, category, description, and usage notes.
~/.claude/skills/<slug>/SKILL.md โ the actual skill definition. If found, the generator embeds it as a syntax-highlighted code block.
content/skills/loop.mdx โ prose docs (written by hand)
~/.claude/skills/loop/SKILL.md โ real skill file, read from disk
โ generate-content.mjs
content/skills.tsx โ generated output (like notes.tsx)To add a skill, create content/skills/<slug>.mdx:
---
title: "/skill-name"
slug: "skill-name"
category: "Category"
description: "One-line description"
---
Prose explanation here.The skill definition is pulled in automatically from ~/.claude/skills/<slug>/SKILL.md. If missing, only the prose shows.