Dialogs on DialogBase with focusable panels that harbor parked focus, popovers on PopoverBase, bubbles, drawers, sheets, a lightbox, an RPG dialogue box, notifications, a pause menu and a dithered title card. Kit.Cue moves from feedback to pixel.
8.3 KiB
AGENTS.md
8bitMyGoUI: pixel-art components in the manner of 16-bit consoles, for MyGo native UI. Go module git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI.
Progress truth: TASKS.md. Requirements: PRD.md and docs/prd/components-*.md.
Stack
| Item | Value |
|---|---|
| Go | 1.27.1 |
| MyGo | github.com/egoist/mygo v0.2.10, pinned in go.mod |
| Font | Ark Pixel Font 12px proportional zh_Hans v2026.09.25 (OFL-1.1, pixel/OFL.txt). Rebuild: go run ./tools/fontpack -in ark-pixel-12px-proportional-zh_hans.bdf (BDF sha256 535f7024…8c86) |
| QR | rsc.io/qr v0.2.0 (BSD-3), encoder behind display.QRCode |
| Web | cmd/web built with GOOS=js GOARCH=wasm; the browser runs the gallery under ui.Tester and blits frames to a canvas |
| Hosting | Cloudflare Workers static assets from web/dist; code on Gitea git.zacharyzhang.com |
Read MyGo source before using an API: $(go env GOMODCACHE)/github.com/egoist/mygo@v0.2.10 (ui/*.go, docs/ui/*.md). Never guess.
Layout
pixel/ engine: settings, palette, canvas, dither, font, sprites, clock, editor, messages
icon/ hand-placed sprites
<category>/ one component per file, package = folder name
gallery/ registry, demos (demos_<category>.go), app view, invariant tests
cmd/gallery/ native window
cmd/web/ js/wasm host
web/ shell, glue JS, build.sh, deploy.sh, wrangler.jsonc, _headers, Playwright tests; web/dist is build output
tools/fontpack/ BDF to font12.bin
internal/archtest/ import and API rules
Tiers
Imports point down only. Same-tier imports are forbidden. internal/archtest enforces it.
0 pixel · 1 icon · 2 layout, text, display · 3 input · 4 feedback · 5 navigation, data, overlay, datetime, chart, fx · 6 game, chat, media, code, account, files, project, shop, agent, desk, tabletop · 7 gallery · 8 cmd
Style rules
- Draw on the dot grid through
pixel.Canvas. One dot isDotDIPs. - Name palette slots, never colors. Every pixel must be a palette color.
- Only axis-aligned rectangles reach the screen: no radius, path, shadow, gradient, opacity, scaled bitmap.
- Tone comes from Bayer dither between two slots.
- Animation steps through
Canvas.Step,Canvas.BusyorKit.Tick. No easing. - Text through the pixel font only.
MyGo rules
- Use Bases, containers,
List,Router,Overlay,DialogBase,PopoverBase,FocusGroup,Local,HandleInput,TextCaret,Shortcut, clipboard. Full list: PRD §2.4. - Never call styled widgets,
ui.Outline(stroked arrows),ui.GridView(theme gaps, tinted choice),Background,Border*,Radius,Shadow,Gradient,Opacity,Stripes,Tooltip,Menu,ContextMenu,Transition,Animate*,Loop,c.Toast*, or painter calls other thanFillandClip. OnlypixelcallsAfter. - A disabled element never paints. Interactive components draw on an enabled shell; the base inside fills it, carries behavior, label and
Disabled, and turns MyGo's focus ring off.
Component rules
- Signatures and results: PRD §5.1. Zero options give the default look. Sizes in options are dots.
k := pixel.K(c)once per component; layout sizes throughk.D(n). Integer options default throughpixel.Or.- Custom-drawn controls end their Draw with
g.Finish(st). Grids of cells moved by arrows usepixel.Pad; one-shot timing steps withk.Since. - A one-shot effect inside a lasting view starts when its cue changes (a
Cueoption read throughk.Cue, or a value that rises), never on its first frame. A state screen (ErrorState,SuccessState) starts when it appears. Decorative motion jumps to its end under Reduce Motion; countdowns and loaders keep time. - Containers take content as a final
func(); interactive results areNameResult. - A Clicked, Shortcut, Changed or drag during the build makes MyGo build the frame again, so what they change shows at once. State changed any other way after its element was built (
Pressed,Edit.Changed, a drag's release) is read at paint time, or set on the element after its children. - Never call
Selected. The pixel theme keeps MyGo's row highlights clear, since scrolled rows sit between dots: draw chosen rows yourself. - A disabled or inert scroll view must not move: cover it, so the wheel stops above it.
- A press focuses the nearest focusable element under it, or nothing, and a focusable element keeps the click: clicks never reach its ancestors. A control that acts on a press is that focusable element. A
DialogBasemoves the focus inside as it opens and keeps Tab there, but only onto something focusable: make its panel focusable, ring off, and build its children inpixel.Harbor(c, panel, …)so controls turning off park their focus there and get it back. A closing overlay hands the focus back to its opener. While a press is on,Hoveredis false for every other element: askPressed. - Enter and Space click a focused clickable element unless a
Shortcuttakes them. Take them wherever a click reads the pointer's position. - Under
pixel.Settings.Inert(a busy veil)pixel.Shelldisables every base and gives the focus back after. A popup sits above any veil: a control closes its popup when disabled or inert. - Elements are known by their place among siblings. Wrap one whose earlier siblings come and go in
ui.Box(c).Key(k), or it loses focus and state. - Hosts send a key's release with its press, so tell a held key by its repeats.
- Clip only boxes that sit on whole dots; split widths with whole-dot shares, not equal
Grow.Growstarts from zero size, so never use it on a content-sized child. Listrow builders run during layout: wrap them inpixel.Lazy(c, row). A click met there lands after the build; keep it inLocalstate, invalidate, and report it next frame.- Single-key shortcuts only while
pixel.Typing(c)is false; MyGo routes shortcuts before a focused field. - Copy shown by a component comes from
k.Msg; business copy comes from the caller. - Fail fast: panic on programmer errors such as bad options. Never swallow an error.
- Composites log state changes:
slog.Debug("8bit: <component> <event>", …). Never log secrets. - No cross-frame
*ui.Elementcaching. No goroutines touching elements. - Files under 500 lines. Comments: one line, ten words at most, only when needed. No dead code, no versioned APIs.
Gallery entry
Each component adds one Entry in gallery/demos_<category>.go: ID, Name, Zh, Category, Summary, Demo. The demo reports every event through log. The code panel shows the demo function's own source.
Tests
go vet ./... && go test ./...before reporting done.- Behavior tests drive
ui.Testerand assert values. Timed components takepixel.Settings.Nowfrom a test clock.Tester.Focused(label)is also true when an ancestor holds the focus. gallerychecks every entry under every palette: only palette colors, whole dots.PIXEL_SHOTS=<dir> go test ./gallery -run TestInvariantswrites PNGs for visual review.- Web:
npm install && npm testinweb/buildsweb/distand runs Playwright on a local server;BASE_URL=<url> npx playwright testchecks a deployed site. Tests readwindow.__8bit(ready,stats(),texts(),page(),find(label),log).
Deploy
npm run deployinweb/(clean tree only) ships in two steps, because a rollout serves old and new versions side by side for a while. Step one promotes the liveindex.htmlwith the new files beside the live ones; step two uploads the new page with both file sets, runs Playwright on its preview URL, promotes it, and runs Playwright on https://8bitmygoui.zhangyanghaha0407.workers.dev. A page from either build then finds its files on either version.- Rollback:
npx wrangler rollback <version-id>inweb/;npx wrangler deployments listshows versions. - Scripts and wasm sit under
a/, named by content hash, and_headersmakes them immutable;index.htmlrevalidates. Never cache a path whose content can change.
Review
Each TASKS.md item is reviewed in a Herdr pane split to the right, by codex --yolo -m gpt-6.1-sol -c model_reasoning_effort=high -c service_tier=fast. At most three rounds. The reviewer reports defects against PRD and this file, ends with VERDICT: PASS or VERDICT: FAIL, and does not reject work for missing enhancements.
Secrets
The Gitea token lives only in the environment (GITEA_TOKEN). Never write it to a file, a commit or a log.