Skip to content

feat(devtools,store): add the DevTools panel - #263

Merged
dangreen merged 23 commits into
mainfrom
feat/devtools-uikit
Sep 26, 2026
Merged

dangreen merged 23 commits into
mainfrom
feat/devtools-uikit

Conversation

@dangreen

@dangreen dangreen commented Sep 22, 2026 •

Copy link
Copy Markdown
Member

Why

The DevTools panel of nano_kit lives on the page and is written on Nanoviews. This pull request brings the package up to the call an application makes to get it: the workspace wiring for one shared core, the Storybook setup, the UI kit, the services and the registry, the Signals and Log tabs with the inspector, the shell the panel takes on the page, the devtools() entry and a README. The panel was tried on the weather example, and what the trial found is fixed here: the owners of the nodes libraries create, the value of a signal nobody has read since a write, and the signals maps, which @nano_kit/store now marks for the panel in development.

What

  • packages/devtools, published as @nano_kit/devtools: nanoviews, @nano_kit/store and @nano_kit/platform-web as dependencies, Storybook on @nanoviews/storybook-vite with a theme toggle in the toolbar and nanoviews kept out of the pre-bundling so a story runs on one core, Vitest with @nanoviews/testing-library, the oxlint config of the package, a library build on Vite, and a tsconfig.build.json that leaves the specs, the stories and the mocks out. exports points at dist in the workspace as well, so an example of the repository runs what an application gets. A README tells what the panel does and how to call it, with screenshots of both tabs in the light and the dark theme in website/src/assets.
  • The icon sprite: one SVG per icon in src/assets/icons, compiled by scripts/svg-sprite.js into assets/sprite.svg with no XML declaration, and put into the tree as markup: Icon refers to a symbol of it with <use href="#name">, the entry puts the sprite into the shadow root of the panel and the preview of Storybook into the page.
  • The UI kit, presentational components only, with a story each and specs for the interactive ones: Theme with the design tokens as custom properties and a light variant, typography and mixins, Icon, Button, IconButton, LinkButton, Input, Tabs with Tab over a native radio group, Table over a real <table> with role treegrid, walked from the keyboard the way a tree grid is, its rows sharing one tab stop, a group row opening on a click anywhere on it, and a TableTreeCell for the chevron, Disclosure, Status, Tag, Duration, a time in milliseconds set apart by a tone, Box with its header, subheader and body, Properties, Notice, Beacon, Pill, Window with its bar and body, WindowEdge, an invisible handle on a side, the bottom edge or a lower corner of a window, half out of it so a scrollbar along it keeps most of its width, which starts no selection of text, and ValueTree with ValueNode, ValueChildren and ValueMore, the presentational half of the value viewer.
  • A component that handles an event of its own root element takes the same handler out of its props and calls it after its own, so the ...restProps spread neither drops nor replaces it: Disclosure for onClick, Tabs for onChange, ValueNode for onKeyDown.
  • The services, src/services, pure logic and the one adapter to the core, all on @nano_kit/store: InspectorService$ listens to onSignal and inspect, tells a node met for the first time from a known one and takes its call stack right there, and hands the events of a task over, each stamped with its time, to every listener in turn, in one batch inside uninspected, a microtask later, and passes a flush on only after events of the application; NamingService$ turns the stack of a creation into a name in the Docker style, a creation site and an owner, with the LibraryDetector$ token for the files to skip on the way to the application, and takes Object.fn and Object.compute, the names V8 gives a body the core runs through its node, for no name, so an effect is owned by its file in Chrome and Edge as in Safari and Firefox; a node libraries alone created inside the body of a computed or an effect, or inside a lifecycle listener, belongs to the owner of the node the core was running, since the search along the stack stops at the frame of callInspected and the registry knows which node runs; before DevTools is said only of a node whose creation was not seen, by libraries of one whose stack holds libraries alone; the registry module tells what a node is, its state from the flags and links, its value read from the node and never evaluated, the value written last for a signal, which is never out of date, a signals map told by the marks of the store package, its version standing for it, the source of the function an effect or a computed runs, and what an update left out of date; the values module previews a value in one bounded line and opens a collection a page at a time, with no serialization in depth; the log module puts the events of a task into transactions, a group per flush, times every run from RunEvent to RunEndEvent, on its own and with the runs nested in it, and cuts a group down to the lines a filter finds.
  • The stores, src/stores: RegistryStore$ keeps an immutable record per node in an IndexedSignalsMap of the store package, applies the events of the inspector to it in one batch, follows the nodes whose bodies or lifecycle listeners run, reads the parent of a new node when it applies the batch rather than when the node is met, since a map marks an entry after its signal is made, counts a new value of an entry as a value of its map and lets an entry go with its key, listens while somebody reads the records, and keeps the record of a node that stopped, out of every link, until the node is collected; LogStore$ keeps the last 200 transactions, newest first, with pause, resume and clear, and the groups the filter leaves; PanelStore$ keeps whether the window is open, the tab, the theme, the frame of the window and the place of the pill in localStorage through localStored of @nano_kit/platform-web, and the filter in memory.
  • The Signals tab: SignalsStore$ builds the table over the records of the registry and holds the selection. SignalsTable groups the signals, computeds, selectors and signals maps by the function or the file they were created in, child signals under their parent and the entries and the index of a map under the map, the nodes of no owner apart by whether they are older than the panel or created by libraries alone, and shows the kind, a preview of the value, the state and the counts of dependencies and subscribers; effects and scopes have no rows and count among the subscribers of what they read, and the filter matches the name, the site, the owner and the preview. Inspector shows the selected node in four boxes: Node with its last run; Value with the value, the body of an effect in its place and the compute of a computed under its value, and Evaluate now for a computed that is out of date or not evaluated yet; Links; and Recent, the last lines of the log about the node. The stories and the specs of the blocks run over blocks/shared/app.mock.ts, a small application with a node in every state a row can show.
  • The Log tab: Log shows a row per transaction with the counts of its lines, the slowest run once it takes a millisecond, the duration and the time of the day, striped by the number of the transaction so a new one on top flips nothing, folded until it is opened; a line tells of a write with its values, a run of a computed or an effect with its own time and the total, a computed left out of date that nobody read, an effect left out of date that did not run, a change of the keys of a map, a lifecycle transition or a stop, a run indented under the run it happened in, and a click on a line selects its node. The filter finds lines by the name, the site, the owner or a value. LineDetail is the block the Log and the Recent box show a line with.
  • The shell, blocks/Panel: Panel shows a pill on the bottom edge, dragged along it, or the window it opens, moved by its bar, resized by its sides, its bottom edge and its lower corners, and kept in the viewport by CSS. Alt+Shift+D, Option+Shift+D on a Mac, opens and collapses it from anywhere on the page, and Escape collapses it while the focus is inside. The bar has the tabs, the filter, the pause, play and clear buttons of the log, the theme switch and the close button, and a tab left for the other is set aside with what was opened in it rather than built again.
  • The entry, src/devtools.ts: devtools(options?), the one export of the package, runs inside uninspected, starts the registry and the log so they listen from the call on, appends a div to body, attaches an open shadow root to it, adopts the styles and mounts Panel there. The styles are one minified string in the bundle: inlineStyles() of the Vite config gives virtual:styles a stand-in and puts the CSS that library mode emits in its place, so the bundler of an application gets no CSS to process, and adoptedStyleSheets applies it on a page whose policy forbids inline styles, where a <style> would be blocked. :host { all: initial !important } keeps the styles of the page out. options.library adds a rule for the files of libraries to the one of node_modules, for a workspace that links the kit as sources. Segoe UI sets its baseline a pixel lower in a line than SF, so the entry adds its faces to the fonts of the document as nano_kit Segoe UI with the metrics that set it where SF does; the monospace token takes Cascadia Mono Light, which Chromium finds on Windows 11, before Consolas.
  • @nano_kit/store: signalNodeAssign($signal, fields) assigns fields to the node of a signal, the way a store marks its nodes for the devtools; it throws outside of development, so a call sits inside if (import.meta.env.DEV), which the production build folds away. SignalsMap marks its version with the map and an entry with the map and its key, and IndexedSignalsMap marks its index with the map alone, so no key of the map is taken for it, and creates its anchor inside uninspected, which keeps the anchor and its links out of the panel. The store skill gets a paragraph about the devtools.
  • Workspace: agera and kida are overridden to the workspace sources in pnpm-workspace.yaml, so Nanoviews, which pins published kida snapshots, the panel and an inspected application share one core instance. @nano_kit/devtools is overridden to the workspace too, for the examples. The weather example on nano_kit DI mounts the panel from a module imported first, with nanoviews kept out of the pre-bundling of Vite, which would give it a copy of the linked core of its own. The integration job of CI builds the devtools before it tests the examples, which take the kit from its sources and the devtools from their build. storybook-static is ignored. skills-lock.json locks the agent skills the package is written with.

Size

An application that calls the panel behind the development flag ships none of it. The store gains signalNodeAssign, an export, so the import * bundles carry it: All publics of store from 6667 to 6716 B gzip and from 6135 to 6148 B brotli against main, with the limits re-pinned to 6.75 and 6.2 kB in a commit of their own; a named import pays nothing, and the marks of the signals maps fold away with import.meta.env.DEV.

Notes

  • The panel rests on the inspection layer of agera in main: RunEndEvent and a first evaluation told by the missing oldValue from feat(agera): report the end of a run with RunEndEvent, and leave oldValue out of a first evaluation #265, and the fire of lifecycle listeners from feat(agera): enclose the lifecycle listeners of a node between FireEvent and FireEndEvent #267. fix(agera): drop the development warning about a signal written inside a computed #266 dropped the development warning about a write inside a computed, which the cache of a query set off on every start.
  • The examples of the repository link the kit as sources, so the weather example calls devtools({ library: file => file.includes('/packages/') }).
  • Checked in the dev tree: oxlint, tsc --noEmit on both configs, the 249 specs of the panel with coverage, the specs and size-limit of the store, the stories in Storybook, the panel on the weather example in Chrome, and the built package on a page under style-src 'self' with styles of its own on body, in Safari 18.4, Chrome 153 and Firefox 156 on macOS Sequoia and Edge 153, Chrome 153 and Firefox 156 on Windows 11 on BrowserStack, and in headless Chrome 153 on Linux. The layout is the same in all of them; Firefox on macOS sets text half a pixel lower than Safari and Chrome, which a page cannot change for SF. The pre-push hook was skipped on purpose: it runs the whole monorepo suite in parallel, which the dev machine does not survive. CI runs the rest.

@codecov

codecov Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.81272% with 104 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.74%. Comparing base (ef084b7) to head (dbcf79b).

Files with missing lines Patch % Lines
packages/devtools/src/blocks/Panel/PanelPill.ts 45.23% 22 Missing and 1 partial ⚠️
packages/devtools/src/blocks/Panel/PanelWindow.ts 54.34% 19 Missing and 2 partials ⚠️
packages/devtools/src/services/log/log.ts 90.90% 1 Missing and 8 partials ⚠️
packages/devtools/src/blocks/shared/app.mock.ts 85.18% 5 Missing and 3 partials ⚠️
packages/devtools/src/stores/registry.ts 92.07% 2 Missing and 6 partials ⚠️
...kages/devtools/src/blocks/Inspector/ValueBranch.ts 80.76% 4 Missing and 1 partial ⚠️
packages/devtools/src/uikit/Table/TableRow.ts 87.50% 3 Missing and 2 partials ⚠️
packages/devtools/src/stores/signals.ts 94.66% 1 Missing and 3 partials ⚠️
...s/devtools/src/blocks/SignalsTable/SignalsTable.ts 92.50% 0 Missing and 3 partials ⚠️
...kages/devtools/src/services/inspector/inspector.ts 95.23% 0 Missing and 3 partials ⚠️
... and 11 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #263      +/-   ##
==========================================
+ Coverage   83.56%   85.74%   +2.17%     
==========================================
  Files          99      166      +67     
  Lines        2634     3766    +1132     
  Branches      592      871     +279     
==========================================
+ Hits         2201     3229    +1028     
- Misses        296      359      +63     
- Partials      137      178      +41     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@dangreen
dangreen force-pushed the feat/devtools-uikit branch 3 times, most recently from d680592 to 3da9c2a Compare September 23, 2026 10:45
@dangreen dangreen changed the title feat(devtools): add the DevTools package with the UI kit of the panel feat(devtools): add the DevTools package with the UI kit, the services and the registry of the panel Sep 23, 2026
@dangreen dangreen changed the title feat(devtools): add the DevTools package with the UI kit, the services and the registry of the panel feat(devtools): add the DevTools package with the UI kit, the services, the registry and the Signals tab of the panel Sep 23, 2026
@dangreen dangreen changed the title feat(devtools): add the DevTools package with the UI kit, the services, the registry and the Signals tab of the panel feat(devtools): add the DevTools package with the UI kit, the services, the registry and the Signals and Log tabs of the panel Sep 24, 2026
@dangreen dangreen changed the title feat(devtools): add the DevTools package with the UI kit, the services, the registry and the Signals and Log tabs of the panel feat(devtools): add the DevTools package with the panel on the page, its Signals and Log tabs, and the devtools() entry Sep 25, 2026
…s and the registry of the panel

`@nano_kit/devtools` is the in-page DevTools panel for nano_kit, a private package until the panel is real. This is the first cut: the package on Nanoviews with Storybook, the icon sprite, and the UI kit of the panel, the `Theme` tokens with a light variant, the typography and mixins, and the presentational components with a story each: `Icon`, `Button`, `IconButton`, `LinkButton`, `Input`, `Tabs`, `Table` as a tree grid, `Disclosure`, `Status`, `Tag`, `Box`, `Properties`, `Notice`, `Beacon`, `Pill`, `Window` and `ValueTree`. Below the components, the services and the registry: `InspectorService$` listens to `onSignal` and `inspect` of the core and hands the events of a task over in one batch, a microtask later; `NamingService$` names a node after the call stack of its creation; the registry module tells what a node is and what state it is in; the values module previews a value in one line and opens it an entry at a time; `RegistryStore$` keeps a record per node in an `IndexedSignalsMap`, written from the events while somebody reads it; `PanelStore$` holds the tab and the filter. The workspace overrides `agera` and `kida` to the workspace sources, so Nanoviews, the panel and an inspected application share one core, and the agent skills the package is written with are locked.
The Signals tab of the panel: `SignalsStore$` builds the table over the records of the registry and holds the selection, and two blocks read it. `SignalsTable` groups the signals, computeds and selectors by the function or the file they were created in, child signals under their parent, and shows the kind, a preview of the value, the state and the counts of dependencies and subscribers; effects and scopes have no rows and count among the subscribers of what they read, and the filter matches the name, the site, the owner and the preview. `Inspector` shows the selected node in three boxes: `Node` with its name, kind, owner, site and state, `Value` with the value tree and `Evaluate now` for a computed that is out of date or not evaluated yet, and `Links` with the dependencies and the subscribers as links to their rows, an effect by its name and site. The stories and the specs of the blocks run over `blocks/shared/app.mock.ts`, a small application with a node in every state a row can show. On the way `InspectorService$` meets an effect created inside a scope on the stack of the body, a level of `ValueTree` where nothing opens has no spacer, `Notice` gets rounded corners and room for its actions, and the package moves to the Nanoviews prerelease of 2026-09-23, which keeps the uninspected mark on the rows of a read-only `for_`; the UI kit takes `inject` and `provide` from `nanoviews/store`.
…n icon of their own

The rows of `Table` share one tab stop, the way a tree grid does: the table holds it until a row is focused and hands the focus to the selected row or the first one; up and down move between the rows, Home and End go to the ends, right opens a row or goes down to its first child, left closes it or goes up to its parent, Enter and Space act as a click, and the chevron stays for the mouse, out of the tab order. The focus ring is drawn by the cells, rounded at the ends like the row. The tree column lines up by drawings, not by boxes: the chevron sits in a slot as wide as an icon, 4 px from the icon, so the chevron of a nested row stands under the icon of its parent and the icon of a nested row under the text of its parent; the last head cell keeps the inset of the last body cell. Selectors in the Signals table get an icon of their own, the diamond of a computed with a dot inside.
…t lines in the Inspector

The Log tab of the panel: `groupEvents` of the log service puts the events of a task into transactions, a group per flush and one for what came after the last flush, and `LogStore$` keeps 200 of them, newest first, with `pause`, `resume` and `clear`, listening while somebody reads them. A line tells of a write with its values before and after, a run of a computed or an effect, a computed an update left out of date and nobody read, a lifecycle transition or a stop; a run is timed on its own and with the runs nested in it, from `RunEvent` to `RunEndEvent`, and a first evaluation is marked as such. The `Log` block shows a row per group with the counts, the slowest run, the duration and the time of the day, folded until it is opened into its lines, a run indented under the run it happened in, and a click on a line selects its node. The Inspector gets `Recent`, the last lines of the selected node from `$selectedRecent` of `SignalsStore$`, and the last run in `Node`. `Duration` joins the UI kit, a time in milliseconds set apart by the tone a block picks, orange from a millisecond and red from a frame, and `LineDetail` is the block both show a line with. On the way `InspectorService$` takes several listeners, stamps every event with its time and passes a flush on only after events of the application, and `RegistryStore$` keeps the record of a node that stopped, out of every link, until the node is collected.
… hotkey, and show the source of effects and computeds
… shadow root with its styles and icons built in
…lue of a selector key and snap the pill to the middle
…r its owner, and show the value last written to a signal
@dangreen dangreen changed the title feat(devtools): add the DevTools package with the panel on the page, its Signals and Log tabs, and the devtools() entry feat(devtools,store): add the DevTools panel on the page with its Signals and Log tabs and the devtools() entry, and signalNodeAssign to mark nodes for it Sep 26, 2026
@dangreen dangreen changed the title feat(devtools,store): add the DevTools panel on the page with its Signals and Log tabs and the devtools() entry, and signalNodeAssign to mark nodes for it feat(devtools,store): add the DevTools panel Sep 26, 2026
@dangreen
dangreen marked this pull request as ready for review September 26, 2026 12:49
@dangreen
dangreen merged commit bc1e51a into main Sep 26, 2026
10 checks passed
@dangreen
dangreen deleted the feat/devtools-uikit branch September 26, 2026 13:01
@github-actions github-actions Bot mentioned this pull request Sep 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant