A host whose bar follows the page's theme restated the shell's four
--white-NN rungs on its own element, inline, so the menu would not draw
white ink on a light page. HanzoIdentity takes ink="page" and marks its
root; INK_CSS, injected once under its own id, gives that scope inks that
follow the color-scheme design sets: design's white in the dark, black at
the same weights in the light, the dimmest pushed to .56 for 4.5:1, the
hairline the theme's --border. Without the prop nothing changes.
The shell renders on every Hanzo property, so a word it draws as bare
English stays English on every page of every locale, whatever the host
translated around it. Each visible string now passes `t()` from
@hanzo/i18n/react: the palettes, both headers, the menus, the footer,
the frame, the account menus, the meter, notifications, the visitor
chat and the launcher. Copy written as data (the registry, the
launcher's apps, the account rows) is marked with `phrase()` where it
is written and drawn through `t()`. A product's name standing alone is
not marked, and host data (palette commands, frame sections, names,
orgs) is drawn as given.
The palettes translate their rows before ranking them, so a reader
searches the words they see. A sentence with a value or a key cap in it
is one message with a placeholder; the meter's span is a sentence per
span. The send arrow mirrors under RTL.
@hanzo/i18n is a required peer (>=0.4.0) so the host's one provider
reaches the shell. With no provider `t` answers the English source: 32
server renders of the components are byte-identical before and after.
The one markup change is the open launcher's "⌘K to switch", now one
sentence around its key cap. i18n.test.mjs parses the source with oxc
and fails on copy that reaches the screen without t() or phrase().
Claude-Session: https://claude.ai/code/session_01BLLMyBFSvAfCt7ZGqYuiEz
`tracks` is a CSS track list along a stack's own axis, so the parent
sizes its children and none of them carries a flex prop:
<YStack tracks="auto 1fr auto"> header, body that fills, footer
<XStack tracks="240px 1fr"> a fixed rail and the page
On web the stack is a grid: a YStack's tracks are its rows (row flow,
one column as wide as the stack), an XStack's its columns. A prop the
author names wins over all of it. Alignment keeps its flex meaning: on
a vertical stack grid's axes swap, so propMapper renames every key it
emits (items -> justifyItems, justify -> alignContent, content ->
justifyContent, and a direct child's self -> justifySelf), which holds
in media, pseudo and variant styles alike. A child learns its parent's
axis from ComponentContext and clears it for its own children.
On native the stack stays flex and each child takes its track as flex
longhands: Nfr is flex N, a length is a fixed basis, anything else is
content-sized.
The compiler leaves a tracks stack and its direct children to the
runtime, which is where both are decided.
Wrap is a row that wraps, the one place content-sized wrapping lives.
All opt in: a stack without tracks renders as it did. Tested in
components-test (web and native), static-tests (the deopt) and a
Playwright run of the integration app that measures the boxes in dev
and in the compiled production build. TracksDemo is in the demos app
and on the stacks docs page.
Claude-Session: https://claude.ai/code/session_01BLLMyBFSvAfCt7ZGqYuiEz
The web emits them as the CSS logical values and native maps them to the
left/right react-native swaps under RTL, but the type was react-native's,
which names only the physical sides, so a caller had to spread them past
the checker.
getThumbInBoundsOffset already carries the direction, and the thumb
multiplied the sum by it again, so under RTL the nudge pushed the thumb
4px past its mirror at 25%. Only the centring half-size turns now; LTR
is unchanged. Measured with @hanzo/ui's mirror gate: 44 of 44 boxes
mirror against this branch.
RN-style start/end props (paddingStart, start, borderTopStartRadius) emitted
padding-start and start: on web, which no browser parses; they map to CSS
logical properties now, and inline logical longhands outrank the physical
sides padding and borderRadius expand to. On native, inline insets land on
start/end and textAlign start/end on the left/right RN swaps under RTL.
useDirection falls back to <html dir> and follows changes, so slider, tabs,
menus, roving focus, switch and progress read the direction i18n sets. The
menu no longer asks for bottom-end under RTL, which floating-ui flipped back
to the left. Group joins, list item icon gaps, select and menu adornments and
every shell side are logical; directional marks mirror via data-i18n-mirror.
On web the details step reads Return itself and skips the Enter that confirms
an IME conversion (isComposing, or keyCode 229): gui's web Input calls
onSubmitEditing on any Enter, which booked a half-converted name. Native keeps
its return key.
The chosen time is the inverse tile, where the ink is the page's colour; its
ring now sits 2px in from the edge, framed by the tile's fill. The compact
embed's "just taken" line takes focus as the Booker's banner does.
Co-authored-by: Hanzo Dev <[email protected]>
Every Press states tabIndex 0 (Safari's Tab skips a <button> without one, so
the month arrows were out of reach there) and role="button" for native. An
unavailable Previous stays focusable as aria-disabled, so reaching the first
month does not drop focus. Back keeps the chosen time and focuses it; a slot
just taken focuses its banner, announced as an alert; a server error in the
form is an alert too; a submit missing a field focuses that field, and Return
in Name or Email books.
The ring is the full ink (the accent pill takes its fill's colour), not
$outlineColor, which a stock config sets at 20% alpha and which matched a
time's hover edge. The roving groups ignore chords, keep an arrow at an end
from scrolling the page, and start over when the day or month changes. The
chosen day and time are aria-current rather than toggles. The event title is
the page's heading and the avatar initial is hidden from readers. Labels keep
their type's leading and times their 8.3.7 height.
Co-authored-by: Hanzo Dev <[email protected]>
Days, the month arrows, the times and the back arrow are real buttons
(press.tsx: a YStack rendered as <button type="button">), so Tab reaches them
and Enter or Space presses them; before, they were divs only a pointer could
use, and Tab reached nothing on the page but the logo. Each draws the theme's
ring when focused.
The open days are one tab stop (Left and Right a day, Up and Down a week, Home
and End), and so are the times (the arrows step through them in order); Tab
lands on the chosen one and comes back to the one the arrows last reached.
Days are named in full ("Friday, October 16, times available"). Choosing a
time moves focus to the details step's heading.
Fields have real <label for> labels, the email field is type="email", and
field text is 16px so iOS Safari does not zoom into it. `hostName` names the
host over the profile's name, which is the booking handle when the backend
keeps no display name.
Co-authored-by: Hanzo Dev <[email protected]>
Each field's control is labelled by its visible label rather than its
placeholder, says whether it is required, and points at its hint when invalid;
the hint is announced. A blocked Confirm looks blocked: half opacity and no
pointer, stated rather than left to the host's config.
CalendarEmbed stacks the times under its header, where the zero basis the
Booker's row uses took the column's height and left every slot off the card;
only the Booker's row shares now.
Booker takes `bare`, for a surface that is already a card, such as an embed's
popup panel.
Co-authored-by: Hanzo Dev <[email protected]>
Name and Email no longer carry sample data as placeholders ("Ada Lovelace",
"[email protected]"); they say "Your name" and "[email protected]", and every field
names the theme's placeholder colour with its value at the full ink, so a hint
is never read as something typed. Fields, open days and time slots are tiles on
the theme's raised ground under its hairline.
Confirm booking is the theme's inverse, a white pill on dark and a black one on
light, and a chosen time is the same; Back is quiet. Nothing is blue or green.
Required fields carry no red mark; the optional one says so.
The columns grow instead of taking flex 1, which is a zero basis under a config
whose styleCompat is "web" and collapsed the month and the form in a column.
The availability dot is placed against its cell.
Co-authored-by: Hanzo Dev <[email protected]>
GET /v1/billing/tier's displayName names the tier class ("Pro" for every Max
rung), so it never names the plan. The removal guard also reads the emitted
declarations, where an aliased type re-export would hide.
Co-authored-by: Hanzo Dev <[email protected]>
usePlan reads GET /v1/billing/tier for the plan slug and names it from
GET /v1/billing/plans; paid is the catalog's price or contact-sales. It no
longer picks from the org-wide subscription list. A host passes a resolved
plan, or a billing origin and headers (X-Org-Id), through HanzoAuth.
The removal guard covers all 24 ladder names in the barrel, ./registry and
the built output, and any deleted module under src/.
Co-authored-by: Hanzo Dev <[email protected]>
HANZO_PLANS restated a plan ladder the catalog no longer sells (Go, Dev,
Plus, Team Max, Custom) with taglines no plan carries, and named the
catalog's Pro "Dev". It is removed with everything only it fed:
APP_ENTITLEMENTS, the entitlements resolver, useEntitlement, HanzoPlans and
HanzoAccessGate. No app imports any of them.
usePlan reads the held subscription itself and shows the plan name the row
carries ("Pro", "Max 5x"). HanzoAppLauncher locks a tile only by the check
its caller passes.
Co-authored-by: Hanzo Dev <[email protected]>
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.