Booker and BookingForm take prefill, the name, email and notes the
details step starts with. hanzo.ai's sales page sends its visitor to
cal.hanzo.ai with the answers they gave, and nobody types them twice.
Claude-Session: https://claude.ai/code/session_01HuMDfgFssPUptYUzgZ6Cp8
A browser strips a tab out of a URL before it parses it, so /<tab>/host arrived
as //host: another site, drawn as this app's own section.
Co-authored-by: Hanzo Dev <[email protected]>
A section points at a path of the app or an https address on another host;
a protocol-relative, script or bare address is not drawn, and the home mark
falls back to the app's root. The scope renders in exactly one place, the bar
or the phone's sheet, so there is one selection and one set of reads. A
notification is marked read or archived only when its id is a single plain
segment, and a reason the server names is looked up in a Map. The scope's
keys are per workspace, not per person, and the README says the host clears
them when the person changes. `.auth/` is ignored.
Co-authored-by: Hanzo Dev <[email protected]>
hanzo.ai and the Hanzo App each drew their own copy of the workspace shell,
and the copies had drifted. The frame is here now, once: the rail of
sections, the sidebar with the workspace switcher at its top and the account
at its foot, the bar, the pane, and the column beside it that a room fills
through Beside.
The rail is the sidebar shut on a laptop (an explicit toggle kept per device,
never hover) and the tab bar on a phone. The far-left strip that stands beside
an open sidebar is opt-in through `strip`, off by default. The account menu is
Profile, Notifications, Appearance, Security, Sign out, and carries no
balance. Scope puts project and environment in the bar, kept per workspace
and never carried across one. Notifications reads the reader's own through
the host's call; the package holds no credential.
The frame follows the host's theme, so its inks are the host's own rungs
(FRAME in theme.ts). Its layout lives in shellStyles.ts beside the chrome's,
injected by an insertion effect so the grid is in place before first paint.
@hanzogui/shell 8.3.19
Co-authored-by: Hanzo Dev <[email protected]>
Every plane hangs inside the header, and the header carries backdrop-filter,
which makes it a backdrop root: a descendant's own blur samples only what the
bar paints, never the page. So the 72% glass plane showed the hero straight
through the menu in Chromium and WebKit alike. The plane is the opaque panel
now, the bar wears that same ground while a plane or the sheet hangs from it,
and the veil dims deep enough to recede without its blur. The scrolled bar is
still glass, where the blur is real. No API change.
@hanzogui/shell 8.3.18
@hanzo/event 0.3.39 and 0.3.40 carry a Hanzo key whose project no
longer exists, so the ingest door answers every beacon from gui with
403 ingest_key_unknown. 0.3.41 carries the current keys. The telemetry
package asks for it, and an override lifts the copy @hanzo/observe
pins to the same version.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
apps/team moved to hanzo-apps/team with its history, and builds there
against the published @hanzo/gui. It leaves the workspace list, the
lockfile, and the Written-at-Hanzo list in every NOTICE (ten names
become nine).
Co-authored-by: Hanzo Dev <[email protected]>
The bar linked Privacy, Terms and Cookies. The Acceptable Use Policy, the
Data Processing Addendum and the subprocessor register — what a procurement
review asks for first — were reachable only from inside another document.
hanzo.ai/legal indexes all of them; the bar now names it.
Co-authored-by: Hanzo Dev <[email protected]>
The first extraction in a test file esbuild-bundles the config and every
component package and then requires the result — seconds of real work,
once per worker, charged to whichever test asks first. Vitest's 5s
default was never a budget for that, so three static-tests failed by
timeout on an idle machine.
Co-authored-by: Hanzo Dev <[email protected]>
transformConfig deep-cloned the whole config and then replaced every
theme variable with its value, so the clone built roughly 300k objects
that were discarded in the same pass. Measured on the default config
(900 themes x 362 variables), the same transform: 3502/6722/5005ms
before, 202/241/29ms after, and the emitted hanzogui.config.json is
byte-identical. It was most of a cold compiler start.
Co-authored-by: Hanzo Dev <[email protected]>
A tint named an accent and then painted the page with it, so yellow put the
section on an olive ground and orange on a brown one — the complaint the
ramp was rebuilt to answer, reached by the other route. The ground and the
section surface now come off the neutral scale under all nine stops, which
measures as zero distance from the untinted ground on both schemes, and a
hue reaches the swatch, the demo, the ink and the stored accent instead.
That settles the rung too. With one ground per scheme rather than nine,
every stop is its solid step on dark, and only the light ground's nearness
to the bright end still pulls green and blue down one and orange two.
Co-authored-by: Hanzo Dev <[email protected]>
Yellow and orange were dropped for failing 3:1 against the light ground at
one uniform rung, which was a fact about the rung. Each stop now names the
rung that shows it on each scheme, and every dot carries an ink hairline —
the edge that draws white on white, black on black, and the yellow that no
rung can both darken and keep yellow. White, grey and black join as stops;
grey still clears the accent, and none of the three washes the page, since
an absolute palette under the other scheme's ink is a page with its text
turned off.
Dark is the default a reader who has named neither scheme gets: the meta
hands the UA dark rather than the machine's own answer, and the setting
that meant "ask the machine" is dropped, since the pre-paint script and the
app resolved it differently and the page turned light a frame after booting.
Co-authored-by: Hanzo Dev <[email protected]>
Hovering a column lights it and steps the others back, and below the
rail's width the four sections are peers rather than one tall column
with three stubs under it.
Co-authored-by: Hanzo Dev <[email protected]>
151 packages of Tamagui-derived code are on npm with no license text in
them. Checked four tarballs at 8.3.5 — @hanzogui/core, @hanzogui/web,
@hanzogui/shell and @hanzo/gui — and not one contains a LICENSE file.
MIT asks for one thing: "The above copyright notice and this permission
notice shall be included in all copies or substantial portions of the
Software." Shipping the code without it does not meet that.
The cause is mechanical rather than deliberate. npm includes a LICENSE
that sits in the package directory, and ours sits at the monorepo root,
where it reaches no tarball. So the license is now in each of the 153
publishable packages, and NOTICE beside it — that one npm will not take on
its own, so 152 manifests name it in `files`.
Verified by dry-run pack: 1.3kB LICENSE, 2.8kB NOTICE.
Already-published tarballs are immutable, so this corrects on the next
publish and not before.
This repository is Tamagui, continued. Its git history runs unbroken through
tamagui/tamagui mainline, and every package in the workspace except ten is
upstream's work carried under the MIT license the LICENSE file already
preserves.
A NOTICE saying so was added in July (61758e3cbc, 51bc03d148) and removed in
August by 2bb4f45f1f, which also rewrote LICENSE and stripped the README of
any statement of descent. Nothing about the facts changed in between, so the
attribution comes back, deliberately and in the house form: NOTICE carries the
whole of it, exact rather than approximate, and names the ten packages written
at Hanzo; the README carries one line of lineage pointing at it.
Five packages published under the @hanzogui scope still listed Nate Wienert as
"author", which claims he wrote a Hanzo package rather than crediting him for
Tamagui. That is worse than no credit. Their author is now Hanzo AI and the
derived-work credit lives in NOTICE, where it can say what it actually means.
LICENSE is untouched.
Co-authored-by: Hanzo Dev <[email protected]>
The platform is what they were, so the menus name it once and the column
under it lists what the platform actually offers.
Co-authored-by: Hanzo Dev <[email protected]>
Cloud and Team are what the platform does rather than products beside
it, so one name carries all three and one door opens the console.
Co-authored-by: Hanzo Dev <[email protected]>
Three of the seven stops were yellow, which over the dark ground is an
olive, and the swatches sat in a Group, whose zeroed radii cut a straight
edge through each selection ring. The ramp now sweeps one arc of the
wheel and every stop is measured: the dimmest clears 3:1 against each of
the seven grounds the page can paint, on both schemes. Picking one writes
the accent through @hanzo/appearance, so the choice outlives the visit.
Co-authored-by: Hanzo Dev <[email protected]>
The framework was documented in hand-written MDX, which drifts the moment a
signature changes. The declarations each package emits already say what it
exports; a page that quotes them says the same thing and cannot say anything
else.
Co-authored-by: Hanzo Dev <[email protected]>
The brand pill pads by 12 and handed all 12 back, which put the NAME itself
exactly on the page's column. That is right for body text and wrong for a
wordmark: set in bold beside the window's edge it reads as flush, and the
gutter looks like it was forgotten.
Four of the twelve are kept. The lit pill still grows outward into the
gutter, and the name now sits a hair inside the column instead of on it.
An open menu is a layer over the page: the plane wears GLASS, the grounded bar
wears the same, so from the top of the screen to the bottom of the menu there is
one ground, one blur and no edge.
App, Bot, CLI, Cloud, Dev, IDE, Platform, Team, each with the line its product
page says under the name. Chat and Base stay products and leave the estate
menu. Every console address is platform.hanzo.ai.
Every door that opened console.hanzo.ai opens platform.hanzo.ai — the same
console, simplified for the people who use it — and the menu row wears the
product's name, Hanzo Platform.
App, Bot, CLI, Dev, IDE, Platform, Team — then Cloud and Chat. CLI and Platform
become flagship products (hanzo.ai/cli and platform.hanzo.ai both serve), and
Team is released rather than beta, so a signed-out reader meets all seven.
The order is stated once, where the menu derives it, so the product table stays
a table of what exists and a new product sorts to the end until it is ranked.
The bottom bar reads left to right as the page does: whose it is, what
governs it, then the mark closing the column.
The brand wears the chevron every other trigger in the bar wears. It was
the one control that opens a menu and did not say so, so a reader looking
for the products read past it.
Every Hanzo surface reads @hanzo/design, @hanzo/tokens,
@hanzo/appearance and @hanzo/font; this site set its own type, so its
header, ramp and ground drifted from hanzo.ai and a reader's appearance
settings reached nothing here. Gui's $n ladder now resolves through the
design ramp, dark is the default, the chrome stays grey while the
landing sections keep their tints, and everything below the fold loads
as it nears the viewport. getFontSize reads the px fallback of a var()
size, which is what keeps icon sizing numeric once sizes live on a ramp.
The loader and the Next plugin only existed to drive webpack; Next runs
on Turbopack and Vite has its own plugin. kitchen-sink and the
integration app serve through vite, the optimizer suite drops the
webpack name, and its fixture packages join the workspace so a fresh
checkout links @my/ui.
setTelemetry(undefined) deleted @hanzo/event's page-client registry — the map
that makes "one stream per page" true. It reads as test hygiene, and that is
where it came from, but setTelemetry(undefined) is also what TelemetryProvider
calls from its cleanup effect: every unmount, every consent change, every prop
change that rebuilds the client.
So the guarantee this package exists to hold was being dropped by this package.
The next mount gets a client that has never seen the page and counts the view
again — one page load, two pageviews on the wire, with nothing on either row to
say they came from different clients. owner.test.tsx is entirely about not
emitting twice and could not see it, because the delete happened inside the very
call the suite used to reset between tests.
The reset belongs to the page, so it belongs to the package that owns the
registry: @hanzo/event 0.3.40 names it resetClients(), and the suites call it in
beforeEach, where "each `it` is its own page load" is true and says so.
Pins the double as a test: mount, unmount, mount, expect one pageview.
PUBLISHABLE_KEY is a BUILD-time variable, and a static export is built once and
served on every brand — so a build-time key is either absent or wrong. Absent
is what the fleet actually has: hanzo.ai's is commented out in .env.example and
both Zoo sites shipped with none, which means every one of those surfaces
mounts a client that collects and sends nothing. An analytics client that
reports nowhere looks exactly like one that reports fine.
A brand's domain already names its org, and an org has exactly one publishable
key. @hanzo/event has derived it from the hostname since org.ts was written,
for precisely this case, and nothing ever called it. Now the key falls back to
`keyForPage()` after config and env, so a page served from a domain we
recognise reports correctly with nothing configured, and one served from a host
no brand claims still reports NOWHERE — filing one brand's visitors in
another's project is the defect the derivation exists to prevent.
Bound once, not twice. `describeTelemetry` restated the same expression to
answer `hasIngestKey`, so the moment this line gained a third source the two
disagreed about the same client: it had a key and its own description said it
did not. That is what the new test caught first.
Node's require never selects a package's react-native condition and
cannot load an ES module whose sources say a bare require, so the
extractor resolves that entry itself and translates such a file to
CommonJS as it loads. Directories are walked with the filesystem's
glob, negative numbers become AST through valueToNode, the swallowed
load error is named under DEBUG, and the optimize fixture is its
source again with the suite restoring it after each run.
Co-authored-by: Hanzo Dev <[email protected]>
Native sources say a bare require, which Metro provides and an ES
module cannot; marking dist/native CommonJS like dist/cjs lets Node
load it as written, so the optimizer's native pass runs again. The
.cjs sibling answers there too, the CLI imports its commands, the
loader names its css file by the emitted name, and the extractor
tolerates a dependency whose tsconfig extends a preset it lacks.
Co-authored-by: Hanzo Dev <[email protected]>
A relative import without its extension survives the emit unchanged
and nothing but a bundler can load it, and the .cjs and directory
shims at package roots pointed at emits no build produces. The exports
map is the one entry; the worker each emit runs is the worker of its
own kind.
Co-authored-by: Hanzo Dev <[email protected]>