Elements are now pass-scoped value handles: tests that read bounds after a frame bind a ui.Handle. pixel tells frames apart by the build pass's root, since the Tester clock no longer moves on its own. Fix the regressions the upgrade brought: - Clipboard reads in input handlers go through c.Services(). - MyGo notes an overlay's opener after building it, so AutoFocus inside lost it; pixel.AutoFocus focuses through a Handle, and archtest bans the raw call. Dialogs hand the focus back again. - Elements around a press stay hovered: Tooltip tells a press-given focus by focus arriving under the pointer. - The web host moves the Tester clock with real time, at most 500 ms a call, and gives components the real clock. The gallery's source index also accepts CRLF checkouts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
13 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.5.5, 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, whose clock the host moves with real time (at most 500 ms a call), 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.5.5 (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
docs/ README image; specs in docs/prd (not committed)
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. Color cycling turns a band of slots a place each step:Canvas.Cycledremaps a drawn picture's slots; procedural art indexespixel.Cycle(band, step), which may repeat a slot. - 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, held by archtest
TestComponentsTakeTheirOptions:NametakesNameOptionsand returnsNameResult, one component per file. Zero options give the default look; required values are parameters. 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; custom-drawn sliders useinput.SlideBase; every clock runs at S5's 4–15 fps (checkFPSpanics otherwise): a blink isg.Step(4)%4 < 2, a timer counts quarter seconds, and a view that changes on whole seconds asksk.At; one-shot timing steps withk.Since, which holds at zero when the clock steps back; a stored start is read throughk.Elapsed(&t), which moves it to now when the clock steps back; drawing from time quantizes it to whole frames (Truncate(time.Second / 15)); a decorative one-shot on a cue steps withk.Shot, which Reduce Motion spends whole; a view that changes at a known moment asksk.At; a two-way slide (bars, fades, wipes) steps withk.Toward, which turns from where it is. - 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.
- Read pointers and keys only while
base.IsDisabled()is false, never by theDisabledoption alone:pixel.Shelldisables the base under Inert too, and a drag begun before stays pressed. MyGo still hands a key to a base that turned off this frame, so keys go throughpixel.Shortcut(archtest bans an element's ownShortcut); a container that stays enabled, or a wrapper around an editor, also checks its own off state.pixel.Editorturns away text typed in a veil's first frame; read an edited value after building its editor, or gate the read. AHandleInputhandler runs before the build learns the base is off: it records, and the build applies only while the base is enabled. - MyGo's value bases (
ToggleBase,SwitchBase,CheckboxBase,RadioBase,SegmentedBase,TabsBase,CollapsibleBase) turn their value while built, beforeDisabledapplies, so a key in a veil's first frame lands. Hand them a copy; keep only turns of an enabled base. - MyGo paints a disabled element and all inside it at half opacity, and only a disabled element reads as disabled to assistive technology. Disable only drawless bases, as
pixel.Shelldoes. A container with drawn content stays enabled, parks its focus, ignores input and saysDISABLEDin its description. - 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: MyGo notes the opener after building the overlay, so focus an element as it appears withpixel.AutoFocus, neverAutoFocus(archtest bans it). While a press is on, the elements around it stayHovered: askPressed, and tell a press-given focus by focus arriving under the pointer. - 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. If the popup held the focus, focus the opener andpixel.Parkit in that frame; tell it byFocusedon the popup's controls, since MyGo links parents only after a frame. - 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. Rows the caller removes are keyed by a name that must be unique: panic on twins. - 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. Ark Pixel 12px lacks some hanzi:TestMessagesHaveGlyphsholds every message to the font,TestGalleryTextHasGlyphsevery gallery name and summary. - Item slots look alike everywhere: draw them with
data.DrawSlotordata.ItemSlot. - 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, nor anything typed or copied: log counts. The gallery logs which demo spoke, not what it said. - A
ui.Elementis a handle that expires before the next build pass: never keep one across passes; bind aui.Handleinstead (Handle.Bounds(c)works between builds). No goroutines touching elements. Contextmethods answer only during a build: an input handler reads the clipboard throughc.Services().- 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. An exported …Base (like input.SlideBase) is an undrawn behavior other packages draw on; it needs no demo of its own.
Tests
go vet ./... && go test ./...before reporting done.- Behavior tests drive
ui.Testerand assert values. Timed components takepixel.Settings.Nowfrom a test clock. The Tester clock stands still untilAdvance; measurepixel.Due()againsttt.Now().Tester.Focused(label)is also true when an ancestor holds the focus. gallerychecks every entry under every palette: only palette colors, whole dots. Demos that callpixel.Scope, and the home and entry pages, may show other palettes.PIXEL_SHOTS=<dir> go test ./gallery -run TestInvariantswrites PNGs for visual review.galleryalso sweeps all demos: keys, typing, clicks and the wheel as a veil falls log nothing; a clock stepping back crashes nothing and shows no new negative number; every Tab stop has a name; every gallery word has glyphs; the README's counts match the registry.- 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). Every page must end without console or page errors; a test that expects some says so in an annotation. The budget test logs first frame, p50 and p95 (Go's share plus the canvas blit) and the wasm's brotli bytes, and holds them to PRD §6.
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. After a third FAIL, the coordinator fixes each defect, proves the fix red then green, and records both in TASKS.md.
Secrets
The Gitea token lives only in the environment (GITEA_TOKEN). Never write it to a file, a commit or a log.