Notes

ยฉ floguo 2025. v3.3.0 ยท changelog ยท ยท rss

05-16-2026

Site Updates

Working changes to the site around May 16, 2026.

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:

  1. Checking out a new branch from the previous branch (or main)
  2. Staging only the files for that group โ€” never git add -A
  3. Running Prettier on staged files before committing
  4. Creating each PR targeting the previous branch, not main
text
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:

typescript
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])
bubble shrink-wrap โ€” before / after
before (css fit-content)
Hi!
What are you working on right now?
ok
That sounds really interesting โ€” how long have you been building this?
after (pretext shrink-wrap)
Hi!
What are you working on right now?
ok
That sounds really interesting โ€” how long have you been building this?

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:

typescript
// 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.

css
@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).

ASCII dandelion
\|//
 :
\|//
 ;
\|//
 ;
=\||
   \
||/_
 --
-\||
   \
      ..  .  ..
         ..   \ | /   ..
              -(:)-
         ..   / | \   ..
            ..  !  ..
                |
                |
                |
           |\   |   /|
           / \  |  / \
          /_ /  |  \ _\
             \  |  /
              \ | /
               \|/

Design tokens and UI primitives

globals.css adds three motion easing tokens, replacing scattered inline cubic-bezier(...) values.

motion easing tokens โ€” click โ–ถ to play
ease-spring
cubic-bezier(0.34, 1.56, 0.64, 1)
ease-smooth-out
cubic-bezier(0.22, 1, 0.36, 1)
ease-standard
cubic-bezier(0.4, 0, 0.2, 1)

button.tsx adds spring press animations on IconButton and OutlineButton using two transitions on the same property:

css
/* 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-400

select.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.

typescript
// 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.

text
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:

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.