LowkeyUI Living Styleguide Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: A dev-only /styleguide route in builder-ui that renders every real lowkeyui component with source pointers, plus a script that exports it as a self-contained HTML snapshot.

Architecture: A React page (app/src/pages-builder/styleguide/) imports real components / real kp-* markup — never copied CSS — registered behind import.meta.env.DEV via route.lazy. A Node script drives headless Chrome to snapshot the rendered page with its compiled CSS into __drops/lowkey-components.html. Components are added section-by-section with per-section user approval.

Tech Stack: React 18 + react-router v7 (route.lazy), Tailwind (compiled via app.css), vitest + renderPage from @testing/setup, playwright-core (channel: chrome) for export.

Environment notes (read first):

  • Repo path on host: ~/.config/af/agents/a1/workspace/builder-ui (bind-mounted into container a1 at ~/workspace/builder-ui).
  • node_modules contains linux native binaries — run pnpm/vitest inside the container: ssh -p 2223 agent@localhost 'cd ~/workspace/builder-ui && <cmd>'.
  • The dev server is already running; the app is at https://local.kualibuild.com:4001 (self-signed cert). Vite hot-reloads file changes — no restarts needed.
  • Work on branch styleguide-lowkey (already created; spec committed).
  • .lowkey/.lowkey-forms are normally added to <html> by feature flags (app/src/index.jsx:82-86); the styleguide page adds its own wrapper div so it renders correctly regardless of flag state.

Task 1: Page scaffold, dev-only route, smoke test

Files:

  • Create: app/src/pages-builder/styleguide/index.jsx

  • Create: app/src/pages-builder/styleguide/section.jsx

  • Create: app/src/pages-builder/styleguide/__tests__/index.test.jsx

  • Modify: app/src/routes.jsx (children of RootLayout, next to the debug/:id/* entry at ~line 501)

  • Step 1: Write the failing smoke test

/* Copyright © 2017-2026 Kuali, Inc. - All Rights Reserved */
import { screen } from '@testing-library/react'
import { renderPage } from '@testing/setup'
import { Component as Styleguide } from '../index'
describe('<Styleguide />', () => {
test('renders banner and warning', async () => {
await renderPage(<Styleguide />)
screen.getByText('LowkeyUI Styleguide')
screen.getByText(/do not copy styles from this page/i)
})
})
  • Step 2: Run test to verify it fails

Run: ssh -p 2223 agent@localhost 'cd ~/workspace/builder-ui && pnpm test:no-coverage app/src/pages-builder/styleguide' Expected: FAIL — cannot resolve ../index

  • Step 3: Create section.jsx (the per-component frame; its own chrome uses plain stone Tailwind classes — deliberately distinct from the components it displays)
/* Copyright © 2017-2026 Kuali, Inc. - All Rights Reserved */
import React from 'react'
export function Section ({ num, title, source, code, children }) {
return (
<section className='mt-10'>
<h2 className='flex items-center gap-2 text-xl font-bold text-stone-900'>
<span className='rounded-md bg-stone-900 px-2 py-0.5 text-sm font-semibold text-stone-50'>
{num}
</span>
{title}
</h2>
<div className='mt-1 text-xs text-stone-500'>
Source of truth: <code>{source}</code>
</div>
{code && (
<pre className='mt-2 overflow-x-auto rounded-md bg-stone-200 p-2 text-xs text-stone-800'>
{code}
</pre>
)}
<div className='mt-3 flex flex-wrap items-center gap-3 rounded-lg border border-stone-300 bg-white p-5'>
{children}
</div>
</section>
)
}
export function Swatch ({ label, children }) {
return (
<div className='flex flex-col items-start gap-1'>
<span className='text-[11px] uppercase tracking-wide text-stone-400'>
{label}
</span>
{children}
</div>
)
}
  • Step 4: Create index.jsx
/* Copyright © 2017-2026 Kuali, Inc. - All Rights Reserved */
import React from 'react'
export function Component () {
return (
<div className='lowkey lowkey-forms min-h-screen bg-stone-100 p-8'>
<div className='mx-auto max-w-5xl' data-styleguide-ready>
<h1 className='text-3xl font-bold text-stone-900'>
LowkeyUI Styleguide
</h1>
<div className='mt-4 rounded-md border border-amber-300 bg-amber-50 p-4 text-sm text-amber-900'>
<strong>For AI sessions and devs:</strong> do not copy styles from this
page. Import these components or use these class names — every section
lists its source of truth. Lowkey styles live in{' '}
<code>app/src/app.css</code> (the <code>.lowkey</code> /{' '}
<code>.lowkey-forms</code> blocks), <code>app/src/ui/</code>,{' '}
<code>app/src/ui/shadcn/</code>, and <code>tailwind.config.js</code>.
</div>
{/* Component sections are appended here, one per inventory item. */}
</div>
</div>
)
}
export default Component
  • Step 5: Register the dev-only route

In app/src/routes.jsx, inside the RootLayout children, directly after { path: 'debug/:id/*', element: <Debug /> }, add:

...(import.meta.env.DEV
? [{ path: 'styleguide', lazy: () => import('./pages-builder/styleguide') }]
: []),

(route.lazy loads the module's Component export on demand; in prod builds Vite replaces import.meta.env.DEV with false and the route — and its chunk — is eliminated. This mirrors the existing showDevRulePresentation conditional-spread pattern at routes.jsx:502-504.)

  • Step 6: Run test to verify it passes

Run: ssh -p 2223 agent@localhost 'cd ~/workspace/builder-ui && pnpm test:no-coverage app/src/pages-builder/styleguide' Expected: PASS (1 test)

  • Step 7: Verify live, including auth behavior

Open https://local.kualibuild.com:4001/styleguide in a browser (Playwright MCP or the user's browser). Expected: title + amber banner render. Contingency: if RootLayout forces a login redirect, that's acceptable for the live page but matters for the export script — note it, and in Task 4 pass a logged-in storageState to Playwright (or move the route next to { path: 'health' } which serves unauthenticated). Record which path was taken in the commit message.

  • Step 8: Commit
cd ~/.config/af/agents/a1/workspace/builder-ui
git add app/src/pages-builder/styleguide app/src/routes.jsx
git commit -m "feat(styleguide): dev-only /styleguide route with scaffold"

Task 2: Discovery sweep — finalize the component inventory

Files:

  • Create: docs/superpowers/specs/2026-06-10-lowkey-styleguide-inventory.md

  • Step 1: Sweep app.css for every kp-* class styled under .lowkey/.lowkey-forms

Run: awk '/\.lowkey/,/^}/' app/src/app.css | grep -oE 'kp-[a-z-]+' | sort -u Cross-check by reading app/src/app.css:234-720 directly (some selectors span lines).

  • Step 2: List every ui component

Run: ls app/src/ui app/src/ui/shadcn Known so far: alerts, checkbox, config-box, indeterminate-checkbox, info-box, input, lookup, pop-up, popover, radios, segmented-control, tabs, theme, toggle, tooltip; shadcn: dropdown-menu, popover, separator, sheet, sidebar, skeleton.

  • Step 3: Find inline lowkey-variant styling and flag-switched components

Run: grep -rl "lowkey:" app/src --include='*.jsx' --include='*.tsx' | sort and grep -rl "lowKeyUi\|lowKeyForms" app/src/components --include='*.jsx' | sort These reveal components styled with the lowkey: Tailwind variant or branching on the flags — candidates the old HTML doc missed.

  • Step 4: Identify the 1/2/3-panel layout patterns

Read app/src/components/app-layout.jsx, app/src/components/home-layout.jsx, app/src/components/sidebar/index.jsx, and one representative screen per shape (e.g. dashboard = sidebar + content; form builder = 3-panel). Record the component/classes that produce each layout.

  • Step 5: Write the inventory doc

docs/superpowers/specs/2026-06-10-lowkey-styleguide-inventory.md: a checklist table — section number, name, kind (atom/molecule/layout), real source file(s), seed-doc status (verified / corrected / hallucinated-dropped / newly-discovered). Order: form controls → buttons-adjacent → data display → feedback → overlays → navigation → molecules → layouts. Buttons are already Task 3; number the rest after it.

  • Step 6: Commit and present to user
git add docs/superpowers/specs/2026-06-10-lowkey-styleguide-inventory.md
git commit -m "docs(styleguide): component inventory from discovery sweep"

Show the user the inventory summary (counts: verified/corrected/dropped/new) and get approval of the build order before continuing.


Task 3: Buttons section (template for all later sections)

Files:

  • Modify: app/src/pages-builder/styleguide/index.jsx

  • Create: app/src/pages-builder/styleguide/sections/buttons.jsx

  • Step 1: Re-read the real button styles before writing markup

Read app/src/app.css:234-378. The variants that exist (everything else is hallucination): kp-button-primary, kp-button-secondary, kp-button-destructive, kp-button-outline, kp-button-ghost, kp-button-link (standard group); kp-button-with-icon, kp-button-primary-with-icon, kp-button-spinner-icon, kp-button-dropdown (icon group); kp-button-transparent (.lowkey-forms, app.css:356-378); modifiers kp-button-sm, kp-button-icon-only, kp-button-active.

  • Step 2: Create sections/buttons.jsx
/* Copyright © 2017-2026 Kuali, Inc. - All Rights Reserved */
import React from 'react'
import * as Icons from '../../../icons'
import { Section, Swatch } from '../section'
export function ButtonsSection ({ num }) {
return (
<Section
num={num}
title='Buttons'
source='app.css:234-352 (.lowkey .kp-button-*); app.css:356-378 (.lowkey-forms .kp-button-transparent)'
code={
"<button className='kp-button-primary'>… variants: -secondary -destructive -outline -ghost -link -with-icon -primary-with-icon -dropdown -transparent · modifiers: kp-button-sm kp-button-icon-only kp-button-active"
}
>
<Swatch label='primary'>
<button className='kp-button-primary'>Publish</button>
</Swatch>
<Swatch label='secondary'>
<button className='kp-button-secondary'>Cancel</button>
</Swatch>
<Swatch label='destructive'>
<button className='kp-button-destructive'>Delete</button>
</Swatch>
<Swatch label='outline'>
<button className='kp-button-outline'>Options</button>
</Swatch>
<Swatch label='ghost'>
<button className='kp-button-ghost'>Dismiss</button>
</Swatch>
<Swatch label='link'>
<button className='kp-button-link'>Learn more</button>
</Swatch>
<Swatch label='disabled'>
<button className='kp-button-primary' disabled>
Publish
</button>
</Swatch>
<Swatch label='sm'>
<button className='kp-button-secondary kp-button-sm'>Apply</button>
</Swatch>
<Swatch label='with-icon'>
<button className='kp-button-with-icon'>
<Icons.Settings />
Settings
</button>
</Swatch>
<Swatch label='with-icon active'>
<button className='kp-button-with-icon kp-button-active'>
<Icons.Settings />
Settings
</button>
</Swatch>
<Swatch label='primary-with-icon'>
<button className='kp-button-primary-with-icon'>
<Icons.Settings />
Share
</button>
</Swatch>
<Swatch label='icon-only'>
<button className='kp-button-outline kp-button-icon-only'>
<Icons.Settings />
</button>
</Swatch>
<Swatch label='dropdown'>
<button className='kp-button-dropdown'>
Actions
<Icons.MenuDown />
</button>
</Swatch>
<Swatch label='transparent'>
<button className='kp-button-transparent'>
<Icons.Settings />
Tools
</button>
</Swatch>
</Section>
)
}

(If Icons.Settings / Icons.MenuDown don't exist, pick the closest real exports — check grep -o "export const [A-Za-z]*" app/src/icons/index.tsx | head -40 — and verify the icon names against a real usage like app/src/pages-builder/form/index.jsx:379.)

  • Step 3: Mount it in index.jsx
import { ButtonsSection } from './sections/buttons'
// inside the wrapper div, replacing the placeholder comment:
<ButtonsSection num={1} />
  • Step 4: Verify against the real platform
  1. Open https://local.kualibuild.com:4001/styleguide — all swatches render, hover/disabled behave.
  2. Open a real screen using these buttons (form builder header uses kp-button-primary-with-icon; any modal footer uses primary/secondary) and screenshot both. Compare: height (40px / 32px sm), radius (6px), colors (primary = var(--primary, #0c4a6e) — sky-tinted dark, hover sky-950), shadow, gap.
  3. Fix discrepancies in the section markup (never in app.css).
  • Step 5: User approval gate

Show the user the section (URL + screenshot). Do not proceed to the next section without an explicit OK.

  • Step 6: Commit
git add app/src/pages-builder/styleguide
git commit -m "feat(styleguide): buttons section"

Task 4: Snapshot export script

Files:

  • Create: scripts/export-styleguide/index.mjs

  • Modify: package.json (devDependency)

  • Step 1: Add playwright-core (pure JS — safe across the linux/mac shared node_modules)

Run: ssh -p 2223 agent@localhost 'cd ~/workspace/builder-ui && pnpm add -D playwright-core'

  • Step 2: Create scripts/export-styleguide/index.mjs
/* Copyright © 2017-2026 Kuali, Inc. - All Rights Reserved */
// Exports the /styleguide page as a self-contained HTML snapshot.
// Run from the repo root ON THE HOST (uses installed Google Chrome):
// node scripts/export-styleguide/index.mjs [url] [outfile]
import fs from 'node:fs'
import path from 'node:path'
import { chromium } from 'playwright-core'
const url = process.argv[2] || 'https://local.kualibuild.com:4001/styleguide'
const out =
process.argv[3] ||
path.resolve(process.cwd(), '../__drops/lowkey-components.html')
const browser = await chromium.launch({
channel: process.env.PW_CHANNEL || 'chrome',
executablePath: process.env.PW_EXECUTABLE_PATH || undefined
})
const context = await browser.newContext({
ignoreHTTPSErrors: true,
viewport: { width: 1280, height: 900 },
storageState: process.env.PW_STORAGE_STATE || undefined
})
const page = await context.newPage()
await page.goto(url, { waitUntil: 'networkidle' })
await page.waitForSelector('[data-styleguide-ready]')
const html = await page.evaluate(() => {
let css = ''
for (const sheet of document.styleSheets) {
try {
for (const rule of sheet.cssRules) css += rule.cssText + '\n'
} catch {
/* cross-origin sheet — skip */
}
}
const doc = document.documentElement.cloneNode(true)
doc
.querySelectorAll('script, link[rel="stylesheet"], style')
.forEach(n => n.remove())
const style = document.createElement('style')
style.textContent = css
doc.querySelector('head').appendChild(style)
return '<!doctype html>\n' + doc.outerHTML
})
const banner = `<!-- GENERATED SNAPSHOT (${new Date().toISOString()}) — do not edit, do not copy styles.
Live page: ${url}
Regenerate: node scripts/export-styleguide/index.mjs
Source of truth: builder-ui app/src/app.css (.lowkey blocks), app/src/ui/, tailwind.config.js -->\n`
fs.writeFileSync(out, banner + html)
console.log(`wrote ${out} (${Math.round(fs.statSync(out).size / 1024)} KB)`)
await browser.close()

Known limitation (acceptable, noted in the file banner): web fonts and images keep their origin URLs, so offline viewing falls back to system fonts; all component styles (the point of the doc) are inlined.

  • Step 3: Run it and verify

Run on the host: cd ~/.config/af/agents/a1/workspace/builder-ui && node scripts/export-styleguide/index.mjs Expected: wrote …/__drops/lowkey-components.html (… KB). If Task 1 Step 7 found a login redirect, export a logged-in storage state first (PW_STORAGE_STATE). Then open ~/.config/af/agents/a1/workspace/__drops/lowkey-components.html — banner + buttons section visually identical to the live page; hover a button to confirm CSS interactivity survived.

  • Step 4: Commit
git add scripts/export-styleguide package.json pnpm-lock.yaml
git commit -m "feat(styleguide): HTML snapshot export script"

Task 5: Discoverability pointers

Files:

  • Modify: CLAUDE.md and/or AGENTS.md (check ls -la CLAUDE.md AGENTS.md — if one symlinks the other, edit the real file once)

  • Step 1: Add this section

## LowkeyUI styleguide
- Live styleguide (dev only): `https://local.kualibuild.com:4001/styleguide`
(port = 4000 + af container index). Renders the real lowkey components with
per-section source pointers.
- Source of truth for lowkey styles: `app/src/app.css` (the `.lowkey` /
`.lowkey-forms` blocks), `app/src/ui/`, `app/src/ui/shadcn/`,
`tailwind.config.js`. Never copy styles from reference docs or snapshots —
import the components or use the `kp-*` classes.
- HTML snapshot for visual verification outside the dev env:
`node scripts/export-styleguide/index.mjs` (writes the workspace
`__drops/lowkey-components.html`).
  • Step 2: Commit
git add CLAUDE.md AGENTS.md
git commit -m "docs: point AI sessions at the lowkey styleguide and style sources"

Task 6: Component build loop (repeat per inventory item)

This task repeats for every remaining item in the approved inventory (Task 2), in inventory order — atoms, then molecules (modal/dialog with field form + footer, card/section with labeled fields, table with mixed gadget cells, search/filter bar), then layouts (1/2/3-panel, rendered scaled-down inside a fixed-height bordered frame). The buttons section (Task 3) is the concrete template; per-section code is written at build time because it depends on discovery results and per-section user feedback.

For each inventory item:

  • Step 1: Read the item's real source (file:line from the inventory) and one real usage in the app. List every variant/state that exists.
  • Step 2: Create app/src/pages-builder/styleguide/sections/<name>.jsx exporting <NameSection num={N} /> built from real imports (app/src/ui/..., ui/shadcn/...) or real kp-* markup. Wrap in formbot-gadget / formbot-config divs when the cascade requires it (e.g. .lowkey-forms .formbot-gadget .kp-input). Render interactive-only states statically too (dropdown open, modal visible, datepicker expanded) so the snapshot captures them. Components needing data get minimal inline mock props — no API calls.
  • Step 3: Mount it in index.jsx with the next section number.
  • Step 4: Verify in the browser against a real platform screen using that component (screenshot both, compare sizes/colors/spacing/states). Fix the section, never the platform styles. Real platform bugs found get reported to the user, not fixed here.
  • Step 5: User approval gate — show the section, wait for OK.
  • Step 6: Commit: git commit -m "feat(styleguide): <name> section".
  • Step 7: Tick the item off in the inventory doc (amend the same commit or batch inventory updates every few sections).

After the final section: re-run the export script (Task 4 Step 3), verify the snapshot end-to-end, and update the smoke test to assert one element from the first and last sections.


Task 7: Wrap-up

  • Step 1: Run the full test suite: ssh -p 2223 agent@localhost 'cd ~/workspace/builder-ui && pnpm test:no-coverage' — expected: green (pre-existing failures, if any, noted to the user).
  • Step 2: Regenerate the snapshot one last time; confirm __drops/lowkey-components.html opens standalone with every section visible.
  • Step 3: Update the spec's status line and commit any doc drift.
  • Step 4: Hand off via superpowers:finishing-a-development-branch (merge/PR decision belongs to the user — note the branch stacks on fix/lowkey-rollout-bugfixes).