No description
- TypeScript 97.7%
- JavaScript 2.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Bump @qxuken/kui to 0.1.0-alpha.19. Nothing in "What breaks" reaches this app, and three of the six alpha.18 wishes are answered: the tooltip out of a hovered control's name (F88), the app's quads apart from the dock's (F92, `hostArea()`), and `frame()`'s doc (F89). The bare bump failed the smoke test's idle-rate check three runs in eleven. alpha.18 fails it too on a loaded machine: a single one-second sample of a once-a-second digit misreads at its edges and under a burst of OS events. It reads five seconds now and judges by the median. - Sliders answer `setValue` (RG42), the only way Windows' UI Automation moves one; rounded and clamped by the same `setDuration` a drag uses. - Headless asserts a hovered SKIP is named `SKIP`, not `SKIP KEY S`. - Smoke's overflow check reads `hostArea()` and the app's own quads, so it holds under a docked devtools pane; the post-dispatch assertion waits for `framesTotal` to move rather than trusting `frame()`. - Two lone spacers become `mainAlign="spaceBetween"`; the slider's name and minutes are baseline-aligned (24 glyphs, 1 px higher). - The three modes are one `radioGroup` at both tiers: one Tab stop, arrows pick the mode. docs/kui-alpha-19.md carries the changelog read against the tree. Verified: typecheck clean, headless ok, smoke ok three runs in three, and the window ran ten seconds plain and under --motion reduced with nothing on stdout or stderr. Co-Authored-By: Claude Opus 5.5 <[email protected]> |
||
| assets | ||
| docs | ||
| fonts | ||
| src | ||
| tools | ||
| .gitignore | ||
| .npmrc | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
LCARS Pomodoro
A pomodoro timer in the Starfleet LCARS style, built on kui from Node: JSX views lowered into kui's IR, Elm-style messages as data, a real window.
npm install # @qxuken/kui comes from the registry in .npmrc
npm start # the window
npm start -- --motion reduced # as the user who asked the OS for less motion sees it
npm run headless # no window: keys, clicks, a drag and a fake clock
npm run smoke # the real window, read back through win.quads() — then again, pinned
npm run typecheck
npm run sounds # regenerate assets/*.wav (already committed)
KUI_DEVTOOLS=1 npm start # the window with kui's own devtools docked in it
The kui version is pinned exactly, not by a range: ^0.1.0-alpha.19 and
~0.1.0-alpha.19 both admit every later alpha of the same 0.1.0, so a range
here is a floor rather than a pin. npm view @qxuken/kui version says what
the newest one is.
Focus, short break and long break in the usual cycle of four, with pause and resume, skip, draggable durations, a chime when a session ends, and optional auto-start of the next one.
Keys: space start/pause, r reset, s skip, 1 2 3 pick a mode,
m chime, a auto-start, d the latency HUD.
Files
src/app.tsx— the model,updateandview, pluslayout(), the breakpoint table. Time is an absoluteendsAt, so a tick just movesnowand pause never drifts. Time enters through the tick and nowhere else: a press cannot anchor a session, so START setsstartingand the next tick — a frame away — writesendsAtfrom its own reading.src/lcars.tsx— the kit: the palette as a token table, with the lit shades as recipes over it; elbows, bars, blocks, pills, sliders and bar buttons.src/window.tsx— what the window is: the loop config with atickthat reads the model — a frame while something is running, a second while nothing is — and theinitthat opens the model at the window's own size, handinginitthe window itself for its resources, its OS settings and its palette — and theteardownthat has the last word when the window goes, ⌘Q included.src/icon.ts— the window's icon, drawn from the palette at 128 px: the elbow, a block and three bars on a black tile. Handed to the window through its options; Windows and X11 show it, the Mac has no window icon.src/resources.ts— the fonts and sounds, registered against one surface and handed back as ids. Called frominit, with the surface it was given, so every core in a suite registers its own.src/main.tsx— runs it;--motion reducedpins the OS's motion setting. Nothing follows itsawait: on a Mac, Quit ends the process before it resolves.src/headless.tsx— the same app driven without a window. Controls are found by name in the access tree, not by colour in the display list.src/smoke.tsx— the same windowmain.tsxopens, driven and read back:win.quads()is the display list the shipping driver painted, andenv().audiois what its output device did with the chime. Then the same window again, a secondrunWindowedin the same process, pinned to reduced motion, wheresettled()can answer — and where the driver's idle rate is read offframeStats().pumps: 30 a second, one frame, stopped, judged by the median of five seconds.tools/make-sounds.mjs— synthesises the chime and the click.docs/— the upgrade notes for each alpha this app has been through, andfindings.md, a review of alpha.8 and what adopting it was like.
What the app leans on
- Messages are typed end to end.
PomoMsgis registered once throughdeclare module '@qxuken/kui/jsx-runtime', soonClickandonDragtake exactly this app's messages andupdateswitches on a real union with no casts. The registration namesPomoMsgrather than the fullMsg, sinceCoreMsg's own tags resolve back through it and naming it there is circular. - The cascade in the elbow's crook loops on its own.
keyframeswithrepeat="alternate"and a per-rowdelay, so the app holds no animation state and never wakes to flip it. The chips ride twogrowweights either side, which divides only the space left after the chip's own width, so the far end lands flush. - The palette is a token table.
C.orangeis'$orange', a name the core resolves as it lowers the node, and a control's hover and pressed shades are derived tokens —orangeHoveris orange lifted 30% toward white, computed by the core rather than by channel arithmetic beside the palette.defineTokenstypes the names, so a colour with no shade declared cannot be handed tohoverOf, and the devtools' inspector names the colour it painted. - It holds still for whoever asked the OS for less motion.
env.system.motion, read into the model byinitand kept current by thesystemmessage the root gets when the setting changes under the open window. The cascade is the only thing that stops: it is decoration, where the alert's blink is the alarm and a slider's fill is following the pointer. With it off nothing in the window is animating at all, so the app wakes for its clock and for input and nothing else.--motion reducedpins the reading at launch, for a machine whose owner did not ask. - The clock costs what the state needs.
tick.everyreads the model: a frame while a countdown runs or the alert blinks, a second while nothing does. A stopped window below the wide tier then draws nothing at all — only SHIP TIME shows the wall second — and the driver's idle backoff reaches it. As of alpha.13 a tick's own frame no longer resets that backoff, so the wide tier with the cascade parked, which does draw the second, idles between digits too: under 2% of a core stopped at every tier but the one with the cascade running. As of alpha.14 the count is the window's to read —frameStats().pumps— and the smoke test asserts it: 30 pumps and one frame a second, pinned and stopped. - The pointer is what the view declares. As of alpha.14 an
onClickbox that says nothing is the arrow, as a native button is, and the hand is where it is written. Every control this app draws as a button — the pills, the sidebar blocks, the bar buttons, the alert — sayscursor="pointer"for itself, the way the stock<button>does; a slider's track saysgrab, andgrabbingwhile its drag runs, from thedraggingthe model already keeps for the fill. Headless,cursorShape()reads each of them back. - The alert announces as well as blinks. It is a live region, so a screen reader reads it out the frame it appears, without being asked; the chime is not the only cue that a session ended.
- The stock widgets are pinned to the dark base. They follow the OS's
appearance as of alpha.11; LCARS is black with a fixed palette, so
initpins the base the tooltips and the focus ring drew before, whatever the desktop says. - The keyboard reaches every control. The root sink hears the
shortcuts with nothing focused, so it no longer holds focus to hear them
— which is what used to swallow Tab. Tab walks the controls with the
ring showing, Space presses the one it is on, and the letter keys still
bubble up to the root. The three modes are a
radioGroup, one Tab stop whose arrows move the choice and press the mode they land on, as a native radio set does. The duration tracks answer a reader's nudge and, for Windows' UI Automation, a value set outright. - Resources belong to the core that registered them. The font and the
two sounds are registered in
init, against the surface it is handed, and the ids ride in the model — so the window, the smoke test and every core in the headless suite draw with their own, and nothing has to be re-pointed at whichever core is about to render. - The layout adapts to the window.
win.size()seeds the model before the first frame andresizemessages keep it current, solayout()is a plain function of the viewport. Three tiers: wide, mid, and compact, which moves the mode and toggle buttons out of the sidebar and into the LCARS strips, leaving the spine decorative and the clock large.minWidthandminHeightat open stop the window below what compact is drawn for. - Hover and pressed states are props, not queries:
hoverBg,pressedBgandhoverGrouplight the multi-piece shapes together, and the buttons sitting in awindow="drag"strip still take their own clicks because children hit-test above the parent. - Sound is data too.
addSoundininit,clickSoundon every button, and the end-of-session chime as an<audio>node the view declares — a playback retained by key, so keying it on the session count is what makes it sound once per finish. It is declared for that finish's frame and no longer:finishmakes the node's removal release the playback rather than stop it, so the file plays itself out and nothing here knows how long it is. No driver diffs a counter and nothing performs it; headless,ctx.audioCommands()is what the tests assert on, down to thestopthat release does not send — and in the real window,env().audiois: one live playback with the node gone, none after the file's length, and notruncated-playback. - Both drivers are covered.
headless.tsxasserts on model state and decoded quads against a fake clock;smoke.tsxopens the real window and asserts on the frame it actually painted, the access tree it exposes, and a control driven throughaccessthe way a screen reader drives one — and it waits onapp.frame(), "the next pump has run", rather than sleeping and hoping.