>_Executive summary
The house rule is GUI-only zgui components: every app builds its UI from the shared zgui-core library (ZGui.*) — modals, dropdowns, pickers, tables — never a hand-rolled or native browser widget. This counts the app-owned native form-control elements a ZGui.* component already covers. Only three apps are real offenders: traderview, Audio-Haxor, and ztranslator. Everything else is clean or down to 1–3 stragglers.
Resolved 2026-07-11 (fleet-wide): all four native widget types now auto-upgrade to their ZGui component at runtime via zgui-core's widget observer — <select>→zs-select, range→zs-range, date→ZGui.datePicker, color→ZGui.colorPicker (ZGui.upgradeSelects / upgradeRanges / upgradeDates / upgradeColors), preserving each element's id / value / change wiring. Rolled to all 16 apps (zgui-core 569cfd8b). The source templates still emit native elements, so the per-app counts below stand as the source inventory — but they render as the ZGui component in every app that loads zgui-core. Opt out with <html data-zg-no-select-upgrade>. (Date/colour replace the element, so verify tight date forms, e.g. traderview, after rebuild.)
~Detection method & what is excluded
Counts are native widget elements in each app's own frontend, by fixed grep, mapped to the ZGui.* component that should replace them. To avoid over-counting, four sources of shared/false hits are excluded — an earlier pass that counted them inflated most apps (e.g. zcite 7→0, zstation 7→0, zgo 2→0):
| Native element | Grep | ZGui replacement |
|---|---|---|
| native dropdown | <select | ZGui.dropdownMenu / combobox / searchSelect |
| native slider | type="range" | ZGui.slider / dbFader / bipolarSlider |
| native colour picker | type="color" | ZGui.colorPicker |
| native date/time | type="date|time|datetime-local" | ZGui.datePicker / dateRangePicker |
Excluded (not the app's own violation): (1) the vendored zgui-core lib, vendor/, node_modules, bundles, dist; (2) the shared zpwr-clip-engine clip UI (clip-* selects), which is embedded — sometimes inline in the host's own index.html — in traderview / zemail / ztranslator / zstation, so it is one shared-component fix, not per-app; (3) embedded zpdf-core viewer subtrees (/zpdf/); (4) comment lines and documented ZGui fallbacks. The JS alert/confirm/prompt metric is also excluded — apps route those through a ZGui.modal.prompt-delegating helper.
#Per-app widget audit
clean = 0 app-owned native · minor = 1–10 · heavy = >10. "ZGui calls" counts distinct ZGui.<component>() invocations (excluding chrome) as a rough adoption signal.
| App | <select> | range | color | date/time | total | ZGui calls | Verdict |
|---|---|---|---|---|---|---|---|
| traderview | 565 | 3 | 4 | 219 | 791 | 14 | heavy |
| Audio-Haxor | 54 | 63 | 17 | 0 | 134 | 14 | heavy |
| ztranslator | 25 | 2 | 0 | 0 | 27 | 36 | heavy |
| zftp | 1 | 1 | 1 | 0 | 3 | 214 | minor |
| zemail | 1 | 1 | 1 | 0 | 3 | 196 | minor |
| zpdf | 1 | 0 | 1 | 0 | 2 | 118 | minor |
| zreq | 1 | 0 | 0 | 0 | 1 | 59 | minor |
| zstation | 0 | 1 | 0 | 0 | 1 | 66 | minor |
| ztunnel | 0 | 1 | 0 | 0 | 1 | 59 | minor |
| zcite | 0 | 0 | 0 | 0 | 0 | 68 | clean |
| zgo | 0 | 0 | 0 | 0 | 0 | 96 | clean |
| zoffice | 0 | 0 | 0 | 0 | 0 | 73 | clean |
| zthrottle | 0 | 0 | 0 | 0 | 0 | 114 | clean |
| zcontainer | 0 | 0 | 0 | 0 | 0 | 605 | clean |
| zphoto | 0 | 0 | 0 | 0 | 0 | 29 | clean |
zterminal (native GPU terminal, no webview) and the JUCE plugins (no window.ZGui) are out of scope for a native-vs-ZGui widget audit.
*Findings & fix targets
- Shared first: zpwr-clip-engine. Its clip UI hand-rolls native
<select>(clip-div / preset / key / scale / swing) and is embedded across traderview / zemail / ztranslator / zstation. Migrating it toZGui.dropdownMenuonce fixes every embedder — highest leverage. - traderview is the biggest app-owned offender: 565 native
<select>+ 219 date inputs (setup wizard, currencies, business-context) against ~14ZGui.*calls. Route selects throughZGui.dropdownMenu, dates throughZGui.datePicker. - Audio-Haxor: 63 native
rangefaders + 17color+ 54 selects →ZGui.dbFader/slider/colorPicker/dropdownMenu. - ztranslator: 25 app-owned
mtx-*routing<select>inztranslator_view.js→ZGui.dropdownMenu. The only non-heavyweight app with a real backlog. - Stragglers (1–3 each): zftp, zemail, zpdf, zreq, zstation, ztunnel — a handful of native selects / range / colour inputs, a bounded cleanup.
- Already clean (0 app-owned native): zcite, zgo, zoffice, zthrottle, zcontainer, zphoto — the reference implementations.
*Beyond form controls — hand-rolled widget modules
A second pass looked past native form elements for whole modules that reimplement a ZGui.* component. Raw greps for native checkbox / number / text / textarea and modal/dialog class names are not reliable — they inflate wildly through vendored/embedded content (e.g. the ztranslator view embedded in traderview) and per-class-name variants, so they are deliberately not tabulated. Named widget modules are the trustworthy signal:
| Module | What it hand-rolls | ZGui.* refs | Verdict |
|---|---|---|---|
traderview/frontend/js/dialog.js | Custom modal dialogs (tConfirm / tPrompt, "replaces native confirm/prompt") — dialog-overlay / dialog-actions chrome | 0 | hand-rolled → ZGui.modal |
traderview/frontend/js/tooltip.js | Custom tooltips — walks data-tip and builds its own popover | 0 | hand-rolled → ZGui.tooltip |
zmax-gui/frontend/menu.js | Menu surface | 62 | already ZGui |
zemail/frontend/menu.js | Bridge to the native macOS menu bar (Tauri), not a webview widget | n/a | not a violation |
Net new fakes: traderview's dialog.js + tooltip.js — two hand-rolled widget modules (0 ZGui refs) reimplementing ZGui.modal / ZGui.tooltip, consistent with traderview being the heaviest offender. Both are traderview-only and a real (non-trivial) refactor: the dialog module has its own promise-based tConfirm/tPrompt API used across setup-wizard / accounts / brokers, so migrating means re-pointing those call sites at ZGui.modal. The menu.js pair are not violations — checked at the source.