Compare commits

...
469 Commits
Author SHA1 Message Date
zandhanzo-dev 843082ef53 feat(event): finish the PUBLISHABLE_KEY rename in hz.js + publish (v0.3.17)
Publish Packages / detect-changes (push) Successful in 15s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Successful in 1m39s
Hanzo CI/CD / cicd (push) Failing after 5m58s
CI/CD / cicd (push) Failing after 6m9s
The EVENT_INGEST_KEY -> PUBLISHABLE_KEY rename (f44d441d8) renamed the build-arg
path (Dockerfile/KMS/core.ts) but left hz.js on data-ingest-key and never bumped
the version, so the rename was unpublished. Finish it: hz.js reads
data-publishable-key first. A static tag has no lockstep build to migrate it (the
guarantee the build-arg rename relied on), so data-ingest-key stays readable as
the retiring spelling — the one surface where a hard cut would silently break
hand-written HTML. Bump 0.3.16 -> 0.3.17 so the whole rename actually ships.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-05 16:43:29 -07:00
zeekayandhanzo-dev f44d441d88 rename EVENT_INGEST_KEY -> PUBLISHABLE_KEY
Hanzo CI/CD / cicd (push) Canceled after 0s
CI/CD / cicd (push) Canceled after 7m59s
The value is a pk- prefixed PUBLISHABLE key (the Stripe vocabulary, and what
the build-time gate already tests for). EVENT_INGEST_KEY hid that.

One substitution covers all three spellings, since the framework prefixes wrap
the same token: EVENT_INGEST_KEY -> PUBLISHABLE_KEY, NEXT_PUBLIC_* and VITE_*
follow. KMS already carries deploy/PUBLISHABLE_KEY (env prod) with the same
value, written and read back first, so no build can reach a name that does not
exist yet.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-05 16:24:50 -07:00
hanzo-dev 3c6ad37130 merge: @hanzo/replay — the browser session-replay recorder
Publish Packages / detect-changes (push) Successful in 19s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Failing after 2m27s
Hanzo CI/CD / cicd (push) Failing after 4m42s
CI/CD / cicd (push) Failing after 5m0s
rrweb records the DOM; this package decides what may leave the device and posts
raw eventWithTime batches to POST /v1/replay, attributed by a publishable pk-
key and nothing else. Masking happens at capture time on @hanzo/observe's
RedactionPolicy rather than a second one, and the recorder refuses to run on
credential-bearing routes at all.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-05 13:37:58 -07:00
hanzo-dev 9200eb72f2 replay: close three ways a secret left the browser
Found by recording a real page in a real browser and grepping the payload for
canaries, which is the only test that can find these. All three passed review and
passed the suite.

maskAllInputs is NOT "mask everything". rrweb reads `true` as "use MY hardcoded
type list instead of yours", and that list has no `hidden` — so the table this
package passed was discarded and a hidden input's value serialized in full. A
CSRF token rode out in the FullSnapshot of a login form (reproduced, canary
`CSRF-LEAK-CANARY-…`). Passing `false` plus our own complete table keeps the
decision in maskInput(), which already refuses SECURE_TYPES whatever the mode
says. Same policy, one authority, and `hidden`/`checkbox`/`radio`/`file` are in
the table now — the types carrying a value the user never typed and never sees.

A secret key was accepted. `publishable()` existed and only the beacon path
consulted it, so an `sk-` key was refused on unload and sent in an Authorization
header on every other batch. The test named "refuses to start without a
publishable key" only ever asserted the empty string. record() now refuses any
non-`pk-` key, so a secret has no carrier out of the browser rather than one
carrier that happens to refuse it.

The rrweb escape hatch is spread first, so an option the gate does not NAME is
one the caller can still set. recordCrossOriginIframes (another origin's DOM,
which our policy cannot reach inside to mask), collectFonts and plugins are now
pinned, as are blockClass/ignoreClass/maskTextClass — renaming those silently
disarms every rr-block already written into the app's markup.

Each fix has a test that fails against the previous commit; verified by running
the new tests against it (4 failed, 66 passed).

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-05 13:25:45 -07:00
hanzo-dev f123023a38 feat(replay): @hanzo/replay — rrweb session recorder on the shared privacy policy
Records the DOM with rrweb and POSTs raw eventWithTime batches to
POST /v1/replay, attributed by a publishable pk- key.

Masking happens at capture time, in the browser, and reuses @hanzo/observe's
RedactionPolicy rather than defining a second one: isPrivate() for subtree
exclusion (selector plus node-level, so a custom privateAttribute is honored),
sensitiveKey() for field identity. Password and cc-* fields are blocked outright;
URLs on Meta/snapshot/mutation events go through @hanzo/event's redactSecrets;
the recorder refuses to run on /callback and /login/oauth/device and stops if the
app routes onto one.

Orthogonal to @hanzo/observe: semantic capture there, DOM movie here.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-05 11:11:14 -07:00
zandhanzo-dev c33e29283c fix(event): make @hanzo/event emit on React Native (v0.3.16)
Publish Packages / detect-changes (push) Successful in 13s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Successful in 1m46s
Hanzo CI/CD / cicd (push) Failing after 4m0s
CI/CD / cicd (push) Failing after 4m0s
React Native defines `window` but no `document`/`window.location`, so
`isBrowser()` (typeof window !== 'undefined') took the browser path there
and `init()` threw on `window.location.search` — the mobile telemetry gap.

Require `document` too. RN now reads as non-browser: `init()` returns early
(skipping attribution + the visibility/unload listeners RN lacks) while
`capture()`/`flush()` — not gated on this and sending via plain `fetch` —
still emit. Browser and SSR/Node behaviour is unchanged (a browser always
has `document`; Node has neither). No second transport.

Version stamped in all four sources (package.json, version.ts, hz.js) so the
consistency tests stay green. 152/152 pass.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-05 10:40:18 -07:00
aandhanzo-dev 3f108879f8 test(ui): a size floor must never outrank a fixed control's height
Hanzo CI/CD / cicd (push) Failing after 4m46s
CI/CD / cicd (push) Failing after 4m47s
The Switch was 36x29 instead of 36x20 for as long as it existed, and nothing
anywhere would have said so. gui's `size` variants return
{ height, minHeight, width }; a wrapper that sets `height` and not `minHeight`
overrides two of the three, and min-height beats height, so the variant's floor
survives. The control is silently the wrong size — and on a pill the browser
then clamps the radius until it is not a pill.

Whole-surface, over the same Gallery the stylesheet is generated from, so the
next component to grow a `size` variant is covered without anyone remembering
to add it.

Scoped to the controls whose geometry IS the contract, because a floor is not
itself a smell. Textarea writes `minH={64}` on purpose — its own doc says rows
are the floor and not a fixed size — and the unscoped version of this test
flagged it. A guard that cries wolf on correct code gets deleted, and then the
real one is not there either.

Two mutations, both verified: drop `minHeight` from the Switch and it reports
`switch: height 20px, floor 29px`; misspell the slot list so the guard matches
nothing and the coverage assertion fails rather than passing green. That second
one is the same shape of silence the guard exists to catch — a check that runs
over an empty set reports success, which is how an absent name passes for a
clean result.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-05 07:47:45 -07:00
zooqueenandhanzo-dev 5cbde80610 ui: 8.0.63 — 8.0.62 shipped design 0.4.6's light ramp, not 0.4.7's
Publish Packages / detect-changes (push) Successful in 12s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Skipped
Hanzo CI/CD / cicd (push) Failing after 4m11s
CI/CD / cicd (push) Failing after 4m11s
b18b61a bumped @hanzo/design to ^0.4.7, which softens light off pure white
(#f7f7f7 background, #fafafa card, #ffffff popover) because a #ffffff page reads
as a lightbox. I rebased onto that commit but never re-ran install, so the build
resolved the 0.4.6 already in node_modules and dist/theme.css went out carrying
the ramp 0.4.7 exists to replace. The manifest said ^0.4.7 the whole time; only
the artifact disagreed.

Nothing in the unit suite could see it — theme.css is composed from whatever
design is installed, so both builds are self-consistent. The consumer gate is
what caught it, by loading the real page in a browser and reading the computed
--background against the literal the spec pins: expected #f7f7f7, received
#ffffff. Exactly the class of defect that gate exists for, on the first run where
it had something to say.

A caret range plus a stale node_modules is a silent artifact change. Install
before you build after a rebase that moved a dependency.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-05 05:24:52 -07:00
zooqueenandhanzo-dev cf3d2be4c0 ui: 8.0.62 / data 1.2.3 — the versions the line had already taken
Publish Packages / detect-changes (push) Successful in 11s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Skipped
Hanzo CI/CD / cicd (push) Failing after 21s
CI/CD / cicd (push) Failing after 21s
8.0.61 and data 1.2.2 were both published while this batch was in flight (the
peer-range fix took 8.0.61; data 1.2.2 went out still pointing at src). Rebased
onto b18b61a rather than over it: its `>=8.1.0` peer ranges stand, and this
batch's `next` optional peer and `@hanzo/data >=1.2.3` join them.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-05 05:16:57 -07:00
zooqueenandhanzo-dev 3a18541802 fix(ui)!: <Input type="password"> was rendering the password in plain text
The wrapper destructured `type` out of props and never forwarded it, passing
`secureTextEntry` alone — and gui DROPS that spelling on web. So every masked
field built on @hanzo/ui's Input rendered its value as readable text in every
browser, with the eye control sitting next to it offering to reveal what was
already visible. This is the exact failure `masked()` was written for, in this
repo, and this component never called it. It calls it now, and only on the
password path: masked(false) states type="text" and would overwrite a caller's
type="email".

`masked` moves to backends/gui/mask.ts, which is where a platform prop-spelling
belongs; product/SecretInput re-exports it, so nothing downstream moves.

`hidePasswordToggle` becomes `reveal` — one word, positive, and no longer a
double negative on a control the caller may already own. Suppressing it does not
unmask: two controls over one boolean is a field with two states that disagree,
where pressing ours leaves the caller's icon reading "show" and neither can say
which one masked the field.

The rest are the component gaps four migrations measured:

CopyButton draws a label when given one. There is no variant to pick, because
the forms differ in exactly one thing — a control standing alone under a minted
key has no neighbours to explain an unlabeled glyph, and inside a code header the
word is noise. The accessible name follows the visible text.

StatusTag speaks invoices — paid, open, past_due, uncollectible, draft, void —
through `tone` in ./tone, a pure lookup a billing surface can assert without
mounting a pill. Still no hue: the four tones are rungs of the grey ladder, and
`stopped` is set apart by an EDGE, the same choice Fieldset makes for its
destructive register. It had to be: `failed` and `pending` painted identical
tokens before, so the vocabulary had four names and three visuals.

FieldText forwards autoComplete and id. It does NOT take a `name`, and that
absence is deliberate and measured: `name` is gui's own prop (it names a styled
component and a theme) and is consumed before it reaches the element, so
accepting one would type-check, render nothing, and leave a caller believing
their form posts a field it does not.

Fieldset grows and floors its width, so a settings TAB can be two columns without
each group being wrapped in a sizing box of its own. Panel takes an icon and a
description — the caption every console was smuggling in as its first child.

Code's language label moves to $color11, the ramp's readable secondary. On this
package's own ramp $color10 already cleared 4.5:1 (8:1 dark, 11.8:1 light), so
the 2.97:1 measured on hanzo.ai/overview came from a HOST redeclaring --color10
at :root — which is the argument for the change, not against it: a shared
component should not need the host to be careful.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-05 01:52:50 -07:00
zooqueenandhanzo-dev 02af62c2ae ui: a barrel that costs a framework is a barrel nobody can import
Four surface migrations measured the same wall this week and each one wrote its
own copy rather than climb it. The wall is packaging, not components.

`export { ThemeToggleNext }` in the product barrel was a STATIC edge to
@hanzogui/next-theme, whose provider imports `next/script`. One line put Next in
the graph of every Vite, Express and Tauri host — the hosts this layer promises
to run on. It has its own subpath now; `<ThemeToggle />` still reaches it by
dynamic import and still degrades when next-theme is absent, and dist.test.ts
asserts BOTH, because a test that only asserted the absence would pass just as
well on a deleted feature. `next` becomes an optional peer: next-theme requires
it and nothing here admitted that.

`@hanzo/ui/product/pure` is the rules with none of the layer — pages(), masked(),
displayName(), tone(), orgScope, filterOptions, resolveBrand. Every module it
re-exports imports nothing and none is stamped 'use client', because a stamped
module is a client REFERENCE on React's server layer and calling pages() through
one in a server component throws instead of paging. The components import the
same modules, so there is one definition rather than a testable copy of a shipped
one. Proven by `require()` in a CHILD node process: under vitest, vite's
transform is already installed and the test would prove nothing.

`./product/*` and `./primitives/*` open deep imports. `@hanzo/ui/css` is
substitute() — jsdom does not resolve var(), so every consumer trying to assert
the contrast of a rung compared a colour to a function call. With no vars map
that is exact, not approximate: jsdom mounts no design sheet, so the fallback IS
what a browser computes. It is not part of ./core, which is ESM-only because
@hanzo/design publishes no require condition — and a jest consumer is precisely
the caller that needs this.

@hanzo/data pointed its exports at src/index.ts and shipped raw TSX. Every
consumer transpiled it, and `require('@hanzo/data')` could not load at all, which
is one of the two edges that break `require('@hanzo/ui')`. It emits dist/ now, in
both formats, from the SAME postbuild.mjs — parameterised rather than copied,
since a second copy is a second place to get the barrel rule wrong.

The utility classes carry `hz-`. This sheet claimed `.row`, `.skeleton`, `.fade`,
`.mono`, `.drag` and `.tnum` at the document level, in a package an app imports
once at its root; an app with its own `.row` got no warning, it got whichever
rule the cascade preferred, from a stylesheet it never opened. The bare selectors
stay as aliases for one minor version and are REMOVED IN 8.1.0.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-05 01:52:50 -07:00
zeekayandhanzo-dev b18b61a580 ui: peer ranges stop letting the gui train split under a consumer
Publish Packages / detect-changes (push) Successful in 12s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Skipped
Hanzo CI/CD / cicd (push) Failing after 4m56s
CI/CD / cicd (push) Failing after 4m56s
@hanzo/ui peer-depended on `@hanzogui/config: >=8.0.0`, and an app satisfying
that with 8.0.1 got `@hanzogui/web@8.0.0` — beside the `web@8.1.0` its direct
`@hanzo/gui@8.1.0` pulls. Two copies of the gui runtime, both typechecking,
both building, and then the config singleton lives in one module while the
components read the other. hanzo.industries hit exactly this on a routine bump.

The loose peer range IS the split, so the fix is here rather than in an override
downstream: @hanzo/gui, @hanzogui/config and @hanzogui/next-theme all move to
`>=8.1.0`. An app can no longer land on the 8.0 train while holding 8.1.

Also closes the hole in the harness that let this reach an app at all. The
consumer test installed @hanzo/ui alone, so the ONE shape that can split — a
real app depending on @hanzo/gui DIRECTLY, beside @hanzo/ui — was never
exercised, and the one-copy check I added last night reported a clean single
copy while a real consumer got two. The test app now installs @hanzo/gui and
@hanzogui/config the way every real app does.

Mutation-proven both ways: restore the `>=8.0.0` peer, have the consumer satisfy
it with config@8.0.1, and the harness now fails naming both versions —
"2 copies of @hanzogui/web resolved: 8.1.0, 8.0.0". Before this commit that same
scenario passed.

289 unit, 28 browser, webpack green, one copy of @hanzogui/web.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-05 01:43:11 -07:00
hanzo-dev bbbef9c65e event: one anonymous identity chain, in one file, for all three distributions
Publish Packages / detect-changes (push) Successful in 12s
Hanzo CI/CD / cicd (push) Failing after 26s
CI/CD / cicd (push) Failing after 26s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Successful in 1m17s
The same browser was several people. 0.3.14 moved the bundled client's anonymous
id onto a cookie shared across *.hanzo.ai, but two other implementations of the
same idea kept running: hz.js minted into hz_id — a key of its own, so a page
carrying both it and the npm client sent two anonymous ids for one visitor — and
the tag hanzoai/cloud hosts at /v1/event.js had a third resolution over the
shared key, so an origin carrying only the tag stayed split from everything else.

The chain now lives in src/anon.js and nothing else implements it:

  - src/storage.ts imports it; anonId() is a call.
  - hz.js has no bundler, so it inlines the marked region VERBATIM, and
    src/anon.test.ts compares the two byte for byte.
  - hanzoai/cloud vendors the same file for its tag.

Resolution is cookie · localStorage hz_anon_id · localStorage hz_id · in-memory ·
mint. Every id already in a browser is ADOPTED and only a browser holding none is
given a new one, so no returning visitor is reset — including the ones who have
only ever met hz.js, whose hz_id is carried onto the shared key instead of being
orphaned beside it.

hz.js also drops its restated UUIDv7 minter and uid.ts re-exports the chain's, so
the version nibble the plane's session rollups admit cannot diverge between
distributions. sessionId() is unchanged: a session stays origin-local.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-05 01:26:48 -07:00
hanzo-dev 9dfc7ae1cd event: 0.3.14 — 0.3.13 was already taken, and published, by the scrub fix
Publish Packages / detect-changes (push) Successful in 12s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Successful in 57s
Hanzo CI/CD / cicd (push) Failing after 4m16s
CI/CD / cicd (push) Failing after 4m16s
Both branches bumped 0.3.12 to 0.3.13 with different payloads, so the merge
resolved without a conflict and would have republished a taken version under a
different meaning. 0.3.13 on npm is the redact-by-parameter-name fix; the
shared anonymous-id cookie is 0.3.14. The CHANGELOG now says which is which —
0.3.13 shipped without an entry.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-05 01:06:27 -07:00
hanzo-dev a871896661 merge forge main into the shared anonymous-id cookie
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-05 01:05:34 -07:00
hanzo-dev 0481e77e27 merge origin/main into the shared anonymous-id cookie
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-05 01:04:59 -07:00
hanzo-dev 73b386d74b event: share the anonymous id across *.hanzo.ai via a first-party cookie
anonId() kept the id in localStorage, which is ORIGIN-scoped, so docs, cloud,
console, studio, pay and www each minted their own for the same browser. One
marketing -> docs -> signup -> checkout journey therefore arrived as several
strangers: 463 anonymous identities carried 545 events in a week, about 1.2
events each, which is a funnel that cannot be read. Signed-in stitching was
never affected -- it joins on the OIDC subject.

The id now lives in a cookie on the registrable domain
(Domain=hanzo.ai; Path=/; SameSite=Lax; Secure, two years, refreshed on read),
which every subdomain shares.

The migration is ADDITIVE: cookie, else the hz_anon_id this package has always
written in localStorage -- adopted into the cookie, never minted over, because
minting there would hand every returning visitor a new identity and detach
their history -- else mint. localStorage keeps being written, so a rollback
finds everyone where it left them.

Degradation is what it was: SSR and prerender return undefined rather than
minting a server-side id; cookies refused falls back to localStorage; both
refused holds one id in memory for the page load instead of letting every
event mint its own. Domain and Secure are omitted off hanzo.ai (localhost,
previews), where either attribute makes the browser drop the cookie outright.
sessionId() is unchanged -- a session stays origin-local.

Cross-registrable-domain identity (hanzo.app, hanzo.chat) is deliberately NOT
attempted here; a cookie cannot cross and third-party storage is blocked.

storage.test.ts is new: the adopt, cookie-wins, shared-jar, mint, SSR,
cookies-refused and both-refused cases. Seven of them fail against the old
anonId, including the one that asserts two *.hanzo.ai surfaces sharing a jar
resolve to the SAME id -- the reported bug, reproduced.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-05 01:02:42 -07:00
aandhanzo-dev cc624c279c fix(ui): the Switch thumb was invisible in both states — the sub-theme inverts
8.0.57 fixed the geometry and broke the thumb, and 8.0.58 and 8.0.59 shipped it
that way. Measured live on the page it was meant to fix: ON was a
rgb(255,255,255) thumb on a rgb(250,250,250) track, OFF a rgb(26,26,26) thumb on
a rgb(26,26,26) track. Two states, both painted onto their own background, so
the control had no visible thumb at all.

gui wraps the thumb in a `t_SwitchThumb` sub-theme that INVERTS the whole ramp.
Measured, at the frame and inside the thumb:

    $color3    rgb(26,26,26)     rgb(171,171,171)
    $color12   rgb(250,250,250)  rgb(10,10,10)

So `$color10` and `$color1`, chosen by reading the frame's palette, resolved
under the thumb to precisely the two track colours they were sitting on.

The fix is not a better pair of guesses. The thumb now takes THE SAME TOKEN as
its track, and the inversion does the rest: naming one token paints both sides
of the pair, and the contrast becomes structural — the thumb cannot come out the
colour of the track beneath it, in either state, whatever the palette does
later.

The test gap is the more useful half. The suite asked whether the two STATES
differ from each other, and they did — `_bg-color10` against `_bg-color1` — so
five green tests and a clean typecheck shipped a control nobody can see. Nothing
asked whether either state differs from the thing behind it, which is the only
question a switch's appearance actually poses. `paints the thumb with its
track's token` asserts that invariant on the markup, and it fails on exactly the
pair that shipped.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-05 00:58:57 -07:00
hanzo-dev 56b69def75 merge forge main
Publish Packages / detect-changes (push) Successful in 12s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Skipped
deploy / image (push) Successful in 2m55s
Hanzo CI/CD / cicd (push) Failing after 3m52s
CI/CD / cicd (push) Failing after 3m52s
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 23:53:51 -07:00
hanzo-dev 88d715d795 event: redact a credential by the name it is filed under, not its shape
Every pattern in SECRET_PATTERNS recognises a secret by what it LOOKS like — a
JWT's three dots, sk-, AKIA, ghp_. An OAuth code, a state, a password-reset
nonce and an invite token are opaque random strings indistinguishable from a
page id, so none of them matched and all of them survived.

The client stamps url = window.location.href on EVERY event, not only
pageviews, so one visit to /callback?code=&state= put a live, still-redeemable
authorization code on the wire once per event, in cleartext.

Redacting by parameter NAME is the only signal available for an opaque token.
The name half is bounded and the value half stops at the first separator, so
neither can backtrack — the discipline the creds-in-URL pattern already
documents. Ordinary params (plan, utm_source, page) are untouched.

hz.js carried a second copy of VERSION that the bump missed; its own test
caught it, which is what that test is for.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 23:53:51 -07:00
hanzo-dev 652b34ddb3 merge origin/main into the ingest-key attribution fix
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 23:47:39 -07:00
hanzo-dev 2c7ca7921c ui.hanzo.ai attributes its anonymous pageviews instead of dropping them
The telemetry client was mounted and could not write. api.hanzo.ai answers
401 ingest_key_required for the key v5.7.6 ships, so every logged-out visitor's
pageview has been discarded at the door — and a static export has no runtime in
which to notice.

The key got there by hand. app/lib/analytics.ts read
NEXT_PUBLIC_HANZO_INGEST_KEY, a spelling neither KMS nor any builder carries,
so the only way to satisfy it was a local `docker build --build-arg`. A
credential with no automated source is satisfied once and then goes stale in
silence, which is exactly what happened.

One name now, end to end: KMS deploy/EVENT_INGEST_KEY -> ARG EVENT_INGEST_KEY ->
ENV NEXT_PUBLIC_EVENT_INGEST_KEY, which is the name @hanzo/event already falls
back to reading. The Dockerfile refuses a build whose key is empty or is not a
pk- key -- the second case rejects the stale pk_ format outright -- and after the
export it greps app/out for the value, because a rename on either side of
process.env leaves the build-arg intact and the bundle blank.

hanzo.yml gains the images: block that declares the image and its build_secret.
hanzoai/ci is the only lane that implements build_secrets, reading KMS
deploy/<NAME> into --build-arg and failing closed on an empty value, so it is the
lane. The hand-rolled job in .hanzo/workflows/deploy.yml is now dispatch-only
rather than deleted: it cannot reach KMS, but deleting a host's only build lane
strands it with no failing run to show for it.

Tag shape changes with the lane. ci publishes sha-<sha7>-amd64, latest, and an
imgver semver derived as max(declared, published)+1 patch -- 5.7.7 next, with no
v prefix -- where the retired job published <short-sha>. universe
charts/app/values/hanzo/ui.yaml pins tag AND digest together and needs both.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 23:47:11 -07:00
hanzo-devandzooqueen dbda60e949 ui(Palette): the command's NAME is what must never collapse
Publish Packages / detect-changes (push) Successful in 12s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Successful in 2m30s
Hanzo CI/CD / cicd (push) Failing after 3m46s
CI/CD / cicd (push) Failing after 3m47s
Seen on the real 501-command catalog: a row lays out by distributing slack,
and the name and its summary were two siblings competing for it. The summary
won, so `deploy application-get` rendered as "ap…" beside a full sentence of
help — the one string being searched for, and the one that has to be read to
choose a row, was the string that disappeared.

They share one flexible cell now and only the HELP may shrink. The same
change fixes a second symptom that looked unrelated: a row with no help at
all pushed its name to the far right edge, because with nothing to absorb
the slack the gap went between the icon and the label.

`shrink`, not `flexShrink` — this package's text primitives take the
shorthand, and the long form does not typecheck.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 23:39:04 -07:00
hanzo-dev 757f40081d merge forge main
Publish Packages / detect-changes (push) Successful in 12s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Successful in 1m34s
deploy / image (push) Successful in 2m52s
Hanzo CI/CD / cicd (push) Failing after 13m6s
CI/CD / cicd (push) Failing after 13m6s
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 23:13:28 -07:00
hanzo-dev e4a60f6b37 event: the bearer wins over the key, not the other way round
A pk- names ONE org for everybody holding it; a bearer names a real principal
and resolves to THAT person's org. So the key is what attributes a visitor
nobody has vouched for, and it must never displace someone who has been.

Key-wins was survivable only while the key had to be passed in code. 0.3.11
also resolves it from the build env, which turned it into a hazard: setting one
variable silently blanks every signed-in user's token and re-files their events
under whichever org minted the key. On a console served to several brands from
one bundle — console.hanzo.ai, console.lux.cloud and console.zoo.cloud are the
same bytes behind the same service — that is a cross-tenant leak introduced by
an env var.

Anonymous traffic still rides the key, which is the whole point of having one.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 23:12:50 -07:00
zeekayandhanzo-dev 20005efbd7 ui: track design's softened light background in the token-ownership test
@hanzo/design moves its light page off pure white (#ffffff -> #f7f7f7) so a
light surface stops reading as a lightbox. This test asserts EQUALITY with
design's declared values rather than a contrast threshold, so design moving is
supposed to fail it — that is the test doing its job, and the expectation moves
WITH design rather than the assertion being loosened.

Lands before design@0.4.7 publishes, so ui's suite is never red against a
palette it has not acknowledged.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 23:03:04 -07:00
hanzo-dev bbdb1570f1 ui: Palette — the ⌘K bar collapses onto one component
Publish Packages / detect-changes (push) Successful in 12s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Skipped
Hanzo CI/CD / cicd (push) Failing after 6m31s
CI/CD / cicd (push) Failing after 6m32s
The follow-up CommandDialog's prop forwarding named: hanzo.app, console and
chat each carry a hand-rolled palette and a hand-written list of what can be
run, and the three lists already disagree with each other and with the API.

Palette is the component half. It composes CommandDialog — the shape that
commit made possible — so the hand-rolled Dialog-around-a-bare-Command does
not come back under a new name. Props in, callbacks out: it does not fetch
commands, does not run them, and does not know what a project is. `children`
is the right-hand slot, which is how hanzo.app keeps its project preview and
console keeps its `>` mode without either forking it.

The other half is cloud's GET /v1/commands — every operation the API answers,
projected from the one route table that already produces the REST routes, the
OpenAPI document, the MCP tools and the CLI. A surface passes that list in.

match.ts is the whole decision, as pure functions over plain values:

  Safe methods browse; unsafe methods must be named. Typing "delete" into a
  bar holding 2,323 operations offers four dozen destructive fleet operations
  to somebody who wanted to delete a project. GET matches fuzzily; every other
  method needs an exact prefix of `group label`. No second list and no
  curation — the method is already in the registry and already means this.

  An empty query hides every route. A fuzzy matcher with no query matches
  everything, and everything is 2,323 rows the moment the dialog opens. The
  Run group is a search, not a browse; what remains on open is the surface's
  own handful of local commands, which is what ⌘K-with-no-query wants anyway.

  A per-group cap, because one keystroke may still match hundreds.

A route command and a local command are ONE type: `method` is present exactly
when it names a route, and it is the only field the rule reads. That is what
lets a surface's own nav entries and 2,323 cloud operations sit in one bar
with one executor callback.

useCommandK moves in from hanzo.app unchanged — ⌘K always, `/` only when the
target is not editable. That second condition is why chat needs no exception
written for it: its composer is a TEXTAREA, so the global `/` never fires
there, and a `/` typed into the palette's own input is a literal slash.

Tested where each part can be: match.test.ts pins the rule without a DOM,
Palette.test.tsx mounts under the real GuiProvider (a build, a typecheck and
a pack all pass on a component that throws on first paint) including at the
2,400-op size that motivated the cap. The rows themselves are portalled and
so are out of static markup's reach — the same limit render.test.tsx records
for every dialog in this package.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 22:56:10 -07:00
zandClaude Fable 5 d219038dfd ui: the components report themselves, and one engine holds the root
Interaction analytics was app work. Every surface wired its own client and
instrumented its own call sites, so a click on the same Button was named three
ways or not at all. `<Hanzo analytics>` is now the whole wiring: one prop on the
root an app already mounts, and every click, change, submit and route change
inside the tree reaches the ONE front door named by the component it happened
on — `card/button[Save]` — with input values withheld.

A prop, not a default. Mounting a component library must never start a network
conversation the app did not ask for; off, no provider renders and no listener
is installed. On, it renders @hanzogui/telemetry's provider, which is where
consent already lives (GPC, DNT, the stored choice a banner writes). No second
client, no second stream, no second consent policy.

Component names are real in production. Every primitive already carries a
`data-slot` through one helper, so `componentName()` reads it — ranked ABOVE the
React fiber owner deliberately, because the fiber name is development-only and a
dashboard grouped on it empties silently at deploy. A named node keeps its
qualifier now (`button[Save]`, not `button`): a component name says what KIND of
thing it is, and a library renders hundreds of each.

It cannot double-count. The engine installs DELEGATED listeners on a root, so
two engines on one root report everything twice — which is exactly what an app
got by following two true sets of instructions at once, since a library provider
starts an engine and observe's own README told apps to mount another. The first
engine to start now claims its root and any later one stays inert. The claim
lives on the page under a `Symbol.for` registry rather than in module scope: two
copies of the package in one bundle have two module scopes and would not see
each other's claim, which is precisely when a duplicate is most likely.

Driving it in Chromium found what jsdom could not: the input debounce was ONE
slot shared across elements, so moving to the next field cleared the previous
field's pending timer and a filled form reported only the last field touched.
Keyed by field now.

hz.js could not authenticate at all. Through 0.3.11 it sent no `Authorization`
and no `?ingest_key=`, so every keyed static surface wrote unattributed, the
door refused it (401 ingest_key_required), and nothing in the page said so — the
tag measured fine in the browser and the surface was simply missing from the
warehouse. `data-ingest-key="pk-…"` now rides the header on fetch and the query
on a headerless unload beacon, the same pair the npm client uses. It also honors
Global Privacy Control and the stored `hz_consent` choice, not DNT alone, and
its stamped `libraryVersion` is pinned to the package version by a test — it had
drifted three patches, dating every static-site row to the wrong release.

The hz.js suite ran none of its tests: Node >= 21 ships a `navigator` whose
descriptor has no setter, so the harness's assignment threw. The one shipped
file with no bundler and no import-time typing had no executed coverage. It runs
again, and now asserts what reaches the wire — transport, URL, headers — not
only the batch.

@hanzo/event 0.3.12, @hanzo/observe 0.1.7, @hanzo/ui 8.0.58.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 22:55:07 -07:00
aandhanzo-dev 54ab0cdf42 fix(ui): the Switch was a 36x29 box in one colour, not a 20px pill with a state
Publish Packages / detect-changes (push) Successful in 10s
Hanzo CI/CD / cicd (push) Failing after 4m5s
CI/CD / cicd (push) Failing after 4m5s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Successful in 3m33s
Three defects, all measured on a live page, all invisible to a build, a
typecheck and a pack because the component rendered fine -- it rendered the
wrong thing.

GEOMETRY. gui's `size` variant returns { height, minHeight, width } for the
default `$true` size. Setting `height` here overrode two of the three, and
min-height beats height, so its 29px floor survived: every Switch in every app
was 36x29 rather than 36x20. At that ratio the browser clamps a 1000px radius
to 10px, so it was not even a pill -- a rounded rectangle with a 16px dot
adrift in the middle of it. `minHeight` is not redundant beside `height` here,
and that is the whole bug.

STATE. gui's checked treatment is `$backgroundActive`, which in this theme
resolves to the value the unchecked track already carries. Measured live: on
and off were pixel-identical -- rgb(36,36,36) track, rgb(204,204,204) thumb,
both states -- and differed only by the thumb's 14px of travel. A state carried
by position alone is one a screenshot, a narrow column and a low-vision reader
all fail to read. `activeStyle` is the hook gui honours for this; it is pulled
out of props before Tamagui can mistake it for the press pseudo-style, and it
replaces `$backgroundActive` rather than layering over it.

WEIGHT. The white is now spent on ON and nowhere else. A resting page is mostly
switches that are off, and a near-white thumb on each of them is a field of
lights with no signal in it, which is what a full page of these looked like.
Off is `$color3` under a `$color10` thumb; on is `$color12` under `$color1`.
Disabled is stated too -- it had no treatment at all, so a disabled switch sat
beside eight live ones with nothing to tell them apart.

One trap worth the paragraph it costs, because all three spellings differ. The
Thumb's activeStyle is typed as the SHORTHAND style set: `backgroundColor`
paints but is not in that type; `background` IS in the type and compiles to a
separate `_background-` class that races the base `_bg-` one on load order
rather than replacing it; `bg` replaces it. Consumers build with
ignoreBuildErrors, so the first would have shipped a type error nobody sees and
the second a colour that lands or does not depending on stylesheet order.

Five tests, each mutation-verified against the defect it covers: drop
`minHeight` and the pill test fails; drop `activeStyle` and the state test
fails; go back to `background` and both the state test and the single-class
guard fail.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 22:48:49 -07:00
zooqueenandhanzo-dev 68447e72ba ui: a component nobody can find is a component everybody rewrites
Publish Packages / detect-changes (push) Successful in 12s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Skipped
Hanzo CI/CD / cicd (push) Canceled after 1m4s
CI/CD / cicd (push) Canceled after 1m4s
Six surfaces hand-build chrome this package already ships. Three of the
five things added here existed — they were just unreachable.

CopyButton lived in chat/Code.tsx, so the only way to import it was
@hanzo/ui/chat, and nobody hunting for a copy button looks in a chat
module: ~30 hand-rolled copies across app, hanzo.ai, chat, console, pay
and billing. It moves to product/; Code imports it rather than owning it.

UserMenu was written inside AppHeader and could not be reached at all, so
five surfaces wrote their own account menu. Extracted whole — AppHeader
now renders it, so the header's menu and a standalone one cannot drift.

OrgSwitcher gains `direction` and `footer`. hanzo.app's local copy names
those two gaps in its own docblock as the reason it exists; that copy can
now go. OrgMark gains emoji marks and a broken-logo fallback — org logos
are tenant-supplied, so a dead URL is the normal case, not the rare one.

New because they were genuinely absent: Fieldset (the titled settings
group the Field* rows sit in — Panel is a dashboard tile, a different
job), SecretInput (mask · reveal · copy), and Pagination, whose `pages`
rule is pure and testable and whose fixed width the four hand-rolled
pagers each broke somewhere.

And one defect the mount tests found: gui DROPS `secureTextEntry` on web.
<FieldText secure> has therefore been rendering API keys in plain text in
every browser since it shipped. `masked()` sets the native and the web
spelling together; both call sites use it and a test locks it.

279 tests (was 253).

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 22:36:51 -07:00
zooqueenandhanzo-dev f6cef0bc04 ui: one focus ring means every theme, not just the two at the root
Publish Packages / detect-changes (push) Successful in 18s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Skipped
Hanzo CI/CD / cicd (push) Failing after 4m24s
CI/CD / cicd (push) Failing after 4m25s
8.0.54 re-based `outlineColor` on `dark` and `light`, measured hanzo.app, and
found the three controls the audit actually named — Sign In, Get started,
Search — still ringing at 1.4:1. gui activates a `Button` sub-theme for every
Button it renders, so those three read `dark_Button`, which the root-only fix
never touched. The page's own `--outlineColor` was already design's; the
buttons standing on it were not.

So the ring is re-based across all 390 themes. The ramp ships 21 distinct ones,
twenty of them hues — a pale blue on `dark_blue_Button`, a pale pink on
`dark_pink` — and every one fails 3:1 on a near-black canvas the same way the
grey did. A sub-theme exists to hold different COLOURS; a focus ring is not a
colour choice, it is a contrast requirement, and this system has exactly one.

The edge and the label stay on the two root themes, deliberately: `dark_accent`
really should label in the accent's colour and `dark_red` really should edge in
red, so spreading those two everywhere would flatten 388 themes and stop being
a re-base. `light_*` sub-themes take the light fallback, `dark_*` the dark one.

24 files / 254 tests, vite and webpack consumer suites green.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 22:29:25 -07:00
zooqueenandhanzo-dev be3f3a7010 ui: the focus ring is a contrast requirement, not a shade of grey
Publish Packages / detect-changes (push) Successful in 12s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Skipped
Hanzo CI/CD / cicd (push) Failing after 3m55s
CI/CD / cicd (push) Failing after 3m56s
`$outlineColor` shipped `hsla(0, 0%, 27%, 0.6)` from the same upstream ramp the
last commit re-based two rungs of. Composited over @hanzo/design's ground it is
rgb(45,45,45) — 1.44:1, measured. WCAG 2.4.11 asks 3:1. On hanzo.app that ring
is what Sign In, Get started and Search draw when a keyboard reaches them, so
the three primary CTAs had no visible focus state at all.

Nothing about the number says so, and that is the point: on a white page the
same grey clears 3:1 comfortably. It is a value inherited from a light-first
substrate, spent on a dark-first product. A ramp cannot know which canvas it
will land on; a token can, which is why design publishes --ring as translucent
white — it lifts with whatever surface is under it. Over the ground it lands at
3.77:1.

So the ring joins the edge and the label: `var(--ring, …)` with design's own
literal behind it, per theme. Fixed once in the theme rather than on each
control — hanzo.app reaches this config through `lib/gui.ts`, which is a
one-line re-export of it, so every gui component on the surface moves together.

The test computes the ratio rather than matching the string, and measures the
old value beside the new one in both themes: dark fails, light passes, which is
the whole shape of the bug and proof the check can fail.

24 files / 253 tests, vite and webpack consumer suites green.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 22:13:40 -07:00
zooqueenandhanzo-dev e8bbdbe73b ui: the scrim gets the weight its neighbours have, and two rungs read design
Two fixes with one cause: a value that was correct in this package and lost
before it reached a pixel.

THE SCRIM. `[data-slot="dialog-overlay"]` set `opacity: 1` and an 80% ground
and carried no `!important`, while every material rule beside it does. gui
compiles the overlay's own opacity and ground into atomic classes (`_o-0--5`,
`_bg-rgba0000--538295333`) that land in an inline <style> a bundler orders
AFTER this sheet — equal specificity, later source, so they won. Measured on
hanzo.app's ⌘K palette: `rgba(0,0,0,.5)` under a further `opacity: .5`, an
effective quarter black, which is the exact double dim this rule was written to
end. It shipped that way for a whole release because the rule was present and
reading it told you nothing. A rule written to beat a compiled class has to be
written with the weight to beat one.

THE TWO RUNGS. `$color1..$color12` is a generic monotonic ramp inherited from
upstream `@hanzogui/themes`, and on two rungs it undid a decision @hanzo/design
had already made in writing:

  $borderColor (= $color4) was `hsla(0, 0%, 14%, 1)` — a SOLID edge, on Button,
  Input, Card, Select, Dialog, Popover, Tooltip, Switch, Checkbox and
  DropdownMenu at once. design's colors.css spends a paragraph refusing exactly
  that: a solid hex hairline stops being a lighter LINE the moment it lands on
  a lifted surface and becomes an unrelated grey.

  $color12 was `hsla(0, 0%, 100%, 1)` — PURE WHITE, and it is the label colour
  for Button default/primary, every Badge and the `accent` recipe, the one loud
  control a page is allowed. design sets --foreground to #fafafa because pure
  white halates on near-black. The ramp reintroduced the halation the token was
  authored to avoid.

Both now read the token: `var(--border, …)` / `var(--foreground, …)`, design's
published literal behind each so a host with neither sheet still gets a value
instead of a dropped declaration. The rest of the ramp is left alone — greys in
a scale of greys, which design has no opinion about. Sub-themes keep their own.

The literals are stated per theme, and compose-theme now teaches design's light
block a bare `.t_light` beside `:root.t_light`, because a NESTED
`<Theme name="light">` — PrimaryButton's white pill inside a dark app — emits
that class on a span, which `:root.t_light` cannot match. Without it a light
island stood on dark tokens, and a --foreground label on a white pill would
have come out white on white. glass.css's own light block gets the same alias
so the two do not disagree about what "light" means.

gui-config.test.ts reads @hanzo/design's stylesheet and fails by rung and by
theme if either column stops matching what design publishes, so the copy cannot
drift; glass.test.ts pins both scrim declarations.

24 files / 250 tests, vite and webpack consumer suites, gallery rendered in
both themes before and after: the scrim darkens to 80%, nothing else moves.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 22:07:26 -07:00
zandhanzo-dev 64e1650210 Point every Discord invite at the current Hanzo server (discord.gg/CJCyAsm9Vr)
deploy / image (push) Successful in 3m46s
Hanzo CI/CD / cicd (push) Failing after 5m14s
CI/CD / cicd (push) Failing after 5m15s
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 21:49:45 -07:00
zooqueenandhanzo-dev 97c9f68326 ui(glass): the ladder owns its drops — a name you don't own is a value someone else sets
Publish Packages / detect-changes (push) Successful in 11s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Skipped
deploy / image (push) Successful in 3m13s
Hanzo CI/CD / cicd (push) Failing after 4m12s
CI/CD / cicd (push) Failing after 4m12s
Rungs 1 and 2 read `var(--shadow-sm|--shadow-lg)` and rung 3 `--shadow-floating`
— @hanzo/design's names, reached with design's own values as fallbacks, checked
against design's sheet so the copy could not drift. All of that was true and
none of it helped, because the ramp is not design's alone: @hanzo/brand declares
--shadow-sm/md/lg/xl too, at :root, tuned for a WHITE canvas — .05 and .1 where
design says .40 and .55. Same names, same specificity, so the winner is whichever
sheet the bundler ordered last, and it was brand. A declared variable also beats
a var() fallback outright, so the mirror could not rescue the rung either.

On #080808 those drops resolve to a shadow you cannot see. The lit edge still
drew, so nothing looked broken — the ladder just went FLAT, rungs 1 and 2 reading
alike, which is the one thing a ladder may not do. hanzo.app had been carrying an
`html:root` block restating both tokens to get its depth back; that workaround
goes now.

Three values are three names, so the ladder declares --glass-shadow-1/2/3 itself.
Nothing else ships a --glass-shadow-*, so no sheet can outrank them and no load
order can change the answer. A theme that wants a different ladder declares these
three and gets one — a hook, where reading the ramp was an accident.

Both themes are stated, and that is not extra. Design tunes the ramp per canvas
(.06/.09/.18 in `.light`) because on white the drop does all the work and must be
light enough not to smudge. Renaming with only the dark fallback would have fixed
dark and put a 6x-too-heavy drop on white — trading one flat ladder for one
smudged one, against this file's own promise of "one composition, both themes".
The values are design's, in both columns, and the test fails if either moves.

Rung 3 is renamed with the other two even though --shadow-floating collides with
nothing today. The ladder is one concept and re-tuning it should mean touching
one namespace; and the defect class is "reads a name it does not own", which
fixing two rungs of three leaves alive.

.paper in motion.css had the identical bug and lost its drop the same way. It
keeps a fallback because that sheet ships on its own, and the test pins the
fallback to the rung so the two cannot disagree.

The test that pinned the fallbacks by name would have gone stale silently, since
`--shadow-sm` simply stops appearing. Replaced with the law that matters: no
sheet may read the size ramp again, by any spelling. Role names — --edge-highlight,
--surface-scrim, --shadow-inset-hairline — are coined for one job by one package
and stay. Verified to bite: restoring the old `var(--shadow-lg, …)` fails it.

--edge-highlight stays design's on purpose. It is uncollided, and design's
zeroing of it in `.light` is the half of the composition that lets one rule work
on both canvases.

243 tests pass, 27 in glass.test.ts.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 20:29:47 -07:00
zeekayandhanzo-dev 09c192b415 ui: Grid gains a column cap, and this package stops splitting the gui train
Grid `max` — the missing half of `min`. A single auto-fill floor cannot say
"2-up on a phone, 4-up on a desktop": 2-up at 390px needs a ~170px floor, and
that same floor yields SIX columns at 1280. `max` raises the floor to one-Mth of
the row so auto-fill cannot fit an (M+1)th track, and below that width the max()
picks the min again and the grid wraps normally — so the cap costs nothing on
small screens. That is why it is expressed as a floor and not a breakpoint.
Proven: remove the cap and 1280px comes back FIVE columns.

@hanzogui/* ranges move to the 8.1 train. The @hanzogui packages pin each other
EXACTLY (toast@8.0.0 -> core@8.0.0, no caret), so one stale range here dragged a
whole second generation in beside a consumer's. Both typecheck, both build, then
the config singleton lives in one copy and the components read the other, and
the app dies on prerender with `Missing theme.` — hanzo.ai hit that and had to
state the invariant by hand in a pnpm.overrides block. The invariant belongs in
the package that caused it.

The whole train moved together, including pkgs/canvas and pkgs/dashboard:
bumping this package alone SPLIT the monorepo the same way, and the ~30 "prop
does not exist" errors that produced were the split wearing a mask, not an API
change.

The consumer harness now asserts ONE copy of @hanzogui/web in the installed app,
before any test runs. Mutation-proven the honest way: the first attempt "passed"
because `pnpm pack` had failed and the check never ran — the real proof pins an
older @hanzogui/web in the consumer and watches the diagnostic name both
versions.

NOT changed: `gui-config.d.ts` ending `}, "default">`. That was reported as a
latent declaration defect; it is not one. `InferGuiConfig`'s eighth parameter IS
`ExtractAnimationDriverKeys<E>`, so `"default"` is the correct resolution for a
config whose animations object has one driver named `default`. An honest lib
check (skipLibCheck off) reports zero errors in that file. Changing it would
have made it wrong.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 20:22:35 -07:00
zeekayandhanzo-dev 055d43a44e ui: CommandDialog forwards the palette's props — the ⌘K blocker
Publish Packages / detect-changes (push) Successful in 15s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Skipped
Hanzo CI/CD / cicd (push) Failing after 4m56s
CI/CD / cicd (push) Failing after 4m56s
CommandDialog rendered `<Command>` bare, so none of the palette's own props
reached it. A host could not read the highlighted row, which makes a two-pane
palette — list left, preview of the highlighted item right — impossible with the
stock component. hanzo.app hit exactly that and rebuilt the dialog by hand
around the bare `Command` primitive; its file still carries the reason:

  "Composed from the @hanzo/ui `Command` primitive inside a wide `Dialog`
   (rather than the stock `CommandDialog`, which doesn't forward
   `onValueChange` — needed to drive the preview panel from the highlighted
   row)."

So the whole palette surface comes through, not just the one prop that was
asked for: value, defaultValue, onValueChange, filter, shouldFilter, loop,
label, vimBindings, disablePointerSelection. Forwarding only onValueChange
would mean the next host needing `loop` files the same bug again.

`value`/`onValueChange` are the SELECTED ITEM, matching Command's own names and
cmdk's before it. The SEARCH string stays CommandInput's `value` — a different
prop on a different component, as it always was.

Mutation-proven in a real browser: the gallery renders an open CommandDialog
driving a host-owned readout from the highlighted row, and typing must narrow
the list. Restore the bare `<Command>` and it fails with "onValueChange never
reached the host".

This only makes the collapse POSSIBLE; the four hand-rolled palettes are a
follow-up, not this commit.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 19:51:55 -07:00
zeekayandhanzo-dev 3d3a14def2 ui: design owns --background/--black/--white — gui stops shadowing them
Publish Packages / detect-changes (push) Successful in 12s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Skipped
Hanzo CI/CD / cicd (push) Failing after 4m9s
CI/CD / cicd (push) Failing after 4m10s
gui redeclared three of design's token names and won, so every app that wires
GuiProvider's theme class onto <html> got gui's palette instead of design's.
Two mechanisms, and fixing only the loud one leaves the page just as wrong:

  :root.t_dark / :root.t_light  (0,2,0) — beats design's :root on SPECIFICITY
  :root { --background: var(--t1) }  (0,1,0) — TIES design and wins on SOURCE
    ORDER, because gui's block is appended after design's

The second is why the acceptance test still read gui's grey after the first
pass. Both are stripped now, at generation time, from the ROOT theme blocks
only.

Sub-themes are untouched, and that is why this is a parser and not a regex:
`.t_accent`, `.t_blue_Button` and 246 others legitimately scope their own
background — that is what a nested theme IS. Only an exact `:root`,
`:root.t_dark` or `:root.t_light` is the root theme.

Also aliases design's light block to gui's spelling. design is `:root` (dark)
with `.light`; gui emits `t_dark`/`t_light`. They never had to agree while gui
was declaring its own --background — it simply won. Now that design owns those
names, an app carrying only gui's `t_light` would get design's DARK palette,
because nothing matches `.light`. So design's light selector also answers to
`:root.t_light`: one added selector, never a second copy of the values.

This retires the two interim workarounds in the field. Both were re-imports of
design's colors.css placed LAST, which cannot beat (0,2,0) — they only appear
to work where the theme class never reaches <html>, and revert the day someone
wires themes correctly. A fix that expires on being fixed.

Acceptance test asserts EQUALITY with design's declared value per theme, not a
contrast threshold: the measured drift was dark grounded at gui's #141414
instead of design's #0a0a0a, which passes any contrast gate and still reads as
"why is our black slightly grey". Mutation-proven — restore the shadow and
--background under t_dark comes back gui's, not #0a0a0a.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 19:28:14 -07:00
zooqueenandhanzo-dev f67e6e5a32 ci: state the interface the consumer app is served on, and keep what vite says
Hanzo CI/CD / cicd (push) Failing after 4m5s
CI/CD / cicd (push) Failing after 4m6s
The gate has never gone green, and both reasons were the harness rather than the
package.

`vite preview` was left to pick its own interface, and which one it picks
differs by machine: on macOS it bound [::1] and 127.0.0.1 refused; on the Linux
runner it did the reverse and `localhost` was the address that never answered.
The poll therefore counted to 60 and declared the app broken AFTER the pack, the
install and the vite build had all succeeded — a message that reads like a
packaging defect and is the harness looking somewhere the server is not.
Swapping one literal for the other only moved which platform it failed on; this
names `--host 127.0.0.1` so the server, the poll and playwright's baseURL are
the same address by construction, everywhere.

`stdio: 'ignore'` is the second half, and it is why the first half took three CI
runs to find. A preview that died on startup and one that was merely slow
produced the identical message thirty seconds apart, on a runner nobody can
attach to, while vite had printed the reason on the very first run and the
harness threw it away. Its output is captured and reported with the failure now,
and an exited process fails immediately instead of waiting out the full timeout
for an answer that cannot come.

Locally: 23/23, exit 0. The 19 non-screenshot assertions are the evidence — the
four screenshots compared against baselines playwright had recorded on the
previous failing run, which proves nothing, and those darwin PNGs are deleted
rather than committed. Baselines here are -chromium-linux and belong to the
runner; a macOS rendering in that set would mask exactly what it exists to
catch.

Nothing ships from here — this package publishes `dist` only — so no version
moves.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 18:56:58 -07:00
zooqueenandhanzo-dev 64f1c8a30c ci: the consumer gate polled an address vite never bound
Hanzo CI/CD / cicd (push) Failing after 5m20s
CI/CD / cicd (push) Failing after 5m21s
`vite preview` binds IPv6 only. It prints "Local: http://localhost:4390/" and
answers on [::1]; 127.0.0.1 refuses the connection outright. The harness polled
`http://127.0.0.1:4390`, so it counted to 60 and threw

    Error: consumer app never came up on http://127.0.0.1:4390

on every run — AFTER the pack, the install and the vite build had all succeeded.
That reads like the package is broken and it is the harness looking at the wrong
address. Measured both ways here: 127.0.0.1 -> connection refused, localhost and
[::1] -> 200, on a dist/ containing one hand-written index.html and no @hanzo/ui
at all.

The same literal made the "someone else is already on this port" guard inert —
its probe could not connect either, so the one thing it exists to catch could
never be caught.

playwright.config.ts carried the same address as its baseURL, so it is the same
fix in both places.

With this, the suite reaches the app and 19 of its 23 assertions pass locally;
the remaining 4 are the screenshots, whose committed baselines are
-chromium-linux and cannot match a darwin run. Nothing is shipped from here —
this package publishes `dist` only — so no version moves.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 18:48:01 -07:00
zooqueenandhanzo-dev f5a9140d92 release(ui): 8.0.48 — and test goes back to meaning the package's own suite
Publish Packages / detect-changes (push) Successful in 15s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Successful in 3m2s
Hanzo CI/CD / cicd (push) Failing after 5m47s
CI/CD / cicd (push) Failing after 5m47s
publish.yml runs `run --if-present test` for every package it ships, and it has
no browser. @hanzo/ui's `test` had grown into vitest + the Vite consumer + the
webpack consumer — the two that pack a tarball, npm-install it into a throwaway
app outside the repo and drive it in chromium. Real gates, and hanzo.yml already
runs both in its own `ui-consumer` job next to the `playwright install` that
gives them a browser. In the publish job there is no such step, so the last gate
before an immutable version could only fail.

That is the same shape this repo has paid for twice already and named both
times: a gate that cannot go green. It is why 8.0.44 through 8.0.47 were hand
publishes.

`test` -> `pnpm run test:unit`, one definition rather than a second copy of
`vitest run`. hanzo.yml is untouched: it already calls test:unit, test:consumer
and test:consumer:webpack by name, so the split it describes is unchanged and
nothing it gates is lost.

241/241 green; the webpack consumer passes against the packed 8.0.48 tarball.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 18:36:26 -07:00
zooqueenandhanzo-dev 1a03b803e9 ui(glass): the chrome material is a package, not a file each app rewrites
The console had eight recipes in lib/chrome.ts and ~120 lines of globals.css
holding the whole law of Hanzo chrome — what a floating surface is made of,
what a resting one is, what dims the page under a modal, how rows are grouped,
which item is current, which control is loud. Every one of them was earned by a
measured defect, and every app that wants the look either imports it from here
or re-derives it and gets a different answer.

`@hanzo/ui/glass` — glass(2|3), scrim, panel, rows, row, selected, accent,
screen — as plain objects that spread onto any @hanzo/gui element. A recipe
rather than a variant because a variant only reaches the one component that
declares it, and half the loud controls in a real app are an XStack, a
SizableText or a Link.

`@hanzo/ui/glass.css` — the material, the ladder, the scrim and the row
separators. Its own entry point AND inlined into theme.css by compose-theme, so
a host that already has the token layer can take the material without it, and
the two can never say different things about what glass is.

Every value is @hanzo/design's, reached by name: --edge-highlight, --shadow-sm/
lg/floating, --surface-scrim, --background, --border. The fallbacks are design's
own, present so a host without design's sheet gets a ladder instead of a
silently-dropped declaration — and glass.test.ts reads @hanzo/design/styles.css
and fails if any of them stops matching, so the copy cannot drift.

Three things this decomplects on the way through:

  - The ladder had four rungs reading `var(--hz-elevation-N, <hardcoded>)`.
    Nothing here, in @hanzo/design, or in any consumer ever defined an
    --hz-elevation-*; every rung resolved to its fallback. Same for .paper's
    --hz-ring and --hz-paper-highlight. An indirection through a name that does
    not exist, reading like a theming hook. All three retired for the real ones.
  - Depth is now the lit edge PLUS the drop, at every rung. On a near-black
    canvas a cast shadow is nearly free of information — black on near-black
    moves no pixels — and the top lip catching light is what actually says one
    surface is above another. design zeroes --edge-highlight in .light, where a
    white line on a white card is nothing, so one composition serves both themes.
  - Glass was described twice and the two disagreed: the component's stand-in
    ground was $color2 while the material is 72% of --background, so a browser
    that could blur and one that could not showed two different colours of menu.
    <Glass> is now the component form of glass(level).

SlideOver moves from the retired 4th rung to 3, where a drawer belongs: no
anchor, a scrim of its own, floats free exactly the way a modal does.

25 new law tests, 241/241 green, build clean.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 18:35:30 -07:00
zeekayandhanzo-dev 6280c947bd Merge branch 'converge/8x-no-tailwind'
Publish Packages / detect-changes (push) Successful in 10s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Skipped
deploy / image (push) Successful in 3m23s
Hanzo CI/CD / cicd (push) Canceled after 4m28s
CI/CD / cicd (push) Canceled after 4m28s
# Conflicts:
#	hanzo.yml

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 18:18:23 -07:00
zeekayandhanzo-dev 7f77ed2a2c ui: font delivery has one owner — @hanzo/design. Fixes the 8.0.46 webpack break
8.0.46 flattened @hanzo/design's stylesheet into ours and inherited its
@font-face rules, whose `url(./assets/fonts/Geist-Variable.woff2)` is relative
to DESIGN's file. Composed into ours it resolves against @hanzo/ui/dist, which
ships no assets. webpack's css-loader resolves url() and fails the build:

  Module not found: Can't resolve './assets/fonts/Geist-Variable.woff2'
  in node_modules/@hanzo/ui/dist

Vite leaves an unresolvable url() alone, so all 23 consumer tests stayed green
and hanzo.app found it instead. design's own sheet warns about exactly this —
"the url()s are relative to THIS file" — and I inlined it anyway.

Fix is (b): design owns font delivery. Copying the .woff2 files into this
package would fix the build and recreate the real problem — two packages
shipping the same font, one fact in two homes, the thing retiring @hanzo/tokens
was about. So the @font-face rules are dropped from the composed sheet. This
package NAMES the families, which is all it ever claimed to do (gui-config: "the
host self-hosts both faces — this only names them"), and a consumer wanting them
self-hosted imports @hanzo/design/styles.css, where the files actually are.

Two defenses, both mutation-proven:
  · compose-theme.mjs refuses to write a sheet containing ANY url() — this
    package ships only "dist", so a relative asset reference is always a lie.
    Proven: keep the @font-face and the build stops with both filenames.
  · a webpack + css-loader consumer now runs beside the Vite one, installing the
    same packed tarball. Proven BOTH directions — re-introduce the bad url and
    webpack fails with the exact hanzo.app error while Vite still passes 23/23.
    That second half is the point: the Vite-only harness was blind to the whole
    class, and now something is not.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 17:52:14 -07:00
zeekayandhanzo-dev 043162e0f1 ui: @hanzo/design is the token source — retire the @hanzo/tokens pin
@hanzo/ui pinned @hanzo/tokens@1.0.0, last touched 2026-03-22, while
@hanzo/design@0.4.6 — "single source of truth for every Hanzo surface" — is what
the design lane and several surfaces already consume. store/billing/industries
followed design; the convergence branches followed the stale package; pushing
either over the other reverted real work. One fact, two homes.

Measured before deciding: theme.css declared 34 custom properties, 19 of which
design already publishes, character for character the same job. design ships 244
— including `--border-focus`, `--border-selected`, `--destructive-hover` that we
never had. It was not a different layer. It was a copy.

So the 19 are gone and design is composed in at BUILD time
(scripts/compose-theme.mjs reads the installed package), never copy-pasted into
source — a copy is how the fork reappears inside the package. What remains in
src/theme.css is only what design does not ship: the chart ramp, the sidebar
set, the Geist bindings, and this package's own rules. Same for the JS side:
@hanzo/ui/tokens re-exports @hanzo/design, so code and stylesheet read one layer.

Flattened, not `@import '@hanzo/design/styles.css'`, for the reason design's own
entry point gives: a bare specifier is not browser-resolvable, and a nested
@import must precede all other rules or it is dropped silently.

The hsl() trap from store's decision doc is now a build-time check, not a memory:
design publishes FINISHED colours, so `hsl(var(--x))` is invalid at
computed-value time and the browser drops the whole declaration without a word.
compose-theme.mjs refuses to write a sheet containing one. Audited: neither
package had any. Mutation-proven — adding `hsl(var(--background))` fails the
build.

`./core` and `./tokens` become ESM-only: @hanzo/design is `"type": "module"`
with no `require` condition, so a CJS build of those subpaths cannot load it.
Better a clear resolution error than a crash deep inside design at run time. The
MAIN barrel is untouched — it pulls ./core/cn directly. Zero consumers import
either subpath (checked across nine repos), so nothing breaks today.

216 unit tests, 23 browser tests. The palette shifts to design's finished values;
baselines refreshed, and "every border is a hairline" plus "components are
actually styled" both still pass, so the shift is the adoption and not a loss.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 17:23:15 -07:00
hanzo-dev 817ef91839 merge main
Publish Packages / detect-changes (push) Successful in 15s
Hanzo CI/CD / cicd (push) Successful in 20s
CI/CD / cicd (push) Successful in 21s
deploy / image (push) Successful in 3m21s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Successful in 1m21s
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 17:06:20 -07:00
hanzo-dev 9783c095f1 event: the key env is the one name the fleet already carries
0.3.10 read NEXT_PUBLIC_HANZO_EVENT_KEY, a fourth spelling of a value that
already had three: KMS holds deploy/EVENT_INGEST_KEY, every Dockerfile takes
EVENT_INGEST_KEY as a build-arg, and re-exports NEXT_PUBLIC_EVENT_INGEST_KEY
for Next to inline. Read that one.

Also stamps VERSION, which is hand-maintained beside package.json: 0.3.10
shipped reporting 0.3.9 as its libraryVersion, so its rows were
indistinguishable from the previous release's. The version test caught it.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 17:06:05 -07:00
zeekayandhanzo-dev 01ac46dce4 ui: layout primitives — Grid, Section, Card.Media, and a Button that cannot clip
Five defects shipped to production this week and every one was invisible to the
type checker and to the build. They are all one shape: a box whose size is
decided by the wrong party.

Grid — real CSS grid, tracks declared by the CONTAINER.
  repeat(auto-fill, minmax(min(Npx, 100%), 1fr)) — responsive with zero
  breakpoint props, and the min() is what stops a 900px track from overflowing a
  390px phone. Fixed counts use minmax(0, 1fr), never a bare 1fr, whose implicit
  `auto` floor lets one long child widen its own column. Replaces hand-rolled
  width="calc(25% - 7.5px)", which is even only while every child agrees.

AspectRatio — the box has a height BEFORE its content loads, so media is never
  zero-height and nothing below it shifts. Child img/video fill it via a rule
  keyed on `data-ratio`, stamped after the props spread so a wrapper renaming
  data-slot cannot silently unhook it — Card.Media renames it, and targeting
  data-slot is exactly how the rule stopped matching in the first draft.

Card — a surface, not a control. Sizes from content. Adds Card.Media, and an
  `interactive` prop that puts role/tabIndex/Enter/Space on the surface itself,
  so nobody wraps a card in a Button to make it clickable.

Section — page rhythm from the space scale, in one place, mobile-aware.

Button — `height` becomes `height:'auto'` + `minHeight`. BOTH halves matter:
  minHeight alone lets GuiButton.Frame supply its own height (measured 44px),
  which re-pins the box AND makes every ordinary button 8px taller. With both,
  an ordinary Button still measures exactly 36 and an oversized child makes it
  grow instead of being cropped to a sliver.

fonts.mono — `$mono` was never defined; gui emits NO class for an unknown font
  token, so 260 fontFamily="$mono" call sites across 74 files in hanzo.app were
  dead silently. Also declared on the ambient type, which is derived from
  defaultConfig and so could not see it.

Mutation-proven, not asserted. Each guard was broken and watched to fail:
  · remove the fill rule      -> object-fit reads "fill"
  · minmax(min(N,100%),1fr)
    -> minmax(N,1fr)          -> the page scrolls sideways at 390px
  · remove fonts.mono         -> $mono resolves to the sans face
  · height:auto+minHeight
    -> height:                -> a 119px child renders in a 36px button
Reported honestly: minmax(0,1fr) and `min-width:0` are REDUNDANT — each alone
holds the row even, so neither mutation fails on its own. Only disabling both
does. That is a property of the design, not a gap in the test.

23 browser tests, 216 unit tests.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 17:04:31 -07:00
zeekayandhanzo-dev 8c14458be2 Delete ci.yml — the canonical lane now gates pkg/ui
Hanzo CI/CD / cicd (push) Canceled after 0s
CI/CD / cicd (push) Canceled after 0s
Superseded by hanzo.yml + .hanzo/workflows/cicd.yml, which gate the package
this repo publishes and were observed green before this commit was pushed.

Nothing is lost with it. Of ci.yml's four jobs, Lint was the only one that
could pass; Build and Test both ran `cd pkgs/ui`, a directory that is not in
this tree, and Type Check died on ~10 TS7016s in a stale registry app resolving
`@hanzo/ui` to a published package with no declarations.

deploy.yml stays: it builds ghcr.io/hanzoai/ui and is green, and a repo should
never be left without a delivery path. registries.yml stays too — it is a
narrow validator for two apps/v4 JSON files that still exist, scoped by path so
it costs nothing on a normal push.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 17:03:27 -07:00
zeekayandhanzo-dev 6ab0225e3c Two publishes that worked reported failure, and one never ran at all
Run 26906 shows four matrix legs. @hanzo/ui passed end to end. The other three
are three separate defects, none of them in the packages:

@hanzo/og and @hanzo/shop — `pnpm publish` printed npm's own success line,
  `+ @hanzo/og@1.0.0`, and the very next command said
  `::error::@hanzo/og@1.0.0 is not on npmjs after publish`. Both are on the
  registry now. npmjs is read-after-write eventually consistent and this asked
  it exactly once, about two seconds after the write. So retry — six times over
  a minute — and fail only if it never appears.

  A false red on a publish is worse than a slow green: it reports that a release
  did not happen when it did, so the next person bumps the version to "fix" it
  and burns a number over a replication lag.

@hanzo/canvas — `bun test`, on a runner with no bun: `spawn ENOENT`. The step
  that exists to stop an untested release was the thing stopping the release,
  and canvas has sat at 0.2.2 here against 0.2.1 on npmjs ever since. Install
  bun, but only when the package's own manifest asks for it, and from npmjs
  rather than bun.sh/install — this job already reaches that registry, and the
  shell installer wants unzip a minimal runner image need not carry.

Also add workflow_dispatch. A publish that failed for an infrastructure reason
could previously only be retried by pushing another commit that touched a
package.json — burning a version number to re-run a job that was never wrong
about the code. detect-changes reads what npmjs SERVES, so a dispatch publishes
exactly the packages that are ahead of the registry and nothing when none are.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 17:03:27 -07:00
hanzo-dev db00800fcc event: the ingest key resolves from the build env, like the DSN
Hanzo CI/CD / cicd (push) Canceled after 0s
CI/CD / cicd (push) Canceled after 0s
CI / Build (push) Failing after 1m43s
CI / Type Check (push) Failing after 2m25s
Publish Packages / detect-changes (push) Successful in 13s
CI / Test (push) Failing after 1m49s
CI / Lint (push) Successful in 3m11s
deploy / image (push) Successful in 3m53s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Failing after 56s
CI / CI Status (push) Failing after 1s
A surface declares its error plane with one inlined variable and its event
plane with none: ingestKey could only be passed in code, so a surface that
did not pass it sent every beacon unattributed.

The door refuses an unattributable write (401 ingest_key_required) rather
than filing it under a tenant its owner cannot read. That is the right
refusal, but it is silent in the page — the only symptom is the host
missing from the warehouse. Reading the key from NEXT_PUBLIC_HANZO_EVENT_KEY
(then HANZO_EVENT_KEY) puts it exactly where the DSN already lives.

Explicit config still wins. Without a key a surface still reports for
whoever is signed in, and drops everyone who is not.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 17:00:07 -07:00
zeekayandhanzo-dev 8c7d75305b CI ran on a directory this repo does not have
Hanzo CI/CD / cicd (push) Successful in 22s
CI/CD / cicd (push) Successful in 22s
CI / Build (push) Failing after 4m2s
CI / Test (push) Failing after 1m43s
CI / Lint (push) Successful in 4m5s
CI / CI Status (push) Canceled after 0s
CI / Type Check (push) Failing after 1m58s
ci.yml has four jobs and three of them could not pass at any commit:

  Build   cd pkgs/ui && pnpm run build:full
  Test    cd pkgs/ui && pnpm test:coverage
          -> both: `cd: pkgs/ui: No such file or directory`. The package is
             pkg/ui, singular. It moved; these two steps did not follow.
  Type    cd app && pnpm run typecheck
          -> ~10x TS7016 `Could not find a declaration file for module
             '@hanzo/ui/animation/*'`. app/ aliases
             "@hanzo/ui": "npm:@hanzo/ui-shadcn@^5", so those imports resolve to
             a published package that ships no declarations. Nothing in this
             tree can fix it.

So the red said nothing about this repo, and a gate that cannot go green stops
being read. Meanwhile the only thing that DID test pkg/ui was publish.yml —
after the version was already immutable.

Move the gate onto the canonical lane: root hanzo.yml holds the config,
.hanzo/workflows/cicd.yml is a 7-line caller into hanzoai/ci. The gate is the
three commands publish.yml already proves green on this runner (21 files, 212
tests), run before a release instead of during one.

.hanzo/workflows, not .github/workflows: CI here runs on git.hanzo.ai, which
resolves only the former, and github.com has no runner for the hanzo-build-*
labels — the same file there would queue forever.

ci.yml stays until this is observed green.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 16:54:59 -07:00
hanzo-dev 5c34a0c2db release(ui): 8.0.44 — the rename lands on top of main's Button restyle
8.0.42 (the rename) was published minutes AFTER someone else published 8.0.43,
and npm points `latest` at the most RECENTLY published version, not the highest
semver — so `latest` went backwards to 8.0.42 and consumers stopped seeing
8.0.43's Button work. 8.0.44 carries both and puts `latest` back on top.

theme.css took main's rewrite wholesale (dark is the default there now); the
only thing re-applied on top was `.hz-elevation-N` -> `.elevation-N`. The
`--hz-elevation-N` custom properties it reads are untouched.

Screenshot baselines refreshed. The four comparisons failed after the merge and
the diff mask says exactly why: only the `default` and `primary` buttons moved,
which is precisely what main's "primary is the macOS dark pushbutton" and "the
unmarked Button is quiet" did. destructive/outline/secondary/ghost/link, the
badges, the card and the inputs are pixel-identical. The rename on its own was
pixel-identical across all 12 tests before this merge, so the restyle is the
whole delta and the baselines were simply never refreshed when it shipped.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 16:25:03 -07:00
hanzo-dev 8f5f70b5f0 Merge remote-tracking branch 'origin/main' into converge/8x-no-tailwind
# Conflicts:
#	pkg/ui/package.json
#	pkg/ui/src/theme.css

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 16:21:14 -07:00
hanzo-dev 8ee117c900 ui: class handles drop the brand — hanzo-button -> btn, hz-mono -> mono
The stable handles a host selects on were branded and inconsistent: two
prefixes (`hanzo-`, `hz-`), a BEM double hyphen, and compound names. They are
now bare single words the way a utility framework names things, with variants
on a single hyphen.

  hanzo-button      -> btn          hz-mono       -> mono
  hanzo-button--*   -> btn-*        hz-tnum       -> tnum
  hanzo-badge       -> badge        hz-row        -> row
  hanzo-badge--*    -> badge-*      hz-row-in     -> row-in
  hz-skeleton       -> skeleton     hz-paper      -> paper
  hz-drag-item      -> drag         hz-menu-in    -> menu-in
  hz-fade / -up     -> fade/fade-up hz-slide      -> slide
  hz-collapse       -> collapse     hz-elevation-N-> elevation-N
  @keyframes hz-shimmer/hz-pulse    -> shimmer/pulse

Bare names, no prefix. Collision was the whole reason the brand was there, and
it is not a risk here: every consumer is ours, and hanzo.app's user-generated
content renders in sandboxed iframes. A clean break with no dual-name window
beats carrying two vocabularies forever.

CSS custom properties deliberately do NOT move. `--hz-elevation-3`, `--hz-ring`,
`--hanzo-accent` are inherited globals a host SETS to theme us; a bare
`--accent` on :root really does collide, and they are not selectors.

src/styles/hanzo-motion.css -> src/styles/motion.css. The public subpath
`@hanzo/ui/styles/motion.css` is unchanged; only the file behind it moved.

buttonVariants dedupes: variant and size share the `btn-` namespace and both
default to `default`, which emitted `btn-default` twice.

Verified: 216 unit tests, and the real-browser consumer test green against the
packed tarball including all four screenshot comparisons at 390px and 1280px in
both themes — pixel output is unchanged, so nothing lost its styling.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 16:19:46 -07:00
zooqueen 8585e4db12 Merge remote-tracking branch 'origin/main' into design/quiet-default
Publish Packages / detect-changes (push) Successful in 14s
CI / Test (push) Failing after 1m42s
CI / Type Check (push) Failing after 1m53s
CI / Build (push) Failing after 3m21s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Failing after 3m54s
CI / Lint (push) Successful in 4m18s
CI / CI Status (push) Failing after 1s
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 15:51:26 -07:00
zooqueenandhanzo-dev 348b258b24 fix(ui): a focused field brightens its edge — it does not grow a second ring
@hanzogui/input ships two halves of one cue and both were wrong, each hiding
the other:

  focusVisibleStyle draws outline: 2px solid $outlineColor at a 2px offset,
  emitted as `:root:root:root:root ._outlineWidth-…:focus-visible { … 2px
  !important }`. No consumer stylesheet can retract that — hanzo.app tried
  `outline: none` and lost — so every field grew a ring held off its own edge.

  focusStyle brightens to $borderColorFocus, which resolves to the SAME value
  as $borderColor here. The cue that was meant to make the ring unnecessary
  rendered nothing, so the ring was the only feedback a focused field had.

Both are set as PROPS, which is the only thing that works: a prop replaces the
variant's atomic class rather than competing with it on specificity. $color06
against a $borderColor rest is ~5:1, so the edge alone carries the indicator
(WCAG 2.4.13).

One rule in one file, shared by Input and Textarea, so the two cannot drift.
Spread before each component's own ...props, so a call site can still say
otherwise.

Verified in the DOM: rest 1px $borderColor / no outline / no shadow; focus
rgba(255,255,255,0.6) / outline 0px none / no shadow. The emitted class goes
_outlineWidth-0focus-visible-2px -> -0px.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 15:51:03 -07:00
hanzo-dev 09c4236836 merge main
Publish Packages / detect-changes (push) Successful in 12s
CI / Test (push) Failing after 1m29s
CI / Build (push) Failing after 1m39s
CI / Type Check (push) Failing after 2m7s
CI / Lint (push) Successful in 2m46s
CI / CI Status (push) Failing after 1s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Failing after 5m15s
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 15:26:04 -07:00
hanzo-dev 6bdb34d879 observe: 0.1.6 — the click carries its position, now with a token that works
Every 0.1.x before this built and packed correctly and then died at the
registry: the NPM_TOKEN sealed in KMS was not a credential npm recognised
(whoami answered {}, publish answered E404). Replaced, verified round-trip.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 15:18:00 -07:00
zandGitHub 6b1cc7688f legal: merge legal/dual-mit-apache into main (HIP-0137)
Publish Packages / detect-changes (push) Successful in 11s
CI / Test (push) Failing after 1m13s
CI / Build (push) Failing after 1m40s
CI / Lint (push) Successful in 2m29s
CI / Type Check (push) Failing after 2m46s
CI / CI Status (push) Failing after 1s
deploy / image (push) Successful in 3m36s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Failing after 4m29s
2026-08-04 14:16:55 -07:00
hanzo-dev fd9e1892d7 publish: read NPM_TOKEN from where it is
Publish Packages / detect-changes (push) Successful in 12s
CI / Test (push) Failing after 1m9s
CI / Build (push) Failing after 1m22s
CI / Type Check (push) Failing after 3m30s
CI / Lint (push) Successful in 4m49s
CI / CI Status (push) Failing after 1s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Failing after 5m24s
The path was documented as a guess and it was wrong: NPM_TOKEN sits at the org
root, while deploy/ holds CLOUDFLARE_*, GIT_TOKEN, KUBECONFIG and
UNIVERSE_PIN_TOKEN. The read also used the older
/v1/kms/orgs/<org>/secrets/... form with .secret.value, which 404s against
cloud's embedded KMS — the flat /v1/kms/secrets/<name> form answers
{name, env, value} and is scoped by the token's own owner claim.

Measured against the live KMS: the flat form returns an npm_-prefixed token.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 13:42:02 -07:00
hanzo-dev baaf00333e publish: forward only, and one package cannot withhold the rest
Publish Packages / detect-changes (push) Successful in 13s
CI / Type Check (push) Failing after 2m5s
CI / Test (push) Failing after 3m5s
CI / Build (push) Failing after 3m7s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Failing after 5m4s
CI / Lint (push) Canceled after 9m53s
CI / CI Status (push) Canceled after 0s
Asking npm what it serves fixed the detection, but the comparison was '!=',
which fires in BOTH directions — @hanzo/data sits at 1.2.1 against npm's 1.2.2
and @hanzo/dashboard at 0.1.0 against 0.2.0, so the run tried to walk the
registry backwards. Only a strictly greater local version is a release.

The matrix legs are independent publishes, so fail-fast withheld eight good
packages because @hanzo/agent-ui builds no types file. They no longer share a
fate.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 13:32:12 -07:00
hanzo-dev acd764d809 publish: ask npm what is served, not what the last commit said
Publish Packages / detect-changes (push) Successful in 15s
CI / Build (push) Failing after 1m35s
CI / Test (push) Failing after 1m50s
CI / Type Check (push) Failing after 2m1s
CI / Lint (push) Successful in 3m43s
CI / CI Status (push) Failing after 1s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Failing after 8m22s
@hanzo/observe 0.1.2 never reached the registry. The version-change check
compares HEAD~1 against HEAD, and HEAD~1 is the FIRST PARENT — so a merge whose
first parent is the feature branch already carries the new version, the diff is
empty, and the publish is skipped while npm keeps serving the old release. The
same class of miss is already recorded in this file: 8.0.29 sat unpublished
while npmjs served 8.0.28.

The registry is the only source of truth for what is published, and asking it
is independent of merge topology and of how many commits the runner fetched.
observe goes to 0.1.3 because 0.1.2 is a version npm never saw.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 12:51:14 -07:00
hanzo-dev af83317eb3 merge main
CI / Type Check (push) Failing after 2m21s
Publish Packages / detect-changes (push) Successful in 7s
CI / Lint (push) Successful in 2m49s
CI / Build (push) Failing after 4m14s
CI / Test (push) Failing after 5m6s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Failing after 2m2s
CI / CI Status (push) Failing after 2s
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 11:59:40 -07:00
zooqueenandhanzo-dev 3e81579bb9 feat(ui): primary is the macOS dark pushbutton, not a white slab
Publish Packages / detect-changes (push) Successful in 8s
CI / Test (push) Failing after 1m38s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Failing after 1m56s
CI / Lint (push) Successful in 2m45s
CI / Build (push) Failing after 3m23s
CI / Type Check (push) Failing after 3m55s
CI / CI Status (push) Failing after 1s
A $color12 fill on an otherwise quiet dark page reads as glare. The
strongest control is now an elevated gray ($color5 fill, white text,
hairline, hover one step up) — the native macOS dark-mode pushbutton.
White backgrounds are no longer any button's default anywhere.

@hanzo/ui 8.0.42

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 11:54:58 -07:00
zooqueenandhanzo-dev 09ac8ee8d5 feat(ui): the unmarked Button is quiet; Glass is the one frosted material
Publish Packages / detect-changes (push) Successful in 7s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Failing after 1m27s
CI / Lint (push) Canceled after 2m35s
CI / Type Check (push) Canceled after 2m34s
CI / Build (push) Canceled after 2m34s
CI / Test (push) Canceled after 2m34s
CI / CI Status (push) Canceled after 0s
default used to be the same value as primary — white filled — so every
unstyled <Button> in every app shouted. The loudest treatment must be asked
for by name: default is now a control on the surface ladder ($color2,
hairline, hover brightens the border more than the fill); primary is
unchanged and explicit.

Glass (backends/gui/glass.tsx, [data-slot=glass]) is a surface, not a
layout: solid $color2 panel that theme.css upgrades to the translucent
blurred material only under @supports (backdrop-filter) — content never
sits on see-through ground the browser cannot blur. For floating chrome
(menus, dialogs, popovers, docked toolbars); page sections stay on the
ladder. Composes with brand/design tokens via var(--background).

@hanzo/ui 8.0.41

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 11:51:08 -07:00
hanzo-dev b2f700ed00 release(ui): 8.0.41 — the package ships its own stylesheet, config and root
`npm i @hanzo/ui` and render a component; it looks right. No gui.config.ts, no
CSS import, no generator script.

  import { Hanzo, Button } from '@hanzo/ui'
  <Hanzo><Button>Ship</Button></Hanzo>

8.0.40 shipped 13,178 bytes of CSS in two files, both of them TOKENS. The rules
that style the components did not exist in the package at all: @hanzo/gui
compiles a style prop to an atomic class the first time something RENDERS it, so
the sheet only exists after a render, and every app was expected to run a
generator of its own and import the output. hanzo.app never did — it shipped 103
`_bg-` classes and 26 `_dsp-` classes against a stylesheet containing zero of
either, every gui-styled element unstyled in production, with a green build for
the whole life of the bug.

The render happens at OUR publish time now. `scripts/gen-css.mjs` renders
`src/gallery.tsx` in both themes and harvests `config.getCSS()` into
`dist/styles.css` — 381,060 bytes, 35 KB gzipped, 340 atomic selectors — and
`<Hanzo>` imports it. Styles gui generates at runtime for props we cannot know
at publish time still reach the document through `insertStyleRules`; the shipped
sheet is what makes the first paint, and every SSR or static render, correct.

Two more things were the app's job and are now the package's:

  the config — `<Hanzo>` passes `config` to `GuiProvider` as a VALUE, never a
    bare `import './gui-config'`. Vite 8 (rolldown) ignores package.json
    `sideEffects` ARRAYS outright: with any array the registration is dropped and
    the first render dies on "Missing hanzogui config"; only `sideEffects: true`
    keeps it, and that costs +63% bundle (404 KB -> 661 KB, measured).
    Correctness does not belong in bundler metadata.

  the theme — gui throws `Missing theme.` for any component with no root theme
    context, so a root is structurally required. Forgetting `<Hanzo>` is a hard
    crash on first paint, never a silently unstyled page.

A green build caught none of the three "classes without rules" incidents in this
estate, so the suite now compares the two directly. `src/styles.test.tsx` takes
every atomic class the gallery renders and every class `dist/styles.css` defines
a rule for, and requires the coverage to be TOTAL, not large.
`test/consumer.spec.ts` packs the tarball, installs it into a throwaway app
outside this repo — never a workspace link, which resolves through src/ and hides
every packaging defect there is — builds it, and asserts computed styles and
screenshots at 390px and 1280px in both themes. `src/gallery.tsx` is the one list
of components all three read.

That suite immediately found four defects it was built to find:

  - the Slider thumb ringed itself in `$color12`, which is #fff on dark: the
    solid-white-on-black border the identity forbids. `$borderColor` was not the
    fix either — gui gives the thumb its own sub-theme where that token is also
    white. A filled knob needs no ring.
  - gui compiles style props to border-WIDTH and border-COLOR and never emits
    border-STYLE, so on the components that are really <button> elements the UA's
    `2px outset` survived every one of them. The Collapsible trigger rendered as
    a white bar across a dark page. theme.css now carries a zero-specificity
    `:where(button, input, textarea)` reset, so a component setting a width and a
    colour actually draws its line.
  - CardContent, CardFooter, TabsContent, ScrollArea and CollapsibleTrigger put
    bare text into a View instead of through `ink()`, against the house rule.
  - theme.css claimed "dark-first" while shipping LIGHT at `:root`, so an app on
    the dark default that read `--background` in its own CSS got white. `:root`
    is dark, `.light` retunes, and both answer to the `.t_light`/`.t_dark` that
    <Hanzo> stamps on the body.

Also: `hanzo.yml` + the canonical `cicd.yml`, so both layers run on every push;
`@hanzo/ui/gallery` and `@hanzo/ui/styles.css` are public subpaths; and
gen-css.mjs exits explicitly, because vite's module runner leaves handles open
and a build step that writes its output and never returns hangs `prepack`, which
hangs `pnpm pack`, which hangs `pnpm publish`.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 11:18:25 -07:00
hanzo-dev 2ceca2af60 feat(observe): a click carries where it happened
onClick took the element off the MouseEvent and discarded the event, so every
$click on the wire named WHICH thing was clicked and never where on the page it
sat. Element identity cannot be drawn as a heat map.

Position rides in the existing props seam, so wireProps carries it to
@hanzo/event unchanged: $x, $y, $target_fixed, $viewport_width,
$viewport_height. Page coordinates, except on a fixed or sticky target, which
does not move with the scroll and is measured against the viewport instead.

Total: an event with no pointer (a synthetic click, a keyboard activation)
contributes no position rather than a click at the origin.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 10:52:30 -07:00
zandhanzo-dev 49daf8c928 Dark is the default here, and the palette is the design system's
CI / Test (push) Failing after 2m33s
CI / Build (push) Failing after 2m57s
CI / Type Check (push) Failing after 3m2s
CI / Lint (push) Successful in 3m36s
CI / CI Status (push) Failing after 1s
TWO SURFACES BUILT FROM THESE PACKAGES CAME OUT BRIGHT WHITE BY ACCIDENT.

theme.css put the LIGHT palette on :root with dark behind `.dark`, so an app
that imported it and did not put class="dark" on <html> rendered oklch(1 0 0) —
pure white. That is also the inverse of @hanzo/design, whose :root is dark with
`.light` to opt out, so an app pulling in both had its theme decided by which
file happened to win. One convention now, the design system's: :root is dark,
.light opts out. `.dark` is kept so an app already stating it is unaffected, and
.light is ordered after so it can still override.

And the token values had drifted to stock shadcn oklch neutrals: `background`
was oklch(0.145), a washed near-#252525 where the system says true black, and
`card` was lighter still — the grey-box-on-grey look. An app on @hanzo/ui did
not resemble an app on @hanzo/design. The CSS in @hanzo/design is the source of
truth and this module is its TS mirror; a second set of numbers is not a second
opinion, it is drift.

destructiveForeground was independently broken: oklch(0.58 0.22 27) is the same
saturated red as `destructive`, so error text sat unreadable on its own fill.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 09:35:46 -07:00
hanzo-dev 84c0a27ff0 legal: packages said BSD-3-Clause, repo LICENSE.md is MIT
Root package.json was already MIT; the 14 sub-package manifests, their
READMEs and CONSOLIDATION.md still claimed BSD-3-Clause. BSD-3 is out of
scope for hanzoai originals under HIP-0137 (`hanzoai/hips`). pkgs/cd stays Apache-2.0
(Argo CD clean-room port, see its NOTICE).
2026-08-04 01:24:49 -07:00
hanzo-dev 70a51cf39e release(ui): 8.0.40 — the barrel, the build-time cd import, and TS7 native
Publish Packages / detect-changes (push) Successful in 7s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Failing after 1m34s
CI / Test (push) Failing after 2m33s
CI / Type Check (push) Failing after 2m56s
CI / Lint (push) Successful in 3m4s
CI / Build (push) Failing after 3m11s
CI / CI Status (push) Failing after 1s
deploy / image (push) Successful in 3m16s
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 01:17:22 -07:00
hanzo-dev 01fd5a8c5a merge origin/main
CI / Build (push) Failing after 2m23s
CI / Lint (push) Successful in 2m31s
CI / Test (push) Failing after 2m44s
CI / Type Check (push) Failing after 3m5s
deploy / image (push) Successful in 3m4s
CI / CI Status (push) Failing after 1s
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 00:41:24 -07:00
hanzo-dev c456353294 wip: preserve agent work interrupted by session limit
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-04 00:30:44 -07:00
zandhanzo-dev 49b7cae73a rpc: /ext/ -> /v1/ on our own hosts
CI / Build (push) Failing after 1m24s
CI / Type Check (push) Failing after 3m7s
CI / Test (push) Failing after 1m27s
CI / Lint (push) Successful in 4m38s
deploy / image (push) Successful in 3m5s
CI / CI Status (push) Failing after 1s
luxd serves /v1/*; /ext/* 404s. Third-party chains keep their own paths.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-03 21:14:53 -07:00
hanzo-dev da8be307f9 build: keep the diff to the fixes themselves
Publish Packages / detect-changes (push) Successful in 7s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Skipped
CI / Type Check (push) Failing after 3m3s
CI / Test (push) Failing after 2m45s
CI / Build (push) Failing after 3m12s
deploy / image (push) Successful in 3m16s
CI / Lint (push) Successful in 4m37s
CI / CI Status (push) Failing after 1s
Drops incidental churn that rode along with the earlier commits: the two
app/public/registry JSONs that `registry:build` rewrites non-deterministically
on every run, and a unicode re-escape of pkg/ui's description that a
JSON.stringify round-trip introduced. pkg/ui/package.json is now byte-identical
to origin/main again.

What remains against origin/main is only the work: the commerce and apps/cd
build fixes, the missing `direction` in pkg/ui's resizable test, `tc` no longer
emitting into commerce/checkout sources, and shadcn declaring the tailwindcss
it imports.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-03 19:44:44 -07:00
hanzo-dev ff58ccdad2 build: fix the two red builds, and drop the second TypeScript compiler
Two builds were red before `turbo run build` reached a single app:

  @hanzo/commerce  index.ts re-exported CardTokenizeRequest/CardTokenizeResult
                   from ./client, which 5022535ec deleted along with tokenizeCard.
                   The PCI removal took the types; the barrel kept naming them.
  cd               vite.config.ts aliased @hanzo/cd to ../../pkgs/gitops, a path
                   the package left when it was renamed to pkgs/cd, and typed its
                   `test` block through vite's defineConfig, which has no such key.

Both green now, so all 21 build tasks pass.

Also removes @typescript/native-preview, which becc0c5af added. It was the wrong
package: native-preview is pinned at 7.0.0-dev.20260707.2, while `typescript`
itself now ships the native Go compiler as stable at 7.0.2 — the platform
binaries are real Go ELF executables, e.g.

  @typescript/typescript-linux-arm64@7.0.2/lib/tsc:
    ELF 64-bit LSB executable, ARM aarch64, statically linked, Go BuildID=...

So native-preview was a second, older copy of the compiler under a second binary
name. There is one compiler and it is called `tsc`; all 16 packages that becc0c5af
pointed at `tsgo` are back on `tsc`.

TypeScript 7 itself does NOT land here yet, and that is measured rather than
assumed. Forcing `pnpm.overrides.typescript=^7.0.2` across the estate takes the
build from 21/21 to 5/21. The cause is not the type system — TS 7.0.2 typechecks
all 16 tsc-driven packages clean, including pkg/ui, event and shop. It is emit:
tsup's rollup-plugin-dts crashes under TS7, and TS7 removed options these
configs still use (TS5108 moduleResolution=node10 in pkg/ui/tsconfig.cjs.json,
TS5011 implicit rootDir in agent-ui).

pkg/ui/tsconfig.cjs.json already documents this exact failure from a previous
attempt, down to the rollup-plugin-dts stack, and asks that TS7 wait for tsup.
This change respects that note instead of rediscovering it in prod: root
typescript stays ^5.9.3, matching the 17 packages that pin 5.x.

Verified: pnpm build → 21 successful, 21 total; pkg/ui 21 files, 212 tests pass.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-03 19:43:28 -07:00
hanzo-dev 0388064c04 wip: preserve agent work interrupted by session limit
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-03 19:18:12 -07:00
hanzo-dev 0d8c2500c4 Revert "build: the two builds that were red, and the Tailwind the root only pretended to have"
This reverts c98de82ae.

The premise changed: this repo keeps its shadcn identity rather than shedding
it, so the root Tailwind config, postcss config, eslintrc and components.json
are part of what it is and stay. Restored verbatim — tailwindcss,
@tailwindcss/oxide, tailwindcss-animate, eslint-plugin-tailwindcss,
autoprefixer and postcss are back in the root package.json.

The two genuine build fixes that rode along in that commit come back on their
own, next, so they are not lost with the premise that carried them.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-03 18:09:51 -07:00
hanzo-dev becc0c5afc types: TypeScript 7 native for the 18 packages where it agrees with tsc
Adds @typescript/native-preview (tsgo 7.0.0-dev.20260707.2) and points
`typecheck`/`tc` at it wherever the Go compiler was verified to reach the same
verdict as tsc. Measured, package by package, both compilers on the same
invocation:

  agree, green (18)   pkg/ui, pkg/data, annotate, canvas, cd, dashboard,
                      observe, observe-native, observe-svelte, products, react,
                      tests, agent-ui, commerce, checkout, apps/cd
  tsgo disagrees (2)  event  — TS2345 on Uint8Array<ArrayBufferLike> vs
                               ArrayBufferView<ArrayBuffer> in src/uid.ts:34
                      shop   — TS2591, does not pick up the node globals tsc
                               resolves for components/PaymentStep.tsx
  red under both (3)  shadcn, @hanzo/ui-web, v4 — pre-existing type errors,
                      left on tsc so the diff is the compiler, not the errors

event and shop stay on tsc. They are the report, not an oversight.

pkg/ui was red before this: `tsc --noEmit` failed on render.test.tsx, where
ResizablePanelGroup was written without its required `direction`. The build
never caught it because tsconfig.build.json excludes tests, so the shipped
package typechecked green in CI and red on a developer's machine. Fixed, and
pkg/ui is now green under both compilers: 21 test files, 212 tests passing.

Emit stays on tsc everywhere, deliberately. tsgo can emit pkg/ui — 920 files,
identical file list, and the only differences are comment retention in .js and
alphabetical member ordering in .d.ts, i.e. semantically equal. But this is a
published package and tsgo is a dev preview, so the artifact keeps the compiler
it has until tsgo ships stable. The flip is a one-line change when it does.

`tc` in commerce and checkout was `tsc` with no flags, against a tsconfig with
noEmit:false and declaration:true — running it emitted 188 .js/.d.ts files
directly into the source trees, next to the .ts they came from. A typecheck
script that writes build output into src is a trap; both are now `tsgo --noEmit`.

shadcn imported `type Config from "tailwindcss"` while declaring no dependency
on it, resolving through the root package.json instead. Now that the root no
longer carries Tailwind, the package declares the version it actually uses.

Verified: pnpm build → 21 successful, 21 total.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-03 18:02:20 -07:00
hanzo-dev c98de82ae1 build: the two builds that were red, and the Tailwind the root only pretended to have
`pnpm build` failed twice before reaching a single app:

  @hanzo/commerce  index.ts re-exported CardTokenizeRequest/CardTokenizeResult
                   from ./client, which 5022535ec deleted along with tokenizeCard.
                   The PCI removal took the types; the barrel kept naming them.
  cd               vite.config.ts aliased @hanzo/cd to ../../pkgs/gitops, a path
                   the package left when it was renamed to pkgs/cd, and typed its
                   `test` block through vite's defineConfig, which has no such key.

Both are now green, so `turbo run build` reaches all 21 tasks for the first time
on this line.

The root's Tailwind was never real. tailwind.config.cjs requires
@tailwindcss/container-queries, which is not installed — the config throws on
load, so nothing has read it in a long time. Its only referent was
.eslintrc.json, which ESLint 10 does not support at all (app/ and apps/v4 each
carry their own flat config, and neither mentions Tailwind). postcss.config.cjs
sat next to them for a root that has no build step. components.json pointed at
tailwind.config.js — a filename that has never existed here — and declared the
same "@hanzo" registry key twice.

So the root drops tailwindcss, @tailwindcss/oxide, tailwindcss-animate,
eslint-plugin-tailwindcss, autoprefixer and postcss. app/ and apps/v4 are
untouched: they declare every one of those themselves, which is why they still
build. This removes the pretence, not the styling.

While in package.json: --filter=hanzo, --filter=hanzo-ui, --filter=www and
--filter=app match no workspace project ("No projects matched the filters"), so
~20 scripts could only fail. The survivors are re-pointed at @hanzo/ui-web, the
name the app/ directory actually publishes under.

Verified: pnpm build → 21 successful, 21 total.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-03 17:46:10 -07:00
hanzo-dev ae86ec8df7 Merge remote-tracking branch 'origin/pci/no-raw-pan' into HEAD
Publish Packages / detect-changes (push) Successful in 7s
CI / Test (push) Failing after 2m40s
CI / Build (push) Failing after 2m58s
CI / Type Check (push) Failing after 3m20s
deploy / image (push) Successful in 3m8s
CI / Lint (push) Successful in 4m40s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Failing after 58s
CI / CI Status (push) Failing after 3s
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-02 11:00:36 -07:00
hanzo-dev 0a54e91dc4 event: mint UUIDv7, release 0.3.9
insights.raw_sessions and raw_sessions_v3 have been empty for their entire
existence, and nothing ever errored. Their materialized views admit only
UUIDv7 session ids —

    bitAnd(bitShiftRight(toUInt128(accurateCastOrNull(`$session_id`,'UUID')),76),15) = 7

— because they derive a session's start instant FROM the id: both the
destination's PARTITION BY and its ORDER BY are
fromUnixTimestamp(intDiv(toUInt64(bitShiftRight(session_id_v7, 80)),1000)).
This client minted with crypto.randomUUID(), which is v4, so every row it
ever sent was discarded at that gate. An MV is an insert trigger: rows that
never arrive never error, and the destination reads as "no traffic" forever.
8,432 of the 8,670 rows on the plane are v4 from this library.

Mint v7 instead. The 48-bit millisecond prefix makes the id carry its own
mint time, which is the whole reason the rollups can key on it, and it
clusters index writes by time rather than scattering them.

ONE minter, in one place: src/uid.ts. It replaces three hand-rolled copies —
core.ts (messageId), storage.ts (anonId/sessionId) and sentry.ts (eventId,
which now just formats the same id for Sentry's 32-hex wire). hz.js, the
no-build distribution, restates the algorithm for the same reason it
restates scrub.ts's redaction: it has no bundler and cannot import.

sessionId(now) mints at the caller's clock, so the instant embedded in the
id and the recorded `last` cannot disagree.

The old minters also fell back to 'a-' + base36 when crypto was absent. That
shape does not parse as a UUID, so accurateCastOrNull returns NULL and the
same gate drops it — 232 such rows are on the plane. uuidv7 degrades
entropy without crypto, never shape.

Tests: the gate and the timestamp extraction are transcribed from the
rollup's own SQL and asserted against minted ids, including the negative
(crypto.randomUUID's nibble is 4). hz.js gets its first test at all — the
real file, run against a browser stub, asserting the session id on the wire.
117 pass.

Existing v4 rows are NOT recoverable into these rollups: read as v7, the
1,586 distinct v4 session ids on the plane imply session starts spanning
1971-09-10 to 2299-12-31 across 54 monthly partitions. They remain queryable
as raw events in insights.sharded_events.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-02 10:00:47 -07:00
hanzo-dev 04010fceaf commerce: call the addresses the server actually serves
@hanzo/commerce defaults its base URL to api.hanzo.ai, and every saved-card call
it makes has been answering 404 there: the billing surface dropped compound
words from its route names, both server repos converged, and this client did
not. Verified on the live edge — /v1/billing/payment-methods 404s,
/v1/billing/methods answers 401, which is what an address that exists says to a
request carrying no token.

This is a published package, so the dead name was not one product's bug: every
consumer that installed it inherited a client that cannot save or list a card.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-01 22:11:23 -07:00
hanzo-dev 5022535ec0 commerce: drop tokenizeCard — the client never sees a PAN
@hanzo/commerce shipped `tokenizeCard({ number, expiryMonth, expiryYear, cvc })`
→ POST /v1/billing/card/tokenize: a typed, documented, published invitation for
any consumer to collect a raw card number in first-party JavaScript. The class
doc demonstrated it with a live-looking PAN and CVC.

Its destination is deleted (commerce, same lane) and it was never reachable
through api.hanzo.ai anyway — no manifest row routes that address. So this
removes a method that could only fail, after putting the consumer's origin in
PCI SAQ-D scope. Card entry is Square Web Payments hosted fields; the nonce they
return is what `addPaymentMethod` already takes.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-01 21:55:34 -07:00
Claude 2e765ef5a0 build: drop the build step for a package that no longer exists
CI / Lint (push) Successful in 2m32s
CI / Test (push) Failing after 2m40s
CI / Type Check (push) Failing after 3m3s
deploy / image (push) Successful in 3m14s
CI / CI Status (push) Failing after 1s
CI / Build (push) Failing after 1m32s
The image build has failed at its first compile step since 2026-07-28:

    #13 ERROR: process "/bin/sh -c cd pkgs/ui && pnpm build"
        did not complete successfully: exit code: 2

exit 2 is `cd` refusing a missing directory, not a compiler. pkgs/ui was
@hanzo/ui-shadcn and was deleted in 5dbdb2943 when shadcn was consolidated to a
single home; that commit did not touch the Dockerfile, so the build kept trying
to enter it and never reached `pnpm build` at all. The shadcn ENOENT warnings
higher in the log are a consequence of the same removal and are not the failure.

app now takes @hanzo/ui from the registry (npm:@hanzo/ui-shadcn@^5), so nothing
in the workspace needs building for it. @hanzo/event still does -- .npmrc sets
link-workspace-packages=true and the lockfile resolves it to link:../pkgs/event
-- so that step stays.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 18:39:17 -07:00
Claude 03e1ccdc63 ci: name a pool the fleet actually advertises
deploy / image (push) Failing after 50s
CI / Build (push) Failing after 2m41s
CI / Test (push) Failing after 3m27s
CI / Lint (push) Successful in 3m43s
CI / CI Status (push) Failing after 1s
CI / Type Check (push) Failing after 3m45s
The deploy job asked for `ubuntu-latest`. This file lives in .hanzo/workflows,
which GitHub never reads -- git.hanzo.ai is the only thing that can run it, and
the fleet advertises no generic ubuntu-* label. That is deliberate and recorded
in the runner ConfigMap: ~1400 mirrored upstream forks all ask for ubuntu-latest,
and the forge errors rather than skipping a job it cannot place, so one bad job
was retried ~520 times across 10 runners.

An unmatched label is not a failure here, it is silence -- the job queues until
the 24h timeout. So this deploy has never produced an image, and nothing ever
said so. Same class as the playground regression, found by checking every
.hanzo/workflows file against the labels the runner ConfigMap actually declares
rather than against the ones I assumed.

hanzo-build-linux-amd64 is registered (verified against cm/git-runner-config).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 16:55:39 -07:00
hanzo-dev e9e1a5bdb6 event: release 0.3.8
Publish Packages / detect-changes (push) Successful in 9s
CI / Test (push) Failing after 1m42s
CI / Type Check (push) Failing after 2m2s
CI / Build (push) Failing after 2m49s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Failing after 51s
CI / Lint (push) Successful in 3m41s
CI / CI Status (push) Failing after 3s
deploy / image (push) Canceled after 0s
0.3.7 is unreleasable, not merely unreleased. detect-changes compares
HEAD~1..HEAD for a version change, so a release is only ever attempted by the
commit that bumps the version — and 0.3.7's bump commit is the one whose build
failed on TS5103. With the build repaired, nothing re-triggers that version:
the detector sees no change to package.json in the tip and skips the package, so
0.3.7 can never be retried from main. A version that no push can publish has to
be superseded.

npmjs goes 0.3.6 -> 0.3.8; there is no 0.3.7 to be confused with, since it was
never published. The three stamps move together, which is what the guard in
core.test.ts pins.

Worth fixing separately: a release whose build fails is stranded rather than
retryable, because the detector is tip-relative. Making it truth-relative —
publish when npmjs does not already hold the manifest's version — would make a
retry a re-run, but it would also try to publish every package whose version is
absent from npmjs, so it is not a change to slip into a bugfix.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-01 12:04:02 -07:00
hanzo-dev eab4929efb event: redact credentials from the location stamped on every event
CI / Lint (push) Canceled after 0s
CI / Type Check (push) Canceled after 6s
CI / Build (push) Canceled after 0s
CI / Test (push) Canceled after 0s
CI / CI Status (push) Canceled after 0s
deploy / image (push) Canceled after 0s
Publish Packages / detect-changes (push) Successful in 9s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Skipped
The location is now stamped on every event rather than only on pageviews, which
is what makes a click attributable to a page. It also multiplies an exposure
that used to cost one row per page load: a password-reset, invite or magic link
carries a JWT in the query and an address in `?email=`, so a single click on
that page shipped both to the warehouse in cleartext, and every later click
repeated it. Measured against the published 0.3.6 bundle, a $click on such a URL
put the JWT and the address on the wire verbatim.

The package already had the answer and was not applying it to the one field that
is always a URL: scrub.ts, a port of the server's errortracking scrub, is
imported only by sentry.ts. url, path and referrer now get exactly that policy —
secrets unconditionally, PII unless capturePII — so there is one definition of
"must not leave the browser" instead of a second URL-specific redactor.

Applied to the ASSEMBLED record, after `...extra` rather than at the reads: a
call site can pass its own location (pageview() passed one until this commit),
and extra merges over the fields build() reads, so scrubbing at the read would
have left the highest-volume event emitting a raw location while appearing
scrubbed. On the assembled record the guarantee holds for call sites not written
yet. Ordinary URLs pass through byte-for-byte — a redactor that mangles them
would destroy the analytics it exists to protect.

pageview() no longer recomputes `url`: build() reads the same value in the same
tick. `path` stays, because a route change fires before window.location catches
up and the caller's value has to win.

hz.js carries the same policy at its own single choke point. It cannot import
scrub.ts — it is the no-bundler distribution — so the URL-relevant subset is
restated there, with identical markers so a warehouse row never reveals which
distribution wrote it. Its $outbound target and the locator's href are URLs too
and are cleaned the same way.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-01 12:02:16 -07:00
hanzo-dev 83166e5c11 publish: gate a release on the package's own tests and declared types
A published version is immutable, so the moment before publish is the last one
where a defect is free. Nothing ran a package's tests before it shipped:
publish.yml went checkout -> install -> build -> publish, and ci.yml's test job
runs `cd pkgs/ui && …`, that one directory. @hanzo/event's suite — including the
assertion that pins the version the client stamps on every event — therefore ran
in no workflow that gates a release, so the drift it exists to catch could still
reach npmjs. It just did, in the commit before this one.

Two steps, both derived from the package's own manifest rather than a list kept
here:

  - `run --if-present test`, so a package with no test script is skipped instead
    of failing, and a new package is covered the day it adds one.
  - the `types` entrypoint must exist after the build. tsup writes dist/*.mjs in
    a separate pass from dist/*.d.ts, so a declaration failure can leave working
    JS and no types — invisible here, and surfacing in dependents as "could not
    find a declaration file". Asserting the manifest's own promise closes that
    without naming any package.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-01 12:02:03 -07:00
hanzo-dev f4e0fc5463 event: stamp 0.3.7, the version this package publishes
package.json moved to 0.3.7 for the hz.js distribution; version.ts stayed at
0.3.6. VERSION is what every event carries as `libraryVersion` and what the
Sentry sdk block reports, so 0.3.7's rows would have been indistinguishable from
0.3.6's in the warehouse — the same drift 0.3.6 was cut to fix, one release
later.

The guard added with 0.3.6 caught it: `expected '0.3.6' to be '0.3.7'`. It ran
here only because it was run by hand — the next commit puts it on the release
path.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-01 12:01:54 -07:00
hanzo-dev bd871a4070 event: build declarations under the TypeScript this package pins
`ignoreDeprecations: "6.0"` was set to keep the declaration build alive under
TypeScript 6, but this package pins `typescript: ^5.9.3` (<6) and publish.yml
builds it with `pnpm --filter @hanzo/event... build`, which resolves the
package's OWN TypeScript. So the release path is always TS 5.9.3, where "6.0" is
not an accepted value:

    error TS5103: Invalid value for '--ignoreDeprecations'
    DTS Build error         tsup exit=1        no .d.ts emitted

That is why 0.3.7 is absent from npmjs while package.json already names it — the
publish job could not get past the build. The reasoning behind the option was
sound and the direction was inverted.

tsup injects `baseUrl: compilerOptions.baseUrl || "."` into the config its
declaration worker runs (tsup/dist/rollup.js), so the deprecation cannot be
dodged by not writing one, and the escape hatch's accepted value is
version-specific. Measured, all six cells, tsup in pkgs/event:

                     TS 5.9.3 (pinned here)   TS 6.0.3 (repo root)
      absent         exit 0, 3 .d.ts          exit 1, TS5101
      "5.0"          exit 0, 3 .d.ts          exit 1, TS5101
      "6.0"          exit 1, TS5103           exit 0, 3 .d.ts

The two are mutually exclusive, so the value has to match the TypeScript the
package declares. "5.0" is not a middle ground — it is byte-identical to absent
under 5.9.3 and still fails under 6 — which leaves absent as the only correct
value and the smallest one.

Every failing cell exits 1, so no configuration here silently ships a typeless
tarball; the claim that tsup exits zero on a declaration failure does not hold
for any of them. Whether it exits zero or not, the publish job now also asserts
the types entrypoint exists before shipping.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-01 12:01:48 -07:00
zeekayandhanzo-dev b64341c147 event: hz.js is a distribution of this client, not a second client
The tag lived in hanzoai/analytics and posted a BARE JSON ARRAY of
{site, ts, type, path, ref, props, anon, sid, w, h} to
analytics.hanzo.ai/v1/event — a second protocol behind an identical path
spelling, served by a second collector with its own Postgres. Measured:

  POST api.hanzo.ai/v1/event       {"batch":[]}  -> 200 {"accepted":0,"dropped":0}
  POST analytics.hanzo.ai/v1/event []            -> 204

One path spelling, two wires, two servers — so a client pointed at the wrong
host failed silently. This package's own README already had to warn about it.

hz.js moves here and emits the canonical WireEvent shape as { batch: [ … ] } to
POST {host}/v1/event, defaulting to api.hanzo.ai. It is the SCRIPT-TAG
distribution of @hanzo/event for surfaces with no bundler, and it carries the one
thing a bundled app does not need and a plain page cannot get: DOM autocapture —
$click (with a compact, PII-light element locator), $outbound, $scroll depth,
$form, $vitals — all on the one stream. data-product names the surface, data-host
overrides the API host, data-capture="0" turns autocapture off, DNT is respected,
and the optional data-ga / data-fb fan-out is unchanged.

Shipped in `files` so it is fetchable from the CDN as
https://unpkg.com/@hanzo/event/hz.js. The Next.js /v1/event door and the
undeployed Go collector that fed it are deleted in hanzoai/analytics.

0.3.6 -> 0.3.7.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-01 11:29:53 -07:00
hanzo-dev 882fe710aa event: keep the declaration build alive under TypeScript 6
CI / Build (push) Failing after 1m16s
CI / Test (push) Failing after 2m5s
CI / Type Check (push) Failing after 2m23s
CI / Lint (push) Successful in 3m37s
CI / CI Status (push) Failing after 2s
deploy / image (push) Canceled after 0s
tsup composes baseUrl into the config its declaration worker runs, and
TypeScript 6 refuses it. The JS build still succeeds and tsup still exits
zero, so the failure is silent: the package publishes with dist/*.mjs and
no .d.ts at all. 0.3.5 was published from a tree in exactly that state and
only kept its types because the build was re-run after this was set.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-01 11:13:35 -07:00
hanzo-dev 5502ed7737 fix(event): correct the version the package stamps on its own events (0.3.6)
Publish Packages / detect-changes (push) Successful in 10s
CI / Build (push) Failing after 1m23s
CI / Type Check (push) Failing after 2m9s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Failing after 2m12s
CI / Test (push) Failing after 4m5s
CI / Lint (push) Successful in 5m28s
CI / CI Status (push) Failing after 2s
deploy / image (push) Canceled after 0s
libraryVersion is the only field naming which build emitted a row, and
VERSION had fallen two releases behind: 0.3.4 and 0.3.5 both shipped
stamping "0.3.3", so three releases' rows are indistinguishable in the
warehouse. A telemetry client that cannot attribute itself cannot be
used to confirm a rollout reached production.

0.3.5 is immutable, so correcting the stamp needs a release of its own.
The guard added alongside it pins VERSION to the published version, so
this cannot drift again.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-01 10:43:48 -07:00
hanzo-dev 211bc6f879 fix(event): stamp the version the package actually publishes
Publish Packages / detect-changes (push) Successful in 8s
CI / Lint (push) Canceled after 14s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Skipped
CI / Build (push) Canceled after 41s
CI / Type Check (push) Canceled after 40s
CI / Test (push) Canceled after 26s
CI / CI Status (push) Canceled after 0s
deploy / image (push) Canceled after 0s
VERSION is hand-maintained — it lives alone so sentry.ts can read it
without importing core.ts — and it fell a release behind: 0.3.4 shipped
stamping libraryVersion "0.3.3". That is the only field naming which
build emitted a row, so two releases' rows were indistinguishable in the
warehouse, and 0.3.5 would have been a third.

The existing assertions compare VERSION to itself, which holds at any
value, so pin it to the published package version instead.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-08-01 10:42:18 -07:00
3befb955f1 workspaces: declare pkg/* — @hanzo/ui was never a workspace member
package.json listed ["app","apps/*","pkgs/*"] while pnpm-workspace.yaml listed
pkg/* as well. The repo has BOTH directories, and the two real packages under the
singular one — @hanzo/ui (268 tracked files) and @hanzo/data (36) — were therefore
invisible to anything reading package.json. It only worked because this repo runs
pnpm, which reads the yaml. npm, yarn or turbo would each have resolved
@hanzo/ui to nothing.

Also removed ~27MB of untracked build litter: eight dist-only directories under
pkg/ (agent-ui, brand, checkout, commerce, gui, react, shop, tokens — six of them
duplicating real packages in pkgs/) and pkgs/ui, which was dist + node_modules
with no src and no package.json. Every deletion was gated on being untracked AND
having no src/, and `git status` stayed at 0 dirty across all of them, which is
the proof nothing tracked was touched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 09:23:39 -07:00
zooqueenandhanzo-dev 03af7fef07 ui: @hanzo/cd is a build-time import, so declare it as one
src/gitops.ts does `export * from '@hanzo/cd'`, but the package was declared
only as an optional peer. peerDependencies create no edge in the workspace
graph, so turbo's `^build` never scheduled @hanzo/cd before @hanzo/ui — and
@hanzo/cd's types are tsup output (dist/index.d.ts), not checked-in source.
On a clean tree the build died at:

  src/gitops.ts(5,15): error TS2307: Cannot find module '@hanzo/cd'

It only ever succeeded where pkgs/cd/dist happened to survive from an earlier
run, which is why this surfaced at publish time rather than in CI.

devDependencies as workspace:* declares the edge that already exists and
cannot drift to a registry copy the way a bare version range can. The
consumer contract is unchanged: @hanzo/cd stays an OPTIONAL peer, so apps
that never import @hanzo/ui/gitops still need not install it.

Verified from a fully cleared tree (pkg/data/types, pkgs/{tokens,products,cd}/dist,
pkg/ui/dist all removed): `turbo build --filter=@hanzo/ui --force` is 6/6 green
with @hanzo/cd built ahead of @hanzo/ui, and vitest is 212/212.

The lockfile additionally picks up the pkgs/annotate importer, which was absent
from it — pre-existing drift, corrected by the same install.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-31 17:11:07 -07:00
hanzo-dev 75ca6696ee fix(ui)!: name the root barrel's exports — export * breaks every Next 16 client
pkg/ui/src/index.ts is a client boundary ('use client' on line 1) and line 18 was
`export * from './backends/gui'`. Next 16 refuses that combination outright:

  It's currently unsupported to use "export *" in a client boundary.
  Please use named exports instead.

So every app importing the root barrel failed to build. In hanzo.app that is 92
files — the whole app, not a corner of it: /, /resources and /usage all 500 on it.
Published 8.0.38 has the same line, so the break is live.

Naming the members also restores tree-shaking, which `export *` defeats: a bundler
cannot prove which members are unused through a star, so importing one Button
pulled the entire component surface into the graph. 92 files import the bare
barrel and none import a subpath, so nothing was shaking.

./backends/gui already declares its surface as explicit named blocks — the same
manifest scripts/gen-primitives.mjs reads to emit ./primitives/* — so this list
mirrors it rather than inventing one: 90 values and 13 types.

Verified by building: dist/index.js re-emitted with 0 star exports and the named
members present. The build still reports errors from unbuilt workspace siblings
(@hanzo/tokens, @hanzo/cd) and one pre-existing type error in pkg/data — 89 of
them are present on unmodified origin/main too, so they are not from this change.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-31 16:58:15 -07:00
hanzo-dev b9204f6fb9 Merge remote-tracking branch 'origin/telemetry/annotate' into HEAD
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-31 14:41:43 -07:00
hanzo-dev 32feedb5ca event: stamp the page onto every event, not just pageviews — 0.3.5
Autocapture reaches the wire through capture(), which passes no location, so
only pageview() ever supplied url and path. Every $click/$input/$change
therefore arrived with an empty url AND path, and `host`, derived from url, was
empty with them — an interaction with no page, which is the one thing a heatmap
cannot use.

Read window.location in build(), the single point every event is constructed,
so it cannot go missing from a call site again. It sits before ...extra so an
explicit pageview() path still wins: a route change fires before
window.location has caught up.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-31 14:28:13 -07:00
hanzo-dev ddf70510d4 ui: CardHeader stacks title over description (8.0.38)
The reference header is a grid with rows [auto_auto] that becomes two
columns ONLY when a CardAction is present. Ported to XStack it was always a
row, so the ordinary
<CardHeader><CardTitle/><CardDescription/></CardHeader> rendered as two
narrow columns with the title wrapping mid-phrase — visible on hanzo.app's
/features, and the shape all 79 CardHeader call sites in that app use.
CardAction appears in none of them; in a column it self-aligns to the
trailing edge.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-30 23:11:21 -07:00
hanzo-dev 8c05e7f89e ui: a Badge shrink-wraps in a block parent too (8.0.37)
BadgeFrame set self: 'flex-start', which is alignSelf — inert unless the
PARENT is flex. Dropped in a plain <div> the XStack became a block-level
flex container and stretched edge to edge; hanzo.app's /features rendered
its "Core Features" pill as a full-width bar. display: inline-flex makes it
shrink-wrap regardless of the parent, and alignSelf still governs when the
parent is flex.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-30 23:02:22 -07:00
hanzo-dev 27fde3b9e6 ui: 8.0.36 — restore the side-effect markers 8.0.35 dropped
8.0.35 was built and published from a tree that had not fetched 8.0.34, so
it shipped sideEffects: ["**/*.css"] and lost the four DATA module entries
that keep bundlers from tree-shaking the config registration away. Same
content, correct markers.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-30 20:44:41 -07:00
hanzo-devandz 34c04b4484 ui: move to the 8.x gui lineage, and fix the 39 errors that predate it (8.0.35)
The gui deps pinned ^7.3.0/^0.1.0, which cannot resolve to 8.x — a consumer
would have installed a 7.x copy alongside the 8.x one, which is the version
split that broke rendering before. Peer floors move to >=8.0.0 so the split
cannot re-form.

The build was already red at 8.0.33 (39 errors, unchanged by the bump).
Cause: pkgs/canvas augments GuiCustomConfig but never declared
@hanzogui/core, and TS drops a `declare module` whose package it cannot
resolve — silently, so every shorthand style prop went untyped and even
`bg` and `p` read as nonexistent. Declaring it fixes 36.

The last 3 are real: Switch and Checkbox are styled() components whose prop
types are enumerated explicitly, so they never accept the config's shorthand
vocabulary. They now use the canonical longhand names.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-30 20:43:19 -07:00
hanzo-dev fc1afcbc5f feat(annotate): stamp every component root with its own name so production keeps the hierarchy
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-30 09:58:23 -07:00
hanzo-dev f7489ddc3b ui: mark the DATA modules as side-effectful so bundlers keep their config registration (8.0.34)
Publish Packages / detect-changes (push) Successful in 6s
CI / Build (push) Failing after 1m30s
CI / Test (push) Failing after 1m33s
CI / Type Check (push) Failing after 3m9s
CI / Lint (push) Successful in 4m8s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Failing after 1m1s
CI / CI Status (push) Failing after 2s
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-30 06:07:55 -07:00
hanzo-dev 1fefdaca18 ui: a barrel is not a client module
CI / Type Check (push) Failing after 1m44s
CI / Build (push) Failing after 1m18s
CI / Test (push) Failing after 1m30s
CI / Lint (push) Successful in 4m24s
CI / CI Status (push) Failing after 1s
postbuild stamped 'use client' on every emitted file, including pure re-export
barrels. Next's flight loader hard-errors on 'export *' inside a client
boundary, so 8.0.32 broke every Next 16 consumer at dist/index.js. The client
boundary belongs to the leaves, which carry their own directive; a module whose
statements are only import/export now goes unstamped.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-29 17:22:00 -07:00
zooqueenandhanzo-dev 91f9d00676 ci: name the KMS path that has no token, and let it be corrected
CI / Build (push) Failing after 1m36s
CI / Type Check (push) Failing after 2m12s
CI / Lint (push) Successful in 2m35s
CI / Test (push) Failing after 1m42s
CI / CI Status (push) Failing after 2s
8.0.32's publish run failed here with a bare 'curl: (22) 404' and no
indication of what was missing. The token is not at hanzo/deploy/NPM_TOKEN --
deploy/ was inferred from the sibling workflows that read Cloudflare creds
from it, and inference is not knowledge. 8.0.32 reached npmjs by hand an hour
later, which is exactly the manual step this was meant to end.

So: the path is a variable (KMS_NPM_PATH, default deploy), settable on the
repo or the org, and the failure now names the full path it tried instead of
reporting a curl exit code. Dropping -f is deliberate -- with it, curl exits
22 on a 404 and the message that would have said WHERE never prints.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-29 06:19:21 -07:00
zooqueenandhanzo-dev bee0655dd3 ui: the web attributes and slots callers actually pass (8.0.33)
CI / Build (push) Failing after 1m51s
Publish Packages / detect-changes (push) Successful in 8s
CI / Type Check (push) Failing after 2m9s
CI / Lint (push) Successful in 2m25s
CI / Test (push) Failing after 1m56s
CI / CI Status (push) Failing after 2s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Failing after 1m15s
Follow-on to 8.0.32's title. Migrating hanzo.app off the shadcn package
surfaced four more props that reach the element at runtime but had no type,
so every call site either dropped the behaviour or declared the package as
an any-typed module -- and that shim hid 79 real errors.

- Button.type: not cosmetic. Inside a form the DOM default is "submit", so a
  control meant to do something else submits the form. Callers write
  type="button" to stop that; dropping it is a bug, not a lost attribute.

- Progress.indicatorClassName: the moving bar, not the track. Callers colour
  the bar by threshold, which the track's own className cannot express
  because they are different elements. Now reaches the Indicator.

- Toaster.richColors: inert, and declared inert, exactly like the className
  and style already beside it -- gui paints from tokens.

- ./dropdown-menu: the module was built and had no export entry, so it could
  not be imported at all.

Deliberately NOT added: a DOM onChange on Textarea. gui's field emits the
text, not a change event, and a comment there already records that the
DOM-only spelling was a type that never matched the runtime. The call sites
using it were silently dead; they move to onChangeText instead. Per-toast
position is likewise absent on purpose -- the viewport owns placement.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-29 06:13:42 -07:00
zooqueenandhanzo-dev 16d1e3edb3 test(ui/chat): cover the Composer's keyboard contract, and remove the wall that blocked it
CI / Build (push) Failing after 1m10s
CI / Type Check (push) Failing after 2m14s
CI / Test (push) Failing after 1m27s
CI / Lint (push) Successful in 3m53s
CI / CI Status (push) Failing after 1s
`render.test.tsx` excluded Composer on the grounds that a themed
`@hanzogui/lucide-icons-2` glyph always throws `Missing theme` under vitest, and
pointed at `pkg/ui/e2e/chat.spec.ts` for end-to-end cover instead. That file has
never existed anywhere in this repo. So the one widget carrying the IME fix had
no coverage of its own: `send.test.ts` proves the RULES, and nothing proved the
WIRING.

The throw was real, but it was ours. `@hanzogui/*` are inlined and resolve
through each package's `source` field to `src`, while `@hanzo/gui` stayed
external and resolved those same modules to `dist`. Two instances of the theme
context: the provider wrote one, every themed icon read the other. Inlining
`@hanzo/gui` alongside them leaves exactly one — one entry, and the wall is
gone. It also turns the whole-surface case in `backends/gui/render.test.tsx`
green, which had been red on main for the same reason and was being carried as
an accepted failing baseline.

`Composer.test.tsx` then mounts the real component against a live DOM and
dispatches real keydowns, because `sends` can be perfectly correct while the
field never calls it — `onKeyDown` reaches the textarea only because gui
forwards unknown props. Each assertion was checked by mutation: dropping
`keyCode` reddens only the Safari case, dropping `isComposing` only that one,
and unwiring `onKeyDown` reddens four.

pkg/ui: 20 files / 202 tests with 1 failing, to 21 files / 212 tests all green.
Nothing shipped changes — all ten `dist/chat/*.js` are byte-identical to the
published 8.0.31.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-29 00:46:53 -07:00
zooqueenandhanzo-dev 2d8856ad70 ui: Button types the tooltip, and publishing can actually authenticate (8.0.32)
CI / Lint (push) Successful in 2m51s
Publish Packages / detect-changes (push) Successful in 8s
CI / Test (push) Failing after 1m27s
CI / Build (push) Failing after 3m35s
CI / Type Check (push) Failing after 4m53s
CI / CI Status (push) Failing after 2s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Failing after 2m5s
Three things that all failed the same way -- by resolving to empty rather
than erroring.

1. publish.yml read secrets.NPM_TOKEN, which exists on NEITHER the hanzoai
   org nor this repo on git.hanzo.ai. An absent secret interpolates to the
   empty string, so npm publish ran unauthenticated and 401'd. That is why
   this package has never shipped from CI and every release was a hand
   publish. It now reads the token from KMS at run time, with the machine
   identity every other workflow here already uses -- one home for the
   secret, and nothing new to rotate.

2. The Hanzo-registry mirror asked for HANZO_REGISTRY_TOKEN, defined
   nowhere. The fleet name is REGISTRY_TOKEN (six other workflows, and the
   org carries it). The step guards on that variable being set, so it took
   the not-set branch and exited 0 on every run: the mirror to api.hanzo.ai
   has never happened, and said so only as a notice.

3. Button did not type the title attribute. It already REACHED the element
   -- unknown props are spread onto Frame, which forwards them -- but
   Frame's props come from the cross-platform stack and name no DOM
   attribute. So hanzo.app, which uses the tooltip at ~60 sites, declared
   the whole package as an any-typed module to compile, and that shim hid
   79 real type errors. Widening the type to match the behaviour is the
   smaller and truer fix.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-28 23:38:46 -07:00
zooqueenandhanzo-dev 2dfa1b0713 fix(ui/chat): sends() still lost the IME on Safari, and dropped force-send (8.0.31)
The extracted `sends()` was the whole point of the chat subpath — one helper so
no surface writes a fifth copy of Enter-to-send. It shipped with two faults
against the behaviour it was extracted from (chat's hooks/Input/useTextarea.ts).

Safari. The source reads three signals and says why:

    // NOTE: isComposing and e.key behave differently in Safari compared to
    // other browsers, forcing us to use e.keyCode instead
    const isComposingInput =
      isComposing.current || e.key === 'Process' || e.keyCode === 229

The extraction kept only `isComposing`. Safari does not set it on the keydown
that accepts a candidate, so on Safari the widget still submits the half-typed
word out from under a Japanese, Chinese or Korean writer — the exact bug the
subpath exists to end, preserved in the thing built to end it. Composer had to
be fixed too: it never forwarded `keyCode`, so no amount of care inside `sends`
could have seen the signal.

Force-send. The source sends on `isNonShiftEnter || isCtrlEnter`, and
`isCtrlEnter` never consults shiftKey. The extraction returned false for any
modifier, so Cmd/Ctrl+Enter — which every surface already taught its users —
silently stopped sending. A test asserted that regression as intended.

`ready` is untouched. IME ownership is its own predicate rather than more
clauses in the boolean: "is this keystroke the IME's?" is a different question
from "does this keystroke send?", and only the first one is browser-quirked.

Measured: build exit 0; chat tests 17 passed (was 14). The one failing suite in
pkg/ui (backends/gui render.test.tsx, "Missing theme.") fails identically at
7fed30e5 without these changes — pre-existing, not a regression here.

8.0.30 and 8.0.29 are committed but were never published: npmjs still serves
8.0.28, which has no ./chat at all. This goes out as 8.0.31.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-28 23:23:25 -07:00
zandhanzo-dev 7fed30e52d release(ui): 8.0.30 — publish the chat surface
CI / Build (push) Failing after 1m10s
CI / Lint (push) Successful in 2m20s
CI / Test (push) Failing after 1m7s
Publish Packages / detect-changes (push) Successful in 6s
CI / Type Check (push) Failing after 2m49s
CI / CI Status (push) Failing after 2s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Failing after 1m19s
First publish from this repo since 8.0.28. The three faults that blocked it are
fixed in 3bf47e6f8 (trigger globbed the wrong package root, the build skipped its
workspace dependencies, TypeScript 7 broke rollup-plugin-dts); that commit
deliberately carried no version change so the pipeline could be proven before
anything shipped. It was: the forge picked up task 7936 for hanzoai/ui, which
only happens if the corrected path filter matched.

What this makes reachable: `@hanzo/ui/chat` — Thread, Message, Composer, Sidebar
(+Header/NewChat/Scroll/Section/Item/Folder/User), Header (+ShareButton/
AsideToggle/Aside), Code, Sources. hanzo/console already declares
`@hanzo/ui: ^8.0.17` and `@hanzo/gui: 7.3.0`, the exact peers this needs, so it
resolves the moment this lands. Until now `^8.0.17` resolved to 8.0.28, which has
no `./chat` export at all — verified against the published tarball.

8.0.30, not 8.0.29: the repo already sat at 8.0.29 unpublished, and
detect-changes compares HEAD~1 to HEAD, so republishing that same number is not
expressible. 8.0.29 never existed on npmjs, so the skipped number references
nothing.

Pre-publish gate, run locally with the exact command CI uses:
  pnpm --filter '@hanzo/ui...' build   -> "pkg/ui build: Done"
  dist/chat/                           -> 50 files
  dist/chat/index.{js,cjs,d.ts}        -> present, .d.ts exposes 30 names
  ./chat export                        -> points at dist/, and files:["dist"]
                                          is what the 8.0.28 tarball actually
                                          ships (1102 dist files, no src/)
That last check matters: a source-mapped export would resolve here and 404 for
every consumer, because src/ is not in the tarball.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-28 22:34:51 -07:00
zandhanzo-dev 3bf47e6f85 fix(build): @hanzo/ui could not build or publish — three faults, one chain
CI / Type Check (push) Failing after 1m39s
CI / Lint (push) Successful in 2m15s
CI / Test (push) Failing after 1m29s
Publish Packages / detect-changes (push) Successful in 8s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Skipped
CI / Build (push) Canceled after 2m28s
CI / CI Status (push) Canceled after 0s
deploy / image (push) Failing after 48s
Nothing has published from this repo since 8.0.28. 8.0.29 sits in git with the
new `./chat` entrypoint while npmjs serves 8.0.28, so every consumer resolving
`^8.0.17` silently gets a version without it — an import error in the dependent
and nothing at all here. Three independent faults, each hiding the next.

1. THE PUBLISH TRIGGER COULD NEVER FIRE FOR @hanzo/ui.
   `on.push.paths` matched `pkgs/*/package.json` only, and @hanzo/ui lives at
   pkg/ui (singular). The detect-changes job was already fixed to walk BOTH
   roots — its comment even says a `pkgs/*` glob made pkg/ packages "invisible …
   so no version bump of either could ever reach npmjs" — but the gate that
   decides whether that job runs was left matching one root. Fixed in the body,
   not in the thing that gates the body.

2. THE BUILD DID NOT BUILD ITS DEPENDENCIES.
   `pnpm --filter @hanzo/ui build` runs alone against siblings with no dist/.
   They type themselves through "types": "dist/index.d.ts", so tsc reports them
   as missing modules and buries the cause under unrelated-looking errors:
     src/product/SiteNav.tsx: Cannot find module '@hanzo/products'
     src/core/tokens.ts:      Cannot find module '@hanzo/tokens'
     src/gitops.ts:           Cannot find module '@hanzo/cd'
   plus a wave of implicit-any in the files that imported them, which reads like
   a source defect in @hanzo/ui and is not. Now `--filter <pkg>...`, which builds
   workspace dependencies in topological order.

3. TYPESCRIPT 7 BROKE EVERY tsup PACKAGE.
   Once the dependencies actually built, they died in rollup-plugin-dts, the
   declaration generator tsup uses:
     TypeError: Cannot read properties of undefined (reading 'useCaseSensitiveFileNames')
   TS7 changed the compiler-host API it reaches into. That killed @hanzo/cd,
   @hanzo/products and @hanzo/tokens, and @hanzo/ui cannot compile without their
   emitted types. The TS7 bump (405280fd4) had reached 2 of 19 packages here;
   the other 17 pin 5.x. Pinned root and pkg/ui back to ^5.9.3 to match them.

   That in turn required an explicit `moduleResolution` in tsconfig.cjs.json:
   it had been relying on TS7 dropping the TS5095 rule that `bundler` needs an
   ES module target, so under TS5 the CJS pass errored before emitting anything.
   Both are noted inline as the pair to revert together, once tsup ships a
   TS7-compatible rollup-plugin-dts.

Verified: `pnpm --filter '@hanzo/ui...' build` succeeds end to end; dist/ holds
50 chat files including the .cjs and .d.ts outputs. tsc errors dropped 15 -> 1
(0 in src/chat) — TS5 is cleaner here, not merely older. Tests 198 passed / 1
failed, and that is an improvement: src/chat/render.test.tsx previously could not
even load (@hanzogui/toast/v2 unresolvable) and now passes; the remaining failure
is a pre-existing missing Tamagui theme provider in
src/backends/gui/render.test.tsx, unrelated to compiler version.

No version bump in this commit on purpose — this proves the pipeline first. The
bump that actually publishes 8.0.29's contents comes next.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-28 22:29:14 -07:00
zandhanzo-dev e3cece8380 feat(ui/chat): Sidebar, Header, Code and Sources — and "Ask anything"
CI / Lint (push) Successful in 2m35s
CI / Type Check (push) Failing after 3m22s
CI / Test (push) Failing after 1m43s
deploy / image (push) Failing after 17s
Publish Packages / detect-changes (push) Successful in 7s
Publish Packages / publish (${{ fromJson(needs.detect-changes.outputs.packages) }}) (push) Skipped
CI / Build (push) Failing after 2m59s
CI / CI Status (push) Failing after 2s
Extends the chat shell rather than restating it. Thread, Message and Composer
already landed in 806770476; what a surface still had to build itself was
everything around the turns:

  Sidebar   + Header / IconButton / NewChat / Scroll / Section / Item /
              Folder / User
  Header    + HeaderButton / ShareButton / AsideToggle / Aside
  Code      + CopyButton
  Sources   + SourceChip

Composed of parts rather than fed a tree, because surfaces genuinely disagree
about what a conversation list holds — hanzo.chat groups into folders, the
console groups by project, hanzo.app is flat. A `sections` prop would have to
model all three; parts let each arrange its own and still get identical rows.
Same reason Code takes no highlighter and Header takes an action slot.

Composer's resting placeholder is now `ASK` = "Ask anything", replacing "Send a
message". It addresses the person rather than the mechanism, and being constant
it never has to be recomputed or re-announced when the model changes — which is
what a model-named placeholder forces a surface to do.

Three things this cost, all now encoded so the next component does not repay
them:

* gui has no `flexShrink` prop — `shrink` is the shorthand. The type error
  names the whole prop object, so it points at the element, not the prop;
  bisecting one prop at a time is what actually finds it.
* `title` is a web-only DOM attribute gui does not type, and it is the only
  hover affordance an icon-only control has. Added `tip()` beside the existing
  `slot()`, which exists for exactly this reason. `aria-label` is not a
  substitute — it names the control for assistive tech and renders nothing on
  hover, so both are set.
* There is no `$mono` font token, only `$body` and `$heading`. Code sets a real
  monospace stack through `style`; `$body` would put code in a proportional
  face where columns stop aligning and l/1/I stop being distinguishable.

Every colour is a `$` token, so brands retune through their own theme and
nothing here carries a Hanzo mark — the same shell ships on Lux and Zoo without
leaking a brand across.

Verified: tsc clean (0 errors in src/chat, repo at its 15-error baseline);
send.test.ts 11/11 pass. render.test.tsx fails to load on an unresolvable
`@hanzogui/toast/v2` import in backends/gui/toaster.tsx — confirmed identical on
a stashed tree, so it is pre-existing and untouched by this change.

Stays 8.0.29: that version is not on the registry yet (latest published is
8.0.28), so these ride in it rather than opening a gap for a version that never
shipped.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-28 20:52:42 -07:00
zooqueenandhanzo-dev 8067704764 feat(ui): the chat shell lives here — Thread, Message, Composer on @hanzo/gui (8.0.29)
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-28 20:38:44 -07:00
zooqueenandhanzo-dev 92fbf33d59 fix(ci): the publish lane could not see pkg/ui, and its filter matched nothing
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-28 20:38:41 -07:00
zooqueenandhanzo-dev b68c2b8664 fix(ui): TypeScript 7 removed moduleResolution node10 — the CJS pass inherits bundler
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-28 20:22:09 -07:00
hanzo-dev 5dbdb29435 ui: one home for shadcn — the legacy copy leaves this monorepo
pkgs/ui was @hanzo/ui-shadcn 5.9.2, a second copy of a component set that was
already extracted to hanzoai/shadcn and published as @hanzo/shadcn. Keeping both
is the exact duplication the consolidation set out to end, and CONSOLIDATION.md
step 5 already called for it.

@hanzo/ui-shadcn is deprecated on npm (5.9.0 and 5.9.1) pointing at @hanzo/shadcn
and @hanzo/ui@8. The two dependents in this repo — the docs/registry app and
pkgs/commerce — resolved it as `workspace:@hanzo/ui-shadcn@^`; they now resolve
the published `npm:@hanzo/ui-shadcn@^5` instead, so nothing they import moves.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-28 18:55:36 -07:00
hanzo-dev 2cc85a6e51 ui: the 44px floor, the drag, and the tree-shake are real on web too (8.0.28)
Every critical/high the review confirmed, fixed at the one place that owns it.

touch() and drag() — backends/gui/gesture.ts. gui classes `hitSlop` as a
native-only prop and DROPS it on web (nativeOnlyProps -> skipProps ->
getSplitStyles), so twelve components claimed a 44px target and shipped 16-36px
in a browser and in Tauri. touch(size, min, axis) now returns hitSlop on native
and data-touch-x/-y on web, backed by transparent ::after rules in theme.css —
no layout shift, no padding hack. The same split fixes the ScrollArea thumb,
which was wired only to the react-native responder system and could not be
dragged at all on web; both it and ResizableHandle now go through one drag().

Button renders on gui's Button.FRAME, not the compound. The compound is
Frame.styleable(), i.e. an HOC: styled() over it does not compile style props to
classes, so `asChild` emitted backgroundcolor="var(--background)" on the child
and styled nothing. asChild is now expressed through gui's `render`, which
merges — Button-as-link comes out as a real styled <a>, asserted in a test.
`fontSize` moved off the frame onto the Text host, so it stops leaking a
font-size="" attribute and `sm` is finally smaller than `default`.

ModelSelector is on style props. It shipped nine Tailwind class strings with no
rules behind them and sized its panel from --radix-popover-trigger-width, a
variable nothing defines now that Radix is gone; the panel measures the trigger.
theme.css drops its Tailwind v4 `@theme inline` block, cn() drops tailwind-merge.

The component surface MOUNTS in CI: render.test.tsx puts all 24 components under
GuiProvider in jsdom and asserts their data-slot markers, the absence of leaked
style attributes, and the touch attribute. 181 -> 185.

Also: DropdownMenu's spec form regained its minWidth=200 default; the product
menu item matches the backend row at 32px and meets the tap floor; ./components
was a byte-identical alias of "." and is gone; react-native-web is declared as an
optional peer (13 packages in the closure import it and nothing declared it);
postbuild stops stamping 'use client' on DATA modules, which is what kept
createGui() from ever running in a prerender; hz-elevation-4 has a rule.

Measured on the packed tarball: `import { Button } from '@hanzo/ui'` costs
77,447 bytes against @hanzo/gui's own Button at 74,651 — 0 chunks, 0 neighbour
components. 0 shadcn, 0 @radix, 0 @apply, 0 Tailwind utilities; 79/79 exports
targets resolve.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-28 18:53:32 -07:00
hanzo-dev 174bf3010f ui: build @hanzo/ui with plain tsc — unbundled per-file ESM, 0 chunks (8.0.28)
tsup is gone. `pnpm build` is two tsc passes (ESM+types, then CJS with
--noCheck) plus scripts/postbuild.mjs, which resolves relative specifiers to
fully-specified paths, folds the CJS emit into dist as .cjs, and stamps
'use client' on every module.

Bundling a LIBRARY defeats the consumer's tree shaking: tsup's code splitting
emitted 11 shared chunk-*.js, so `import { Button } from '@hanzo/ui'` dragged
in chunk-RCMDRI6V.js (48K of source). Measured with esbuild against the packed
tarballs, that import costs 4717 bytes from 8.0.27 and 2021 from this build.
Output now mirrors src/ one-for-one — 220 .js, 220 .cjs, 220 .d.ts, 0 chunks —
so every exports subpath resolves by construction, with no entry list to drift.

rollup-plugin-dts (tsup's dts worker) was also the one thing that could not
run on TypeScript 7; declarations come from tsc, so pkg/ui builds on 7.0.2.

pkgs/canvas 0.2.2: it ships raw TS as its entry, so a consumer's compiler sees
its side-effect CSS import. Its css.d.ts was never loaded (nothing imports it);
index.ts now references it, which TS 7 requires (TS2882).

Verified on TS 7.0.2 against the pnpm-packed tarball: build exit 0, 181 tests
pass, all 24 exports subpaths (111 with primitives expanded) bundle under both
the import and require conditions, ESM + CJS load in Node, and a consumer
typechecks clean under moduleResolution bundler.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-28 18:25:51 -07:00
hanzo-dev 2cfc6fc28d ui: drop tailwind-merge and fix the @hanzo/products workspace pointer
cn() had a tailwind-merge step to collapse conflicting Tailwind utilities. On
@hanzo/gui there are none - it styles through props, not class names - and the
whole library called cn() from one file. clsx alone composes correctly.

@hanzo/products was declared workspace:* but no such package exists in this
workspace (it lives at pkgs/, and is published at 0.2.0), so tsc could never
resolve it and the build failed on main.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-28 17:51:45 -07:00
hanzo-dev 8f85c84f9b feat(ui): one substrate — @hanzo/ui renders on @hanzo/gui, shadcn is cut out (8.0.27)
The root barrel, `./primitives`, `./primitives/*` and `./components` were three
doors into the same Radix + Tailwind room. They now open onto the gui backend,
which is the only component surface left.

- gen-primitives.mjs reads src/backends/gui/index.ts as its single source of
  truth; the 90 per-member entrypoints regenerate from it (idempotent).
- src/backends/shadcn (25 files) is deleted. It lives in hanzoai/shadcn.
- 16 components rewired onto their @hanzogui/* primitive (avatar, button, card,
  checkbox, collapsible, dialog, input, label, popover, progress, select,
  separator, slider, switch, tabs, tooltip) to join the 8 net-new ones.
- ONE DropdownMenu: the compound surface grew the declarative `trigger`/`items`
  form that used to be a second component in product/menu, and renders the spec
  through its own parts. `@hanzo/ui` and `@hanzo/ui/product` export the same one.
- Dependencies: @hanzo/ui-shadcn, 18 @radix-ui/* packages, cva, cmdk, sonner and
  lucide-react are gone (ModelSelector moved to @hanzogui/lucide-icons-2).
- ./billing, ./wallet and ./network were re-export shims into @hanzo/ui-shadcn
  and are removed with it; nothing on 8.x imported them.
- Every exports target is now a real file in the tarball. 18 of them pointed at
  ./src/* which `files: [dist]` never shipped, so ./components, ./models, ./core,
  ./tokens, ./theme.css and all 90 ./primitives/* were broken on the registry.
- Module scope is side-effect free (/* @__PURE__ */ on forwardRef/createContext,
  no displayName assignment), so one symbol no longer drags its neighbours in.

Verified: build exit 0, 181 tests in 17 files pass, 0 shadcn and 0 @radix-ui
references in the packed tarball (was 107 files), 447/447 export targets resolve
(8.0.26: 18 broken), one-symbol import bundles 4.7KB against 29.4KB for the
whole barrel.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-28 16:49:16 -07:00
hanzo-dev 81ac687370 ui: drop the Svelte backend — it ships as @hanzo/svelte now
52 components, the ./svelte export, the svelte + svelte-check dependencies, the
check:svelte script and tsconfig.svelte.json all leave. They live on at
github.com/hanzoai/svelte as @hanzo/svelte@1.0.0, extracted with their history
and verified at 0 svelte-check errors.

A component library should be one stack. This one declared peer svelte>=5 while
devDepending svelte ^4.2.20 — two answers to one question — and every consumer of
@hanzo/ui resolved svelte whether they rendered a Svelte component or not. Nothing
in this package imported the backend; only docs referenced it.

Verified this removes nothing else: the build reports the same 16 pre-existing
errors before and after (missing @hanzo/tokens, @hanzo/ui-shadcn/*, @hanzo/products
and four implicit-any in SiteNav), and dist still emits.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-28 12:35:00 -07:00
hanzo-dev f72516c6e6 feat(ui): converge on ONE ./framework entry — 8.0.24's surface, superset behind it
8.0.24 shipped a first lift of the DocType renderer at `@hanzo/ui/framework`
while this one landed at `@hanzo/ui/doctype`. Two entries for one concept is the
thing we do not do, so this collapses them: the published name `./framework`
wins, and the implementation behind it is the superset.

Every 8.0.24 name still resolves, so nothing that consumes it breaks:
  · `Transport` aliases `FrameworkTransport` — one seam, two spellings.
  · `Loader` aliases the in-flow `Loading`.
  · `setupDescription`/`setupBullets` go back to OPTIONAL. The defect was never
    that a default existed; it was that the default was ONE LANE'S copy, so an
    ERP org read about Pages and Posts. Unset now derives from the lane's own
    `label`, which is true for every lane.
  · `renderBuilder`/`renderMedia` still win when supplied — but a host that
    supplies neither now gets the real built-in `CollectionBuilder`/`MediaGrid`
    instead of no affordance at all.

What the superset adds on top of 8.0.24: the content-type builder, the DAM over
a `MediaStore` port with a bucket PARAMETER, the mobile-first card list and
container-measured layout, the 44px tap floor, the `inListView` column
projection, the metadata-driven media document body, the docstatus gate on
Edit/Delete, and a `./framework/core` entry that binds the engine with no React
and no @hanzo/gui in scope.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-28 11:13:37 -07:00
hanzo-dev 346a9d1504 feat(ui): the DocType renderer is a library, and it is mobile first
Every host that renders the Hanzo Framework engine (hanzoai/cloud
clients/framework, live at /v1/framework/*) needs the SAME list, card, detail,
builder and DAM — a CMS Page, an ERP Sales Order, a Helpdesk Ticket and a CRM
Company are one kind of thing with one renderer. It lived in the console. Now it
lives here once, as `@hanzo/ui/doctype`, and an app lane is a `module` filter
plus its own copy.

MOBILE FIRST is a rule, not a fallback. The layout is decided from the
CONTAINER's measured width (onLayout — a 420px detail rail on a 1440px screen is
a phone, and a viewport media query cannot know that), and the answer to "not
measured yet" is PHONE. So the first paint, SSR included, is a stacked card per
record; the @hanzo/data table is the enhancement applied once the box proves it
can hold one. Server and client render the same tree, so there is no hydration
mismatch. Every control clears the 44px tap floor (WCAG 2.5.5) at phone width,
long unbroken values wrap instead of running off the screen, and the builder's
field row stacks into labelled bands rather than crushing six controls onto one
line.

Three defects the move fixed, each of them the kind a shared library prevents:

  · The table rendered EVERY non-hidden field as a column. `listHiddenFields` —
    the DocType's own `inListView` projection — was implemented and unit-tested
    but never passed to a view, so a twelve-field ERP document rendered twelve
    columns. That is the whole reason it was a horizontal scroll wall.
  · MediaGrid wrote a hardcoded {title,file,mime,size,width,height} — the CMS
    Media fixture's schema. The engine drops unknown keys silently, so a lane
    whose media type labels its rows `caption` created untitled rows, quietly.
    `mediaDocPayload(dt, facts)` now writes the DocType's OWN title field and
    only the facts it declares.
  · Edit and Delete rendered on a submitted document, which ops.go refuses
    ("document is not a draft (docstatus %d); cannot edit"). A user filled in a
    whole form and got an error card on Save. Both are gated on `isDraft`, and
    the surface says why the actions are gone.

Ports, not bindings. The client takes a FrameworkTransport (paths relative to
the framework root — the host picks the origin and the credential); the DAM takes
a MediaStore and a BUCKET PARAMETER, because the constant it used to carry was
named after one lane and ERP attachments were about to land in `cms-media`;
routing is onOpen/onCreate/onBack/onView. The first-run copy lost its default for
the same reason a default lane is the wrong default.

Two entries because they are two different things: ./doctype/core is the
contract (types, client, mapping, builder projection, media model) and imports no
React and no @hanzo/gui, so a data layer or a node test can bind the engine
without loading a component tree; ./doctype re-exports it plus the views.

SelectMenu gains `required` (the null row dropped — a required picker and a
filter differ only in whether "none" is an option, so it stays one control rather
than growing a fourth select), plus `minHeight`/`disabled`/`ariaLabel`.

78 new unit tests (159 total, all green): the mapping, the card projection, the
metadata-driven media body, the docstatus precondition, the transport's exact
paths, the media port against a fake store, and the mobile-first layout rule.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-28 11:09:43 -07:00
hanzo-dev acb9b7ff97 feat(ui): a button's own label names its event — interpolated labels were anonymous
The live capture came back correct but nameless:

    {"event":"feature_used","properties":{"component":"PrimaryButton","action":"click"}}

no `id`. The console's sign-in CTA is `<PrimaryButton>Log in with {brand}</PrimaryButton>`,
which React hands the component as an ARRAY, and `typeof children === 'string'`
is false for an array — so every interpolated label (which is most of the
branded ones) reported as an unnamed click. A lens could count presses but never
say WHICH button, which is nearly the whole value.

`labelOf` flattens the string/number parts and trims; non-textual children (an
icon element) still yield undefined rather than a guess. Shared by PrimaryButton
and MenuItemView so both name their events the same way — one rule, not two.

8.0.21 -> 8.0.22. 8 unit tests.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-28 09:43:26 -07:00
hanzo-dev 6487987bde fix(ui): PrimaryButton wrapped its own wrapper — one real click blew the stack
Found by clicking it in a real browser, which is the only place it could be
found: the unit tests exercise `emit`, and a type-checker is perfectly happy
with a function that calls itself.

    const onPress = (e) => { track(...); props.onPress?.(e) }   // closes over `props`
    props = { ...props, onPress }                               // ...then REBINDS it

`props.onPress` inside the closure resolves at CALL time, and by then `props` is
the new object whose `onPress` is the closure. So the handler invoked itself
forever. On console/signin one press produced

    RangeError: Maximum call stack size exceeded

and thousands of duplicate feature_used events queued behind it. Instrumentation
that changes behaviour is worse than none — an observer must observe.

Destructuring `onPress` out of the props first makes the original handler a VALUE
captured at render, not a lookup on a mutable binding, so there is nothing left
to recurse into. Values, not places.

8.0.20 -> 8.0.21.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-28 09:37:57 -07:00
hanzo-dev 6f09d4a3b5 fix(ui): publish with pnpm — npm ships workspace:* and breaks every consumer
8.0.19 (and 8.0.17 before it) went out via `npm publish`, which copies
package.json verbatim. `pnpm publish` is the one that rewrites the workspace
protocol to a concrete version. So both tarballs declared

    "@hanzo/products": "workspace:*",
    "@hanzo/tokens":   "workspace:*"

and any pnpm consumer outside this monorepo died on install:

    ERR_PNPM_WORKSPACE_PKG_NOT_FOUND  "@hanzo/products@workspace:*" is in the
    dependencies but no package named "@hanzo/products" is present in the workspace
    This error happened while installing the dependencies of @hanzo/ui@8.0.19

Caught by hanzoai/console, the first consumer to resolve fresh rather than from a
lockfile. 8.0.20 is republished through pnpm and resolves to @hanzo/products@0.2.0
and @hanzo/tokens@1.0.0.

A note in a doc would not have stopped the next one, so the fix is a
`prepublishOnly` that refuses the wrong tool at the only moment it matters.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-28 09:16:03 -07:00
hanzo-dev b0a09ffd35 feat(ui): instrument the SHARED components, so 100 products report for free
Every product was flying blind past the pageview. The telemetry client
(@hanzogui/telemetry, already a dependency here) has always been able to send
product events — nothing was calling it. So the choice was: instrument 100 apps,
or instrument the components all 100 apps already render.

This is the second. One new module, src/product/instrument.ts, is the ONE place a
shared component says what a user did, and the 17 components below now call it:

  DataTable       sort (column + next direction), select (row + list size)
  PrimaryButton   click (labelled by its own children — no app names it)
  SlideOver       open / close, keyed by the drawer title
  ConfirmDelete   confirm / cancel / error (the one irreversible action)
  Segmented       filter (which pill)
  SearchInput     search (query LENGTH, never the query)
  ComboBox        open / close / select (+ how many options were on screen)
  MenuItemView    select — every dropdown and context menu in the fleet
  OrgSwitcher     select (which org a session moved to)
  ThemeToggle     change
  Toast           an error toast is a product OUTCOME, not chrome
  EmptyState      click on either CTA — the zero-data conversion point
  Field{Text,TextArea,Switch,Select,Slider}
                  change, named by the FieldRow label the caller ALREADY typed

ONE event name, not 17. @hanzo/event's taxonomy is a closed set whose own rule is
"the product-specific moment is the `action` property, never a new event name" —
so every interaction is EVENTS.FEATURE_USED carrying {component, action, id,
value, surface}. Funnel moments (signup, checkout, deploy) keep their dedicated
names and stay owned by the surface that runs them, not by a button. A closed
verb set is what lets one funnel join across products.

Privacy is in the primitive, not in a reviewer's memory: free text is reported as
`textSize()` — a LENGTH — so a search box and a password field emit the shape of
the behaviour and never its content. Transport, consent, DNT/GPC and SSR-safety
all come from @hanzogui/telemetry, which is fail-soft by construction, so no
component here adds an error policy of its own.

`<InstrumentSurface value="billing">` is the only thing an app may optionally add;
with no provider the events still carry component+action, and product+path already
come from the client. Nothing else to wire, nothing to remember, nothing to drift.

Proven: 5 unit tests on the wire shape, and all 17 emit sites verified present in
the built bundle. 8.0.17 -> 8.0.19 (8.0.18 is the published latest).

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-28 09:04:46 -07:00
zandhanzo-dev dc7a49854e lint: stop enforcing React Compiler rules on vendored shadcn (48 errors -> 0)
The Lint job has never once passed — 7 runs, 7 failures — on 48 errors that are
all in apps/v4, and apps/v4 is a VENDORED upstream mirror. Every flagged file
arrives from a sync commit ("feat: sync upstream shadcn/ui"): hooks/use-layout.tsx
and examples/{base,radix}/** from b74fc5759, components/command-menu.tsx from
fd9d7843f. The examples/base and examples/radix copies are byte-identical to each
other, so a third of the list is the same file counted twice.

eslint-config-next 16 turns the React Compiler rules on. Upstream shadcn does not
satisfy them, so we were linting somebody else's code against a stricter config
than they use. Patching it would fork ~20 vendored files from upstream and have
to be redone at the next sync, to quiet advisories in a demo app.

This is not a new exemption, it is finishing an existing one. The config already
turned off react-hooks/incompatible-library and react-hooks/purity for precisely
this reason, and TWO files already carried inline
`// eslint-disable-next-line react-hooks/set-state-in-effect`. The same rule was
being suppressed already — just inconsistently, in 2 of the 20+ places it fires.
Those two inline directives are now redundant and are removed here; eslint
flagged them itself once the rule went off.

  set-state-in-effect  "Calling setState synchronously within an effect can
                        trigger cascading renders"                  42 of 48
  refs                 "Cannot access refs during render"            6 of 48

Scope is exactly apps/v4: that config governs nothing else. Our own source —
app/, pkgs/, packages/ — lints under its own config, which does NOT disable any
react-hooks rule, and still passes. Nothing we author is exempted.

If these rules should hold for vendored code too, the fix belongs upstream in
shadcn/ui rather than as a local fork of 20 files.

Verified with the workspace eslint (9.39.4, the version CI installs — a stray
npx picks up 10.5.0 and crashes eslint-plugin-react before linting anything):
`turbo run lint` is 2 successful / 2 total, 0 errors, 0 warnings.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-28 04:21:39 -07:00
zandhanzo-dev 3c0c98d8d3 ci: publish with GHCR_TOKEN/GHCR_USER — REGISTRY_TOKEN never existed
The image job has failed on every run with

  ::error::REGISTRY_TOKEN is unset, so nothing was published. Add the org secret.

The guard is doing its job; the name is simply wrong. The hanzoai org defines
exactly GHCR_TOKEN, GHCR_USER, GH_PAT, KMS_CLIENT_ID, KMS_CLIENT_SECRET,
OCI_TOKEN and OCI_USER. There is no REGISTRY_TOKEN and never was, so the
suggested remedy — add the org secret — would have minted a second credential
for the registry every other repo already logs into with GHCR_*.

Only ui and papers invented that name. Both now use the GHCR_* pair, and take
the username from GHCR_USER instead of hardcoding hanzo-dev, so rotating the
publishing identity stays a single org-level change.

Tagging is untouched: still the short HEAD sha, which is what a CR pins.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-28 03:41:56 -07:00
zandhanzo-dev d15b84c760 ai-voice: local Speech API types, so the file stops depending on lib.dom version
This file has now broken from BOTH directions in one day, which is the useful
part of the story.

Declaring the Web Speech names inside `declare global` MERGES with whatever
lib.dom already provides rather than shadowing it, so on a lib that HAS them
every small difference is an error: `readonly` vs mutable (TS2687), a second
`[index: number]` (TS2374), and `error: string` against the real
`SpeechRecognitionErrorCode` union (TS2717) — 7 errors.

Deleting the declarations fixed that locally and immediately broke CI the other
way, on a lib that does NOT carry them:

  TS2552: Cannot find name "SpeechRecognitionEvent"

Same file, same code, opposite failure — because the Speech API is only partly
standardised and how much of it ships varies by TypeScript version. There is no
"correct" set of globals to declare; the premise was wrong.

So the types are module-LOCAL now (SpeechRecognizer, SpeechRecognitionEventLike,
…), describing exactly the surface this component touches. Local names collide
with nothing and depend on nothing. The constructor and `webkitAudioContext` are
read through narrow casts instead of a `Window` augmentation, for the same
reason: lib.dom may already declare those properties, and re-declaring them with
a different type is itself the error.

Behaviour is unchanged except that the missing-API paths are now explicit —
`getSpeechRecognizerCtor()` returns undefined instead of `"webkitSpeechRecognition"
in window` guarding a `window.SpeechRecognition` read, and a browser with no
AudioContext throws a named error rather than `new undefined()`.

Verified: app typecheck is 0 errors under BOTH the workspace TypeScript (5.9.3,
what node resolves) AND 7.0.2 (the binary CI actually invokes). pkgs/ui stays 0.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-28 03:10:12 -07:00
15d5c98e15 ci: make the type check and build actually pass (16 errors -> 0)
The CI added yesterday has never gone green. Three separate causes, all real —
nothing here suppresses a check.

1. Bare specifiers had no mapping. `types/nav` and `types/unist` are imported
   as bare specifiers and only ever resolved via an implicit project root.
   The obvious fix is `baseUrl`, and it is wrong: TypeScript 7 REMOVED the
   option and says so itself —

     TS5102: Option baseUrl has been removed. Please remove it from your
             configuration. Use "paths": {"*": ["./*"]} instead.

   So the mapping is explicit in `paths`, which resolves relative to the
   tsconfig and needs no baseUrl. This also cleared every TS7006 implicit-any
   error, because those were downstream: an unresolved module widens to `any`,
   and mapping over `any` reports each parameter separately. 8 errors, one
   cause.

2. ai-voice.tsx re-declared Web Speech types that lib.dom now ships. Inside
   `declare global` a re-declaration MERGES with the built-in rather than
   shadowing it, so every difference is an error: `readonly` vs mutable
   (TS2687), a second `[index: number]` (TS2374), and `error: string` against
   the real `SpeechRecognitionErrorCode` union (TS2717). Removed the four the
   platform provides; kept the vendor `webkit*` aliases and the constructor,
   which it still does not.

3. CI built one hardcoded package instead of the app dependency graph, so both
   the typecheck and the Build job failed on
   `Cannot find module @hanzo/event` / `Module not found: Can't resolve @hanzo/event`.
   @hanzo/event resolves to the workspace copy, whose package.json points types
   at ./dist/index.d.ts, and nothing built it. Now `turbo run build
   --filter=@hanzo/ui-web^...` — the set is derived from the workspace graph, so
   it stays correct when an import is added rather than needing another name
   pasted in.

Verified locally: app typecheck 16 errors -> 0, and pkgs/ui `tc` is already 0
against its own tsconfig.build.json (my first reading of 92 was me running the
wrong tsconfig).

Not fixed here: the Lint job, whose log simply ENDS mid `pnpm install` at
"Packages: +2770". That is a killed job, not a lint failure, and it needs the
runner looked at rather than the code.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-28 02:58:51 -07:00
zandhanzo-dev 7accfa799f ci: re-run — the previous run was killed, not failed
Its log ends mid-step with no error line, no failure marker and no summary:
the truncated signature of a killed job. It ran during the window when
git.hanzo.ai was down (the forge pod was preempted and then blocked on an RWO
Multi-Attach while a storage-migration job held its volume), so the runner lost
the control plane mid-build.

Nothing in this repo changed. Empty commit to get a clean signal now the forge
is back.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-28 01:15:48 -07:00
zandhanzo-dev 5b0e1a5ce1 ci: upload/download-artifact v3 — v4 cannot run on this forge
git.hanzo.ai is Gitea, which presents itself as GHES, and the v4 artifact
actions refuse on that basis before transferring anything:

    @actions/artifact v2.0.0+, upload-artifact@v4+ and download-artifact@v4+
    are not currently supported on GHES

That makes the step fail for a reason unrelated to whatever the job was
checking. hanzoai/papers is the clearest case: every paper compiled and the run
printed "missing: 0", yet the run was red because this one step could not
execute — a CI failure that looks exactly like the thing the job exists to
catch.

v3 speaks the artifact protocol the forge implements. Names, paths and
retention are unchanged.

Swept the estate for the same pattern: bot, engine, kms, papers and ui all
carried it.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-28 00:30:19 -07:00
hanzo-dev ebda15a5c2 feat(ui): ship the ONE type/radius/space scale with the components (8.0.17)
gui.config.ts lived in the console. That was fine while the console was the only
app rendering @hanzo/ui/product; the dedicated Hanzo Social app makes it a fork
waiting to happen — two admins would render the same components at different
sizes, radii and spacing, and nobody would notice until a screenshot.

So the scale ships with the components as `@hanzo/ui/gui-config`, and the console
imports it instead of declaring it. @hanzogui/config joins peerDependencies,
since the scale is built on its v5 defaults.

Also exports the social surface at its own subpaths, so an app that only renders
Publish does not pull the whole product barrel:
  ./product/social      the surface (React)
  ./product/social/api  the contract alone — imports NOTHING, so a data layer
                        (and its node tests) can bind it without a component tree.
Two entries because they are two different things, not two ways to one thing.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-27 23:29:08 -07:00
hanzo-dev 1606dfbd13 feat(ui/product/social): the whole surface, not just its parts (8.0.13)
The social pieces were extracted here, but the thing that MAKES them a product
was not: the /v1/social client and the orchestration that fetches, lays out and
mutates. Those lived only in the console (src/lib/api/social.ts + a 601-line
SocialModule), so a second host — the dedicated social.hanzo.ai app — could only
copy-paste them, and this folder's own types had already forked three ways: a
`Post` in PostCard, a `ProviderCapability` in ProviderReadinessList, a
`SocialSummary` in SocialSummaryBar, none of them the shape the backend sends.

So the contract moves in and the parts read from it:

- api.ts — the ONE typed /v1/social contract (cloud clients/social): the domain
  types, the defensive normalizers, and `createSocialApi(rest)`. The TRANSPORT is
  injected: paths are relative to /v1/social and the host owns the origin, the
  credential and the error class, so this layer never picks any of them. A post's
  `media` round-trips (cloud's PUT rebuilds the row from the body — dropping it
  would wipe it).
- PostCard / PostAgenda / ProviderReadinessList / SocialSummaryBar / PostComposer
  now import their types from api.ts; ChannelBadge's hand-written network union
  becomes the backend's ONE ordered PROVIDERS list.
- SocialResource — the whole product, assembled from those pieces: summary bar,
  list + calendar, compose/schedule/publish-now, connect, detail drawer, honest
  loading/empty/BackendState. Sibling of CommerceResource. Every host renders THIS.

Nothing here imports a router, an auth module or an app's ~/lib, so the console
and the standalone app are the same component with different transports.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-27 23:09:27 -07:00
hanzo-dev d238cb67f9 cd: publish the CD surface under its real name
The package was never published under EITHER name — `npm view @hanzo/gitops`
and `@hanzo/cd` both returned E404 — so `pkg/ui/src/gitops.ts`
(`export * from '@hanzo/gitops'`) and the `>=0.1.0` peer dep pointed at
nothing and broke every clean install of @hanzo/ui.

Publishing exposed a second break one level down: @hanzo/gitops peers on
@hanzo/canvas >=0.2.0, and the registry still had canvas at 0.1.0 while the
monorepo was on 0.2.1. Publishing the first package alone only moved the
ETARGET deeper. Both are now on the registry, and a clean
`npm install @hanzo/ui@8.0.12` resolves — 673 packages, no errors.

pkgs/gitops -> pkgs/cd and @hanzo/gitops -> @hanzo/cd: the surface is the CD
surface (cd.hanzo.ai, apps/cd), and the old name only ever described the
mechanism. Every reference in the monorepo moved with it; the old name is
deprecated on npm pointing here rather than left as a second way to install
the same code.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-27 20:26:53 -07:00
zandhanzo-dev 405280fd44 build: typescript ^7.0.2 (native compiler)
TypeScript 7 is the native Go compiler; the npm package is a shim resolving a
platform-specific native binary. turbo drives per-package bundlers, so tsc is
typecheck-only here and no published artifact changes.

Gated on the package turbo actually builds, not the repo-root config — that
root is a settings base nothing compiles, and running tsc against it sweeps
apps/, templates/ and deprecated/ for a meaningless six-figure count.
pkgs/ui: 92 -> 92 errors, so the compiler swap introduces nothing new.

Co-Authored-By: Hanzo Dev <dev@hanzo.ai>
2026-07-27 19:37:04 -07:00
hanzo-dev 096fbe4d0d fix(ui/product): one idiom for a web DOM ref on a Gui view — the cast, not @ts-ignore
SlideOver reached for `@ts-ignore` where menu/items.tsx already solved the
identical problem with `as never`. A bare `@ts-ignore` suppresses whatever
error appears on that line, including ones nobody has seen yet; the cast says
exactly which assumption is being made and keeps every other check live.
Two idioms for one problem is one too many.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-27 18:51:01 -07:00
hanzo-dev 92c455e0b4 Merge branch 'sweep/product-admin-surface' into feat/social-ui-extraction
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-27 18:47:29 -07:00
hanzo-dev faf3d1433c fix(ui/product): ship the component classes with the components; @ts-ignore the Gui ref
hz-mono/hz-tnum/hz-paper/hz-menu-in/hz-row were only defined in the console's
globals.css, so every OTHER host of these components rendered them unstyled.
They now live in the package's one stylesheet next to hz-skeleton, with the
elevation variables falling back so a host that defines its own --hz-* still wins.

SlideOver's panel ref used @ts-expect-error, which is itself an error wherever the
consumer's resolved @types/react accepts the ref (the commerce admin does).

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
(cherry picked from commit 9e4557bcd7)
2026-07-27 18:47:00 -07:00
hanzo-dev 1c951d3848 feat(ui/product): surfaces.data + @hanzo/products dep — the launcher list stays canonical
(cherry picked from commit 359f96ab34)

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-27 18:47:00 -07:00
hanzo-dev 242f4612b2 feat(ui/product): the ONE admin component set — console's ahead versions + the honest-state/list layer (8.0.12)
@hanzo/ui/product is the single component set both the console and the standalone
Commerce admin render. Console's src/components/ui was a byte-drifted fork that had
run AHEAD of this layer; that drift is folded back in, and the admin-only pieces that
only existed in the console are hoisted here host-agnostically:

- DataTable  — sortable headers, skeleton rows, mono/right-aligned numeric columns
- PageHeader — responsive wrap (actions take their own line on phones)
- Charts/ComboBox/SelectMenu/Metric/StatusTag/Field/SlideOver/EmptyState/PrimaryButton
  — console's refined typography, hz-paper menus, wider status vocabulary
- accent      — the org-accent external store PrimaryButton reads (pure, SSR-safe)
- host        — the ONE seam an app injects its router/auth effects through, so this
                layer never imports next/navigation or an auth module
- BackendState — honest /v1 failure states, classified duck-typed on  so any
                 client's error class works
- ConfirmDelete / Filters — the one destructive-confirm panel, the one segmented+search
- CommerceResource — the ONE store-list surface (console Store category + Commerce admin)
- OrgMark/OrgSwitcher/scope/menu — brought forward from main for the shared admin chrome

exports now resolve types from src as well as code, so a workspace consumer builds
against the source with no prebuilt types step (one artifact, no drift).

(cherry picked from commit b4f189bcb2)

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-27 18:46:44 -07:00
hanzo-dev 52d91f4e3b fix(build): one working build order — drop the duplicate @hanzo/data, declare the icon peer
Three defects, all of the same shape as the duplicate `dependencies` key: a
second copy of something silently shadowing the real one.

- pkgs/data was a second workspace project ALSO named @hanzo/data (private
  0.1.0, a strict subset of pkg/data@1.2.1 which superseded it — see
  CONSOLIDATION.md). pnpm tolerated the collision; turbo refuses to build a
  graph with it ("Failed to add workspace @hanzo/data ... it already exists"),
  so `pnpm build` at the root has been dead, and with it the ONE mechanism
  that orders pkgs/{tokens,products} ahead of pkg/ui. Nothing linked it. Gone.

- turbo.json `typecheck` declared no dependsOn, so it ran before its deps'
  .d.ts existed; and `build` listed only dist/** as an output while pkg/data
  emits to types/**, so a cache hit restored nothing and left @hanzo/ui
  unable to resolve @hanzo/data. Both edges now declared.

- @hanzogui/lucide-icons-2 is imported by 16 files in pkg/ui/src and shipped
  in dist/product/index.js, but was only a devDependency: `require('@hanzo/ui
  /product')` from a clean consumer install threw MODULE_NOT_FOUND. It sits on
  the gui 7.x train and pulls @hanzogui/core@7.3.0, so it goes where
  @hanzogui/next-theme already is — peerDependencies, one core per app — not
  into dependencies, which would license a second copy.

From a wiped dist tree: turbo run build --filter=@hanzo/ui... -> 4/4 tasks,
then FULL TURBO restores pkg/data/types from cache. In pkg/ui: npx tsc
--noEmit -> 0 errors, npm run build -> ESM + CJS + .d.ts.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-27 18:45:54 -07:00
hanzo-dev d13e6b1d94 Merge remote-tracking branch 'origin/main' into feat/social-ui-extraction
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-27 18:42:17 -07:00
zandhanzo-dev c3f592e9fc fix(rpc): chain config calls /v1/, not the dead /ext/
luxd serves exactly one HTTP prefix and it is /v1. Measured 2026-07-27:
api.lux.network/ext/bc/C/rpc -> 404 (5/5 probes), /v1/bc/C/rpc -> 200
chainId 0x17871. So these callers were already broken, not merely legacy.

The bc/ segment is required (/v1/C/rpc is malformed and 404s), the env is
the hostname and never a path segment, and the brand is the hostname and
never a chain alias -- api.lux.network/v1/bc/hanzo/rpc is 404 now that each
L1 has its own sovereign gateway.

api.avax.network is deliberately untouched: it is a third-party host that
serves /ext/ (200) and not /v1/ (404). Rewriting it would break Avalanche.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-27 18:07:03 -07:00
zandhanzo-dev 93bd87dd92 build: TypeScript 7 — module=esnext + moduleResolution=bundler
`moduleResolution: node` is removed in TS7, so this base config could not
be used by the native compiler at all. esnext+bundler is the verified-clean
pair for a bundled monorepo (node16 was tried first and rejected: it forces
node16 module semantics and surfaced TS5097 across the template and fixture
trees this root sweeps).

The root is a settings base — `turbo run build` compiles the packages, not
this file. Verified on the packages that actually build, TS7 vs TS5.9:
commerce 0/0, shadcn 3/3, ui 92/92 — identical. TypeScript 7 introduces no
new errors here; the 92 in pkgs/ui are pre-existing.

Co-Authored-By: Hanzo Dev <dev@hanzo.ai>
2026-07-27 18:07:02 -07:00
zandhanzo-dev b39a2ec318 build: migrate tsconfig to TypeScript 7 (native compiler)
TypeScript 7 is the native Go compiler and removes `baseUrl` and
`moduleResolution: node|node10`. Both appear here, so `tsc` from TS7
refuses the config outright (TS5102 / TS5108) and cannot typecheck.

`paths` targets resolve relative to `baseUrl` when it is set and relative
to the tsconfig file otherwise. Every `baseUrl` folded here already
pointed at the config's own directory, so dropping it moves nothing and
the targets are left byte-identical. Where a baseUrl pointed elsewhere,
each affected target was rewritten as join(baseUrl, target).

`moduleResolution` was chosen from the declared `module`: commonjs ->
node16, esnext/preserve -> bundler. Configs whose `module` is unset or
exotic were left alone rather than guessed at.

The result is accepted by BOTH toolchains, so nothing has to upgrade
TypeScript in lockstep. Verified on hanzo/chat packages/api: tsc 5.9
779 -> 778 errors (no regression), and tsc 7.0.2 now runs the project
in 2s where it previously refused the config.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-27 18:07:02 -07:00
4b42816bce fix: class name typo (#398)
* fix: class name

* fix: class names order

---------

Co-authored-by: shadcn <m@shadcn.com>
2026-07-27 18:07:02 -07:00
hanzo-dev 3821f58797 fix(ui): restore the 24 dependencies a duplicate JSON key silently deleted
pkg/ui/package.json carried TWO "dependencies" objects. JSON is last-key-wins,
so the 3-entry block appended after "author" in 57cc6541b (the telemetry
subpath) silently overwrote the real 24-entry block added in 1ee64263f (the
shadcn backend + core/tokens). @hanzo/ui therefore declared none of the
packages its own source imports:

  npx tsc --noEmit  -> 43 errors
  npm run build     -> "Build failed with 25 errors" (ESM and CJS alike)

Nothing was wrong with the components. Every error was an unresolvable import:
Radix/cva/clsx/tailwind-merge/cmdk/sonner from src/backends/shadcn + src/core/cn
-- which the ROOT barrel re-exports (src/index.ts -> ./backends/shadcn) -- plus
@hanzo/tokens. The TS2339 on ButtonProps and the TS7006 implicit-anys were
cascades off those unresolved modules, not separate defects.

Merging the two blocks into one restores the manifest AND the bundling: tsup
externalizes declared dependencies, so Radix is now imported by dist/index.js
instead of inlined into it. Two copies of @radix-ui/react-dialog in one app
means two React contexts and dead dialogs.

@hanzo/tokens also moves file:../../pkgs/tokens -> workspace:*. pnpm rewrites
the workspace: protocol to a real version on pack; a file: specifier publishes
verbatim and resolves for nobody.

pkgs/{tokens,products}/tsconfig.json: drop ignoreDeprecations "6.0". Both
declare typescript ^5.9.3, where the only accepted value is "5.0", so tsup's
dts worker died with TS5103 and these two never emitted the .d.ts that
@hanzo/ui imports. Neither config uses an option deprecated in 5.0 -- the
escape hatch was inert dead weight that only broke the build.

Gates in pkg/ui: npx tsc --noEmit -> 0 errors; npm run build -> ESM+CJS+dts
success from a clean dist; vitest 45/45. The 8 social components verified
through the published exports map (consumer tsc + esbuild bundle).

Most of the pnpm-lock.yaml diff is pre-existing churn -- a plain `pnpm install`
with zero source changes rewrites 3169 lines on this repo. The delta owed to
this fix is +594/-20; the removals are Radix peer-hash dedup (one copy, not two).

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-27 17:14:48 -07:00
hanzo-dev 643c180793 feat(ui/product/social): the publish surface as reusable parts (8.0.12)
Extend the existing @hanzo/ui/product/social home so the social surface is
assembled from shared parts instead of living only inside console's
SocialModule. Presentational and host-agnostic per the layer's contract —
data and handlers are injected, nothing reaches for an app's ~/lib.

- format.ts: the ONE pure time/preview module (formatPostTime, postDayBucket,
  postPreview, parsePostTime) + 12 unit tests. Names are social-scoped to keep
  the product barrel a single collision-free namespace. PostCard's private
  near-duplicate date formatter folds into it.
- SocialSummaryBar, ViewToggle, PostAgenda, PostComposer,
  ProviderReadinessList: lifted from console's SocialModule.
- PostComposer owns the form state, the validation, and the one mapping from a
  compose intent to the (status, scheduleAt) the backend stores; the host keeps
  persistence and failure classification via an injected total onSubmit.
- ProviderReadinessList renders publish-readiness straight from the server's
  report, so a deployment missing OAuth-app credentials shows exactly which
  ones — never a fabricated "connected".
- Channel gains ChannelLike: the badge already fell back to `x` for unknown
  values at runtime, so the type now says what it really accepts and callers
  holding a plain `string` from the API no longer have to narrow first.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-27 15:50:52 -07:00
hanzo-dev e0423f6e3a ci: drop the Gitea mirror-sync nudge
Superseded: the Hanzo GitHub App pushes a webhook, so the forge tracks GitHub
without a per-repo workflow. This file called git.hanzo.ai/api/v1/.../mirror-sync
— a Gitea API for a system we no longer drive — and would sit inert in every repo.

One mechanism, in one place, instead of ~350 copies of a cron.
2026-07-27 10:46:08 -07:00
zeekayandhanzo-dev 18d9436b5e ci: restore the pkgs/* publisher and add the ui image build
publish.yml was deleted this morning without a replacement. It is the sole
publisher of every non-private @hanzo/* in pkgs/* — @hanzo/ui, @hanzo/event,
tokens, shadcn — and its loss left them with no producer anywhere: no workflow, no
failing run, just packages that would stop moving. It is back, byte-identical, at
.hanzo/workflows/publish.yml where the forge reads it.

ci.yml comes back too, minus the deploy-preview job that ran `vercel deploy` on
every pull request; previews come from our own stack or not at all, and the status
job never depended on it. Its test job now runs pnpm test:coverage, so the lcov it
already uploaded actually exists — which is what coverage.yml was separately
running the same suite to produce. One workflow does it.

registries.yml keeps the apps/v4 registry check. Its other job labelled and
commented on pull requests through the gh CLI, an API the forge does not serve.

deploy.yml is new, and it is the piece that never existed: crs/ui.yaml in
hanzoai/universe is live and promoted, but no workflow ever built the image it
pins — every tag up to v5.7.6 was pushed by hand. It publishes
ghcr.io/hanzoai/ui:<sha> from the Dockerfile already in the repo and stops there. A
build never deploys itself; the CR names the tag that serves.

.github/workflows now holds the canonical sync.yml alone.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-27 08:46:04 -07:00
hanzo-dev 5fe1ddfc58 ci: nudge git.hanzo.ai to pull on push
git.hanzo.ai mirrors this repo by PULL on a ~10-minute interval, and arcd runs
CI/CD there — so every push waited out that interval before anything built.
This asks Gitea to pull HEAD immediately.

Latency only: the repo already mirrors via the App webhook, so a missing
HANZO_GIT_TOKEN or a failed curl is non-fatal and never fails the push.
Idempotent (mirror-sync just pulls HEAD) and concurrency-coalesced.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-27 08:01:06 -07:00
hanzo-dev 2f6b25aad2 event: resolve the DSN from product — 0.3.4
An app declares WHAT it is; this package knows WHERE its errors go. Declaring
`product: 'console'` is now sufficient to report errors — no DSN literal, no
build argument, no per-app config.

That indirection is what makes it shippable. NEXT_PUBLIC_* is inlined at BUILD
time, and the native builder passes exactly ONE build arg (VERSION, from the
image tag — clients/platform/k8s.go). There is no mechanism to hand an image a
DSN, which is why the sibling ARG NEXT_PUBLIC_EVENT_INGEST_KEY has sat dead in
the Dockerfiles. A committed registry sidesteps the gap entirely.

A browser Sentry DSN is PUBLIC by construction: it ships in the client bundle,
is readable in devtools on any deployed page, and grants only event submission
— never reads. Committing it records a public identifier, it does not leak a
secret. (The server-side collector DSN in team-analytics-sentry stays a Secret;
that one is genuinely not public.)

Resolution order, most specific first: explicit `dsn` > NEXT_PUBLIC_HANZO_EVENT_DSN
> product registry. An unregistered product stays inert rather than guessing a
destination and posting one surface's errors into another's project.

Registered: console -> hanzo-console, app -> hanzo-app, site -> hanzo-ai. A
literal map, not `hanzo-${product}` — `site` lives in hanzo-ai, and a derivation
rule plus an exception table is the same data with a trap in it.

core.test.ts now uses the deliberately-unregistered product `test-app`, so those
tests exercise the client rather than whichever real products are registered.

typecheck clean; 100 tests pass; all 3 DSNs verified present in dist/.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-27 07:59:30 -07:00
hanzo-dev 88078cbb45 ci: remove GitHub CI — native git.hanzo.ai + cd.hanzo.ai only
Inherited upstream workflow, repointed at a self-hosted ARC pool at fork time.
ARC is being decommissioned: CI runs inside git.hanzo.ai (Gitea Actions,
.hanzo/workflows) and delivery is cd.hanzo.ai reconciling the reviewed image
pin in hanzoai/universe. GitHub is a mirror and runs nothing.

This repo has no .hanzo/workflows today; it is a fork whose upstream CI we do
not rely on. If it needs a build, it gets a native pipeline — not an ARC pool.
2026-07-27 07:27:54 -07:00
hanzo-dev 43634bb30f ci: remove GitHub CI — native git.hanzo.ai + cd.hanzo.ai only
Inherited upstream workflow, repointed at a self-hosted ARC pool at fork time.
ARC is being decommissioned: CI runs inside git.hanzo.ai (Gitea Actions,
.hanzo/workflows) and delivery is cd.hanzo.ai reconciling the reviewed image
pin in hanzoai/universe. GitHub is a mirror and runs nothing.

This repo has no .hanzo/workflows today; it is a fork whose upstream CI we do
not rely on. If it needs a build, it gets a native pipeline — not an ARC pool.
2026-07-27 07:27:52 -07:00
hanzo-dev 94d6553c00 ci: remove GitHub CI — native git.hanzo.ai + cd.hanzo.ai only
Inherited upstream workflow, repointed at a self-hosted ARC pool at fork time.
ARC is being decommissioned: CI runs inside git.hanzo.ai (Gitea Actions,
.hanzo/workflows) and delivery is cd.hanzo.ai reconciling the reviewed image
pin in hanzoai/universe. GitHub is a mirror and runs nothing.

This repo has no .hanzo/workflows today; it is a fork whose upstream CI we do
not rely on. If it needs a build, it gets a native pipeline — not an ARC pool.
2026-07-27 07:27:49 -07:00
hanzo-dev 0b9555ff28 ci: remove GitHub CI — native git.hanzo.ai + cd.hanzo.ai only
Inherited upstream workflow, repointed at a self-hosted ARC pool at fork time.
ARC is being decommissioned: CI runs inside git.hanzo.ai (Gitea Actions,
.hanzo/workflows) and delivery is cd.hanzo.ai reconciling the reviewed image
pin in hanzoai/universe. GitHub is a mirror and runs nothing.

This repo has no .hanzo/workflows today; it is a fork whose upstream CI we do
not rely on. If it needs a build, it gets a native pipeline — not an ARC pool.
2026-07-27 07:27:47 -07:00
hanzo-dev 1778d98195 ci: remove GitHub CI — native git.hanzo.ai + cd.hanzo.ai only
Inherited upstream workflow, repointed at a self-hosted ARC pool at fork time.
ARC is being decommissioned: CI runs inside git.hanzo.ai (Gitea Actions,
.hanzo/workflows) and delivery is cd.hanzo.ai reconciling the reviewed image
pin in hanzoai/universe. GitHub is a mirror and runs nothing.

This repo has no .hanzo/workflows today; it is a fork whose upstream CI we do
not rely on. If it needs a build, it gets a native pipeline — not an ARC pool.
2026-07-27 07:27:45 -07:00
hanzo-dev 986f71b9c9 ci: remove GitHub CI — native git.hanzo.ai + cd.hanzo.ai only
Inherited upstream workflow, repointed at a self-hosted ARC pool at fork time.
ARC is being decommissioned: CI runs inside git.hanzo.ai (Gitea Actions,
.hanzo/workflows) and delivery is cd.hanzo.ai reconciling the reviewed image
pin in hanzoai/universe. GitHub is a mirror and runs nothing.

This repo has no .hanzo/workflows today; it is a fork whose upstream CI we do
not rely on. If it needs a build, it gets a native pipeline — not an ARC pool.
2026-07-27 07:27:43 -07:00
hanzo-dev bfa989a7b2 ci: remove GitHub CI — native git.hanzo.ai + cd.hanzo.ai only
Inherited upstream workflow, repointed at a self-hosted ARC pool at fork time.
ARC is being decommissioned: CI runs inside git.hanzo.ai (Gitea Actions,
.hanzo/workflows) and delivery is cd.hanzo.ai reconciling the reviewed image
pin in hanzoai/universe. GitHub is a mirror and runs nothing.

This repo has no .hanzo/workflows today; it is a fork whose upstream CI we do
not rely on. If it needs a build, it gets a native pipeline — not an ARC pool.
2026-07-27 07:27:41 -07:00
hanzo-dev 6047656650 ci: remove GitHub CI — native git.hanzo.ai + cd.hanzo.ai only
Inherited upstream workflow, repointed at a self-hosted ARC pool at fork time.
ARC is being decommissioned: CI runs inside git.hanzo.ai (Gitea Actions,
.hanzo/workflows) and delivery is cd.hanzo.ai reconciling the reviewed image
pin in hanzoai/universe. GitHub is a mirror and runs nothing.

This repo has no .hanzo/workflows today; it is a fork whose upstream CI we do
not rely on. If it needs a build, it gets a native pipeline — not an ARC pool.
2026-07-27 07:27:39 -07:00
hanzo-dev 4a9f4546f1 event: raise the floor to ^0.3.3 (>=0.3.2 has the error envelope)
@hanzo/event <=0.3.1 shipped NO Sentry envelope code — captureError() collected
and dropped. 0.3.2 added it.

This repo already RESOLVED to 0.3.3 (the caret ranges permit it), so nothing
changes here at install time. The defect is in what the observe packages
PUBLISH: declaring ^0.3.0 lets any downstream consumer legitimately resolve to
0.3.0 or 0.3.1 and silently get a dead error plane. The floor now states the
real requirement.

pnpm install --frozen-lockfile green; @hanzo/observe 28 tests pass.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-26 23:47:07 -07:00
hanzo-dev 57d3a5a451 fix(ci): refresh pnpm-lock — a stale lockfile has been blocking EVERY publish
Publish Packages fails at its first step:

  ERR_PNPM_OUTDATED_LOCKFILE  Cannot install with "frozen-lockfile" because
  pnpm-lock.yaml is not up to date with pkgs/products/package.json

CI sets frozen-lockfile by default, so this is not a warning — it is a hard stop
before anything is built or published. Nothing in pkgs/* has been able to reach
npm since it appeared, which is why @hanzo/products sat at 0.2.0 and
@hanzo/ui-shadcn at 5.9.1 while main moved on.

It is accumulated drift, not one mistake. 4c2599d2 added @types/node to
pkgs/products without updating the lockfile, and pkg/ui/package.json has since
grown next-themes, svelte, svelte-check, @hanzo/products and @hanzogui/telemetry,
and moved @hanzo/canvas, @hanzo/dashboard, @hanzo/gitops, @hanzo/ui-shadcn and
@hanzo/usage from workspace:* to peer ranges — none of it recorded.

So the lockfile is regenerated rather than patched. The workspace:* -> range
entries look alarming in the diff but are the lockfile CATCHING UP to what
package.json already declares; no dependency is bumped by this commit. The
remaining version churn is peer-resolution hashes (@types/node@25.9.4 ->
25.9.5 inside vitest's key), not package upgrades.

Verified with the exact CI gate: `pnpm install --frozen-lockfile` now completes
across all 25 workspace projects.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-26 22:40:35 -07:00
hanzo-dev 1bdb153d27 feat(ui-shadcn): MarketingNav — one bar for the five properties that copied it
hanzo.id, hanzo.network, hanzo.one, sensei.group and hanzo.app each carried their
own 160-line DesktopNav.tsx. Measured, they were not variations on a theme:

  hanzo.one vs sensei.group   BYTE-IDENTICAL
  hanzo.network vs hanzo.app  2 lines — text-neutral-400 → text-purple-400
  hanzo.network vs hanzo.id   ~11 lines

Five copies of one menu, drifting an accent colour at a time. So the menus become
data (menus.ts), the accent becomes a prop, and there is one renderer.

ROUTER-AGNOSTIC by construction, which is the part that matters. The copies
hard-imported react-router-dom's <Link to>; that is precisely why hanzo.ai — a Next
app using <Link href> — could never share them and grew a SIXTH nav instead. The
host now passes `link`, so a Vite SPA, a Next app and a plain-anchor page use the
same component. External destinations bypass it and render a guarded anchor,
because a client router cannot navigate off-site — and the data carries `external`
so the renderer decides from the value, not from a string check at the call site.

Placed in the v5/Tailwind lane beside hanzo-shell because that is the lane these
properties are on. hanzo-shell is the SIGNED-IN app chrome (billing, account,
console, chat, platform); this is the signed-out marketing bar. Same repo,
different job — deliberately not a second take on either.

NOT sourced from @hanzo/products: that models the product FAMILY (six-product
launcher, installs, per-property HEADERS), a different menu with an overlapping
name. hanzo.ai's own nav proves the distinction, carrying Philosophy, Papers,
Startups and Security that no product catalogue knows about. One value model per
concern.

6 tests pin the data: bar order, every href absolute-or-rooted, `external` set iff
the URL is absolute, no duplicate destination within a column, glyph+note only on
the two featured rows.

5.9.1 -> 5.9.2 (npm latest is 5.9.1; patch forward).

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-26 22:31:09 -07:00
hanzo-dev 036b117e2c feat(ui): render the ecosystem shell — SiteNav + SiteFooter (re-land)
Re-land of 62608ec9, dropped when ui main was force-pushed. The commit object
survived but SiteNav.tsx/SiteFooter.tsx were gone from main entirely.

@hanzo/products already decides WHAT the navigation is (HEADERS per property,
MEET_HANZO_MENU, FOOTER). Nothing rendered it, so every property hand-wrote its
own bar: six DesktopNav copies, ten MobileMenu copies, fifty-seven footers, each
free to drift from the spec and from each other. This adds the missing half — the
renderer, and only the renderer.

  SiteNav     bar + full-bleed launcher + the small-screen arrangement
  SiteFooter  the six columns + legal bar

Three things the old menus got wrong, fixed structurally: the launcher is
FULL-BLEED (the old ones anchored a floating card to whichever word opened it, so
a wide panel hung off-centre); hover switches menus but never opens one; and there
is ONE call-to-action taken from SiteHeader.action — cloud.hanzo.ai was rendering
"Get API key" beside two separate "Sign in" buttons, which a single action makes
inexpressible.

Both sit at AppHeader's 52px, border and background, so the signed-out marketing
bar and the signed-in shell bar read as one object.

The products half is re-applied on TOP of upstream rather than restored from the
stale copy — main had since reworked hanzo.app's localNav, so only the two
additions carry forward: hanzo.app leads with Community, and Community becomes a
DESTINATIONS entry so the nav link and its footer twin resolve to one address.

138 products tests pass; renderer typechecks against the models.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-26 20:36:36 -07:00
hanzo-dev eb40a1c449 merge(ui): canonical --hz-* design tokens + embeddable world data-stream components
Collapses the shadcn, dash, world and legacy hz-ui palettes onto one canonical
--hz-* token source, so a theme change happens in exactly one file. Adds the
embeddable world components (market ticker, news stream, prediction market,
instability score) and the shared billing CreditModal.

The hardcoded dark palette main had been maintaining by hand is now derived:
token-proof.mjs resolves every alias chain and confirms the dark surface still
lands on rgb(0,0,0)/rgb(31,31,31), so the black-monochrome canon is unchanged.
Keeps the @hanzo/ui-shadcn 5.9.1 name and version over the branch's older
@hanzo/ui 5.7.5, and unions the ./account and ./world entry points.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-26 19:39:31 -07:00
hanzo-dev b8c5ee1ef1 merge(ui/product): social surface — ChannelBadge, PostCard, CampaignCard
The @hanzo/canvas half of this branch already landed on main; this brings the
remaining piece, the social product components, and re-exports them from
@hanzo/ui/product.

Keeps main's @hanzo/ui 8.0.11 and @hanzo/canvas 0.2.1 over the branch's older
versions, keeps usage living in @hanzo/usage/panel, and drops the leftover
changeset file.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-26 19:36:13 -07:00
hanzo-dev 11fea8ac71 merge(main): integrate the org-mark treatment alongside the landed branch work
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-26 19:31:30 -07:00
hanzo-dev 3215b6f467 merge(ui): backend-flexible @hanzo/ui — gui/tokens primitives + svelte and shadcn backends
Brings in the canonical component library: a shared design core on
@hanzo/gui + @hanzo/tokens with per-backend implementations (shadcn-compatible
web surface, the @hanzo/gui product layer, and a svelte backend), plus the
generated primitives set and theme.css.

Forward resolutions against main: keep the built dist/ packaging and 8.0.10
version over the branch's source-only exports; union the peer dependencies
(both next-themes and @hanzogui/next-theme are imported, both optional) and
the tsconfig build excludes; keep main's tsup build pipeline and add the
branch's gen:primitives script. Also drops the last changesets leftover — a
semver bump in package.json is the one publish trigger.
2026-07-26 19:30:39 -07:00
zeekayandhanzo-dev 10004da466 feat(ui): an org wears its OWN mark — one treatment, and a switcher that is the account control's peer
A customer's console must show the CUSTOMER's identity. `OrgMark` is now the one
organization treatment: the org's own logo when IAM carries one, else its
MONOGRAM on a neutral tile — the same rule @hanzo/iam's account widget applies to
a person, so a workspace and a user read as one system. It is never a house
glyph. The switcher's private copy of that avatar is gone (it split words on
whitespace alone, so `acme-labs` read "A" instead of "AL").

The switcher trigger is sized as the PEER of the account control — 44px tall, a
30px mark, the same type and the same hit area — because "which workspace" and
"who I am" are two halves of one identity, not a caption over a control.

New optional `current` prop: a host that has already resolved the org (display
name + logo) injects it, so the switcher and the chrome's org mark can never
disagree — and a user with no cross-tenant list still gets their own logo.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-26 19:28:43 -07:00
hanzo-dev ecad5a469f merge(publish): mirror @hanzo/* to the Hanzo registry alongside npmjs
Adds a best-effort second publish of the same tarball to
api.hanzo.ai/v1/packages/hanzo/npm so an install does not have to reach npmjs.
Skips with a notice when no token is configured; an already-published version
is treated as success. npmjs stays authoritative.

Keeps main's @hanzo/ui 8.0.10 and its ./oss entry point over the branch's
older 8.0.9 tree.
2026-07-26 19:27:22 -07:00
hanzo-dev 24862cb036 merge(products): canonical ecosystem-shell spec — one surfaces source, live-resolved addresses
Collapses SURFACES/HANZO_APPS into a single source in @hanzo/products and
resolves every shell address against the live properties. Adds header, footer,
menu, family, destinations and link modules with tests.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-26 19:26:00 -07:00
hanzo-dev 4abd4c6829 Merge origin/main
# Conflicts:
#	pkg/ui/package.json
#	pkg/ui/tsup.config.ts
#	pkgs/data/package.json

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-26 19:11:34 -07:00
hanzo-dev 69384cee93 feat(oss): one OSS catalog module, shared by every surface that shows it
The same 1000+-app open-source catalog is rendered by the console App Store,
platform.hanzo.ai and the public gallery at oss.hanzo.ai. Each carried its own
copy of the catalog shape, the normalizer, the asset-URL builders and a reader
for the blueprint's docker-compose.yml — three implementations of one format,
free to drift until two surfaces disagree about what a deploy will start.

@hanzo/ui/oss is that one implementation. It is pure and framework-free — no
React, no config import, the base URL is injected — so a React console, a Next
app and a plain static page can all use it, and it is testable without a DOM or
a network.

parseBlueprint stays a small structural reader rather than a YAML
implementation: what it cannot read is absent, never guessed, so a surface
degrades to "no blueprint detail" instead of asserting something false about
what will run. Environment KEYS only; the values are routinely secrets.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-26 19:10:13 -07:00
hanzo-dev 27c22c6639 fix(publish): make the npmjs release step idempotent
The desired state is "npmjs holds this version". A version already there
satisfies it, and npm versions are immutable, so EPUBLISHCONFLICT means the
release already happened — a re-run of the workflow, or a hand publish ahead of
the merge. That is success. Treating it as a failure turns every such merge into
a red release for a no-op.

Anything that is not a conflict still fails, and now fails harder: the step
confirms the version is actually queryable on npmjs afterwards rather than
trusting the exit code.
2026-07-26 15:19:28 -07:00
hanzo-dev 32428c9995 fix(products): resolve every shell address against the live properties
The draft pinned 23 addresses that 404: the installs pointed at hanzo.app
/download/*, the SDK index at docs.hanzo.ai/developers/sdks, the CLI and
quickstart references at docs.hanzo.ai/{cli,quickstarts}, the legal bar at a
/legal/<slug> namespace that is not served, and the App/Bot headers at nav
paths those properties do not have. Host-only assertions passed because they
never checked a path.

Every address now resolves (200, or 401 where the app is behind sign-in) and
agrees with the independent U table in @hanzogui/shell:

  installs      -> hanzo.ai/{download,extension,desktop,cli}
  sdks          -> hanzo.ai/sdks
  cli reference -> docs.hanzo.ai/docs/cli
  quickstarts   -> docs.hanzo.ai/docs/getting-started
  learn         -> hanzo.ai/learn
  apps          -> docs.hanzo.ai/docs/apps
  community     -> hanzo.app/community
  legal         -> hanzo.ai/{privacy,terms,cookies}

Drops the VS Code install and the showcase/changelog resources (no such pages
exist); support takes the freed slot. Corrects the hanzo.app and hanzo.bot
local nav and Bot's CTA to /get-started.

Adds addresses.ts — the one enumeration of every address the spec claims, each
tagged with the surface claiming it — so shell.test.ts guards resolve relative
nav against its own property, and addresses.live.test.ts (HANZO_LIVE_LINKS=1)
fetches all of them. That live check is what pins paths.

Resolves the platform launcher hosts from ORIGIN instead of six inline
literals, and derives each surface's subtitle from its own href, making
ORIGIN's "only place a host lives" invariant true and testable.
2026-07-26 15:18:26 -07:00
hanzo-dev 346dc77bb3 merge(blue/hanzo-products-spec): canonical Meet Hanzo ecosystem-shell spec
Conflict was pkg/ui/package.json alone: the branch adds @hanzo/products
(the collapsed SURFACES/HANZO_APPS source), main added @hanzogui/telemetry
after the branch was cut. Union — both belong.
2026-07-26 13:58:04 -07:00
hanzo-dev 24fe9a3d34 fix(publish): make the npmjs release step idempotent
The desired state is "npmjs holds this version". A version already there
satisfies it, and npm versions are immutable, so EPUBLISHCONFLICT means the
release already happened — a re-run of the workflow, or a hand publish ahead of
the merge. That is success. Treating it as a failure turns every such merge into
a red release for a no-op, which is exactly what the mirror step below already
reasons its way out of; this brings the authoritative step in line.

Anything that is not a conflict still fails, and now fails harder: the step
confirms the version is actually queryable on npmjs afterwards rather than
trusting the exit code.
2026-07-26 13:56:33 -07:00
hanzo-dev fa43ad4aa0 fix(products): resolve every shell address against the live properties
The draft pinned 23 addresses that 404: the installs pointed at hanzo.app
/download/*, the SDK index at docs.hanzo.ai/developers/sdks, the CLI and
quickstart references at docs.hanzo.ai/{cli,quickstarts}, the legal bar at a
/legal/<slug> namespace that is not served, and the App/Bot headers at nav
paths those properties do not have. Host-only assertions passed because they
never checked a path.

Every address now resolves (200, or 401 where the app is behind sign-in) and
agrees with the independent U table in @hanzogui/shell:

  installs      -> hanzo.ai/{download,extension,desktop,cli}
  sdks          -> hanzo.ai/sdks
  cli reference -> docs.hanzo.ai/docs/cli
  quickstarts   -> docs.hanzo.ai/docs/getting-started
  learn         -> hanzo.ai/learn
  apps          -> docs.hanzo.ai/docs/apps
  community     -> hanzo.app/community
  legal         -> hanzo.ai/{privacy,terms,cookies}

Drops the VS Code install and the showcase/changelog resources (no such pages
exist); support takes the freed slot. Corrects the hanzo.app and hanzo.bot
local nav and Bot's CTA to /get-started.

Adds addresses.ts — the one enumeration of every address the spec claims, each
tagged with the surface claiming it — so shell.test.ts guards resolve relative
nav against its own property, and addresses.live.test.ts (HANZO_LIVE_LINKS=1)
fetches all of them. That live check is what pins paths.

Resolves the platform launcher hosts from ORIGIN instead of six inline
literals, and derives each surface's subtitle from its own href, making
ORIGIN's "only place a host lives" invariant true and testable.
2026-07-26 13:54:20 -07:00
hanzo-dev 2630146cf8 merge(origin/main): Meet Hanzo ecosystem-shell spec
Only conflict was pkg/ui/package.json: both sides added a dependency at the
same line. Union — this branch's @hanzo/products (the collapsed SURFACES/
HANZO_APPS source) alongside main's @hanzogui/telemetry.

Lockfile is main's, unchanged: this branch carried none, so the merge simply
adopts it.
2026-07-26 13:38:06 -07:00
hanzo-dev 4e50e49cfe publish: give @hanzo/* a second home on our own registry
api.hanzo.ai/v1/packages is live and correct and holds nothing — a registry
nobody publishes to. Every @hanzo/* package goes to npmjs and stops there, so
an install of our own packages depends on npmjs being reachable.

Republishes the SAME tarball the npmjs step just built to
api.hanzo.ai/v1/packages/hanzo/npm. Same artifact, same version: no rebuild, no
re-version, and npmjs stays authoritative and untouched (the mirror runs with
its own --registry and its own npmrc, so a failure there cannot affect it).

Best-effort by construction, so it can land before the credential exists:
without HANZO_REGISTRY_TOKEN it prints a notice naming the secret and exits 0,
and it starts working the moment the token is added — no second change. An
already-published version counts as success, because the registry holding that
version is the desired state either way; anything else is a warning, never a
failed release.

Verified: actionlint reports exactly the four findings it reported before this
step (two self-hosted-label, two SC2086 in detect-changes), so nothing new; the
run block passes bash -n.

The same step belongs on the other package repos once the token exists; ui is
the largest publisher and the natural first.
2026-07-26 11:32:07 -07:00
z 57cc6541bd feat(ui): re-export telemetry at @hanzo/ui/telemetry (8.0.8 -> 8.0.9)
Telemetry already has one home, @hanzogui/telemetry. Apps that depend on
@hanzo/ui had to add a second dependency to reach it, which is how a surface
ends up defined twice.

Adds the subpath so the import is available with no new dependency:

    import { TelemetryProvider } from '@hanzo/ui/telemetry'

Pure re-export — no logic here, so there is still exactly one definition and
nothing to keep in sync. Declared as an optional peer, so it is only resolved
when the subpath is actually used.

Verified @hanzogui/telemetry@0.1.0 is published (source: gui/pkgs/telemetry).
2026-07-26 01:47:51 -07:00
z 0fa1a76b42 fix(data): stop pkgs/data from being publishable over the real package
Two packages in this repo claim the name @hanzo/data. pkg/data is 1.2.1,
matches npm, and is what pkg/ui depends on. pkgs/data is a 0.1.0 stub with
half the source — and it sits under pkgs/*, which publish.yml auto-publishes
on any version change. A bump there would ship the stub and npm would tag it
latest, silently downgrading every consumer.

publish.yml already skips private packages, so marking it private removes the
hazard without deleting anyone's in-flight work. This is the same shape as the
@hanzo/event incident: two homes for one name, the wrong one wired to ship.
2026-07-25 15:22:18 -07:00
zandhanzo-dev cf65e02cec docs(event): ship TAXONOMY.md in the tarball
The README links ./TAXONOMY.md as the canonical spec, but `files` omitted it,
so a consumer reading the package offline had a dead link. Rides the next
version bump; no version change of its own.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-25 15:16:46 -07:00
Hanzo AI ed68819d02 fix(event): 0.3.3 — survive a hostile throwable, and bump so the fix can ship
Two things, one of them a process bug that would have made the rest invisible.

1. A THROWN OBJECT IS UNTRUSTED INPUT. `name`, `message` and `stack` are
   ordinary getters and a throwable may define any of them to throw. 0.3.2 read
   them directly, so a getter bomb made buildSentryEvent throw and the crash
   report was lost on BOTH planes — measured 0/0. normalizeError is now total
   (every read guarded, String() coercion guarded) and BOTH planes share that
   one normalizer; previously each rolled its own reader, so the stream still
   lost a report the error plane had already survived. Measured 5/5 on the
   attack table: circular properties, throwing getter in properties, event
   transport throwing, getter bomb on the throwable, baseline.

2. THE VERSION HAD TO MOVE. 0.3.2 is already on npm, and publish.yml fires on a
   version CHANGE — so landing this as 0.3.2 would have published nothing while
   apps pinning ^0.3.2 kept resolving to the release without it. That is the
   same silent-gap shape as the original incident, one layer up. Bumped to
   0.3.3, package.json and version.ts together (they must never drift: the
   dimension that tells you WHICH client sent a batch would quietly lie).

Also bounds the email rule to the RFC 5321 maxima — same unbounded-backtracking
class as the creds-in-URL rule fixed in 0.3.2, on a long run of local-part-legal
characters that never reaches an '@'.

CHANGELOG now records 0.3.2 truthfully — it documented only the FUNNELS work,
while the release that actually shipped also carried the entire error plane, the
plane-isolation fix, the scrub DoS bound and the Luhn gate.

Verified against the live ingest, not asserted: a TypeError whose `stack` getter
throws, carrying circular `properties`, reported from the 0.3.3 build and landed
as a real fatal issue. 93 tests pass.
2026-07-25 15:12:39 -07:00
Hanzo AI ebc70ebcf5 fix(event): bound the email rule, survive a hostile thrown object
Two hardenings that hold regardless of how errors are transported.

The email pattern was the second quadratic backtracker after the creds-in-URL
rule: an unbounded local-part run that never reaches '@' cost 20ms on an 8KB
input, synchronously, per captured error. Bounded to the RFC 5321 maxima
(local-part 64, domain 255), which excludes no real address. With this and the
input cap, the whole scrub is 1-3ms at every input size (was 19ms -> 284ms ->
1108ms as input grew 8K -> 32K -> 64K).

normalizeError read err.name/message/stack directly. Those are ordinary getters
and the thrown object is the least trustworthy input this library handles — a
throwing getter took the entire report with it. Read each defensively and fall
back to a total String() coercion that a throwing toString cannot escape either.
2026-07-25 15:08:15 -07:00
Hanzo AI ac7a331acd Merge remote-tracking branch 'origin/main' into feat/event-error-plane
# Conflicts:
#	pkgs/event/package.json
#	pkgs/event/src/core.ts
2026-07-25 15:06:56 -07:00
zeekay b718cedfad fix(event): report errors before flushing, bound the scrub, correct the description
Three defects an adversarial review found in 0.3.2 before it shipped:

sendError ran after flush(), inside one try — a circular or throwing value in
properties killed BOTH planes and the buffered batch with it. It now runs first.

The URL-credentials pattern backtracked quadratically: 128KB of colon-rich text
froze the main thread for over a minute from inside captureError. Bounded.

The package description still advertised a server-side fan-out into Sentry that
does not exist. That claim is why nobody set a DSN and the fleet reported zero
errors; shipping it again would have taught the next integrator the same thing.
2026-07-25 15:04:25 -07:00
zandhanzo-dev 78eeef2ef7 feat(event): FUNNELS — the one funnel registry + the shared taxonomy (0.3.2)
Funnels become data next to the vocabulary that defines them, so the spec
cannot drift: every step names an EVENTS value and funnels.test.ts fails the
build otherwise. GOALS stop restating their steps — they carry a funnelId and
derive `funnel` from the registry.

Fixes the Signup goal: its funnel required signup_verified, which no surface
emits (IAM-internal), so step 3 was always empty and the goal read 0%.

New names, each load-bearing in a shipped funnel: login_completed (a returning
sign-in is not a signup), build_started (intent, distinct from app_created),
generation_completed/_failed (any model output, carrying durationMs),
deploy_succeeded/_failed (a live URL is its own event, never inferred),
model_switched (the strongest dissatisfaction signal a chat surface has).

A cross-origin funnel must declare join:'aggregate' — two origins mean two
anonymousIds for a logged-out visitor, so a per-person rate would be a lie.

VERSION had drifted to 0.3.0 while the package was 0.3.1, making libraryVersion
wrong on every event.

TAXONOMY.md documents the whole thing: naming convention, property rules,
identify/group semantics, the per-app funnels, and the exact emit site of every
event in hanzo.ai / hanzo.app / hanzo.chat.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-25 15:04:09 -07:00
Hanzo AI dce06b2781 feat(event): send errors to Sentry as a real envelope, not a phantom fan-out
@hanzo/event <=0.3.1 documented errors as "lensed server-side into ... error
tracking (sentry)" and shipped only a type:'error' event on POST /v1/event.
No such fan-out exists. Cloud's handler folds the exception into
properties.$exception, writes one row to the event warehouse, and stops
(clients/analytics has zero references to sentry or errortracking; the sentry
surface is a separate, unconnected route tree). Every Hanzo property inherited
that false claim, so the fleet reported ZERO errors to the Sentry dashboard.

Add the error plane the claim assumed: a captured exception is now ALSO framed
as a real Sentry envelope and POSTed to the DSN's ingest route,

  POST {dsn.origin}/v1/sentry/{projectId}/envelope/?sentry_key=<version>:<hmac>

which is what the o11y ingest actually registers. The envelope builder, DSN
parser and client-side scrubber are merged in from the unpublished fork that
had been carrying them in hanzoai/analytics (verified against the server's
parseEnvelope/normalizeEvent), so there is one implementation again.

- Both planes share one session and one subject id, so an error and the
  pageview before it join up. Never email/PII: secrets and PII are scrubbed
  before anything leaves the device; the server scrubs again.
- The DSN key rides ?sentry_key= only; the event stream's bearer/pk_ key is
  never attached to the error host. The two planes authenticate independently.
- No DSN => the plane is INERT, exactly as documented: nothing sent, nothing
  thrown, event stream untouched. errorPlaneEnabled exposes the wiring so an
  app can assert it instead of discovering the silence months later.
- Errors are sent one envelope per event, immediately. Batching a crash report
  is how you lose it.
- Transport gains an optional contentType; existing transports are unaffected.

Proven end to end against the live ingest, not asserted: the built client's
own bytes produced a real grouped issue (correct type, level and crash-site
culprit), and a browser at the production origin https://hanzo.ai delivered an
envelope with HTTP 200 that landed as an issue.

19 new tests cover the regression directly: inert-without-DSN, the derived
ingest URL, envelope framing and UTF-8 byte length, identity correlation,
fatal-vs-error level, scrubbing, credential isolation, and never throwing back
into the host app.
2026-07-25 14:27:45 -07:00
zeekay 68b455a693 fix(analytics): carry a publishable ingest key, so events are accepted
Removing Vercel analytics left the site posting to /v1/event with no
credential, and every batch came back 403 — telemetry replaced with
telemetry that ingests nothing.

The door does not trust the request Host on purpose, because a Host
header is spoofable, so a static page proves its org by carrying a
publishable key. pk_ keys are write-only and HMAC-verified with no
database hop, which is what makes one safe inside a public bundle. The
build supplies it, so each deployment reports as the org that built it,
and a build without one stays inert instead of posting rejects.

Verified against prod: POST /v1/event with a pk_ returns
{"accepted":1,"dropped":0} on both the header and beacon paths.
2026-07-25 14:04:47 -07:00
zeekay 43ecb8a415 build: static 0.5.2 — compressed responses and a real cache policy 2026-07-25 13:37:47 -07:00
zeekay ef17494e8c chore: lockfile without rainbowkit 2026-07-25 13:19:05 -07:00
zeekay ce093cd655 feat(web3): connect over EIP-1193, drop RainbowKit and WalletConnect
The docs site pulled in RainbowKit, which bundles a WalletConnect
connector. That connector phoned pulse.walletconnect.org on every page
load — third-party telemetry from a documentation page — and it could
not have worked anyway: the project id fell back to the literal
'YOUR_PROJECT_ID'.

The browser wallet already speaks EIP-1193, so wagmi's injected
connector is all the identity demo needs: the extension is the only
party in the flow, with no bridge service and no vendor id. ConnectWallet
replaces RainbowKit's modal using our own Button.

wagmi and viem stay — they are how the page reads and writes contracts.
Verified: a clean build exports 8,879 files and the shipped JS contains
no walletconnect reference.
2026-07-25 13:18:55 -07:00
zeekay db582992b0 build: serve on static 0.5.1, which resolves a directory index in place
On 0.4.1 every HTML route answered 301 -> /index.html, so the internal
filename showed up in the address bar and in the URLs Next derives for
route prefetches.
2026-07-25 13:04:19 -07:00
zeekay d1e902c9ac build: serve ui.hanzo.ai from our own stack
The site was on Cloudflare Pages as a direct upload — no git connection,
last deployed 2026-03-28 — while a GitHub Pages workflow ran on every
push against a repo that has Pages disabled (the API 404s). Two deploy
paths, neither of which shipped what main said.

One way now: a Dockerfile builds the static export and serves it with
ghcr.io/hanzoai/static, exactly like every other Hanzo static site, on
our own runners and our own ingress.

Along the way, drop what those two vendors left behind:
- output:'export' was gated on GITHUB_ACTIONS || CF_PAGES, so a build
  anywhere else silently produced no export at all. The site is a static
  export wherever it is built.
- basePath pointed at a '/react-sdk' GitHub Pages subdirectory that is
  not this project.
- highlight-code keyed off GITHUB_ACTIONS to pick server vs client
  highlighting; it means 'is this a production export', so it says that.
- pages/api/components returned a JSON blob and 404s in production,
  because a static export cannot serve an API route. The live index is
  the generated /api/registry/components.json.
2026-07-25 12:46:18 -07:00
zeekay 9960bcc81e fix(ci): commit pnpm-lock.yaml so the build is reproducible
.gitignore contradicted itself: line 8 ignored the lockfile while the
lockfiles block declared 'pnpm-lock.yaml - needed for CI/CD, do not
ignore'. With no lockfile in the repo and 'pnpm install
--no-frozen-lockfile', every CI run resolved whatever was newest, so the
Pages deploy has been red since 2026-07-22 on '@x402/*' — optional lazy
imports inside a dependency that a newer release started pulling in and
nothing pinned.

This lockfile is the resolution the app builds green on locally
(verified: full next build, 312+ static pages, exit 0).
2026-07-25 12:28:03 -07:00
zeekay 4ed313e810 fix(ci): build @hanzo/event before the docs build
link-workspace-packages=true, so the app resolves @hanzo/event to
pkgs/event no matter what the range says, and its exports point at dist/
— which only exists once the package is built. The workflow already
builds pkgs/ui for the same reason; event needs the same step. Declare
the dep by published range like every other @hanzo/* dep in the app.
2026-07-25 11:36:41 -07:00
zeekay 603a0c1db1 fix(analytics): point the site at @hanzo/event, the canonical client
@hanzo/analytics (pkgs/capture) is the superseded duplicate and is gone
from the tree, so depending on it would not resolve. @hanzo/event is the
one telemetry client — one Event type, one door (POST /v1/event). Its
host already defaults to the one edge, so the site needs no config
beyond naming itself.
2026-07-25 10:59:00 -07:00
zeekay d775b18766 fix(analytics): use @hanzo/analytics, drop @vercel/analytics
ui.hanzo.ai is a static export on GitHub Pages, not Vercel, so
@vercel/analytics injected /_vercel/insights/script.js — which 404s and
is then refused as text/html, on every page. It was also a second way to
do a thing we already own: pkgs/capture ships @hanzo/analytics.

Route both call sites through one client (lib/analytics.ts): the mount
sends pageviews, trackEvent sends events. Neither knows the vendor —
they name what happened and the client owns where it goes.
2026-07-25 10:57:38 -07:00
zandhanzo-dev e24a4b81d3 feat(ui): menus on ONE Portal path (fix gui-native "Missing theme") + ship compiled dist (fix Next flight loader) — 8.0.8
Two consumer-caught blockers in 8.0.7:

1. gui-native "Missing theme". The gui Popover's SheetController re-roots the trigger
   subtree and reads the theme from React context; on gui-native hosts
   (@hanzogui/react-native-web-lite, theme in context, no CSS-class fallback) that throws
   at MOUNT. FIX: drop the gui Popover entirely — every menu now rides ONE Portal path
   (the ContextMenu approach that already worked on gui-native):
   - menu/FloatingMenu.tsx — shared floating primitive: gui Portal + PortalTheme
     (re-applies the captured theme inside the portal) + anchor positioning (trigger rect
     or cursor point) + edge-flip + dismiss + roving keys.
   - DropdownMenu, ContextMenu, SelectMenu (now a thin DropdownMenu), ComboBox all use it.
     No Popover, no Sheet, no re-root.
   Verified on a Vite + rnw-lite harness (theme in React context, NO root theme fallback):
   NEW DropdownMenu + SelectMenu render correctly themed through the Portal, dark + light.

2. Next 16 flight-loader parse error. 8.0.7 shipped raw `.ts` with inline
   `export { X, type Y }`; Next's flight-client loader parses node_modules `'use client'`
   modules WITHOUT TS and choked on `type`. FIX: ship a COMPILED dist —
   - tsup → ESM (.js) + CJS (.cjs), every dep external, all 11 subpath entries.
   - tsc → .d.ts (dist). scripts/add-use-client.mjs stamps `'use client'` on every output
     (tsup banner misses split chunks). CSS copied.
   - package.json main/module/exports repointed at dist; files=[dist].
   `node --check` passes on all 28 compiled files; the 8.0.7 offender is now valid JS with
   the TS `type` stripped — the flight-loader parse error is structurally impossible.

pnpm typecheck: 0 · pnpm test: 19/19 · pnpm build: exit 0.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-25 09:35:14 -07:00
zandhanzo-dev 839f2ac5b9 fix(ui): menu style props use @hanzo/gui config shorthands (green pkg build)
The @hanzo/gui v5 config omits longhand style aliases that have a shorthand, so
the strict pkg/ui build (createGui augmentation) rejected backgroundColor/alignItems/
justifyContent/minWidth/maxHeight/paddingHorizontal/paddingVertical/marginVertical/
flexShrink/borderRadius/userSelect. Convert the menu primitives + the Popover.Content
shells to the config vocabulary (bg/items/justify/minW/maxH/px/py/my/shrink/rounded/
select). Runtime is unchanged (Tamagui accepts both) — this is the published 8.0.7 code.

pnpm typecheck: 0 errors · pnpm test: 19/19 · pnpm build: exit 0 (48 d.ts emitted).

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-25 08:33:24 -07:00
zandhanzo-dev 1635c3c581 feat(ui): ONE menu system — portal-theme-safe DropdownMenu + ContextMenu, agnostic ThemeToggle (8.0.7)
@hanzo/ui/product gains the shared menu primitives every Hanzo surface
(app/chat/desktop) needs, all on @hanzo/gui/Tamagui, all on ONE item spec so
menus are pixel-identical across the fleet.

- menu/items.tsx — the ONE spec: MenuPanel ($color2 surface, hairline, radius-12,
  pad-4), MenuItemView (h30, px8, gap8, 16px icon slot left, 13px label, right
  affordance shortcut/check/chevron; hover/focus/press → purple accent-soft;
  selected → check+accent; disabled muted; optional 2nd-line description),
  MenuSeparatorView (1px hairline, 4px margin), MenuLabelView (11px uppercase),
  renderMenuItems. Geometry literal px on the 8-grid; colour theme-adaptive tokens
  + brand purple via var(--hanzo-accent[-soft]).
- menu/DropdownMenu.tsx — click menu on gui Popover (bottom-start, allowFlip,
  useControllableState).
- menu/ContextMenu.tsx — right-click menu on gui Portal, cursor-positioned (fixed),
  edge-flip, dismiss on outside/Escape/scroll/resize/blur.
- menu/portal-theme.tsx — PortalTheme: captures useThemeName() at the trigger and
  re-applies <Theme name> INSIDE portaled content, so menus render correctly through
  a portal under a nested <Theme> (light+dark). Fixes GAP 1 (desktop "Missing theme").
- menu/roving.ts — shared Arrow/Home/End/Escape keyboard nav.
- SelectMenu + ComboBox — adopt the shared spec + PortalTheme fix (DRY; identical rows).
- ThemeToggle — framework-agnostic: controlled via theme + onToggle/onThemeChange
  (NO framework dep, for Vite/Tauri/Express); uncontrolled falls back to the OPTIONAL
  @hanzogui/next-theme via ThemeToggleNext (lazy, ErrorBoundary→DOM), so console/Next
  stays backward-compatible and non-Next hosts build.
- package.json — 8.0.7; @hanzogui/next-theme added as an OPTIONAL peer.

Verified against the real published @hanzo/gui@7.3.0: product source type-checks
clean; a Vite harness renders DropdownMenu + ContextMenu + SelectMenu + ThemeToggle
through portals under a nested <Theme name="dark"/"light"> — panels correctly themed
(t_dark rgb(20,20,20) / t_light rgb(247,247,247)) while root stays light, no errors.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-25 08:16:25 -07:00
z 639a5caed5 docs: modernize + LLM.md + cross-links (one-way SDK model) 2026-07-24 12:41:27 -07:00
z 5f1d942ea0 docs: modernize + LLM.md + cross-links (one-way SDK model) 2026-07-24 12:41:25 -07:00
Hanzo AI ae13e0743e feat(commerce): schema.org microdata on ProductCard/AddToCart + data-slot on TextLink for zero-config capture 2026-07-24 01:54:42 -07:00
hanzo-dev 4c2599d2a9 feat(products): canonical Meet Hanzo ecosystem-shell spec + collapse SURFACES/HANZO_APPS into one source 2026-07-23 12:59:02 -07:00
hanzo-dev 25ab403d81 feat(observe-native): native + desktop binding for @hanzo/observe
Makes @hanzo/gui (Tamagui) / React Native and Tauri desktop emit the SAME
canonical Events as the web — same semantic hierarchy, privacy gate, and ONE front
door (POST /v1/event via @hanzo/event).

- '.' (React/Tamagui): ObserveProvider + ObserveScope compose the semantic path
  from the React tree (no DOM); useObserve().press/changeText/event/screen wrap
  RN/Tamagui handlers; useEventStream for playback
- './tauri' (react-free): bindTauri runs the @hanzo/observe DOM engine in the
  webview + forwards Tauri native events; @tauri-apps/api is a runtime-optional peer
- pure emit/semantic/redact core reuses @hanzo/observe wireProps/labelFor/
  sensitiveKey so native and web are byte-identical on the wire

19 tests green (semantic/emit/tauri + React mount via react-dom/client).
2026-07-22 22:57:09 -07:00
hanzo-dev 06188248bb feat(observe-svelte): Svelte adaptor for @hanzo/observe
Reuses the framework-agnostic @hanzo/observe engine; binds it to Svelte idioms:
- createObserver(client) — bootstrap capture in a root +layout (idempotent, SSR-safe)
- observe action — use:observe={{ name }} stamps a stable component name the engine
  labels interactions with; { private } / { view } opt out / into a subtree
- stream store — a Svelte-readable live window of interactions for session playback

Emits through @hanzo/event to POST /v1/event. 7 tests green; svelte is an optional
peer (action + store satisfy Svelte contracts structurally).
2026-07-22 22:45:00 -07:00
hanzo-dev eef30824b4 feat(observe): default-on semantic interaction capture for React
@hanzo/observe — watches every click/input/nav/visibility across the tree,
annotates each with a semantic hierarchy (component path / role / data-testid /
aria) auto-derived from the DOM as a small JSON-LD document, and emits it through
@hanzo/event to the ONE front door (POST /v1/event).

- framework-agnostic engine ('.'): Observer + annotate + redact + Stream
- React default experience ('./react'): ObserveProvider + useEventStream (session
  playback) + useObserver
- privacy-first: input values withheld by default, sensitive fields always
  redacted, data-hz-private subtree opt-out; fail-soft throughout

28 tests green (annotate/redact/stream/observer).
2026-07-22 22:45:00 -07:00
hanzo-dev eb2d960286 docs(event): document @hanzo/event as the ONE telemetry client + supersessions 2026-07-22 22:14:24 -07:00
hanzo-dev 5b0b3507ed fix(event): emit CJS as .cjs so require() works under type:module
The 0.3.0 CJS bundle went to dist/index.js, which Node parses as ESM under
"type": "module" — require('@hanzo/event') threw "exports is not defined in
ES module scope". Emit CJS as .cjs (ESM stays .mjs) and map each exports
condition to its own types. No API change. Bumps 0.3.0 -> 0.3.1.
2026-07-22 22:04:20 -07:00
hanzo-dev bc76d56dad feat(event): emit the canonical {batch} wire to the ONE /v1/event door
Repoint @hanzo/event onto Hanzo Cloud's single ingestion front door
(POST /v1/event, body {batch:[Event,...]}), replacing the deprecated
/v1/analytics + /v1/tracker split and the interim /v1/ingest key door.
One door, one wire, for cookie / bearer / publishable-key auth alike.

Errors now reach the error-tracking lens. The batched Event wire carries
`type`, which cloud folds to event_type='error' (foldException +
canonicalType); the exception rides the top-level `error` field, lifted
into properties.$exception. The four-field {event,distinctId,time,
properties} array has no `type`, so it can never be lensed as an error —
the batched wire is the one that lights up web + product + error.

Publishable keys authenticate directly on /v1/event now: Authorization:
Bearer pk_ on fetch, ?ingest_key=pk_ on a headerless unload beacon.

Delete the orphan @hanzo/analytics@0.1.0 thin dup (pkgs/capture, a
leftover of the @hanzo/capture -> @hanzo/event rename that nothing
imports) so the repo carries exactly one telemetry client.

VERSION 0.2.0 -> 0.3.0.
2026-07-22 18:08:55 -07:00
hanzo-dev 083f375f09 merge: manual-only release trigger
# Conflicts:
#	.changeset/hanzo-capture-initial.md
2026-07-22 02:44:24 -07:00
hanzo-dev 3762821185 merge: @hanzo/ui svelte backend 2026-07-22 02:43:58 -07:00
hanzo-dev 962cccc4dd merge: @hanzo/ui gui-canonical component library
# Conflicts:
#	LLM.md
2026-07-22 02:43:55 -07:00
Hanzo AI e07325845b @hanzo/ui: canonical cross-surface list (surfaces.data) + bot/chat + current-aware AppHeader
ONE framework-agnostic surfaces.data module (id doubles as icon key) is now the
single source every launcher consumes. Adds hanzo.bot + hanzo.chat, collapses the
redundant cloud/console pair, renders a distinct per-surface icon, and omits the
current surface (no self-link). Publishes 8.0.6.
2026-07-21 23:56:10 -07:00
hanzo-dev 89abd53c97 chore: consolidate local working changes 2026-07-21 17:19:09 -07:00
hanzo-dev 79f8eae74a merge(feat/analytics-client): consolidate onto main 2026-07-21 17:19:09 -07:00
hanzo-dev 1ee64263ff feat(ui): @hanzo/ui — canonical, backend-flexible, gui/tokens-based component library
Rework pkg/ui (@hanzo/ui@8) into THE canonical Hanzo UI package: one design core
(@hanzo/tokens, Hanzo dark-first identity, Geist Sans/Mono) with per-backend
implementations. The root barrel now exposes the shadcn-compatible component API
apps import, so consumers drop the @hanzo/ui-shadcn alias and point @hanzo/ui here
with zero import churn.

Organization (src/):
- core/      backend-agnostic: cn (clsx+tailwind-merge), tokens (re-export of
             @hanzo/tokens), fonts (Geist vars)
- theme.css  self-contained standard-token CSS vars + Geist — the identity
- backends/  shadcn/ (Radix + Tailwind, the web API) and gui/ (@hanzo/gui product
             layer); README documents adding a backend (svelte/solid/…)
- models/    the unified ModelSelector + catalog helpers
- primitives/ GENERATED per-member entrypoints (scripts/gen-primitives.mjs) so a
             host that modularizes @hanzo/ui imports resolves unchanged

Ported the full component surface hanzo.app imports (Badge, Button, Card*,
Checkbox, Dialog*, DropdownMenu*, Input, Toaster, Avatar*, Tabs*, Select*,
Tooltip*, Popover*, Command*, Collapsible*, ScrollArea, Slider, Switch, Progress,
Separator, Label, Textarea, AspectRatio) + models. Behaviour-heavy components keep
Radix for a11y/portal/keyboard, styled with STANDARD tokens only.

Token-bug fix: every component uses standard design tokens (bg-popover,
border-border, bg-primary, text-muted-foreground, …); the app-private tokens that
rendered transparent (bg-bg-dark, bg-bg-secondary, text-text-secondary,
bg-divider, bg-brand, bg-level-2, hard-coded bg-gray-*) are gone. No hard-coded
font: UI inherits Geist Sans, code Geist Mono, portaled surfaces bind font-sans.

Root barrel is pure web (no @hanzo/gui/react-native-web pull); the gui product
layer + gui theme tokens live on the explicit /product and /gui subpaths.

Also: fix @hanzo/tokens dts build under TS 6 (ignoreDeprecations). Verified:
tsconfig.check.json typecheck green; runtime export test — all 66 required
components + ModelSelector export from @hanzo/ui root; per-member entrypoints
resolve.
2026-07-21 09:27:10 -07:00
Hanzo AI 4145756e46 fix(event): commit the actual @hanzo/event content (prior commit was the bare rename)
0a212316 renamed the dir but a bad `git add` pathspec dropped the content edits,
so pkgs/event shipped as @hanzo/capture@0.1.1 and `pnpm --filter event` matched
nothing (nothing published). This lands the real change: name @hanzo/event@0.2.0,
the captureError/captureException surface, auto error handlers, React ErrorBoundary,
Exception type, and the 6 error-capture tests (28/28 green locally).
2026-07-20 16:13:01 -07:00
Hanzo AI 0a212316b4 feat(event): @hanzo/capture → @hanzo/event — the ONE telemetry client (errors are events)
Rename the capture SDK to @hanzo/event and fold error tracking into it, so ONE
client emits every kind of event — pageview/event/identify/group AND errors — on
one batched stream. The server lenses that one stream into product analytics, web
analytics, and error tracking (insights/analytics/sentry.hanzo.ai). Subsumes
@sentry: no second SDK, no second pipe.

- New 'error' EventKind + Exception type; WireEvent carries the exception.
- Analytics.captureError()/captureException(): normalize any throwable, emit a
  type:'error' event, flush at once (a crash may unload the page). Never throws
  back into the app.
- Auto-capture (config captureErrors, default on, browser-only): window.onerror +
  unhandledrejection → error events. The drop-in @sentry replacement.
- React ErrorBoundary (./react): reports render errors React swallows before
  window.onerror sees them — the React half of the replacement.
- 0.1.1 → 0.2.0. 28/28 tests pass (6 new error-capture specs), tsup build green.

Consumers (app/chat/console/hanzo.ai/operator) migrate @hanzo/capture →
@hanzo/event next; @hanzo/capture stops shipping new versions.
2026-07-20 16:05:22 -07:00
Hanzo AI e7af32fec4 ui-shadcn 5.9.1: allow framer-motion ^12 beside ^11 — the peerOptional ^11 range ERESOLVE'd any consumer whose tree carries motion@12 (every @hanzo/gui 7.3 surface); the peer is optional and range-only, no code change 2026-07-20 13:38:37 -07:00
Hanzo AI 75b88dced8 dashboard: finish the pipeline/pipeline -> pipeline/stages rename — repoint the two imports (the rename itself rode the prior commit) 2026-07-20 13:37:52 -07:00
Hanzo AI 7c849e4169 ui 8.0.5: @hanzo/canvas peer floor >=0.1.0 — 0.2.0 was never published, the optional peer was unsatisfiable and ERESOLVE'd every consumer install 2026-07-20 13:37:26 -07:00
Hanzo AI 60a5af77fb commerce 7.6.3: publish the @hanzo/ui-shadcn@^5 peer repin 2026-07-20 13:09:48 -07:00
Hanzo AI 7502288b90 ui 8.0.4: shared shell — AppHeader + BrandMark (@hanzo/logo) + OrgSwitcher + orgScope (console contract hoisted, #36); canonical 7-path shaded HanzoMark; v8 lane docs + legacy repin (@hanzo/ui-shadcn@^5) 2026-07-20 13:07:50 -07:00
Hanzo AI cb661f297f products: team CatalogEntry — hanzo.team app, /v1/team, team plan 2026-07-20 10:46:41 -07:00
Hanzo AI 24b4056e28 merge @hanzo/products — canonical catalog on the locked 10-category taxonomy (pkgs/products from claude/products-snapshot-reconcile) 2026-07-20 10:43:55 -07:00
hanzo-dev c9f51db1da cd: bind the real /v1/deploy wire — argoproj shape + the wired tree route
Live verification against prod found two contract mismatches the offline build
could not see:

- the fleet bound to ZERO rows: /v1/deploy/applications serves argoproj-shaped
  items (metadata.name, spec.source.*, status.{sync,health}.status,
  status.summary.images[]) but the adapter read only the flat native keys.
  normalizeDeployApp now reads BOTH wires, flat first, then the nested fields.
- the resource tree 404d: the wired route is applications/:name/resource-tree;
  the shorter :name/tree belongs to an unregistered handler.

Live now: 78 applications (69 Healthy / 9 Degraded), env chips, detail with the
real APP->DEPLOYMENT->POD topology, 0 page errors, 0px mobile overflow. The live
payload is pinned as a regression fixture. 38/38 unit, e2e desktop+mobile green.
2026-07-19 18:26:28 -07:00
hanzo-dev 43dbaced96 ui: Hanzo CD — dedicated cd.hanzo.ai dashboard on @hanzo/gitops
A native Hanzo CD app (Vite + React 19) that replaces the ArgoCD React fork:
fleet list, app detail with the live resource tree, sync/rollback, over cloud
/v1/deploy. Mobile-first, Geist, small mark, 245KB (the fork was ~18MB).
Ships as a static build to the s3://cdn/cd plane cd.hanzo.ai already serves.
2026-07-19 18:14:49 -07:00
hanzo-dev 980027e5f2 gitops+cd: fix the tree crash, the health/sync mislabel, and the diff OOM
Red review of the cd.hanzo.ai app found three real defects in the SHARED
components (so this hardens the console surfaces too):

- tree: buildResourceGraph seeded `children` lazily per node, so any tree that
  listed a child before its parent — or any ownerRef cycle — dereferenced
  undefined and threw during render. K8s object lists are not topologically
  sorted, so this fired on ordinary data and, with no error boundary, blanked
  the whole dashboard. Pre-seed every id before the edge pass.
- health/sync: the substring fallback up-guessed a BAD state to a GOOD one
  (NotReady/unavailable -> Healthy, notsynced -> Synced). An incident shown
  green is worse than one shown Unknown, so the positive up-guess is gone;
  unrecognized folds to Unknown. foldSync also normalizes separators so the
  canonical out-of-sync folds at the source.
- diff: lineDiff always built the full O(n*m) LCS matrix; a large ConfigMap
  (reachable — not Secret-excluded) froze the tab. Size-guard to a block diff.

Also: an app-level ErrorBoundary so one render throw can never white-screen the
dashboard, client log caps + array-shaped log tolerance, a Secret-kind skip in
the tree, and vitest no longer globs the Playwright specs.

Tests assert the fixed behavior: 35/35 unit (incl. the adversarial fuzz suite
flipped from codifying the bugs), 42/42 gitops, 2/2 e2e desktop+mobile.
2026-07-19 18:14:36 -07:00
hanzo-dev 13396f3e8e feat(cd): dedicated Hanzo CD dashboard on @hanzo/gitops (cd.hanzo.ai)
Replace the ArgoCD React fork (deploy/ui, webpack argo-cd-ui) with a focused,
mobile-first CD dashboard built on the shared Hanzo component packages over the
native cloud CD plane (/v1/deploy). Its job stays: see every operator App CR with
stats · sync · health · resource tree · logs · sync/rollback.

- Vite + React 19 STATIC SPA → dist/ (index.html + login.html + CNAME + assets),
  published to the existing s3://cdn/cd static plane cd.hanzo.ai already serves
  (ingress staticFiles/spaMode; /v1/deploy peeled to cloud). No ingress/backend
  change.
- @hanzo/gitops (framework-free ArgoCD-replacement views): GitopsAppList for the
  fleet; GitopsSyncPanel + GitopsAppTree + GitopsNodeInfo + GitopsRollbackDialog
  composed for the app detail (lazy per-node /resource + /logs). Resolved via Vite
  path alias off the built dist so it builds without a full monorepo install.
- src/lib/adapt.ts maps the real /v1/deploy DTOs into the @hanzo/gitops
  view-models, reusing the package's foldHealth/foldSync (strips [-_] so the
  hyphenated 'out-of-sync' folds to OutOfSync, not Unknown).
- Auth: the admin-console PKCE login.html (ported) → hanzo_iam_token cookie the
  cloud binary validates (SuperAdmin gate); same-origin credentialed /v1/deploy.
- Lean self-contained topbar (Geist, small mark, env scope) in the interim; the
  @hanzo/ui-shadcn shared shell + @hanzo/canvas map are registry-install-gated
  follow-ons (the map already ships green in hanzoai/console).
- Verify: tsc 0; vitest 8/8 (adapter + the sync-fold fix); playwright render +
  mobile (no horizontal body scroll at 390).
2026-07-19 12:52:11 -07:00
zandhanzo-dev 3a11c68259 feat(models): ModelSelector — THE unified family-grouped model picker (5.9.0)
One selector for hanzo.app/chat/console: family groups (Enso, Zen,
Anthropic, OpenAI first), cmdk search, premium marks, context hints,
fetchModelCatalog off /v1/models. catalog.ts pure + SSR-safe.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-19 08:49:37 -07:00
zandhanzo-dev 95964b55d1 feat(ui/models): unified ModelSelector + catalog helpers
Adds the one model selector for every Hanzo app to the existing ./models
subpath: hanzo.chat-style family grouping in a compact Radix Popover + cmdk
Command combobox (grouped sections, family headers, premium markers, context
suffixes, keyboard nav, type-to-filter search over 12 models). Monochrome,
dark-first, data-agnostic.

- catalog.ts: ModelCatalogEntry, familyOf, groupModelsByFamily, isChatModel,
  filterChatModels, fetchModelCatalog (pure, SSR-safe, no caching/state)
- ModelSelector.tsx: ModelSelector + ModelSelectorProps
- wired through src/models/index.ts; version 5.8.0 -> 5.9.0 (minor, additive)

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-19 08:47:37 -07:00
zandhanzo-dev fcbd7c0a1a feat(ui): @hanzo/ui/account — unified self-service account surfaces (5.8.0)
CreditsMeter, UsagePanel, PlanCard/PlansGrid, PaymentMethods, InvoiceTable,
OrgIdentityRow, TeamMembersTable, SettingsSection + shared types. Props-driven,
React 18+19, monochrome. One account UI kit for hanzo.app + hanzo.chat + console.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-18 23:36:52 -07:00
zandhanzo-dev 1e167588b6 feat(ui): unified @hanzo/ui/account self-service surfaces (v5.8.0)
One composable, data-agnostic account-surface kit so hanzo.app, hanzo.chat,
and console stop hand-rolling billing/usage/payments/org/team/settings UI and
render it all from @hanzo/ui.

Components (props-driven, no data fetching, monochrome-neutral by default;
semantic color only on invoice status + danger settings):
- CreditsMeter (compact + full, animated monochrome fill)
- UsagePanel (period select, total, empty state)
- PlanCard + PlansGrid (standard 3-tier)
- PaymentMethodRow + PaymentMethodsList
- InvoiceTable (paid/open/failed status pills)
- OrgIdentityRow (emoji|image|initials, sm/md)
- TeamMembersTable (role select, remove, invite row, pending)
- SettingsSection (danger variant)

Shared types + formatters exported from the same subpath. Reuses existing
primitives (Button, Badge, Avatar, Select, Table, DropdownMenu). Wired as the
./account export (tsup entry + package.json exports); builds clean incl. .d.ts.
react peer stays ^18 || ^19 (React 18 Vite + React 19 Next). Additive minor:
5.7.5 -> 5.8.0. Not published — CTO review gate.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-18 23:36:02 -07:00
zandhanzo-dev 742daa7d6c fix(ui): Button asChild no longer crashes with React.Children.only (v5.7.5)
The Button always rendered two JSX children (a loading-spinner slot + children).
Under asChild, Comp is a Radix Slot which calls React.Children.only on that
2-element array and throws 'expected to receive a single React element child',
crashing the consuming tree. A slotted button can't host an injected spinner
anyway (the child replaces the button), so with asChild we now pass children
through as the single child Slot requires. Non-asChild loading behavior is
unchanged. This kills the whole <Button asChild> crash class for consumers
(hanzo.app hit it on /projects and /dev).

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-18 16:34:36 -07:00
zandhanzo-dev 4444231e77 docs: @hanzo/ui v8 one-surface subpaths — the 8 newest component kits
Document the canvas/wallet/network/billing/dashboard/usage/gitops/data subpaths
(thin re-exports of their home packages, optional peers) + the pkg/ui publish
caveat, so consoles import every recent component from the single @hanzo/ui.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-15 18:18:00 -07:00
zandhanzo-dev dbfacf40e8 feat(ui): expose the 8 newest components via @hanzo/ui subpaths + publish lagging homes
Complete the one-surface pattern the @hanzo/ui/gitops subpath started: add thin
re-export subpaths so a console imports every recent component from the single
@hanzo/ui entry while each lives once in its home package (zero duplication,
optional peers — only pulled when the subpath is used):

  @hanzo/ui/canvas    -> @hanzo/canvas         (ProjectCanvas, ServiceNode, DeployTimeline, EnvSwitcher, ServiceDetailDrawer, ServiceStatusBadge)
  @hanzo/ui/wallet    -> @hanzo/ui-shadcn/wallet   (WalletMenu, injectedEvmAdapter [EIP-1193], walletAvailable, ensureEvmNetwork)
  @hanzo/ui/network   -> @hanzo/ui-shadcn/network  (NetworkSwitcher, useNetwork, configureNetworks, HANZO_NETWORKS)
  @hanzo/ui/billing   -> @hanzo/ui-shadcn/billing  (CreditModal)
  @hanzo/ui/dashboard -> @hanzo/dashboard
  @hanzo/ui/usage     -> @hanzo/usage          (UsageMeter, UsageProviderCard, UsageDashboard)

Together with the existing ./gitops and ./data subpaths that is the 8 newest
component kits reachable from @hanzo/ui. Bump @hanzo/ui 8.0.2 -> 8.0.3.

Publish the two lagging homes via ARC (pkgs/* -> publish.yml):
  @hanzo/canvas 0.2.0 -> 0.2.1  (npm was behind at 0.1.0)
  @hanzo/gitops 0.1.0 -> 0.1.1  (first publish)

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-15 17:59:40 -07:00
hanzo-dev 5a74747c20 feat(ui/product/social): ChannelBadge, PostCard, CampaignCard (8.0.2)
Extract the pure Hanzo Social display components into @hanzo/ui on the @hanzo/gui
idiom (Card/XStack/YStack/Text, $-tokens), data + handlers injected via props,
reusing the shared StatusTag. Additive: new product/social/* + one barrel line;
no existing export changed. Published @hanzo/ui 8.0.2.
2026-07-15 00:03:38 -07:00
hanzo-dev 442e0bb72a ui: expose @hanzo/ui/gitops subpath (re-export @hanzo/gitops)
Thin subpath so a console can import from '@hanzo/ui/gitops' while the code lives
once in @hanzo/gitops. Optional peer dependency — only pulled when the subpath is
used.
2026-07-14 16:21:03 -07:00
hanzo-dev 6ef7ca7eca gitops: demo tree gaps fit static render (all pods visible) 2026-07-14 16:20:31 -07:00
hanzo-dev dc15849c4a gitops: static SSR demo (real components over fixtures)
demo/demo.tsx server-renders the sync panel + resource tree + node drill-in
(diff tab) + applications list into a self-contained demo/index.html for visual
review. Reproduce command in the file header.
2026-07-14 16:18:00 -07:00
hanzo-dev 24a88be599 gitops: fixtures + render tests (tree, node panel, diff, badges, list)
happy-dom + Testing Library render proofs: tree draws a card per resource with
SVG edges + collapse hides pods + selection fires; node panel shows manifest and
switches to diff/events/logs; diff classes add/del + tallies; app list filters by
search. 42 tests green (33 pure + 9 render).
2026-07-14 16:14:58 -07:00
hanzo-dev 0745e72b02 gitops: sync/rollback dialogs + app detail shell + public barrel
- Dialog: minimal portal-free modal (backdrop + Escape close)
- SyncPanel (GitopsSyncPanel) + GitopsSyncDialog: action bar + sync options
  (prune, dry-run); effects injected as callbacks
- RollbackDialog (GitopsRollbackDialog): deploy-history picker -> rollback
- ApplicationDetails (GitopsAppDetails): composes sync panel + tree + node
  drawer + rollback, wiring selection between them
- index.ts: the @hanzo/gitops public export contract
Full package typechecks + tsup builds (CJS+ESM+DTS, . and ./pure) green.
2026-07-14 16:12:00 -07:00
hanzo-dev dcc1836782 gitops: applications list (GitopsAppList)
Table/grid of applications with name/project, sync + health badges, revision,
source and age. Search across name/project/namespace/source, multi-select
health + sync filters, sortable columns (name/health/sync/age via the pure
ranks). Data-prop-driven; row-open callback.
2026-07-14 16:09:31 -07:00
hanzo-dev 1d9abaa6e7 gitops: diff view + node info panel (manifest/diff/events/logs)
- DiffView (GitopsDiffView): two-gutter colorized diff from a unified string
  (classifyDiff) or raw live/desired manifests (lineDiff), +/- stats header
- NodeInfoPanel (GitopsNodeInfo): tabbed drill-in over a selected resource —
  live manifest, desired-vs-live diff, events, logs; reuses canvas relativeTime
2026-07-14 16:08:33 -07:00
hanzo-dev e0b2ea3e1e gitops: status marks, badges, resource node + tree topology
- styles.tsx: one theme-aware stylesheet (GITOPS_CSS + THEME_VARS, --hz-* vars)
- glyphs.tsx: inline-SVG health/sync marks (heart/broken-heart/ghost/pause/
  spinner/check/arrow-up) + category-mapped resource-kind icons (zero icon dep)
- HealthBadge / SyncBadge: the color-coded status pills
- ResourceNode: the node card (kind glyph + name + health + sync marks)
- ResourceTree (GitopsAppTree): SVG-edge topology over buildResourceGraph,
  CSS-transform pan/zoom, collapse toggles, auto-fit; no graph dependency
2026-07-14 16:07:03 -07:00
hanzo-dev 6747fe3050 gitops: pure core — health/sync folds, resource-tree layout, diff classing
- types.ts: the data contract (HealthStatus/SyncStatus orthogonal axes, tree,
  managed resource, diff, events, logs, revision history)
- health.ts / sync.ts: foldHealth/foldSync + Argo-hue palettes + rank + rollup
- tree.ts: buildResourceGraph reuses @hanzo/canvas layoutGraph (parentRefs ->
  positioned graph, collapse pruning, normalized origin)
- diff.ts: classifyDiff (unified) + lineDiff (LCS live-vs-desired) + diffStats
- 33 unit tests green; tsc + tsup dts pipeline validated
2026-07-14 16:02:37 -07:00
hanzo-dev a5090ad436 gitops: scaffold @hanzo/gitops package (Apache-2.0 port of argo-cd UI)
Presentational, data-prop-driven CD/GitOps React surface. tsup build + vitest,
mirrors pkgs/canvas + pkgs/capture conventions. Reuses @hanzo/canvas/pure
(layoutGraph) for resource-tree topology; no separate graph dependency.
Attribution to argoproj/argo-cd in NOTICE.
2026-07-14 15:56:36 -07:00
hanzo-dev 6092cec1ab ci: kill changesets — a semver bump is the one publish trigger
Publishing is now a single step: bump a package's version in its
package.json, merge to main, and publish.yml publishes the changed
@hanzo/* package to npm.

Remove the changeset machinery (.changeset/, changeset-version.js) and
the redundant publish paths — release.yml (version-PR bot),
npm-publish.yml (manual dispatch), publish-on-tag.yml (tag trigger),
prerelease*.yml (betas) — leaving publish.yml as the only path. Drop the
@changesets deps and the `changeset version` scripts; refresh the
CONTRIBUTING and LLM docs.
2026-07-14 14:19:54 -07:00
hanzo-dev ff0725fe0c ci: kill changesets — a semver bump is the one publish trigger
Publishing is now a single step: bump a package's version in its
package.json, merge to main, and publish.yml publishes the changed
@hanzo/* package to npm.

Remove the changeset machinery (.changeset/, changeset-version.js) and
the redundant publish paths — release.yml (version-PR bot),
npm-publish.yml (manual dispatch), publish-on-tag.yml (tag trigger),
prerelease*.yml (betas) — leaving publish.yml as the only path. Drop the
@changesets deps and the `changeset version` scripts; refresh the
CONTRIBUTING and LLM docs.
2026-07-14 14:15:31 -07:00
54fb03bff3 ci(release): manual-only (stop churny auto-release) (#247)
* ci(release): build only @hanzo/capture in the publish path (drop shadcn:build — OOM + upstream-fork name)

* fix(changeset): repo shadcn-ui/ui -> hanzoai/ui (changelog-github null.author crash; fork leftover)

* ci(release): manual-only trigger (stop auto version-bump/publish churn; needs npm scope auth)

---------

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-14 12:03:38 -07:00
hanzo-dev d337a7a18b ci(release): manual-only trigger (stop auto version-bump/publish churn; needs npm scope auth) 2026-07-14 12:03:32 -07:00
820ee100b7 chore(release): version packages (#246)
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-14 11:48:28 -07:00
fbaa5eb739 fix(changeset): changelog repo -> hanzoai/ui (unblocks version/publish) (#245)
* ci(release): build only @hanzo/capture in the publish path (drop shadcn:build — OOM + upstream-fork name)

* fix(changeset): repo shadcn-ui/ui -> hanzoai/ui (changelog-github null.author crash; fork leftover)

---------

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-14 11:45:05 -07:00
hanzo-dev 17a7019df5 fix(changeset): repo shadcn-ui/ui -> hanzoai/ui (changelog-github null.author crash; fork leftover) 2026-07-14 11:44:59 -07:00
07b7ff0c07 ci(release): build only @hanzo/capture in the publish path (drop shadcn:build — OOM + upstream-fork name) (#244)
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-14 11:39:59 -07:00
hanzo-dev ed53d09a4b ci(release): build only @hanzo/capture in the publish path (drop shadcn:build — OOM + upstream-fork name) 2026-07-14 11:39:52 -07:00
0cd2445172 fix(pkgs/data): pin @hanzogui/config 7.2.2 -> 7.3.0 (7.2.2 unpublished; matches all sibling pkgs) (#243)
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-14 11:32:01 -07:00
9e4c4358f6 ci(release): pin npm@11 (npm@latest=12 needs node>=22; job pins node20) — keeps OIDC publish (#242)
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-14 11:29:40 -07:00
bc307003d7 ci(release): fix fork-leftover owner gate shadcn-ui -> hanzoai (unblocks changeset publish) (#241)
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-14 11:27:50 -07:00
62550fb4b0 @hanzo/capture: shared product-analytics capture client (#240)
* @hanzo/analytics: shared product-analytics capture client

Tiny batched client emitting pageview/event/identify/group to Hanzo Cloud
(/v1/analytics + /v1/tracker) — never to insights-capture directly. First-touch
UTM/referrer/refCode attribution persisted and attached to every event;
beacon-on-unload; dual cookie/bearer auth; SSR-safe; tenant is stamped
server-side (never sent by the client). Ships a framework-agnostic core and a
@hanzo/analytics/react provider + hooks, plus the shared EVENTS/GOALS/COHORTS
vocabulary. 22 unit tests + tsup build (cjs/esm/dts) green.

* rename @hanzo/analytics -> @hanzo/capture (name collision)

The intended name @hanzo/analytics is already a LIVE, hanzoai-owned npm package
(team-manager's, latest 0.6.4, an incompatible providers/Analytics API).
Publishing this new capture client under that name would move the 'latest' tag
onto a different codebase and confuse/​break bare + latest consumers. Renamed to
the free, single-word @hanzo/capture, which also matches cloud's 'capture plane'.
One-line revert if the owner prefers to supersede team-manager instead.

---------

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-14 11:25:13 -07:00
hanzo-dev a8c97ecb0a rename @hanzo/analytics -> @hanzo/capture (name collision)
The intended name @hanzo/analytics is already a LIVE, hanzoai-owned npm package
(team-manager's, latest 0.6.4, an incompatible providers/Analytics API).
Publishing this new capture client under that name would move the 'latest' tag
onto a different codebase and confuse/​break bare + latest consumers. Renamed to
the free, single-word @hanzo/capture, which also matches cloud's 'capture plane'.
One-line revert if the owner prefers to supersede team-manager instead.
2026-07-14 11:02:17 -07:00
Hanzo AI 17a7875ea0 refactor(ui): drop orphaned usage kits — usage has ONE home (@hanzo/usage)
Zero-consumer AI-usage duplicates removed so <UsagePanel> in @hanzo/usage is the
single source: @hanzo/ui product usage kit (UsageDashboard/UsageMeter/
UsageProviderCard) and @hanzo/ui-shadcn billing usage-panel. Verified no
remaining references across pkg/pkgs/apps; the panel now lives at
@hanzo/usage/panel (gui/Tamagui) with the DOM <UsageDashboard> at /react.

(Phase-A intent from feat/usage-panel-phase-a, applied onto current main — that
branch sits on the retired v8 line and is not itself mergeable.)
2026-07-14 09:41:30 -07:00
hanzo-dev 655aac94b6 @hanzo/analytics: shared product-analytics capture client
Tiny batched client emitting pageview/event/identify/group to Hanzo Cloud
(/v1/analytics + /v1/tracker) — never to insights-capture directly. First-touch
UTM/referrer/refCode attribution persisted and attached to every event;
beacon-on-unload; dual cookie/bearer auth; SSR-safe; tenant is stamped
server-side (never sent by the client). Ships a framework-agnostic core and a
@hanzo/analytics/react provider + hooks, plus the shared EVENTS/GOALS/COHORTS
vocabulary. 22 unit tests + tsup build (cjs/esm/dts) green.
2026-07-13 16:21:27 -07:00
z 76215d2b88 fix: @hanzo/ui .dark --card/--popover oklch(0.045≈#000)→0.06 (#050505) so panels are visible on true-black 2026-07-10 14:15:28 -07:00
z ba7366320b feat: converge @hanzo/ui .dark to true-black #000 (DESIGN.md §4) — web matches native 2026-07-10 10:19:52 -07:00
zandhanzo-dev 7974306b26 feat(data): @hanzo/data — cross-platform metadata-driven data-app layer
Typed field system (26 types) → record table / card / detail, on @hanzo/gui
(web + native + desktop), shorthand style props, zero Tailwind. The universal
object/field/record/view core for any Base-backed CRM, CMS, or commerce app.
Registry-dispatched (add a type = one registerField call). Ships TS source
(zero-build internal package). tsc --noEmit clean against @hanzo/gui 7.2.2.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-10 10:19:52 -07:00
125dd5c16d feat(canvas): @hanzo/canvas — Railway-grade PaaS project canvas (#239)
A new source-only @hanzo/gui package: a pannable/zoomable board of service
nodes (ProjectCanvas over @xyflow/react) with live status (ServiceStatusBadge),
metric sparklines (MetricSparkline), deploy timelines (DeployTimeline), an
environment switcher (EnvSwitcher), and a tabbed service detail drawer
(ServiceDetailDrawer) — plus the ServiceNode card and SourceRef/ReplicaPill
primitives. Presentational + data-prop-driven; brand/white-label aware via the
design tokens (semantic status palette overridable per brand). Pure folds
(status normalization, layered graph layout, relative time) are unit-tested
(17 tests). React-free logic re-exported at @hanzo/canvas/pure so data mappers
and their tests never pull in JSX/xyflow.


Co-authored-by: hanzo-dev <dev@hanzo.ai>
2026-07-10 03:17:23 -07:00
hanzo-dev 105c1f43b9 feat(canvas): @hanzo/canvas — Railway-grade PaaS project canvas
A new source-only @hanzo/gui package: a pannable/zoomable board of service
nodes (ProjectCanvas over @xyflow/react) with live status (ServiceStatusBadge),
metric sparklines (MetricSparkline), deploy timelines (DeployTimeline), an
environment switcher (EnvSwitcher), and a tabbed service detail drawer
(ServiceDetailDrawer) — plus the ServiceNode card and SourceRef/ReplicaPill
primitives. Presentational + data-prop-driven; brand/white-label aware via the
design tokens (semantic status palette overridable per brand). Pure folds
(status normalization, layered graph layout, relative time) are unit-tested
(17 tests). React-free logic re-exported at @hanzo/canvas/pure so data mappers
and their tests never pull in JSX/xyflow.
2026-07-10 03:07:29 -07:00
hanzo-dev 9235e2d19f feat(ui): canonical --hz-* design tokens; world/dash/shadcn/hz-ui alias onto one source
- style/tokens.css: single source of truth (black monochrome, Geist, radius 6-8px, dark-first)
- world/dash tokens.css: thin aliases of --hz-*; retires dash tinted #141419 surface + blue primary
- hanzo-default-colors.css: theme-agnostic bridge; kills purple 266deg secondary + grey oklch dark card
- globals.css: import tokens first; font/radius default to canon
- token-proof.mjs: browser proof every alias chain resolves to canon (dark+light)
- bump 5.7.0 -> 5.7.5
2026-07-08 16:57:55 -07:00
2ded655346 feat(network,wallet): shared NetworkSwitcher + WalletMenu — the ONE hanzo.network standard (#238)
* fix(ui-shadcn): self-referencing imports use the package's own name

The @hanzo/ui -> @hanzo/ui-shadcn rename (name freed for v8) left 29 files
importing themselves via the OLD name, breaking tsc/dts and making dist
resolve against npm @hanzo/ui@8.x at runtime. Self-reference by own name
resolves correctly under any install name (including npm: aliases).

* feat(network,wallet): the ONE hanzo.network selector + wallet menu

<NetworkSwitcher/> + <WalletMenu/> at @hanzo/ui-shadcn/{network,wallet} —
the shared network/wallet standard for desktop, app, chat, team, console.

- Network = (env, label, networkID, evmChainID, rpcEndpoint, apiEndpoint):
  sovereign L1, networkID === evmChainID; envs mirror the hanzo CLI
  (mainnet 36963 / testnet 36964 / devnet 36965 / local 31337; one
  api.hanzo.ai across public envs; per-env rpc.hanzo[-test|-dev].network).
- Selection persists env name only; endpoints always re-resolve from code.
- WalletAdapter seam keeps custody per-surface (desktop lux-wallet PQ HD,
  web injected EIP-1193 — non-custodial, no key material, no storage).
- 20 vitest cases; v5.7.2.

* feat(network,wallet): menuSide prop — menus open upward from footers (v5.7.3)

* fix(network): align to genesis-canonical chain IDs (v5.7.4)

The published 5.7.3 mirrored pre-reconcile CLI values. cli#3 + console#139
(both merged) fixed the canonical set to match genesis
(lux/genesis/configs/hanzo-*). Align EXACTLY to console main
src/lib/network.ts + cli main src/commands/network.rs:

  testnet  36964 -> 36962  (rpc.hanzo-test.network -> rpc.testnet.hanzo.network)
  devnet   36965 -> 36964  (rpc.hanzo-dev.network  -> rpc.devnet.hanzo.network)
  local    31337 -> 1337   (:9650/ext/bc/C/rpc     -> :9630/v1/bc/C/rpc)

Sovereign L1 networkID === evmChainID preserved. mainnet 36963 unchanged.
The shared component is the ONE place — it MUST match genesis.

vitest 254/254; tsup + tsc dts green.

---------

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-08 09:12:11 -07:00
hanzoandz 2cec1d9a87 feat(contracts): wire live Hanzo L1 addresses (mainnet 36963 / testnet 36962 / devnet 36964)
AIToken/ChainConfig/HUSD/Faucet/HanzoRegistry deployed + cast-verified on all 3 sovereign nets.
2026-07-07 22:10:40 -07:00
hanzo-dev 3443c673fe chore(world): tidy theme-proof brand tokens 2026-07-07 22:08:53 -07:00
hanzo-dev 58930de728 feat(world): embeddable OSS data-stream components in @hanzo/ui
Port the highest-value hanzoai/world panels to reusable, themeable React
components under @hanzo/ui/world (mirrors the /dash module pattern):

- NewsStream        — news/signal feed with severity rails, relative time
- MarketTicker      — quotes + inline sparkline + change (list & strip variants)
- InstabilityScore  — 0-100 risk gauge card (level, trend, driver breakdown)
- PredictionMarket  — yes/no probability bars

All pure-props with loading/error/empty states, plus /v1/world data hooks
(useWorldData + typed wrappers; native fetch, polling, abort-on-unmount).
Default look = world.hanzo.ai Geist/black-monochrome; re-skins to any host by
overriding --world-* tokens (default / light / host-brand all proven).

Registered ./world + ./world/* + ./world/tokens.css exports and tsup entries;
ships src/{dash,world}/tokens.css in the npm tarball. Build verified: tsup
bundle + tsc .d.ts emit clean, SSR render of dist asserts correct data,
Tailwind/Geist screenshots confirm the look.
2026-07-07 22:08:26 -07:00
90b61e482e build(ui,data): compiled .d.ts — retire raw-.tsx type surface (8.0.1/1.2.1) (#236)
* feat(@hanzo/data): Twenty-grade record views (table/board/detail/editors) — clean-room

Bring the Hanzo Base data-app layer to Airtable/Twenty-class polish, 100% original
(no Twenty code — GPL kept at arm's length; Twenty observed as a running-UI reference only).

New in pkg/data (published as @hanzo/data@1.2.0):
- RecordsView shell: table <-> board switch, search, filter builder, sort builder,
  board group-by, optional saved views — one ViewConfig, applied via pure view/logic.
- DataTable (Twenty-grade): click-to-sort headers, drag-resize + drag-reorder columns,
  row selection + select-all, inline cell editing, pagination, hover-open, honest states.
- BoardView: kanban grouped by a select/status/boolean/relation field; drag-between-lanes
  emits the record patch (optimistic, reverts on failure).
- RecordDetail: titled, inline-editable panel + related slot; RecordForm gains fieldOptions.
- Field editors upgraded: searchable select dropdown w/ color chips, month-grid calendar,
  relation record-picker, file upload, validated JSON, sliding boolean toggle.
- Pure logic (sort/filter/search/group/paginate/view) — gui-free, 37 unit tests, exported
  on subpaths (@hanzo/data/{table,board,view}/logic).
- Self-contained primitives (Menu/CheckBox/Toggle/Calendar) on the minimal proven gui surface.
- gui.config.ts + gui.d.ts so the package type-checks the v5 shorthands standalone.

@hanzo/ui/primitives/bases/data re-exports @hanzo/data (the bases surface convention).

tsc --noEmit clean; vitest 37/37.

* feat(@hanzo/ui): v8 unified lib on @hanzo/gui — product + record (@hanzo/data) layers

The one cross-platform, presentational, host-agnostic, clean-room component
library. Product/app layer (charts, metrics, page headers, status tags, empty
states, combobox, slide-over, toasts, drag-reorder, field rows, marks) at
'@hanzo/ui'; metadata-driven record layer composed from @hanzo/data at
'@hanzo/ui/data'; calm dark-first tokens + motion vocabulary. Web + native + desktop.

Retires the shadcn @hanzo/ui (5.x) → @hanzo/ui-shadcn; this gui-based line
carries the name forward at 8.0.0. tsc --noEmit clean, vitest 12/12.
Manifest: CONSOLIDATION.md.

* refactor(ui): retire shadcn @hanzo/ui → @hanzo/ui-shadcn (name freed for v8)

Rename the legacy shadcn/Radix line (pkgs/ui, 5.7.0) to @hanzo/ui-shadcn so the
gui-based unified library can carry the @hanzo/ui name forward at v8. Code is
untouched — only the package name changes; the published @hanzo/ui@5.7.1 stays
on npm for external ^5.x consumers.

Internal workspace consumers keep resolving the shadcn line with ZERO source
edits via workspace aliases (@hanzo/ui → @hanzo/ui-shadcn):
  - app (@hanzo/ui-web): dependency workspace alias
  - commerce: devDependency workspace alias (source imports @hanzo/ui/*),
    peer stays @hanzo/ui>=5.0.0 for external consumers
  - checkout, agent-ui: peer ranges unchanged (no source imports)
Proven: app + commerce node_modules/@hanzo/ui resolve to @hanzo/ui-shadcn@5.7.0.

Drive-by (unblocks workspace install/CI): pkgs/data pinned the now-unpublished
@hanzogui/config@7.2.2 — patch-forward to 7.3.0 (published latest, same major,
matches pkg/ui).

* build(ui,data): emit compiled .d.ts — retire raw-.tsx type surface

@hanzo/ui@8.0.1 + @hanzo/data@1.2.1: types/exports now point at flat
compiled declarations (tsc -p tsconfig.build.json → types/), matching
@hanzo/gui@7.3.0. src/gui-env.d.ts bakes the GuiCustomConfig augmentation
into each package's own compilation so shorthand props resolve internally —
consumers no longer type-check vendor .tsx (the 171-phantom-error/hoisted-
linker fragility).

---------

Co-authored-by: Hanzo AI <ai@hanzo.ai>
Co-authored-by: hanzo-dev <dev@hanzo.ai>
2026-07-07 17:55:44 -07:00
hanzo-devandGitHub 10155aad16 Merge pull request #237 from hanzoai/feat/ui-8-unified
feat(ui): v8 unified lib + shared AI usage components
2026-07-07 16:16:46 -07:00
Hanzo AI 8fd771aa02 Merge remote-tracking branch 'origin/main' into feat/ui-8-unified
# Conflicts:
#	pkgs/data/package.json
#	pkgs/ui/package.json
2026-07-07 16:16:32 -07:00
Hanzo AI e644e1f202 feat(ui): add shared AI usage components (UsageMeter, UsageProviderCard, UsageDashboard)
The ONE cross-platform AI-usage surface every Hanzo app (console, desktop,
app, chat) renders: a labeled rate-limit bar with % left + honest reset
countdown, a per-provider quota card (session/weekly/extra windows, spend,
history sparkline) mirroring the Codex menu card, and a dashboard grid with a
totals header. Composes existing Charts.Sparkline + Metric idioms on @hanzo/gui
primitives only; presentational, host-agnostic, web + native + desktop.

Bumps @hanzo/ui 8.0.0 -> 8.0.1.
2026-07-07 15:12:12 -07:00
8a389ccc90 feat(billing): shared CreditModal (trial + prepaid buckets, injected top-up) (#235)
Presentational, self-contained credit/top-up modal any Hanzo app
(console2, hanzo.app, billing.hanzo.ai) can mount with its own data +
handlers. All data/handlers injected via props (cents-based, commerce
balance shape); no network calls, no app coupling.

- Two distinct buckets: non-cash trial credit vs. real prepaid money,
  with a combined breakdown + explicit total.
- Welcome celebration state when a new user's trial credit just landed.
- Top-up affordance (preset amounts + custom) that calls the injected
  onTopUp(amountCents); Square/HUSD payment flow stays in the caller,
  optionally rendered via children. Reuses the existing handler-prop
  pattern (onAddFunds / SquareCardForm) rather than duplicating it.
- Reuses the package radix Dialog primitive for focus trap, escape,
  overlay + aria; styled with the billing semantic tokens.
- Exported from the billing barrel; 8 vitest cases.

Co-authored-by: hanzo-dev <dev@hanzo.ai>
2026-07-04 14:54:44 -07:00
hanzo-dev c32bd0b92b feat(billing): shared CreditModal (trial + prepaid buckets, injected top-up)
Presentational, self-contained credit/top-up modal any Hanzo app
(console2, hanzo.app, billing.hanzo.ai) can mount with its own data +
handlers. All data/handlers injected via props (cents-based, commerce
balance shape); no network calls, no app coupling.

- Two distinct buckets: non-cash trial credit vs. real prepaid money,
  with a combined breakdown + explicit total.
- Welcome celebration state when a new user's trial credit just landed.
- Top-up affordance (preset amounts + custom) that calls the injected
  onTopUp(amountCents); Square/HUSD payment flow stays in the caller,
  optionally rendered via children. Reuses the existing handler-prop
  pattern (onAddFunds / SquareCardForm) rather than duplicating it.
- Reuses the package radix Dialog primitive for focus trap, escape,
  overlay + aria; styled with the billing semantic tokens.
- Exported from the billing barrel; 8 vitest cases.
2026-07-04 14:36:34 -07:00
zandGitHub ef88ea8a4d Merge pull request #234 from hanzoai/feat/dashboard-layer
feat(dashboard): @hanzo/dashboard — reusable dashboard layer on @hanzo/gui
2026-07-04 12:35:35 -07:00
hanzo-dev a2b7ccbc05 polish(dashboard): align docstrings to clean export names
Drop 'LivingOverview'/'LineChart'/'BarChart'/'BarRows' from prose comments;
match the de-branded single-word exports. No code change (typecheck still clean).
2026-07-04 12:34:14 -07:00
hanzo-dev 919faf0dd4 docs(dashboard): README (exports, usage, provenance, console2 swap plan)
Documents every export + usage snippet, maps each back to its console2 source,
the generalizations made, and the exact per-component swap plan for consuming
@hanzo/dashboard back in console2 (follow-up pass). Tidy a double import.
2026-07-04 12:32:43 -07:00
hanzo-dev d7ae0eac67 fix(dashboard): bind gui shorthand types (typecheck + build clean)
Augment GuiCustomConfig in BOTH @hanzogui/web and @hanzogui/core, and declare
@hanzogui/web + @hanzogui/core as devDeps so the 'declare module' targets resolve
under pnpm strict nesting (npm-flat installs like console2 hoist them; pnpm does
not). Result: tsc --noEmit clean (0 errors) and tsc build emits dist cleanly.
2026-07-04 12:30:47 -07:00
hanzo-dev 9b961867b3 feat(dashboard): landing kit + deploy pipeline + barrel
- landing/: Landing (Hero/Metrics/Samples/Rail) + pure link logic — brand/docs
  via LandingConfig props (no host-app config coupling)
- pipeline/: pure pipeline.ts (de-branded stage model) + small stages-driven
  Pipeline component; de-branded CSS classes (hz-pipe-*)
- src/index.ts: clean single-word public API
No other-company brand names anywhere; single-word exports per naming guidance.
2026-07-04 12:25:35 -07:00
hanzo-dev 9cc91f4108 feat(dashboard): overview driver + composable primitives
- overview/config.ts: declarative OverviewConfig contract (generalized icon type)
- overview/logic.ts: pure tile decisions (format/delta/selectors/health/virtualize)
- overview/primitives.tsx: Kpi / Feed / Board + Panel/Skeleton chrome (direct props)
- overview/tiles.tsx: config-tile adapters + Tile router (delegate to primitives)
- overview/Overview.tsx: the poll-loop driver (isGlobalAdmin as prop; inline
  header/fade/error — no host-app coupling)
- charts: rename to clean single words (Line/Columns/Bars)
Clean single-word exports per naming guidance; no 'Living' prefix.
2026-07-04 12:20:38 -07:00
hanzo-dev 01c55b1183 feat(dashboard): scaffold @hanzo/dashboard + charts + motion layer
Extracted from Hanzo Cloud Console (console2):
- charts/Charts.tsx: Sparkline/LineChart/BarChart/Donut/BarRows (monochrome SVG)
- motion/motion.ts: pure count-up/ring/poll-clock math
- motion/hooks.ts: useReducedMotion/usePageHidden/useCountUp/usePoll
- dashboard.css: motion keyframes (reduced-motion-guarded)
Mirrors the @hanzo/data package convention (@hanzo/gui peer, source-shipped, tsc).
2026-07-04 12:12:47 -07:00
Hanzo AI b6bcb6d597 refactor(ui): retire shadcn @hanzo/ui → @hanzo/ui-shadcn (name freed for v8)
Rename the legacy shadcn/Radix line (pkgs/ui, 5.7.0) to @hanzo/ui-shadcn so the
gui-based unified library can carry the @hanzo/ui name forward at v8. Code is
untouched — only the package name changes; the published @hanzo/ui@5.7.1 stays
on npm for external ^5.x consumers.

Internal workspace consumers keep resolving the shadcn line with ZERO source
edits via workspace aliases (@hanzo/ui → @hanzo/ui-shadcn):
  - app (@hanzo/ui-web): dependency workspace alias
  - commerce: devDependency workspace alias (source imports @hanzo/ui/*),
    peer stays @hanzo/ui>=5.0.0 for external consumers
  - checkout, agent-ui: peer ranges unchanged (no source imports)
Proven: app + commerce node_modules/@hanzo/ui resolve to @hanzo/ui-shadcn@5.7.0.

Drive-by (unblocks workspace install/CI): pkgs/data pinned the now-unpublished
@hanzogui/config@7.2.2 — patch-forward to 7.3.0 (published latest, same major,
matches pkg/ui).
2026-07-03 15:54:24 -07:00
Hanzo AI 651108f1c5 feat(@hanzo/ui): v8 unified lib on @hanzo/gui — product + record (@hanzo/data) layers
The one cross-platform, presentational, host-agnostic, clean-room component
library. Product/app layer (charts, metrics, page headers, status tags, empty
states, combobox, slide-over, toasts, drag-reorder, field rows, marks) at
'@hanzo/ui'; metadata-driven record layer composed from @hanzo/data at
'@hanzo/ui/data'; calm dark-first tokens + motion vocabulary. Web + native + desktop.

Retires the shadcn @hanzo/ui (5.x) → @hanzo/ui-shadcn; this gui-based line
carries the name forward at 8.0.0. tsc --noEmit clean, vitest 12/12.
Manifest: CONSOLIDATION.md.
2026-07-03 15:46:45 -07:00
zandhanzo-dev 5b5e97b4fa fix(data): @hanzogui/config 7.2.2 (unpublished) -> 7.3.0 — unblock workspace install + @hanzo/ui publish
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-03 09:04:33 -07:00
zandhanzo-dev b9f3eb2169 chore(ui): release @hanzo/ui 5.7.1 — design unification (Basel Grotesk + Geist Mono canonical, DESIGN.md)
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-03 08:56:52 -07:00
zandGitHub 59ce5da076 Merge pull request #233 from hanzoai/design/unify-basel-geist-mono
design: unify Basel Grotesk + Geist Mono, sidebar icon, panels, dark tokens
2026-07-03 08:46:51 -07:00
zandhanzo-dev 8294e87b40 design: make Basel Grotesk + Geist Mono canonical; add DESIGN.md source of truth
- Typography: fontSans -> Basel Grotesk via next/font/local (self-hosted,
  Book 400 + Medium 500, --font-basel-sans); keep fontMono = Geist Mono.
  Point both tailwind configs' sans at var(--font-basel-sans). Basel replaces
  Geist Sans as the default; DM Sans/Figtree/Inter stay optional .theme-*
  variants only, never defaults.
- DESIGN.md: the single source of truth for the shared Hanzo look — canonical
  typography (Basel + Geist Mono), the sidebar toggle icon (lucide PanelLeft),
  sidebar/panel specs (16rem width, border-border, monochrome hover/active),
  and the true-black dark palette (#000 canvas / #0a0a0a surface / white-10
  borders / #ededf1 text).

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-03 01:53:40 -07:00
Hanzo AI dbc822b09b feat(@hanzo/data): Twenty-grade record views (table/board/detail/editors) — clean-room
Bring the Hanzo Base data-app layer to Airtable/Twenty-class polish, 100% original
(no Twenty code — GPL kept at arm's length; Twenty observed as a running-UI reference only).

New in pkg/data (published as @hanzo/data@1.2.0):
- RecordsView shell: table <-> board switch, search, filter builder, sort builder,
  board group-by, optional saved views — one ViewConfig, applied via pure view/logic.
- DataTable (Twenty-grade): click-to-sort headers, drag-resize + drag-reorder columns,
  row selection + select-all, inline cell editing, pagination, hover-open, honest states.
- BoardView: kanban grouped by a select/status/boolean/relation field; drag-between-lanes
  emits the record patch (optimistic, reverts on failure).
- RecordDetail: titled, inline-editable panel + related slot; RecordForm gains fieldOptions.
- Field editors upgraded: searchable select dropdown w/ color chips, month-grid calendar,
  relation record-picker, file upload, validated JSON, sliding boolean toggle.
- Pure logic (sort/filter/search/group/paginate/view) — gui-free, 37 unit tests, exported
  on subpaths (@hanzo/data/{table,board,view}/logic).
- Self-contained primitives (Menu/CheckBox/Toggle/Calendar) on the minimal proven gui surface.
- gui.config.ts + gui.d.ts so the package type-checks the v5 shorthands standalone.

@hanzo/ui/primitives/bases/data re-exports @hanzo/data (the bases surface convention).

tsc --noEmit clean; vitest 37/37.
2026-07-02 23:47:23 -07:00
Hanzo AI 146be601fa feat(products): add the 10 missing customer products to the snapshot (92 -> 102)
The @hanzo/products snapshot (the shared-registry fallback + commerce seed + the
source docs' gen-services-nav derives from) had drifted behind the canonical console
registry (hanzoai/console src/lib/products/registry.tsx). Ten customer products that
shipped in recent console waves were absent from the snapshot, so docs coverage +
the derived services nav under-counted the real product set.

Purely additive — the existing 92 rows are byte-identical; ten rows appended, each
at the end of its category run (diff is insertions only):
  Observe: open-edition, analytics    Data: records    Platform: apps
  Apps: crm, cms, erp, helpdesk, accessibility, templates

Fields derived to match the existing snapshot shape + the package maps:
brandColor = defaultColorKey(id) (curated pin or FNV-1a hash), iconKey = the
registry icon component, slug=id, route=/id, docsUrl=/docs/services/<id>,
apiPath /v1-prefixed, brands derived from category. All package invariants hold
(icon-drift guard over the 1760-icon vocab, swatch keys, 10 canonical categories,
/v1 apiPaths, unique slug==id). Count assertion + doc-comment 92 -> 102.

vitest 90/90, tsc --noEmit clean.
2026-07-02 23:28:26 -07:00
Hanzo AI 9982ea93da feat(products): @hanzo/products canonical catalog on the locked 10-category taxonomy
The single source of truth for the Hanzo product taxonomy/icons/colors that
console, docs, site, and pricing all derive from — committed on the CTO-locked
10-category cut so every surface groups by ONE axis.

CATEGORY_ORDER (13 -> 10): AI · Compute · Data · Network · Security · Observe ·
Platform · Web3 · Apps · Commerce. The three cuts:
  - Training -> AI          (finetuning, kubeflow — AI training)
  - Dev -> Platform         (cli, sdks, api, integrations, ide, desktop, api-keys —
                             the platform's developer surface)
  - Settings -> removed     (settings/team/profile are account/avatar-menu items,
                             not products — dropped from the catalog grid)

Snapshot: 92 products (95 - 3 dropped); every row's brands[] re-derived from
category (never hand-authored — the drift-killer). Sovereign brand scope
(lux/zoo/pars) follows the merge: Web3 · Network · Security · Platform, so the
chains keep CLI/SDKs/API keys; account settings move to the avatar menu.

types/categories/brands/docs/snapshot + LLM.md updated; tests assert the new
canonical set (CATEGORY_ORDER.length === 10, none of Training/Dev/Settings
remain, sovereign+hanzo-only scopes partition the 10). 90 tests green,
tsc --noEmit clean, tsup build OK.
2026-07-01 22:52:52 -07:00
94e0031933 feat(data): every field type editable — relation/files/links/json/fullName/address inputs (#232)
The @hanzo/data field registry had Displays for all 24 types but Inputs for only
~15 — records were read-mostly. Fill the gaps so a Base record is FULLY editable
in table/detail (the CRM/CMS foundation):

- RelationInput — single/to-many record picker over host-injected candidate
  options (metadata.options), falls back to raw-id entry when none injected.
- FilesInput / LinksInput — add/remove chip lists.
- JsonInput — parses on change, keeps text on invalid so typing isn't lost.
- FullNameInput (first/last) + AddressInput (street/city/state/zip) — composite
  sub-field editors.
- relation metadata gains options + maxSelect; files gains accept + maxSelect.

Only the true system types (uuid/position/actor) stay display-only. Built on the
same @hanzo/gui primitives + FieldInputProps as the existing inputs (cross-
platform, shorthand style). registry.test.ts asserts every non-system type now
has an Input (mocking @hanzo/gui). 11/11 tests pass, tsc clean.

Co-authored-by: z <z@zeekay.io>
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-01 18:45:40 -07:00
zandhanzo-dev 72ff9accc7 feat(data): every field type editable — relation/files/links/json/fullName/address inputs
The @hanzo/data field registry had Displays for all 24 types but Inputs for only
~15 — records were read-mostly. Fill the gaps so a Base record is FULLY editable
in table/detail (the CRM/CMS foundation):

- RelationInput — single/to-many record picker over host-injected candidate
  options (metadata.options), falls back to raw-id entry when none injected.
- FilesInput / LinksInput — add/remove chip lists.
- JsonInput — parses on change, keeps text on invalid so typing isn't lost.
- FullNameInput (first/last) + AddressInput (street/city/state/zip) — composite
  sub-field editors.
- relation metadata gains options + maxSelect; files gains accept + maxSelect.

Only the true system types (uuid/position/actor) stay display-only. Built on the
same @hanzo/gui primitives + FieldInputProps as the existing inputs (cross-
platform, shorthand style). registry.test.ts asserts every non-system type now
has an Input (mocking @hanzo/gui). 11/11 tests pass, tsc clean.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-01 18:45:23 -07:00
zandhanzo-dev 7fce244597 test(data): field registry + theme tests (@hanzo/data was untested)
9 bun tests / 64 assertions on the pure core: registry dispatch (register/
retrieve/override/read-only-omits-Input/enumerate) + theme (all 9 tag colors
legible, tagTone fallback, surface tokens = Hanzo zinc-on-black). No @hanzo/gui
needed (type-only imports). Adds `test` script.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-06-30 23:03:56 -07:00
zandhanzo-dev be7af671a6 refactor(data): canonical createGui (not createHanzogui) + GuiCustomConfig augmentation
Match the console pattern: createGui + gui.d.ts augmenting @hanzogui/web's
GuiCustomConfig, pinned to @hanzo/gui 7.3.0 (which renamed createHanzogui →
createGui). Shorthand props typecheck clean.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-06-30 15:35:12 -07:00
zandhanzo-dev 2b9bc976f8 feat(data): @hanzo/data — cross-platform metadata-driven data-app layer
Typed field system (26 types) → record table / card / detail, on @hanzo/gui
(web + native + desktop), shorthand style props, zero Tailwind. The universal
object/field/record/view core for any Base-backed CRM, CMS, or commerce app.
Registry-dispatched (add a type = one registerField call). Ships TS source
(zero-build internal package). tsc --noEmit clean against @hanzo/gui 7.2.2.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-06-30 15:13:34 -07:00
z 9f720e924d docs(brand): add hero banner 2026-06-28 20:07:59 -07:00
z 20df9bcf0a chore(brand): dynamic hero banner 2026-06-28 20:07:57 -07:00
zeekay d6604a30cb ci(publish): use canonical org NPM_TOKEN (stale repo NPM_AUTH_TOKEN → E401)
The repo-level NPM_AUTH_TOKEN (2026-03-29) shadowed the org token and was
expired → 'npm error 401 Unauthorized' on every tag publish. Converge on the
org-level NPM_TOKEN (2026-06-18, the one @hanzo/iam published 0.13.0 with).
One token, one way.
2026-06-25 00:47:02 -07:00
zeekay dfe9a32add ci: drop cache:'pnpm' from remaining workflows (no committed lockfile) 2026-06-24 23:38:22 -07:00
zeekay 1b8846c94b ci: drop broken cache:'pnpm' (no committed lockfile) — unblock @hanzo/ui publish 2026-06-24 23:37:37 -07:00
05f1cebfe6 feat(auth): composable <SignIn> surface — password + social + web3, brand-neutral (#231)
@hanzo/ui becomes the PRESENTATION layer over @hanzo/iam's mechanism. Atomic,
composable, knows nothing of token exchange — it just starts a login per method.

- <SignIn providers={[...]}> composes <PasswordForm> + one <SocialButton> per
  provider + <Web3Connect>. providers is the ONLY app-level knob.
- <SocialButton provider> delegates to startIamLogin with the provider knob
  (rides as &provider on /v1/iam/oauth/authorize); apps never register per-app
  Google/GitHub clients. Composition escape hatch: inject onLogin (e.g. the
  @hanzo/iam SDK's startLogin) so @hanzo/ui stays dependency-light.
- buildIamAuthorizeUrl adds the provider knob to iam.ts, mirroring the SDK.
- IAMLoginButton de-hexed: brand via CSS tokens (bg-primary, border-input, …),
  not literals. Every atom is monochrome/brand-neutral by construction.
- demo/sign-in-demo.tsx: the CONFIGURATION layer — zero auth code, wires
  <SignIn> to the @hanzo/iam SDK (startLogin + loginWithPassword).
- auth.test.tsx (10 cases, vitest+happy-dom): authorize URL carries PKCE-S256 +
  provider hint for google/github/web3, no /api/; <SignIn> renders a method per
  provider; rendered markup has zero hex (brand-neutral); <SocialButton>
  delegates to the injected starter with the provider knob.
- declare happy-dom (vitest env, was referenced but undeclared).

Co-authored-by: zeekay <z@zeekay.io>
2026-06-24 19:18:10 -07:00
5514155ecf feat(auth): canonical IAM OIDC paths + PKCE-S256 for @hanzo/ui auth (HIP-0111) (#230)
The zero-dep auth components hand-rolled the OAuth authorize URL against the
legacy '/login/oauth/authorize' path WITHOUT PKCE, and the shell hook fetched
userinfo from '/api/userinfo'. Both violate HIP-0111 (canonical paths are
'/v1/iam/oauth/*'; PKCE S256 is always required).

- new auth/iam.ts: one canonical helper (IAM_OIDC_PATHS mirroring @hanzo/iam's
  OIDC_PATHS; startIamLogin() does authorization-code + PKCE-S256 to
  /v1/iam/oauth/authorize). One way to start a login.
- IAMLoginButton + AuthGuard: route through startIamLogin() instead of
  hand-rolling a PKCE-less authorize URL on the legacy path.
- useHanzoAuth: /api/userinfo -> /v1/iam/oauth/userinfo.

@hanzo/ui stays dependency-light, so iam.ts mirrors the SDK's path contract
byte-for-byte rather than pulling in @hanzo/iam; identical endpoints, PKCE on.

Co-authored-by: z <z@zeekay.io>
2026-06-24 19:15:49 -07:00
e01bed3993 fix(ui): resolve React #418 hydration mismatch in command menu (#229)
The mod-key glyph was computed during render via isMacOS() (window.navigator),
so the static-export SSR produced 'Ctrl' while the macOS client hydrated '⌘' on
the same <span>, tripping React error #418 (hydration text mismatch) on every
page (CommandMenu is in the global SiteHeader).

Start modKey from the SSR-stable 'Ctrl' and upgrade to the platform glyph in a
client-only mount effect, so SSR and first hydration render agree. Verified via
CDP: pre-fix decoder showed args[]=text with the exact Ctrl->⌘ text node.

Co-authored-by: Hanzo CTO <ai@hanzo.ai>
2026-06-23 18:30:44 -07:00
hanzo-dev 132b99bd2b chore: restore shadcn/ui attribution (NOTICE), OSS compliance 2026-06-21 07:19:40 -07:00
7148a19631 ci: run on self-hosted ARC pool (hanzo-build-linux-amd64/deploy), not GitHub-hosted (#225)
Co-authored-by: zeekay <z@hanzo.ai>
2026-06-19 20:39:27 -07:00
Artem AshandGitHub 1900acf0e5 decouple @hanzogui, standardize on 'pkgs/', etc (#228)
* refactor: decouple @hanzo/ui from hanzogui

  Make @hanzo/ui shadcn-only — no hanzogui/Tamagui coupling.

  - remove primitives/bases (gui/admin/svelte/vue) re-exports
  - drop @hanzogui peerDependencies + peerDependenciesMeta entries
  - add check-no-hanzogui guard, wired into build
  - fix latent NodeJS.Timeout types to keep the build green

* refactor: remove duplicate @hanzo/brand from the monorepo

  @hanzo/brand is owned by the standalone hanzoai/brand (the npm-canonical
  source); this monorepo's copy was unused and had drifted.

  - delete pkg/brand
  - add check-no-brand-pkg guard (wired into `check`) so it can't reappear

* refactor(ui): stop importing from the app; use own cn util

  pkg/ui pulled `cn` from @/lib/utils (../../app) — an inverted dependency on
  the consuming app. Point the animation components at pkg/ui's own cn and
  drop the @/app, @/registry, @/lib tsconfig path aliases.

* fix(ui): type errors; stop tracking the root lockfile

  - ModelCard: lucide-react dropped the Github icon → use Code
  - drawer: annotate DrawerTrigger/DrawerClose (radix type portability)
  - untrack root pnpm-lock.yaml; CI → --no-frozen-lockfile

* release: bump @hanzo/ui to 5.7.0

* refactor: rename pkg/ → pkgs/, merge packages/ into it

  standardize on 'pkgs/': move pkg/* and packages/* into pkgs/.
  Update workspace globs, CI, scripts, and config references accordingly.
2026-06-19 15:30:30 -10:00
04c6cfcfab feat(commerce): add server-side usage-metering hook (@hanzo/commerce/metering) (#227)
The TypeScript counterpart of github.com/hanzoai/go-sdk/metering — the one
way every Hanzo product meters usage to commerce (the billing source of
truth) so everything can be paid for, not just the LLM/cloud path. Shares
an identical wire contract with the Go client.

- Metering class composes the existing Commerce client (no HTTP dup):
  authorize() pre-request balance gate (fail-closed by default; 402 vs 503),
  record() post-request usage write. tierAware gates on effectiveAvailable
  (prepaid + included plan allotment).
- S2S auth: Authorization: Bearer COMMERCE_SERVICE_TOKEN (KMS-sourced) +
  X-IAM-Org-Id. Metering.fromEnv() for canonical env wiring.
- identityFromHeaders(): reads gateway-minted X-User-Id/X-Org-Id.
- client.ts: getTier() + custom-headers support on request/getBalance/
  addUsageRecord (DRY enablers for the S2S org header).
- 14 contract tests (mock fetch) mirroring the Go suite; isolated tsc clean.
- exports: ./metering ; index re-exports ; version 7.6.1 -> 7.6.2.

Refs universe task #28.

Co-authored-by: Antje Worring <worringantje@gmail.com>
2026-06-19 10:22:54 -07:00
Artem Ash 6d99362599 added publish script 2026-06-18 18:49:05 -10:00
255beac271 feat(navigation): surface web3.hanzo.ai in the Hanzo app switcher (#226)
The Hanzo cross-app console (the shared header / app switcher rendered by
@hanzo/ui across console, platform, billing, chat, etc.) is driven by a
single canonical registry in navigation/hanzo-shell/types.ts. The
bootnode-powered chain orchestration surface at web3.hanzo.ai was live but
absent from that registry, so it never appeared in the switcher.

Add "Web3" as an Infrastructure-category app, threaded through every
structure that enumerates the registry so the list stays orthogonal:

- DEFAULT_HANZO_APPS: the static hanzo.ai default list.
- OrgDomains type + all four ORG_DOMAINS maps (hanzo, lux, zoo, pars):
  white-label by org domain — web3.hanzo.ai / web3.lux.network /
  web3.zoo.ngo / web3.pars.network.
- getAppsForOrg(): the org-aware URL builder.
- AppSwitcher APP_GROUPS: place "web3" in the Infrastructure section
  (between cloud and storage) so it renders grouped, not under "Other".

No icon is set — the switcher renders label + description only; every
existing entry is icon-less, so this matches the one established pattern.

Description: "Deploy & manage blockchain validators across Bitcoin,
Ethereum, Solana, Lux, and any Lux-derived L1".

Co-authored-by: zeekay <z@zeekay.io>
2026-06-18 20:47:28 -07:00
7d74ffefb5 chore(deps): vite 8 + plugin-react 6, fumadocs 16.10.3, dep sweep (#224)
Land the Dependabot dependency upgrades with real migration + build
verification.

templates/vite-monorepo (standalone workspace):
- vite 7.3.1 -> ^8.0.16 (closes #151)
- @vitejs/plugin-react 5.1.4 -> ^6.0.2 (closes #143)
- @types/node -> ^25.5.0 (closes #135)
  vite.config needed no migration (only react()+tailwindcss() plugins);
  verified `tsc -b && vite build` under Node 26 -> "vite v8.0.16 ... built".

apps/v4 (docs site):
- fumadocs-ui 16.0.5 -> 16.10.3, fumadocs-core 16.0.5 -> 16.10.3 (closes #119)
- fumadocs-mdx 13.0.2 -> 15.0.12, fumadocs-docgen 2.0.0 -> 3.0.10 (lockstep)
- unist-builder 3.0.0 -> 4.0.0 (closes #122)
  Migration: fumadocs-mdx 15 splits generated output into .source/server.ts
  (no barrel index.ts), so lib/source.ts imports `docs` from "@/.source/server".
  Verified full `next build` -> 1023 static routes prerendered (205 /docs/*).

pkg/ui:
- @next/third-parties 16.2.1 -> ^16.2.7 (closes #120); verified tsup build.

packages/shadcn:
- @dotenvx/dotenvx 1.48.4 -> ^1.73.1 (closes #118); verified tsup build.

app:
- puppeteer 24.40.0 -> ^25.1.0 (lockstep with root, closes #121);
  verified import + launch API under Node 26.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-06-17 04:37:47 -07:00
hanzo-dev 1fc67d85af chore: update 2026-06-10 14:11:16 -07:00
hanzo-dev 1ae75a0cc9 cleanup: remove AI-slop summary / status / plan / report files
Reverts violations of the durable rule that current-state docs belong
in LLM.md and history belongs in git log. Removed files were session
handoffs, agent-style "complete success" / "1000%" reports, dated
audit dumps, and stub NOTES.
2026-06-07 13:36:17 -07:00
hanzo-dev 7d5fccc5a6 chore: drop stale .disabled files
sync-forks.yml.disabled and market-overview-alt.mdx.disabled are fossilized
copies left over from earlier work. Stale .disabled files in a tracked tree
are dead code that lives forever; either re-enable or delete.
2026-06-07 13:36:17 -07:00
hanzo-devandGitHub 4b77ebcb14 Merge pull request #117 from hanzoai/dependabot/npm_and_yarn/faker-js/faker-10.3.0
chore(deps): bump @faker-js/faker from 10.3.0 to 10.4.0
2026-06-04 01:33:14 -07:00
hanzo-devandGitHub 7bc665b701 Merge pull request #124 from hanzoai/dependabot/npm_and_yarn/unist-util-visit-5.1.0
chore(deps-dev): bump unist-util-visit from 4.1.2 to 5.1.0
2026-06-04 01:33:02 -07:00
hanzo-devandGitHub e7dda5292f Merge pull request #126 from hanzoai/dependabot/npm_and_yarn/templates/vite-app/eslint/js-10.0.1
chore(deps-dev): bump @eslint/js from 9.39.4 to 10.0.1 in /templates/vite-app
2026-06-04 01:31:54 -07:00
hanzo-devandGitHub 0e34303cb4 Merge pull request #137 from hanzoai/dependabot/npm_and_yarn/templates/react-router-app/react-router/serve-7.13.2
chore(deps): bump @react-router/serve from 7.13.1 to 7.16.0 in /templates/react-router-app
2026-06-04 01:31:48 -07:00
hanzo-devandGitHub 921e7bd069 Merge pull request #140 from hanzoai/dependabot/npm_and_yarn/templates/astro-monorepo/globals-17.4.0
chore(deps-dev): bump globals from 14.0.0 to 17.6.0 in /templates/astro-monorepo
2026-06-04 01:31:44 -07:00
hanzo-devandGitHub fe01adeec6 Merge pull request #148 from hanzoai/dependabot/npm_and_yarn/templates/react-router-monorepo/react-router/node-7.13.2
chore(deps): bump @react-router/node from 7.12.0 to 7.16.0 in /templates/react-router-monorepo
2026-06-04 01:31:39 -07:00
hanzo-devandGitHub c7b3913b75 Merge pull request #152 from hanzoai/dependabot/npm_and_yarn/templates/next-monorepo/eslint-plugin-turbo-2.8.20
chore(deps-dev): bump eslint-plugin-turbo from 2.8.7 to 2.9.16 in /templates/next-monorepo
2026-06-04 01:31:33 -07:00
hanzo-devandGitHub ba78ed8db0 Merge pull request #154 from hanzoai/dependabot/npm_and_yarn/templates/start-app/typescript-6.0.2
chore(deps-dev): bump typescript from 5.9.3 to 6.0.3 in /templates/start-app
2026-06-04 01:31:30 -07:00
hanzo-devandGitHub 36044182d5 Merge pull request #161 from hanzoai/dependabot/npm_and_yarn/templates/react-router-monorepo/vite-8.0.2
chore(deps-dev): bump vite from 7.3.1 to 8.0.16 in /templates/react-router-monorepo
2026-06-04 01:31:23 -07:00
hanzo-devandGitHub 413946f053 Merge pull request #163 from hanzoai/dependabot/npm_and_yarn/templates/next-monorepo/typescript-eslint/parser-8.57.2
chore(deps-dev): bump @typescript-eslint/parser from 8.55.0 to 8.60.1 in /templates/next-monorepo
2026-06-04 01:31:18 -07:00
hanzo-devandGitHub 557a5ca2e4 Merge pull request #165 from hanzoai/dependabot/npm_and_yarn/templates/start-monorepo/vite-tsconfig-paths-6.1.1
chore(deps): bump vite-tsconfig-paths from 5.1.4 to 6.1.1 in /templates/start-monorepo
2026-06-04 01:31:15 -07:00
hanzo-devandGitHub 62f0a17885 Merge pull request #171 from hanzoai/dependabot/npm_and_yarn/templates/start-app/vitest-4.1.2
chore(deps-dev): bump vitest from 2.1.9 to 4.1.8 in /templates/start-app
2026-06-04 01:30:54 -07:00
hanzo-devandGitHub be6f658c35 Merge pull request #174 from hanzoai/dependabot/npm_and_yarn/templates/next-app/eslint-10.2.0
chore(deps-dev): bump eslint from 10.1.0 to 10.4.1 in /templates/next-app
2026-06-04 01:30:43 -07:00
dependabot[bot]andGitHub 74a979fb36 chore(deps): bump @faker-js/faker from 10.3.0 to 10.4.0
Bumps [@faker-js/faker](https://github.com/faker-js/faker) from 10.3.0 to 10.4.0.
- [Release notes](https://github.com/faker-js/faker/releases)
- [Changelog](https://github.com/faker-js/faker/blob/next/CHANGELOG.md)
- [Commits](https://github.com/faker-js/faker/compare/v10.3.0...v10.4.0)

---
updated-dependencies:
- dependency-name: "@faker-js/faker"
  dependency-version: 10.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-04 08:14:02 +00:00
dependabot[bot]andGitHub 7ebd0bc664 chore(deps-dev): bump unist-util-visit from 4.1.2 to 5.1.0
Bumps [unist-util-visit](https://github.com/syntax-tree/unist-util-visit) from 4.1.2 to 5.1.0.
- [Release notes](https://github.com/syntax-tree/unist-util-visit/releases)
- [Commits](https://github.com/syntax-tree/unist-util-visit/compare/4.1.2...5.1.0)

---
updated-dependencies:
- dependency-name: unist-util-visit
  dependency-version: 5.1.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-04 08:13:41 +00:00
dependabot[bot]andGitHub b7525ec00c chore(deps): bump @react-router/node in /templates/react-router-monorepo
Bumps [@react-router/node](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-node) from 7.12.0 to 7.16.0.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-node/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/@react-router/node@7.16.0/packages/react-router-node)

---
updated-dependencies:
- dependency-name: "@react-router/node"
  dependency-version: 7.13.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-04 08:11:32 +00:00
dependabot[bot]andGitHub 39cb4efec3 chore(deps-dev): bump @eslint/js in /templates/vite-app
Bumps [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) from 9.39.4 to 10.0.1.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/commits/v10.0.1/packages/js)

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-version: 10.0.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-04 08:11:21 +00:00
dependabot[bot]andGitHub ebc06d3901 chore(deps): bump @react-router/serve in /templates/react-router-app
Bumps [@react-router/serve](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-serve) from 7.13.1 to 7.16.0.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-serve/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/@react-router/serve@7.16.0/packages/react-router-serve)

---
updated-dependencies:
- dependency-name: "@react-router/serve"
  dependency-version: 7.13.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-04 08:11:11 +00:00
dependabot[bot]andGitHub 44ad415163 chore(deps-dev): bump globals in /templates/astro-monorepo
Bumps [globals](https://github.com/sindresorhus/globals) from 14.0.0 to 17.6.0.
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v14.0.0...v17.6.0)

---
updated-dependencies:
- dependency-name: globals
  dependency-version: 17.4.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-04 08:11:10 +00:00
dependabot[bot]andGitHub 8667dc05af chore(deps-dev): bump eslint-plugin-turbo in /templates/next-monorepo
Bumps [eslint-plugin-turbo](https://github.com/vercel/turborepo/tree/HEAD/packages/eslint-plugin-turbo) from 2.8.7 to 2.9.16.
- [Release notes](https://github.com/vercel/turborepo/releases)
- [Changelog](https://github.com/vercel/turborepo/blob/main/RELEASE.md)
- [Commits](https://github.com/vercel/turborepo/commits/v2.9.16/packages/eslint-plugin-turbo)

---
updated-dependencies:
- dependency-name: eslint-plugin-turbo
  dependency-version: 2.8.20
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-04 08:09:20 +00:00
dependabot[bot]andGitHub 2ff83ab818 chore(deps-dev): bump typescript in /templates/start-app
Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.9.3 to 6.0.3.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.9.3...v6.0.3)

---
updated-dependencies:
- dependency-name: typescript
  dependency-version: 6.0.2
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-04 08:08:51 +00:00
dependabot[bot]andGitHub 252e248ddd chore(deps-dev): bump vite in /templates/react-router-monorepo
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.3.1 to 8.0.16.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.16/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.2
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-04 08:08:23 +00:00
dependabot[bot]andGitHub b8978e8e5c chore(deps): bump vite-tsconfig-paths in /templates/start-monorepo
Bumps [vite-tsconfig-paths](https://github.com/aleclarson/vite-tsconfig-paths) from 5.1.4 to 6.1.1.
- [Release notes](https://github.com/aleclarson/vite-tsconfig-paths/releases)
- [Commits](https://github.com/aleclarson/vite-tsconfig-paths/compare/v5.1.4...v6.1.1)

---
updated-dependencies:
- dependency-name: vite-tsconfig-paths
  dependency-version: 6.1.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-04 08:07:55 +00:00
dependabot[bot]andGitHub ced2f464be chore(deps-dev): bump @typescript-eslint/parser
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 8.55.0 to 8.60.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.60.1/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.57.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-04 08:06:04 +00:00
dependabot[bot]andGitHub b2452edb33 chore(deps-dev): bump eslint in /templates/next-app
Bumps [eslint](https://github.com/eslint/eslint) from 10.1.0 to 10.4.1.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.1.0...v10.4.1)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 10.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-04 08:05:12 +00:00
dependabot[bot]andGitHub 79604435a1 chore(deps-dev): bump vitest from 2.1.9 to 4.1.8 in /templates/start-app
Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 2.1.9 to 4.1.8.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.8/packages/vitest)

---
updated-dependencies:
- dependency-name: vitest
  dependency-version: 4.1.2
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-04 08:05:11 +00:00
hanzo-devandGitHub 4f1cc53e4a Merge pull request #125 from hanzoai/dependabot/npm_and_yarn/templates/react-router-app/typescript-6.0.2
chore(deps-dev): bump typescript from 5.9.3 to 6.0.2 in /templates/react-router-app
2026-06-03 13:15:36 -07:00
hanzo-devandGitHub ade5e66f55 Merge pull request #129 from hanzoai/dependabot/npm_and_yarn/templates/astro-monorepo/astro-6.0.8
chore(deps): bump astro from 5.18.1 to 6.0.8 in /templates/astro-monorepo
2026-06-03 13:15:30 -07:00
hanzo-devandGitHub 78dd10cd23 Merge pull request #128 from hanzoai/dependabot/npm_and_yarn/templates/astro-app/astrojs/react-5.0.1
chore(deps): bump @astrojs/react from 4.4.2 to 5.0.1 in /templates/astro-app
2026-06-03 13:15:26 -07:00
hanzo-devandGitHub dd4b4d361d Merge pull request #127 from hanzoai/dependabot/npm_and_yarn/templates/next-app/typescript-6.0.2
chore(deps-dev): bump typescript from 5.9.3 to 6.0.2 in /templates/next-app
2026-06-03 13:15:22 -07:00
hanzo-devandGitHub 91d0d7af31 Merge pull request #130 from hanzoai/dependabot/npm_and_yarn/templates/vite-app/typescript-6.0.2
chore(deps-dev): bump typescript from 5.9.3 to 6.0.2 in /templates/vite-app
2026-06-03 13:15:18 -07:00
hanzo-devandGitHub 2bc086f102 Merge pull request #132 from hanzoai/dependabot/npm_and_yarn/templates/astro-monorepo/eslint/js-10.0.1
chore(deps-dev): bump @eslint/js from 9.39.4 to 10.0.1 in /templates/astro-monorepo
2026-06-03 13:15:14 -07:00
hanzo-devandGitHub c8f480373d Merge pull request #131 from hanzoai/dependabot/npm_and_yarn/templates/react-router-app/react-router-7.13.2
chore(deps): bump react-router from 7.13.1 to 7.13.2 in /templates/react-router-app
2026-06-03 13:15:10 -07:00
hanzo-devandGitHub bbb8319082 Merge pull request #133 from hanzoai/dependabot/npm_and_yarn/templates/vite-app/globals-17.4.0
chore(deps-dev): bump globals from 14.0.0 to 17.4.0 in /templates/vite-app
2026-06-03 13:15:06 -07:00
hanzo-devandGitHub 1d4484ce71 Merge pull request #134 from hanzoai/dependabot/npm_and_yarn/templates/astro-app/eslint/js-10.0.1
chore(deps-dev): bump @eslint/js from 9.39.4 to 10.0.1 in /templates/astro-app
2026-06-03 13:15:02 -07:00
hanzo-devandGitHub 00a60e5eea Merge pull request #136 from hanzoai/dependabot/npm_and_yarn/templates/next-monorepo/zod-4.3.6
chore(deps): bump zod from 3.25.76 to 4.3.6 in /templates/next-monorepo
2026-06-03 13:14:56 -07:00
hanzo-devandGitHub 2daea8d1ec Merge pull request #141 from hanzoai/dependabot/npm_and_yarn/templates/react-router-app/react-router/node-7.13.2
chore(deps): bump @react-router/node from 7.13.1 to 7.13.2 in /templates/react-router-app
2026-06-03 13:14:47 -07:00
hanzo-devandGitHub f4c06186fa Merge pull request #142 from hanzoai/dependabot/npm_and_yarn/templates/astro-app/astro-6.0.8
chore(deps): bump astro from 5.18.1 to 6.0.8 in /templates/astro-app
2026-06-03 13:14:43 -07:00
hanzo-devandGitHub 14a311890f Merge pull request #145 from hanzoai/dependabot/npm_and_yarn/templates/astro-app/globals-17.4.0
chore(deps-dev): bump globals from 14.0.0 to 17.4.0 in /templates/astro-app
2026-06-03 13:14:37 -07:00
hanzo-devandGitHub 71e9b498bb Merge pull request #146 from hanzoai/dependabot/npm_and_yarn/templates/astro-monorepo/astrojs/react-5.0.1
chore(deps): bump @astrojs/react from 4.4.2 to 5.0.1 in /templates/astro-monorepo
2026-06-03 13:14:33 -07:00
hanzo-devandGitHub 4903d944d7 Merge pull request #147 from hanzoai/dependabot/npm_and_yarn/templates/react-router-app/react-router/dev-7.13.2
chore(deps-dev): bump @react-router/dev from 7.13.1 to 7.13.2 in /templates/react-router-app
2026-06-03 13:14:29 -07:00
hanzo-devandGitHub 7ad181d7c9 Merge pull request #150 from hanzoai/dependabot/npm_and_yarn/templates/astro-app/typescript-6.0.2
chore(deps-dev): bump typescript from 5.9.3 to 6.0.2 in /templates/astro-app
2026-06-03 13:14:23 -07:00
hanzo-devandGitHub 5bdcbf1251 Merge pull request #149 from hanzoai/dependabot/npm_and_yarn/templates/astro-monorepo/typescript-6.0.2
chore(deps-dev): bump typescript from 5.9.3 to 6.0.2 in /templates/astro-monorepo
2026-06-03 13:14:18 -07:00
hanzo-devandGitHub 15ec88065e Merge pull request #157 from hanzoai/dependabot/npm_and_yarn/templates/vite-monorepo/tailwindcss/vite-4.2.2
chore(deps-dev): bump @tailwindcss/vite from 4.2.1 to 4.2.2 in /templates/vite-monorepo
2026-06-03 13:14:05 -07:00
hanzo-devandGitHub e60acaa547 Merge pull request #159 from hanzoai/dependabot/npm_and_yarn/templates/next-monorepo/typescript-6.0.2
chore(deps-dev): bump typescript from 5.9.3 to 6.0.2 in /templates/next-monorepo
2026-06-03 13:13:59 -07:00
hanzo-devandGitHub c6a34ad930 Merge pull request #160 from hanzoai/dependabot/npm_and_yarn/templates/vite-monorepo/zod-4.3.6
chore(deps): bump zod from 3.25.76 to 4.3.6 in /templates/vite-monorepo
2026-06-03 13:13:55 -07:00
hanzo-devandGitHub d830b7c69b Merge pull request #164 from hanzoai/dependabot/npm_and_yarn/templates/react-router-monorepo/react-router-7.13.2
chore(deps): bump react-router from 7.12.0 to 7.13.2 in /templates/react-router-monorepo
2026-06-03 13:13:43 -07:00
hanzo-devandGitHub d2c5599a82 Merge pull request #167 from hanzoai/dependabot/npm_and_yarn/templates/start-monorepo/types/node-25.5.0
chore(deps-dev): bump @types/node from 25.3.0 to 25.5.0 in /templates/start-monorepo
2026-06-03 13:13:37 -07:00
hanzo-devandGitHub fd8020027a Merge pull request #166 from hanzoai/dependabot/npm_and_yarn/templates/next-monorepo/next-16.2.1
chore(deps): bump next from 16.1.6 to 16.2.1 in /templates/next-monorepo
2026-06-03 13:13:33 -07:00
hanzo-devandGitHub af060dfa34 Merge pull request #168 from hanzoai/dependabot/npm_and_yarn/templates/vite-app/vite-8.0.3
chore(deps-dev): bump vite from 8.0.2 to 8.0.3 in /templates/vite-app
2026-06-03 13:12:49 -07:00
hanzo-devandGitHub 10aef3f986 Merge pull request #169 from hanzoai/dependabot/npm_and_yarn/templates/start-app/types/node-25.5.0
chore(deps-dev): bump @types/node from 20.19.37 to 25.5.0 in /templates/start-app
2026-06-03 13:12:45 -07:00
hanzo-devandGitHub f57ecaa08f Merge pull request #170 from hanzoai/dependabot/npm_and_yarn/templates/vite-app/types/node-25.5.0
chore(deps-dev): bump @types/node from 20.19.37 to 25.5.0 in /templates/vite-app
2026-06-03 13:12:41 -07:00
hanzo-devandGitHub d608cbd53c Merge pull request #172 from hanzoai/dependabot/npm_and_yarn/templates/start-app/vite-tsconfig-paths-6.1.1
chore(deps): bump vite-tsconfig-paths from 4.3.2 to 6.1.1 in /templates/start-app
2026-06-03 13:12:35 -07:00
hanzo-devandGitHub 1762c9c0a9 Merge pull request #173 from hanzoai/dependabot/npm_and_yarn/templates/start-app/vite-8.0.3
chore(deps-dev): bump vite from 8.0.2 to 8.0.3 in /templates/start-app
2026-06-03 13:12:31 -07:00
hanzo-devandGitHub de60e1b74b Merge pull request #175 from hanzoai/dependabot/npm_and_yarn/templates/next-app/next-16.2.2
chore(deps): bump next from 16.1.7 to 16.2.2 in /templates/next-app
2026-06-03 13:12:00 -07:00
hanzo-devandGitHub f5f7641982 Merge pull request #176 from hanzoai/dependabot/npm_and_yarn/templates/next-app/eslint-config-next-16.2.2
chore(deps-dev): bump eslint-config-next from 16.1.7 to 16.2.2 in /templates/next-app
2026-06-03 13:11:55 -07:00
hanzo-devandGitHub 3f02e6c092 Merge pull request #177 from hanzoai/dependabot/npm_and_yarn/templates/next-app/types/node-25.5.2
chore(deps-dev): bump @types/node from 20.19.37 to 25.5.2 in /templates/next-app
2026-06-03 13:11:51 -07:00
hanzo-dev 5efbf597f2 chore(deps): pnpm up --latest --recursive 2026-06-02 11:35:03 -07:00
hanzo-dev 56dadc2ea5 chore: brand-neutral cleanup — remove cross-tenant references 2026-05-25 15:14:48 -07:00
hanzo-dev 46b93005de chore: update 2026-05-25 15:14:48 -07:00
hanzo-dev 4ae7ae8cb3 chore(brand): scrub Tamagui mentions, use @hanzo/gui v7 / Hanzo GUI
Brand policy: do not reference Tamagui by name on disk. The product is
@hanzo/gui v7 (Hanzo GUI). Internal workspace umbrella is `hanzogui`
(lowercase). Source code imports `from 'hanzogui'`. NPM publish:
@hanzo/gui.

This commit replaces "Tamagui v7" → "Hanzo GUI v7" / "@hanzo/gui v7"
in pkg/ui/BASES.md and pkg/ui/src/primitives/bases/{gui,svelte,vue}/
header docstrings + placeholder error messages.
2026-04-27 13:36:07 -07:00
hanzo-dev e81bae9b82 chore(brand): scrub Tamagui mentions, use @hanzo/gui v7 / Hanzo GUI
Brand policy: do not reference Tamagui by name on disk. The product is
@hanzo/gui v7 (Hanzo GUI). Internal workspace umbrella is `hanzogui`
(lowercase). Source code imports `from 'hanzogui'`. NPM publish:
@hanzo/gui.

This commit replaces "Tamagui v7" → "Hanzo GUI v7" / "@hanzo/gui v7"
in pkg/ui/BASES.md and pkg/ui/src/primitives/bases/{gui,svelte,vue}/
header docstrings + placeholder error messages.
2026-04-27 13:36:07 -07:00
hanzo-dev 923f6901c9 feat(@hanzo/ui): bases re-export structure (admin/gui/svelte/vue subpaths)
Add framework-base re-exports under @hanzo/ui/primitives/bases/* so
consumers can swap framework backends without changing imports:

- bases/admin → @hanzogui/admin (Tamagui v7 admin chrome, canonical)
- bases/gui   → hanzogui (Tamagui v7 primitives umbrella)
- bases/svelte → throws (placeholder until Svelte port lands)
- bases/vue    → throws (placeholder until Vue port lands)

Source-of-truth files stay in ~/work/hanzo/gui/ — this package only
re-exports. Component names are identical across bases by contract,
so swapping a base is a one-line import change in consumer code.

Adds @hanzogui/admin, @hanzogui/lucide-icons-2, hanzogui as optional
peer deps. See pkg/ui/BASES.md for the full doc.
2026-04-27 13:33:01 -07:00
hanzo-dev 489b54e9e1 feat(@hanzo/ui): bases re-export structure (admin/gui/svelte/vue subpaths)
Add framework-base re-exports under @hanzo/ui/primitives/bases/* so
consumers can swap framework backends without changing imports:

- bases/admin → @hanzogui/admin (Tamagui v7 admin chrome, canonical)
- bases/gui   → hanzogui (Tamagui v7 primitives umbrella)
- bases/svelte → throws (placeholder until Svelte port lands)
- bases/vue    → throws (placeholder until Vue port lands)

Source-of-truth files stay in ~/work/hanzo/gui/ — this package only
re-exports. Component names are identical across bases by contract,
so swapping a base is a one-line import change in consumer code.

Adds @hanzogui/admin, @hanzogui/lucide-icons-2, hanzogui as optional
peer deps. See pkg/ui/BASES.md for the full doc.
2026-04-27 13:33:01 -07:00
hanzo-dev a0a4652269 chore(release): @hanzo/brand@1.3.1 2026-04-23 20:29:24 -07:00
hanzo-dev cbb0a2740a chore(release): @hanzo/brand@1.3.1 2026-04-23 20:29:24 -07:00
hanzo-dev 87a0715cbe chore(brand): gitignore dist/, node_modules, turbo cache 2026-04-23 20:28:36 -07:00
hanzo-dev 73f8ea15fc chore(brand): gitignore dist/, node_modules, turbo cache 2026-04-23 20:28:36 -07:00
hanzo-dev f12874ccff fix(brand): separation of concerns — hanzo owns hanzo only
@hanzo/brand was a cross-org registry (hanzo + lux + zoo + pars all
bundled). Brand belongs per-org:
  Zoo       → @zooai/brand       (github.com/zooai/brand)
  Lux       → @luxfi/brand       (github.com/luxfi/brand)
  Liquidity → @partner/brand (github.com/partner/brand)

Delete lux/zoo/pars blocks from orgs.ts, narrow OrgId to 'hanzo',
narrow index.ts exports. 200-line reduction.

No callers broken — nothing in ~/work/{hanzo,lux,zoo,liquidity}
imports @hanzo/ui/brand currently (verified via grep). This is
dead cross-org code being removed.
2026-04-23 19:48:35 -07:00
hanzo-dev 4d15ac09c4 fix(brand): separation of concerns — hanzo owns hanzo only
@hanzo/brand was a cross-org registry (hanzo + lux + zoo + pars all
bundled). Brand belongs per-org:
  Zoo       → @zooai/brand       (github.com/zooai/brand)
  Lux       → @luxfi/brand       (github.com/luxfi/brand)

Delete lux/zoo/pars blocks from orgs.ts, narrow OrgId to 'hanzo',
narrow index.ts exports. 200-line reduction.

No callers broken — nothing in ~/work/{hanzo,lux,zoo}
imports @hanzo/ui/brand currently (verified via grep). This is
dead cross-org code being removed.
2026-04-23 19:48:35 -07:00
hanzo-dev 311332c5be fix(brand): correct Zoo canonical identities (IAM=zoo, GitHub=zooai, Twitter=@zoo_labs)
Each surface has a different handle — don't normalize them:
- orgHandle (IAM owner):  zoolabs  → zoo        (IAM org slug is just "zoo")
- githubOrg:              zoolabs  → zooai      (github.com/zooai is canonical)
- social.twitter:         @zoolabs → @zoo_labs  (actual Twitter handle)
- social.github:          zoolabs  → zooai

Legal entity 'Zoo Labs Foundation' unchanged.
2026-04-23 19:43:57 -07:00
Artem AshandGitHub ce8cbed8a3 admin: extract Tamagui component packages to hanzoai/gui (#178)
pkg/gui/ was a Tamagui subtree living in hanzoai/ui — the 57
  @hanzogui/* components (button, card, dialog, popover, switch, and
  the supporting primitives) belong alongside the rest of the
  @hanzogui/* engine in hanzoai/gui, not here. History for these
  packages was preserved via git filter-repo.

  - Deleted pkg/gui/ (57 packages, 744 files)
  - Removed "pkg/gui/*" entry from pnpm-workspace.yaml
  - Deleted scripts/publish-gui.ts — legacy ad-hoc publisher
    hardcoded to pkg/gui and an ancient version string
  - Narrowed .github/workflows/publish.yml from "@hanzo/*|@hanzogui/*"
    to "@hanzo/*" so this repo no longer tries to publish Tamagui
    components
  - Removed stale "gui/ GUI component packages (@hanzogui/*)" line
    from LLM.md
2026-04-21 15:02:14 -07:00
hanzo-dev 40378c96ef fix: only @hanzo/* packages publish from this repo
Mark shadcn as private — upstream, not ours.
Publish workflow scans both pkg/ and packages/, filters by @hanzo/* org.
2026-04-09 08:02:07 -07:00
dependabot[bot]andGitHub 72a6f8772c chore(deps-dev): bump @types/node in /templates/next-app
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.19.37 to 25.5.2.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.5.2
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-06 14:15:08 +00:00
dependabot[bot]andGitHub bdb0eaf501 chore(deps-dev): bump eslint-config-next in /templates/next-app
Bumps [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) from 16.1.7 to 16.2.2.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v16.2.2/packages/eslint-config-next)

---
updated-dependencies:
- dependency-name: eslint-config-next
  dependency-version: 16.2.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-06 14:15:00 +00:00
dependabot[bot]andGitHub f1c54a4321 chore(deps): bump next from 16.1.7 to 16.2.2 in /templates/next-app
Bumps [next](https://github.com/vercel/next.js) from 16.1.7 to 16.2.2.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v16.1.7...v16.2.2)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 16.2.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-06 14:14:48 +00:00
Darkhorse7starsandhanzo-dev 032e4579f0 chore: bump @hanzo/ui to 5.6.2 (Square card form fix)
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-03-31 15:07:25 -05:00
Darkhorse7starsandhanzo-dev cf7419478b fix(billing): use SquareCardForm in PaymentMethodManager
Replace the server-side CardForm (which calls /card/tokenize and gets
503 due to PCI compliance) with SquareCardForm which uses the Square
Web Payments SDK for client-side card tokenization.

The Square sourceId token is passed as _sourceToken on the PaymentMethod
object so consuming apps can send it to commerce's payment-methods
endpoint for real $1 pre-auth card verification.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-03-31 15:02:52 -05:00
dependabot[bot]andGitHub df24296867 chore(deps-dev): bump vite from 8.0.2 to 8.0.3 in /templates/start-app
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 8.0.2 to 8.0.3.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@8.0.3/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-30 15:37:24 +00:00
dependabot[bot]andGitHub 7d75d9bec8 chore(deps): bump vite-tsconfig-paths in /templates/start-app
Bumps [vite-tsconfig-paths](https://github.com/aleclarson/vite-tsconfig-paths) from 4.3.2 to 6.1.1.
- [Release notes](https://github.com/aleclarson/vite-tsconfig-paths/releases)
- [Commits](https://github.com/aleclarson/vite-tsconfig-paths/compare/v4.3.2...v6.1.1)

---
updated-dependencies:
- dependency-name: vite-tsconfig-paths
  dependency-version: 6.1.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-30 15:37:15 +00:00
dependabot[bot]andGitHub 63d97c1f4b chore(deps-dev): bump @types/node in /templates/vite-app
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.19.37 to 25.5.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.5.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-30 15:36:55 +00:00
dependabot[bot]andGitHub f64396ccf1 chore(deps-dev): bump @types/node in /templates/start-app
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.19.37 to 25.5.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.5.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-30 15:36:47 +00:00
dependabot[bot]andGitHub 879455b2f5 chore(deps-dev): bump vite from 8.0.2 to 8.0.3 in /templates/vite-app
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 8.0.2 to 8.0.3.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@8.0.3/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-30 15:36:40 +00:00
hanzo-dev 5308913127 chore: lint publish-on-tag workflow 2026-03-29 17:24:50 -07:00
hanzo-dev 91cba606e7 fix: rename turbo.json pipeline → tasks (Turbo 2.0) 2026-03-28 14:52:09 -07:00
hanzo-dev 9e17fd7f9f chore: gitignore generated static API files (build artifacts) 2026-03-28 14:41:32 -07:00
hanzo-dev 83561bb96e fix: force-static on all API routes for CF Pages export
Add dynamic=force-static + generateStaticParams to registry routes.
Replace server-only chat/search routes with static stubs.
Remove conflicting /api/registry/route.ts (dir collision).
Update next.config.mjs for CF_PAGES env.
2026-03-28 00:15:05 -07:00
hanzo-dev 81e9be1529 chore: add generated static registry API files 2026-03-27 21:29:16 -07:00
hanzo-dev a40d2905e4 fix: add missing AI SDK deps for chat and search routes
The chat API route (app/api/chat/route.ts) imports @ai-sdk/openai-compatible
and ai but neither was declared in app/package.json. Also skip puppeteer
Chrome download in .npmrc since it's only used for optional screenshot
capture and its postinstall failure breaks pnpm install in CI.
2026-03-27 20:28:30 -07:00
hanzo-dev 71c1339010 feat: add AI chat and search API routes for ui.hanzo.ai
- /api/chat: streaming RAG chat about UI components (zen-coder-flash)
- /api/search: proxy to Hanzo Cloud search-docs with publishable key
- lib/search.ts: search config (cloud backend, pk-hanzo-ui-search-2026)

Same pattern as docs.hanzo.ai AI search. Users can chat on the site
and ask questions about components.
2026-03-27 19:31:18 -07:00
hanzo-dev df9a15a619 feat: add registry API routes and static build for MCP tool
Add Next.js API routes at /api/registry/ for dynamic component queries
(server mode). Add build-registry-api.mts script that generates static
JSON files at /api/registry/ for CF Pages / static hosting.

Endpoints:
- /api/registry — component list
- /api/registry/components/{name} — component with source
- /api/registry/search?q= — search
- /api/registry/index — full manifest (single payload)

Static files built during `pnpm build` step.
2026-03-27 19:15:54 -07:00
hanzo-dev 1dd175a992 fix: add component-helpers dep to checkbox/button/list-item 2026-03-27 17:26:12 -07:00
hanzo-dev 917978da51 fix: ALL UI deps 3.0.0->3.0.1, publish 3.0.2 2026-03-27 16:50:15 -07:00
hanzo-dev 40bac74f03 fix: purge workspace:*, publish 3.0.1 2026-03-27 15:54:38 -07:00
hanzo-dev 03ad5fef47 feat: all UI packages at 3.0.0 (unified) 2026-03-27 14:52:23 -07:00
hanzo-dev 0fbe65665d fix: ALL UI packages 2.0.8, internal deps point to correct published versions 2026-03-27 14:18:17 -07:00
hanzo-dev a62132b3c4 fix: ALL UI packages 2.0.7, helpers->component-helpers in all dist 2026-03-27 13:35:34 -07:00
hanzo-dev debb4fc4bd fix: button/list-item import useCurrentColor from component-helpers, bump 2.0.6 2026-03-27 12:04:16 -07:00
hanzo-dev 1389ae5539 fix: all imports @hanzo/gui-* -> @hanzogui/*, TamaguiRoot -> GuiRoot in dist, bump 2.0.5 2026-03-26 20:27:38 -07:00
hanzo-dev d5f5ed8a2c fix: repair JSON formatting in package.json 2026-03-24 20:32:33 -07:00
hanzo-dev 481f07c4be fix: correct package names @hanzo/gui-* -> @hanzogui/* 2026-03-24 20:31:50 -07:00
hanzo-dev 9b12207ee9 fix: bump UI packages to 2.0.4 with consistent deps 2026-03-24 20:31:14 -07:00
hanzo-dev 8280a70d93 fix: resolve workspace:* to 2.0.1, bump UI packages 2026-03-24 19:22:01 -07:00
hanzo-dev b324886b24 docs: update LLM.md with upstream sync info and current structure 2026-03-24 19:13:53 -07:00
dependabot[bot]andGitHub fd1d95f39d chore(deps-dev): bump @types/node in /templates/start-monorepo
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.3.0 to 25.5.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-25 02:08:31 +00:00
dependabot[bot]andGitHub cd0205b898 chore(deps): bump next from 16.1.6 to 16.2.1 in /templates/next-monorepo
Bumps [next](https://github.com/vercel/next.js) from 16.1.6 to 16.2.1.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v16.1.6...v16.2.1)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 16.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-25 02:08:31 +00:00
dependabot[bot]andGitHub f5c1837c84 chore(deps): bump react-router in /templates/react-router-monorepo
Bumps [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) from 7.12.0 to 7.13.2.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router@7.13.2/packages/react-router)

---
updated-dependencies:
- dependency-name: react-router
  dependency-version: 7.13.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-25 02:08:11 +00:00
dependabot[bot]andGitHub 686fd566bc chore(deps): bump zod from 3.25.76 to 4.3.6 in /templates/vite-monorepo
Bumps [zod](https://github.com/colinhacks/zod) from 3.25.76 to 4.3.6.
- [Release notes](https://github.com/colinhacks/zod/releases)
- [Commits](https://github.com/colinhacks/zod/compare/v3.25.76...v4.3.6)

---
updated-dependencies:
- dependency-name: zod
  dependency-version: 4.3.6
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-25 02:07:54 +00:00
dependabot[bot]andGitHub 19ab5f4cdb chore(deps-dev): bump typescript in /templates/next-monorepo
Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.9.3 to 6.0.2.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.9.3...v6.0.2)

---
updated-dependencies:
- dependency-name: typescript
  dependency-version: 6.0.2
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-25 02:07:48 +00:00
dependabot[bot]andGitHub 501a7e96e0 chore(deps-dev): bump @tailwindcss/vite in /templates/vite-monorepo
Bumps [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) from 4.2.1 to 4.2.2.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.2.2/packages/@tailwindcss-vite)

---
updated-dependencies:
- dependency-name: "@tailwindcss/vite"
  dependency-version: 4.2.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-25 02:07:42 +00:00
dependabot[bot]andGitHub cce62f86f6 chore(deps-dev): bump typescript in /templates/astro-app
Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.9.3 to 6.0.2.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.9.3...v6.0.2)

---
updated-dependencies:
- dependency-name: typescript
  dependency-version: 6.0.2
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-25 02:07:24 +00:00
dependabot[bot]andGitHub 6e12aa705c chore(deps-dev): bump typescript in /templates/astro-monorepo
Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.9.3 to 6.0.2.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.9.3...v6.0.2)

---
updated-dependencies:
- dependency-name: typescript
  dependency-version: 6.0.2
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-25 02:07:24 +00:00
dependabot[bot]andGitHub 0a070f159f chore(deps-dev): bump @react-router/dev in /templates/react-router-app
Bumps [@react-router/dev](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dev) from 7.13.1 to 7.13.2.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dev/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/@react-router/dev@7.13.2/packages/react-router-dev)

---
updated-dependencies:
- dependency-name: "@react-router/dev"
  dependency-version: 7.13.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-25 02:07:17 +00:00
dependabot[bot]andGitHub 927674c589 chore(deps): bump @astrojs/react in /templates/astro-monorepo
Bumps [@astrojs/react](https://github.com/withastro/astro/tree/HEAD/packages/integrations/react) from 4.4.2 to 5.0.1.
- [Release notes](https://github.com/withastro/astro/releases)
- [Changelog](https://github.com/withastro/astro/blob/main/packages/integrations/react/CHANGELOG.md)
- [Commits](https://github.com/withastro/astro/commits/@astrojs/react@5.0.1/packages/integrations/react)

---
updated-dependencies:
- dependency-name: "@astrojs/react"
  dependency-version: 5.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-25 02:07:16 +00:00
dependabot[bot]andGitHub c824239b6c chore(deps-dev): bump globals in /templates/astro-app
Bumps [globals](https://github.com/sindresorhus/globals) from 14.0.0 to 17.4.0.
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v14.0.0...v17.4.0)

---
updated-dependencies:
- dependency-name: globals
  dependency-version: 17.4.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-25 02:07:15 +00:00
dependabot[bot]andGitHub 7cc36825a8 chore(deps): bump astro from 5.18.1 to 6.0.8 in /templates/astro-app
Bumps [astro](https://github.com/withastro/astro/tree/HEAD/packages/astro) from 5.18.1 to 6.0.8.
- [Release notes](https://github.com/withastro/astro/releases)
- [Changelog](https://github.com/withastro/astro/blob/main/packages/astro/CHANGELOG.md)
- [Commits](https://github.com/withastro/astro/commits/astro@6.0.8/packages/astro)

---
updated-dependencies:
- dependency-name: astro
  dependency-version: 6.0.8
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-25 02:07:10 +00:00
dependabot[bot]andGitHub afc4e090ff chore(deps): bump @react-router/node in /templates/react-router-app
Bumps [@react-router/node](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-node) from 7.13.1 to 7.13.2.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-node/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/@react-router/node@7.13.2/packages/react-router-node)

---
updated-dependencies:
- dependency-name: "@react-router/node"
  dependency-version: 7.13.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-25 02:07:09 +00:00
dependabot[bot]andGitHub cba62324f5 chore(deps): bump zod from 3.25.76 to 4.3.6 in /templates/next-monorepo
Bumps [zod](https://github.com/colinhacks/zod) from 3.25.76 to 4.3.6.
- [Release notes](https://github.com/colinhacks/zod/releases)
- [Commits](https://github.com/colinhacks/zod/compare/v3.25.76...v4.3.6)

---
updated-dependencies:
- dependency-name: zod
  dependency-version: 4.3.6
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-25 02:07:00 +00:00
dependabot[bot]andGitHub b23b546395 chore(deps-dev): bump @eslint/js in /templates/astro-app
Bumps [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) from 9.39.4 to 10.0.1.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/commits/v10.0.1/packages/js)

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-version: 10.0.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-25 02:06:57 +00:00
dependabot[bot]andGitHub 4c108d8356 chore(deps-dev): bump @eslint/js in /templates/astro-monorepo
Bumps [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) from 9.39.4 to 10.0.1.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/commits/v10.0.1/packages/js)

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-version: 10.0.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-25 02:06:55 +00:00
dependabot[bot]andGitHub 387c913fec chore(deps-dev): bump globals in /templates/vite-app
Bumps [globals](https://github.com/sindresorhus/globals) from 14.0.0 to 17.4.0.
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v14.0.0...v17.4.0)

---
updated-dependencies:
- dependency-name: globals
  dependency-version: 17.4.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-25 02:06:55 +00:00
dependabot[bot]andGitHub be059951f0 chore(deps): bump react-router in /templates/react-router-app
Bumps [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) from 7.13.1 to 7.13.2.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router@7.13.2/packages/react-router)

---
updated-dependencies:
- dependency-name: react-router
  dependency-version: 7.13.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-25 02:06:54 +00:00
dependabot[bot]andGitHub e266bb3d68 chore(deps-dev): bump typescript in /templates/vite-app
Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.9.3 to 6.0.2.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.9.3...v6.0.2)

---
updated-dependencies:
- dependency-name: typescript
  dependency-version: 6.0.2
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-25 02:06:51 +00:00
dependabot[bot]andGitHub 4ce740aa49 chore(deps): bump astro in /templates/astro-monorepo
Bumps [astro](https://github.com/withastro/astro/tree/HEAD/packages/astro) from 5.18.1 to 6.0.8.
- [Release notes](https://github.com/withastro/astro/releases)
- [Changelog](https://github.com/withastro/astro/blob/main/packages/astro/CHANGELOG.md)
- [Commits](https://github.com/withastro/astro/commits/astro@6.0.8/packages/astro)

---
updated-dependencies:
- dependency-name: astro
  dependency-version: 6.0.8
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-25 02:06:48 +00:00
dependabot[bot]andGitHub 3a4f330159 chore(deps): bump @astrojs/react in /templates/astro-app
Bumps [@astrojs/react](https://github.com/withastro/astro/tree/HEAD/packages/integrations/react) from 4.4.2 to 5.0.1.
- [Release notes](https://github.com/withastro/astro/releases)
- [Changelog](https://github.com/withastro/astro/blob/main/packages/integrations/react/CHANGELOG.md)
- [Commits](https://github.com/withastro/astro/commits/@astrojs/react@5.0.1/packages/integrations/react)

---
updated-dependencies:
- dependency-name: "@astrojs/react"
  dependency-version: 5.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-25 02:06:47 +00:00
dependabot[bot]andGitHub 3b95866db9 chore(deps-dev): bump typescript in /templates/next-app
Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.9.3 to 6.0.2.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.9.3...v6.0.2)

---
updated-dependencies:
- dependency-name: typescript
  dependency-version: 6.0.2
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-25 02:06:47 +00:00
dependabot[bot]andGitHub ab79c4b2ba chore(deps-dev): bump typescript in /templates/react-router-app
Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.9.3 to 6.0.2.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.9.3...v6.0.2)

---
updated-dependencies:
- dependency-name: typescript
  dependency-version: 6.0.2
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-25 02:06:40 +00:00
hanzo-dev fd9d7843f5 feat: sync upstream shadcn/ui — shadcn@4.1.0, v4 app, font system, chart improvements
Upstream sync from shadcn-ui/ui (1686 commits since last merge base).

Key changes:
- packages/shadcn upgraded to 4.1.0 (CLI font transformers, scaffold from github, chart colors)
- apps/v4 replaces apps/www as the primary docs/registry app
- Font system: 25+ heading font registries, font markers utility, @supports override
- Chart improvements: recharts v3 compat, radial chart fixes
- templates/ updated with monorepo variants (astro, vite, react-router, start)
- skills/shadcn AI skill definitions added
- deprecated/ removed (auth, auth-firebase, cli, www)

Preserved Hanzo-only paths: app/, pkg/, demo/, docs/, template/next/
2026-03-24 19:04:31 -07:00
hanzo-dev bd11bfeca5 fix: update checkout client 2026-03-24 18:42:34 -07:00
hanzo-dev c6251ef799 feat: migrate tamagui primitives to @hanzogui/*, rename tokens/tamagui to tokens/gui 2026-03-24 18:42:34 -07:00
hanzo-dev 3c4a9a6e76 feat: migrate UI packages from @hanzo/gui-* to @hanzogui/* scope, purge all tamagui refs, v2.0.0 2026-03-24 18:42:34 -07:00
Darkhorse7starsandhanzo-dev b53e0c896d fix: ESLint 10 config compatibility and cleanup temp files
- Rewrite app/eslint.config.mjs to use @typescript-eslint/parser directly
  (fixes "Class extends value undefined" error with typescript-eslint 8.x)
- Add typescript-eslint override in root package.json for ESLint 10 compat
- Remove deprecated .eslintignore (use flat config ignores instead)
- Delete temp/backup files (.bak, .old, .tmp)

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-03-23 16:55:41 -05:00
Darkhorse7starsandhanzo-dev cff1532d93 fix: resolve CI type check and lint failures
- Downgrade app/ react-resizable-panels to v3 (code uses v3 API names)
- Add missing @eslint/js dependency for eslint.config.mjs
- Fix TS18048 in chart-line-dots-custom.tsx (null check cx/cy)
- Fix TS7006 in ai-code.tsx (explicit any types for monaco callbacks)

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-03-23 12:41:22 -05:00
Darkhorse7starsandhanzo-dev dd04a87739 fix: use react-resizable-panels v3 API for compatibility
Changed imports from v4 names (Group, Separator) to v3 names
(PanelGroup, PanelResizeHandle) to fix build errors in consumers
using react-resizable-panels v3. Updated peer dep to ^3.0.0.

Bump @hanzo/ui to 5.5.1.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-03-23 12:22:29 -05:00
hanzo-dev 22579ba9bf fix: resolve 34 missing gui workspace deps to published npm versions
The gui-* packages were imported from hanzo/gui but 34 underlying
utility/core packages (gui-build, gui-core, gui-helpers, gui-web, etc.)
were not included in the workspace. Changed their workspace:* refs to
the published 2.0.0-rc.29 versions so pnpm can resolve from registry.
2026-03-22 19:02:28 -07:00
hanzo-dev a72d7eadb1 chore: brand + gui publish artifacts 2026-03-22 18:38:17 -07:00
hanzo-dev eb07222a18 feat: add @hanzo/gui-* UI component packages from hanzo/gui rebrand 2026-03-21 13:08:01 -07:00
hanzo-dev db28fc376a chore: update all dependencies to latest 2026-03-21 11:44:34 -07:00
hanzo-dev f180c88329 chore: bump @hanzo/ui to 5.5.0 (dash components) 2026-03-18 14:52:20 -07:00
hanzo-dev 62fad98f71 fix: clean names — dash/layout, dash/sidebar, dash/table, dash/form, dash/crud 2026-03-18 14:49:17 -07:00
hanzo-dev 33a4e8c706 fix: rename admin->dash in tsup build config 2026-03-18 14:47:05 -07:00
hanzo-dev 707211cdd1 chore: bump @hanzo/ui to 5.4.0 (adds @hanzo/ui/dash) 2026-03-18 14:40:24 -07:00
hanzo-dev 01bcdf273a feat: add @hanzo/ui/dash — shared dashboard UI (layout, data table, forms, CRUD) 2026-03-18 14:39:39 -07:00
8913 changed files with 179635 additions and 379171 deletions
-11
View File
@@ -1,11 +0,0 @@
{
"$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
"changelog": ["@changesets/changelog-github", { "repo": "shadcn-ui/ui" }],
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["v4", "tests"]
}
+3
View File
@@ -0,0 +1,3 @@
{
"extends": ["@commitlint/config-conventional"]
}
-12
View File
@@ -1,12 +0,0 @@
// ORIGINALLY FROM CLOUDFLARE WRANGLER:
// https://github.com/cloudflare/wrangler2/blob/main/.github/changeset-version.js
import { execSync } from "child_process"
// This script is used by the `release.yml` workflow to update the version of the packages being released.
// The standard step is only to run `changeset version` but this does not update the pnpm-lock.yaml file.
// So we also run `pnpm install`, which does this update.
// This is a workaround until this is handled automatically by `changeset version`.
// See https://github.com/changesets/changesets/issues/421.
execSync("npx changeset version", { stdio: "inherit" })
execSync("pnpm install --lockfile-only", { stdio: "inherit" })
+40
View File
@@ -4,3 +4,43 @@ updates:
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/templates/astro-app"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/templates/astro-monorepo"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/templates/next-app"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/templates/next-monorepo"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/templates/react-router-app"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/templates/react-router-monorepo"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/templates/start-app"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/templates/start-monorepo"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/templates/vite-app"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/templates/vite-monorepo"
schedule:
interval: "weekly"
+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="640" viewBox="0 0 1280 640" role="img" aria-label="ui">
<rect width="1280" height="640" fill="#0A0A0A"/>
<svg x="96" y="215" width="210" height="210" viewBox="0 0 67 67"><path d="M22.21 67V44.6369H0V67H22.21Z" fill="#fff"/><path d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z" fill="#fff"/><path d="M22.21 0H0V22.3184H22.21V0Z" fill="#fff"/><path d="M66.7198 0H44.5098V22.3184H66.7198V0Z" fill="#fff"/><path d="M66.7198 67V44.6369H44.5098V67H66.7198Z" fill="#fff"/></svg>
<text x="378" y="276" font-family="Inter,system-ui,-apple-system,sans-serif" font-size="78" font-weight="800" letter-spacing="-2" fill="#ffffff">ui</text>
<text x="378" y="322" font-family="Inter,system-ui,sans-serif" font-size="30" fill="#ffffff" opacity=".66">React component library for AI applications</text>
<rect x="378" y="338" width="806" height="3" rx="1.5" fill="#ffffff" opacity=".9"/>
<text x="378" y="390" font-family="Inter,system-ui,sans-serif" font-size="24" font-weight="600" fill="#ffffff" opacity=".5">github.com/hanzoai</text>
<text x="1184" y="390" text-anchor="end" font-family="Inter,system-ui,sans-serif" font-size="24" font-weight="600" fill="#ffffff" opacity=".5">hanzo.ai</text>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+1 -1
View File
@@ -4,7 +4,7 @@
import { exec } from "child_process"
import fs from "fs"
const pkgJsonPath = "pkg/cli/package.json"
const pkgJsonPath = "pkgs/shadcn/package.json"
try {
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath))
exec("git rev-parse --short HEAD", (err, stdout) => {
+1 -1
View File
@@ -4,7 +4,7 @@
import { exec } from "child_process"
import fs from "fs"
const pkgJsonPath = "pkg/cli/package.json"
const pkgJsonPath = "pkgs/shadcn/package.json"
try {
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath))
exec("git rev-parse --short HEAD", (err, stdout) => {
-261
View File
@@ -1,261 +0,0 @@
name: CI
on:
push:
branches:
- main
- develop
pull_request:
branches:
- main
- develop
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 9
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run linting
run: pnpm run lint
typecheck:
name: Type Check
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 9
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build UI package
run: cd pkg/ui && pnpm run build
- name: Run type checking
run: |
cd app && pnpm run typecheck
cd ../pkg/ui && pnpm run tc --noEmit
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 9
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build UI package
run: cd pkg/ui && pnpm run build:full
- name: Build app
run: cd app && pnpm run build
env:
NEXT_PUBLIC_APP_URL: https://ui.hanzo.ai
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-artifacts
path: |
app/.next
pkg/ui/dist
test:
name: Test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 9
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run pkg/ui tests
run: cd pkg/ui && pnpm test -- --run
- name: Run app tests
run: |
if [ -f "app/package.json" ] && grep -q '"test"' app/package.json 2>/dev/null; then
cd app && pnpm test || echo "No tests configured"
else
echo "No test script found"
fi
- name: Upload coverage
if: always()
uses: codecov/codecov-action@v3
with:
files: ./pkg/ui/coverage/lcov.info
flags: unit
deploy-preview:
name: Deploy Preview
runs-on: ubuntu-latest
needs: [lint, typecheck, build]
if: github.event_name == 'pull_request'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 9
- name: Install Vercel CLI
run: npm install --global vercel@latest
- name: Pull Vercel Environment Information
run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
- name: Build Project Artifacts
run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
- name: Deploy Project Artifacts to Vercel
id: deploy
run: |
url=$(vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }})
echo "preview_url=$url" >> $GITHUB_OUTPUT
- name: Comment PR with preview URL
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '🚀 Preview deployed to: ${{ steps.deploy.outputs.preview_url }}'
})
status:
name: CI Status
runs-on: ubuntu-latest
needs: [lint, typecheck, build, test]
if: always()
steps:
- name: Check status
run: |
if [ "${{ needs.lint.result }}" != "success" ] || \
[ "${{ needs.typecheck.result }}" != "success" ] || \
[ "${{ needs.build.result }}" != "success" ] || \
[ "${{ needs.test.result }}" != "success" ]; then
echo "CI checks failed"
exit 1
else
echo "All CI checks passed!"
fi
+9
View File
@@ -0,0 +1,9 @@
name: CI/CD
on:
push: { branches: [main], tags: ["v*"] }
pull_request:
workflow_dispatch:
jobs:
cicd:
uses: hanzoai/ci/.github/workflows/build.yml@v1
secrets: inherit
-116
View File
@@ -1,116 +0,0 @@
name: Code check
on:
pull_request:
branches: ["*"]
jobs:
lint:
runs-on: ubuntu-latest
name: pnpm lint
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 20
- uses: pnpm/action-setup@v2.2.4
name: Install pnpm
id: pnpm-install
with:
version: 8.6.1
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install
#- run: pnpm lint
format:
runs-on: ubuntu-latest
name: pnpm format:check
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 20
- uses: pnpm/action-setup@v2.2.4
name: Install pnpm
id: pnpm-install
with:
version: 8.6.1
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install
- run: pnpm format:check
tsc:
runs-on: ubuntu-latest
name: pnpm typecheck
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 20
- uses: pnpm/action-setup@v2.2.4
name: Install pnpm
id: pnpm-install
with:
version: 8.6.1
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install
- run: pnpm typecheck
-60
View File
@@ -1,60 +0,0 @@
name: Test Coverage
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
coverage:
name: Test Coverage
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 9
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run tests with coverage
run: cd pkg/ui && pnpm test:coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
files: ./pkg/ui/coverage/lcov.info
flags: unit
name: hanzo-ui-coverage
- name: Comment PR with coverage
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const coverage = JSON.parse(fs.readFileSync('./pkg/ui/coverage/coverage-summary.json', 'utf8'));
const total = coverage.total;
const comment = `## Test Coverage Report\n\n` +
`Lines: ${total.lines.pct}%\n` +
`Statements: ${total.statements.pct}%\n` +
`Functions: ${total.functions.pct}%\n` +
`Branches: ${total.branches.pct}%`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});
-96
View File
@@ -1,96 +0,0 @@
name: Deploy to GitHub Pages
on:
push:
branches: [main]
workflow_dispatch:
inputs:
capture_screenshots:
description: 'Capture component screenshots (slow, optional)'
required: false
type: boolean
default: false
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 9
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: |
pnpm install --frozen-lockfile
- name: Build @hanzo/ui package
run: |
cd pkg/ui && pnpm build
- name: Capture screenshots (optional)
if: github.event.inputs.capture_screenshots == 'true'
working-directory: ./app
run: pnpm capture:registry
timeout-minutes: 5
- name: Build documentation
working-directory: ./app
run: |
pnpm build
touch out/.nojekyll
env:
NODE_ENV: production
GITHUB_ACTIONS: true
NEXT_PUBLIC_APP_URL: https://ui.hanzo.ai
SKIP_SCREENSHOTS: true
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: ./app/out
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
-78
View File
@@ -1,78 +0,0 @@
name: Deprecated
on:
pull_request_target:
types: [opened, synchronize]
permissions:
issues: write
contents: read
pull-requests: write
jobs:
deprecated:
runs-on: ubuntu-latest
steps:
- name: Checkout PR
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v46
with:
files: |
apps/www/**
files_ignore: |
apps/www/public/r/**
base_sha: ${{ github.event.pull_request.base.sha }}
sha: ${{ github.event.pull_request.head.sha }}
- name: Comment on PR if www files changed
if: steps.changed-files.outputs.any_changed == 'true'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const changedFiles = `${{ steps.changed-files.outputs.all_changed_files }}`.split(' ');
const wwwFiles = changedFiles.filter(file =>
file.startsWith('apps/www/') &&
!file.startsWith('apps/www/public/r/') &&
file !== 'apps/www/package.json'
);
if (wwwFiles.length > 0) {
const comment = `Looks like this PR modifies files in \`apps/www\`, which is deprecated.
Consider applying the change to \`apps/v4\` if relevant.`;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: comment
});
// Add deprecated label
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['deprecated']
});
} else {
// Remove deprecated label if no www files are changed
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
name: 'deprecated'
});
} catch (error) {
// Label doesn't exist, which is fine
console.log('Deprecated label not found, skipping removal');
}
}
-45
View File
@@ -1,45 +0,0 @@
# Adapted from vercel/next.js
name: "Stale issue handler"
on:
workflow_dispatch:
schedule:
# This runs every day 20 minutes before midnight: https://crontab.guru/#40_23_*_*_*
- cron: "40 23 * * *"
jobs:
stale:
runs-on: ubuntu-latest
if: github.repository_owner == 'shadcn-ui'
steps:
- uses: actions/stale@v9
id: issue-stale
name: "Mark stale issues, close stale issues"
with:
repo-token: ${{ secrets.STALE_TOKEN }}
ascending: true
days-before-issue-close: 7
days-before-issue-stale: 365
days-before-pr-stale: -1
days-before-pr-close: -1
remove-issue-stale-when-updated: true
stale-issue-label: "stale?"
exempt-issue-labels: "roadmap,next"
stale-issue-message: "This issue has been automatically marked as stale due to one year of inactivity. It will be closed in 7 days unless theres further input. If you believe this issue is still relevant, please leave a comment or provide updated details. Thank you. (This is an automated message)"
close-issue-message: "This issue has been automatically closed due to one year of inactivity. If youre still experiencing a similar problem or have additional details to share, please open a new issue following our current issue template. Your updated report helps us investigate and address concerns more efficiently. Thank you for your understanding! (This is an automated message)"
operations-per-run: 300
- uses: actions/stale@v9
id: pr-state
name: "Mark stale PRs, close stale PRs"
with:
repo-token: ${{ secrets.STALE_TOKEN }}
ascending: true
days-before-issue-close: -1
days-before-issue-stale: -1
days-before-pr-close: 7
days-before-pr-stale: 365
remove-pr-stale-when-updated: true
exempt-pr-labels: "roadmap,next,bug"
stale-pr-label: "stale?"
stale-pr-message: "This PR has been automatically marked as stale due to one year of inactivity. It will be closed in 7 days unless theres further input. If you believe this PR is still relevant, please leave a comment or provide updated details. Thank you. (This is an automated message)"
close-pr-message: "This PR has been automatically closed due to one year of inactivity. Thank you for your understanding! (This is an automated message)"
operations-per-run: 300
-186
View File
@@ -1,186 +0,0 @@
name: NPM Publish
permissions:
contents: write
id-token: write
on:
workflow_dispatch:
inputs:
package:
description: 'Package to publish'
required: true
type: choice
options:
- ui
- ui-mcp
- auth
- commerce
- checkout
- brand
- react
- all
version_bump:
description: 'Version bump type'
required: true
type: choice
options:
- patch
- minor
- major
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with:
version: 8
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: pnpm install
- name: Build packages
run: |
if [ "${{ github.event.inputs.package }}" = "all" ] || [ "${{ github.event.inputs.package }}" = "ui" ]; then
cd pkg/ui && pnpm build
cd ../..
fi
if [ "${{ github.event.inputs.package }}" = "all" ] || [ "${{ github.event.inputs.package }}" = "ui-mcp" ]; then
cd pkg/ui-mcp && pnpm build
cd ../..
fi
if [ "${{ github.event.inputs.package }}" = "all" ] || [ "${{ github.event.inputs.package }}" = "auth" ]; then
cd pkg/auth && pnpm build
cd ../..
fi
if [ "${{ github.event.inputs.package }}" = "all" ] || [ "${{ github.event.inputs.package }}" = "commerce" ]; then
cd pkg/commerce && pnpm build
cd ../..
fi
if [ "${{ github.event.inputs.package }}" = "all" ] || [ "${{ github.event.inputs.package }}" = "checkout" ]; then
cd pkg/checkout && pnpm build
cd ../..
fi
if [ "${{ github.event.inputs.package }}" = "all" ] || [ "${{ github.event.inputs.package }}" = "brand" ]; then
cd pkg/brand && pnpm build
cd ../..
fi
if [ "${{ github.event.inputs.package }}" = "all" ] || [ "${{ github.event.inputs.package }}" = "react" ]; then
cd pkg/react && pnpm build
cd ../..
fi
- name: Bump version and publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_CONFIG_PROVENANCE: true
run: |
# Set up npm and pnpm auth
npm config set //registry.npmjs.org/:_authToken $NODE_AUTH_TOKEN
echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" >> ~/.npmrc
publish_package() {
local pkg_dir=$1
local pkg_name=$(cd $pkg_dir && node -p "require('./package.json').name")
local pkg_version=$(cd $pkg_dir && node -p "require('./package.json').version")
echo "=== Publishing $pkg_name@$pkg_version from $pkg_dir ==="
# Change to package directory
pushd "$pkg_dir"
# Bump version manually using node to avoid any pnpm/npm hooks
echo "Bumping version (${{ github.event.inputs.version_bump }})..."
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
const [major, minor, patch] = pkg.version.split('.').map(Number);
const bump = '${{ github.event.inputs.version_bump }}';
if (bump === 'major') pkg.version = (major + 1) + '.0.0';
else if (bump === 'minor') pkg.version = major + '.' + (minor + 1) + '.0';
else pkg.version = major + '.' + minor + '.' + (patch + 1);
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
console.log('New version:', pkg.version);
"
local new_version=$(node -p "require('./package.json').version")
echo "Version is now: $new_version"
# Pack the tarball
echo "Running pnpm pack..."
pnpm pack --pack-gzip-level 9
local tarball=$(ls -t *.tgz | head -1)
echo "Created tarball: $tarball"
# Verify tarball contents
echo "Tarball package.json:"
tar -xzf "$tarball" -O package/package.json
# Copy to temp dir and publish
local temp_dir="/tmp/publish-$$"
mkdir -p "$temp_dir"
cp "$tarball" "$temp_dir/"
echo "Publishing from $temp_dir..."
cd "$temp_dir"
npm publish "$tarball" --access public
cd -
# Cleanup
rm -rf "$temp_dir"
rm -f "$tarball"
popd
}
case "${{ github.event.inputs.package }}" in
ui)
publish_package "pkg/ui"
;;
ui-mcp)
publish_package "pkg/ui-mcp"
;;
auth)
publish_package "pkg/auth"
;;
commerce)
publish_package "pkg/commerce"
;;
checkout)
publish_package "pkg/checkout"
;;
brand)
publish_package "pkg/brand"
;;
react)
publish_package "pkg/react"
;;
all)
publish_package "pkg/ui"
publish_package "pkg/ui-mcp"
publish_package "pkg/auth"
publish_package "pkg/commerce"
publish_package "pkg/checkout"
publish_package "pkg/brand"
publish_package "pkg/react"
;;
esac
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: 'chore: bump ${{ github.event.inputs.package }} version to ${{ github.event.inputs.version_bump }}'
title: 'chore: bump ${{ github.event.inputs.package }} version'
body: |
Automated version bump for ${{ github.event.inputs.package }} package(s).
Version bump type: ${{ github.event.inputs.version_bump }}
branch: version-bump-${{ github.event.inputs.package }}-${{ github.run_number }}
-65
View File
@@ -1,65 +0,0 @@
# Adapted from create-t3-app.
name: Write Beta Release comment
on:
workflow_run:
workflows: ["Release - Beta"]
types:
- completed
jobs:
comment:
if: |
github.repository_owner == 'hanzoai-ui' &&
${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
name: Write comment to the PR
steps:
- name: "Comment on PR"
uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
for (const artifact of allArtifacts.data.artifacts) {
// Extract the PR number and package version from the artifact name
const match = /^npm-package-hanzoai-ui@(.*?)-pr-(\d+)/.exec(artifact.name);
if (match) {
require("fs").appendFileSync(
process.env.GITHUB_ENV,
`\nBETA_PACKAGE_VERSION=${match[1]}` +
`\nWORKFLOW_RUN_PR=${match[2]}` +
`\nWORKFLOW_RUN_ID=${context.payload.workflow_run.id}`
);
break;
}
}
- name: "Comment on PR with Link"
uses: marocchino/sticky-pull-request-comment@v2
with:
number: ${{ env.WORKFLOW_RUN_PR }}
message: |
A new prerelease is available for testing:
```sh
npx hanzoai-ui@${{ env.BETA_PACKAGE_VERSION }}
```
- name: "Remove the autorelease label once published"
uses: actions/github-script@v6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: '${{ env.WORKFLOW_RUN_PR }}',
name: '🚀 autorelease',
});
-60
View File
@@ -1,60 +0,0 @@
# Adapted from create-t3-app.
name: Release - Beta
on:
pull_request:
types: [labeled]
branches:
- main
jobs:
prerelease:
if: |
github.repository_owner == 'hanzoai' &&
contains(github.event.pull_request.labels.*.name, '🚀 autorelease')
name: Build & Publish a beta release to NPM
runs-on: ubuntu-latest
environment: Preview
steps:
- name: Checkout Repo
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Use PNPM
uses: pnpm/action-setup@v2.2.4
with:
version: 8.6.1
- name: Use Node.js 18
uses: actions/setup-node@v3
with:
node-version: 18
cache: "pnpm"
- name: Install NPM Dependencies
run: pnpm install
- name: Modify package.json version
run: node .github/version-script-beta.js
- name: Authenticate to NPM
run: echo "//registry.npmjs.org/:_authToken=$NPM_ACCESS_TOKEN" >> pkg/cli/.npmrc
env:
NPM_ACCESS_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
- name: Publish Beta to NPM
run: pnpm pub:beta
- name: get-npm-version
id: package-version
uses: martinbeentjes/npm-get-version-action@main
with:
path: pkg/cli
- name: Upload packaged artifact
uses: actions/upload-artifact@v2
with:
name: npm-package-hanzoai-ui@${{ steps.package-version.outputs.current-version }}-pr-${{ github.event.number }} # encode the PR number into the artifact name
path: pkg/cli/dist/index.js
-154
View File
@@ -1,154 +0,0 @@
name: Publish on Tag
on:
push:
tags:
- 'v*' # Match @hanzo/ui version (e.g., v5.1.1)
jobs:
test:
name: Run Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with:
version: 9
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build packages
run: |
cd pkg/ui && pnpm build && cd ../..
cd pkg/auth && pnpm build && cd ../..
cd pkg/commerce && pnpm build && cd ../..
cd pkg/brand && pnpm build && cd ../..
cd pkg/react && pnpm build && cd ../..
- name: Run tests
run: |
cd pkg/ui && pnpm test
cd ../react && pnpm test
cd ../..
publish:
name: Publish to NPM
needs: test
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with:
version: 9
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build all packages
run: |
cd pkg/ui && pnpm build && cd ../..
cd pkg/auth && pnpm build && cd ../..
cd pkg/commerce && pnpm build && cd ../..
cd pkg/brand && pnpm build && cd ../..
cd pkg/react && pnpm build && cd ../..
- name: Configure npm authentication
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
run: |
npm config set //registry.npmjs.org/:_authToken $NODE_AUTH_TOKEN
npm whoami
- name: Check and publish packages
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
run: |
echo "Checking all packages for unpublished versions..."
PUBLISHED_COUNT=0
SKIPPED_COUNT=0
PUBLISHED_PACKAGES=""
for package in ui auth commerce brand react; do
cd "pkg/$package"
CURRENT_VERSION=$(node -p "require('./package.json').version")
PACKAGE_NAME=$(node -p "require('./package.json').name")
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📦 Checking $PACKAGE_NAME@$CURRENT_VERSION"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# Check if this version already exists on npm
if npm view "$PACKAGE_NAME@$CURRENT_VERSION" version 2>/dev/null; then
echo "⏭️ Already published - skipping"
SKIPPED_COUNT=$((SKIPPED_COUNT + 1))
else
echo "🚀 Publishing to npm..."
npm publish --access public
echo "✅ Successfully published $PACKAGE_NAME@$CURRENT_VERSION"
PUBLISHED_COUNT=$((PUBLISHED_COUNT + 1))
PUBLISHED_PACKAGES="$PUBLISHED_PACKAGES\n- $PACKAGE_NAME@$CURRENT_VERSION"
fi
cd ../..
done
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📊 Publishing Summary"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "✅ Published: $PUBLISHED_COUNT package(s)"
echo "⏭️ Skipped: $SKIPPED_COUNT package(s)"
if [ $PUBLISHED_COUNT -gt 0 ]; then
echo ""
echo -e "Published packages:$PUBLISHED_PACKAGES"
fi
# Save for GitHub release notes
echo "PUBLISHED_COUNT=$PUBLISHED_COUNT" >> $GITHUB_ENV
echo "PUBLISHED_PACKAGES<<EOF" >> $GITHUB_ENV
echo -e "$PUBLISHED_PACKAGES" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Create GitHub Release
if: ${{ env.PUBLISHED_COUNT > 0 }}
uses: softprops/action-gh-release@v1
with:
generate_release_notes: true
body: |
## 📦 NPM Packages Published
${{ env.PUBLISHED_PACKAGES }}
### Installation
```bash
# Install latest versions
npm install @hanzo/ui @hanzo/auth @hanzo/commerce @hanzo/brand @hanzo/react
```
files: |
CHANGELOG.md
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-76
View File
@@ -1,76 +0,0 @@
name: Publish Packages
on:
push:
branches: [main]
paths:
- 'pkg/*/package.json'
jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
packages: ${{ steps.changed.outputs.packages }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Detect changed packages
id: changed
run: |
CHANGED_PACKAGES=()
for pkg_json in pkg/*/package.json; do
pkg_dir=$(dirname "$pkg_json")
pkg_name=$(basename "$pkg_dir")
# Check if package.json version changed
if git diff HEAD~1 HEAD --quiet "$pkg_json" 2>/dev/null; then
continue
fi
# Get old and new versions
OLD_VERSION=$(git show HEAD~1:"$pkg_json" 2>/dev/null | jq -r '.version' || echo "0.0.0")
NEW_VERSION=$(jq -r '.version' "$pkg_json")
if [ "$OLD_VERSION" != "$NEW_VERSION" ]; then
echo "Version change detected: $pkg_name ($OLD_VERSION -> $NEW_VERSION)"
CHANGED_PACKAGES+=("$pkg_name")
fi
done
if [ ${#CHANGED_PACKAGES[@]} -eq 0 ]; then
echo "packages=[]" >> $GITHUB_OUTPUT
else
JSON=$(printf '%s\n' "${CHANGED_PACKAGES[@]}" | jq -R . | jq -s -c .)
echo "packages=$JSON" >> $GITHUB_OUTPUT
fi
publish:
needs: detect-changes
if: needs.detect-changes.outputs.packages != '[]'
runs-on: ubuntu-latest
strategy:
matrix:
package: ${{ fromJson(needs.detect-changes.outputs.packages) }}
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Build package
run: pnpm --filter ${{ matrix.package }} build
- name: Publish package
run: pnpm --filter ${{ matrix.package }} publish --no-git-checks --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
-60
View File
@@ -1,60 +0,0 @@
# Adapted from create-t3-app.
name: Release
on:
push:
branches:
- main
permissions:
id-token: write
contents: write
pull-requests: write
jobs:
release:
if: ${{ github.repository_owner == 'hanzoai-ui' }}
name: Create a PR for release workflow
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Use PNPM
uses: pnpm/action-setup@v4
with:
version: 9.0.6
- name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20
registry-url: "https://registry.npmjs.org"
cache: "pnpm"
- name: Update npm for OIDC support
run: npm install -g npm@latest
- name: Install NPM Dependencies
run: pnpm install
# - name: Check for errors
# run: pnpm check
- name: Build the package
run: pnpm shadcn:build
- name: Create Version PR or Publish to NPM
id: changesets
uses: changesets/action@v1
with:
commit: "chore(release): version packages"
title: "chore(release): version packages"
version: node .github/changeset-version.js
publish: npx changeset publish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_ENV: "production"
-100
View File
@@ -1,100 +0,0 @@
name: Sync Forks
on:
push:
branches: [main]
workflow_dispatch:
inputs:
fork:
description: 'Fork to sync (luxfi or zoo or all)'
required: false
default: 'all'
jobs:
sync-luxfi:
if: github.event.inputs.fork == 'luxfi' || github.event.inputs.fork == 'all' || github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- name: Checkout Hanzo UI
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Git
run: |
git config --global user.name "Hanzo Bot"
git config --global user.email "bot@hanzo.ai"
- name: Clone Luxfi Fork
run: |
git clone https://github.com/luxfi/ui.git luxfi-ui
cd luxfi-ui
git remote add upstream https://github.com/hanzoai/ui.git
git fetch upstream
- name: Sync and Rebrand
run: |
cd luxfi-ui
# Merge upstream changes
git checkout main
git merge upstream/main --no-edit || true
# Run rebrand script
cp ../scripts/rebrand.sh ./
chmod +x rebrand.sh
./rebrand.sh luxfi
# Commit changes
git add -A
git commit -m "sync: Update from hanzoai/ui and rebrand for Luxfi" || true
- name: Push to Luxfi
env:
GITHUB_TOKEN: ${{ secrets.FORK_SYNC_TOKEN }}
run: |
cd luxfi-ui
git push https://$GITHUB_TOKEN@github.com/luxfi/ui.git main || true
sync-zoo:
if: github.event.inputs.fork == 'zoo' || github.event.inputs.fork == 'all' || github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- name: Checkout Hanzo UI
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Git
run: |
git config --global user.name "Hanzo Bot"
git config --global user.email "bot@hanzo.ai"
- name: Clone Zoo Fork
run: |
git clone https://github.com/zooai/ui.git zoo-ui
cd zoo-ui
git remote add upstream https://github.com/hanzoai/ui.git
git fetch upstream
- name: Sync and Rebrand
run: |
cd zoo-ui
# Merge upstream changes
git checkout main
git merge upstream/main --no-edit || true
# Run rebrand script
cp ../scripts/rebrand.sh ./
chmod +x rebrand.sh
./rebrand.sh zoo
# Commit changes
git add -A
git commit -m "sync: Update from hanzoai/ui and rebrand for Zoo" || true
- name: Push to Zoo
env:
GITHUB_TOKEN: ${{ secrets.FORK_SYNC_TOKEN }}
run: |
cd zoo-ui
git push https://$GITHUB_TOKEN@github.com/zooai/ui.git main || true
-76
View File
@@ -1,76 +0,0 @@
name: E2E and Visual Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
schedule:
# Run tests weekly on Sunday at 2 AM UTC to catch breaking updates
- cron: '0 2 * * 0'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
NODE_OPTIONS: --max-old-space-size=8192
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
matrix:
node-version: [20.x]
steps:
- uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 9.0.6
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Install Playwright browsers
run: pnpm exec playwright install --with-deps
- name: Build the project
run: pnpm build
env:
NODE_OPTIONS: --max-old-space-size=8192
NEXT_PUBLIC_APP_URL: https://ui.hanzo.ai
- name: Run component health check
run: pnpm health-check
continue-on-error: true
- name: Run E2E tests
run: pnpm test:e2e
timeout-minutes: 30
env:
CI: true
- name: Run visual regression tests
run: pnpm test:visual
timeout-minutes: 20
continue-on-error: true
env:
CI: true
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: tests/reports/playwright-report/
retention-days: 30
-53
View File
@@ -1,53 +0,0 @@
name: Validate Registries
on:
pull_request:
paths:
- "apps/v4/public/r/registries.json"
- "apps/v4/registry/directory.json"
push:
branches:
- main
paths:
- "apps/v4/public/r/registries.json"
- "apps/v4/registry/directory.json"
jobs:
validate:
runs-on: ubuntu-latest
name: pnpm validate:registries
permissions:
contents: read
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 20
- uses: pnpm/action-setup@v4
name: Install pnpm
id: pnpm-install
with:
version: 9.0.6
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install
- name: Validate registries
run: pnpm --filter=v4 validate:registries
+1
View File
@@ -80,3 +80,4 @@ playwright-report/
.notes
.claude/settings.local.json
pkg/ui/dist-test/
app/public/api/
+26
View File
@@ -0,0 +1,26 @@
# The whole caller. Every decision this pipeline makes — what to test, what to
# build, where to roll it — is read from the root hanzo.yml, so this file only
# ever names the triggers and the reusable.
#
# `.hanzo/workflows`, not `.github/workflows`, and that is not a style choice:
# CI for this repo runs on git.hanzo.ai, which resolves ONLY `.hanzo/workflows`.
# github.com has zero self-hosted runners registered for the `hanzo-build-*`
# labels this pipeline asks for, so the same file under `.github/workflows`
# would queue there forever and never report anything at all.
name: CI/CD
on:
push:
branches: [main]
tags: ['v*']
pull_request:
workflow_dispatch:
concurrency:
group: cicd-${{ github.ref }}
cancel-in-progress: true
jobs:
cicd:
uses: hanzoai/ci/.hanzo/workflows/build.yml@v1
secrets: inherit
+70
View File
@@ -0,0 +1,70 @@
name: deploy
# RETIRED as an automatic lane. The image is built by hanzo.yml's `images:` block
# through hanzoai/ci (.hanzo/workflows/cicd.yml), and this job is kept only as a
# manually-dispatched fallback.
#
# The reason is one capability, not tidiness. ui.hanzo.ai is a static export, so
# its ingest key has to be BAKED IN at build; the key lives in KMS as
# `deploy/PUBLISHABLE_KEY`, and hanzoai/ci is the only lane that reads it and
# passes it as `--build-arg PUBLISHABLE_KEY=…`. The `docker build -t "$image" .`
# below cannot — there is no KMS hop in it and nowhere to put the value — so any
# image it produced would ship a client with no key at all, and cloud answers
# `401 ingest_key_required` for an unattributed write. With the Dockerfile now
# failing closed on an empty key, this job fails on every run instead of quietly
# publishing an inert site.
#
# Left in place rather than deleted, and dispatch-only rather than on push:
# deleting a repo's only build lane strands the host with no failing run to show
# it, and two push-triggered builders for one image means one commit yields two
# images under two tag schemes. Dispatching this publishes `<short-sha>` with an
# EMPTY key and the Dockerfile will refuse it — so if you dispatch it, pass the
# key yourself or expect the gate to fire.
on:
workflow_dispatch:
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: false
jobs:
image:
# hanzo-build-linux-amd64, not ubuntu-latest. This file lives in
# .hanzo/workflows, which GitHub never reads -- git.hanzo.ai is the only
# thing that can run it, and the fleet deliberately advertises no generic
# ubuntu-* label (~1400 mirrored forks all ask for it; one bad job was
# retried ~520 times across 10 runners). An unmatched label is not an
# error here, it is silence: the job queues until the 24h timeout, so this
# deploy has never produced an image and nothing ever said so.
runs-on: hanzo-build-linux-amd64
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- name: build and push
# GHCR_TOKEN/GHCR_USER, which are the org secrets that actually exist.
# This job asked for REGISTRY_TOKEN, which was never defined anywhere —
# the org has exactly GHCR_TOKEN, GHCR_USER, GH_PAT, KMS_CLIENT_ID,
# KMS_CLIENT_SECRET, OCI_TOKEN, OCI_USER — so the guard below fired on
# every run and nothing was ever published. Every other image build in
# the org already uses the GHCR_* pair; this file and papers were the
# two that invented a name.
#
# The username comes from the secret too, rather than being hardcoded to
# hanzo-dev, so rotating the publishing identity is one org-level change.
env:
GHCR_TOKEN: ${{ secrets.GHCR_TOKEN }}
GHCR_USER: ${{ secrets.GHCR_USER }}
run: |
set -euo pipefail
if [ -z "${GHCR_TOKEN:-}" ] || [ -z "${GHCR_USER:-}" ]; then
echo "::error::GHCR_TOKEN/GHCR_USER are unset, so nothing was published."
exit 1
fi
tag="$(git rev-parse --short HEAD)"
image="ghcr.io/hanzoai/ui:${tag}"
echo "${GHCR_TOKEN}" | docker login ghcr.io -u "${GHCR_USER}" --password-stdin
docker build -t "${image}" .
docker push "${image}"
echo "published ${image} — set image.tag AND image.digest in universe charts/app/values/hanzo/ui.yaml"
+343
View File
@@ -0,0 +1,343 @@
name: Publish Packages
on:
push:
branches: [main]
paths:
# BOTH package roots, and the singular one is not a typo — `@hanzo/ui` and
# `@hanzo/data` live under pkg/, everything else under pkgs/.
#
# ⚠️ This list and the `for pkg_json in …` loop below are ONE FACT. The loop
# was already fixed to walk both roots, with a comment explaining that a
# `pkgs/*` glob made pkg/ packages invisible — but this gate was left
# matching `pkgs/*` only, so for a pkg/-only change the corrected job could
# never be REACHED. The fix was reasoned about in the body and not applied
# to the thing that decides whether the body runs.
#
# Cost: @hanzo/ui 8.0.29 sat unpublished while npmjs served 8.0.28, so
# every consumer resolving `^8.0.17` silently got a version without the
# `./chat` entrypoint — an import error in the dependent, nothing at all in
# this repo. Add a root here whenever one is added to the loop.
- 'pkg/*/package.json'
- 'pkgs/*/package.json'
# A publish that fails for an infrastructure reason — the runner missing a
# tool, npmjs answering slowly — could only be retried by pushing another
# commit that touches a package.json, i.e. by burning a version number to
# re-run a job that was never wrong about the code. detect-changes reads what
# npmjs SERVES, so a dispatch is idempotent by construction: it publishes
# exactly the packages that are ahead of the registry, and nothing when none
# are.
workflow_dispatch:
jobs:
detect-changes:
runs-on: hanzo-build-linux-amd64
outputs:
packages: ${{ steps.changed.outputs.packages }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Detect changed packages
id: changed
run: |
CHANGED=()
# Both package roots. `@hanzo/ui` and `@hanzo/data` live under pkg/ (singular)
# and were invisible to a `pkgs/*` glob, so no version bump of either could
# ever reach npmjs from here.
for pkg_json in pkg/*/package.json pkgs/*/package.json; do
[ ! -f "$pkg_json" ] && continue
# Skip private packages
IS_PRIVATE=$(jq -r '.private // false' "$pkg_json")
[ "$IS_PRIVATE" = "true" ] && continue
# Only publish @hanzo/* packages
PKG_NAME=$(jq -r '.name' "$pkg_json")
case "$PKG_NAME" in
@hanzo/*) ;;
*) continue ;;
esac
# What npm SERVES, not what the previous commit said. HEAD~1 is the
# FIRST PARENT, so a merge whose first parent is the feature branch
# already carries the new version and the diff is empty — the publish
# is skipped and npm keeps serving the old release, silently. Asking
# the registry is independent of merge topology and of how many
# commits the runner fetched.
NEW=$(jq -r '.version' "$pkg_json")
OLD=$(curl -sf "https://registry.npmjs.org/${PKG_NAME}" \
| jq -r '."dist-tags".latest // "0.0.0"' 2>/dev/null || echo "0.0.0")
# FORWARD ONLY. `!=` fires in both directions, and several packages
# sit BEHIND npm (a release cut elsewhere, a revert) — publishing
# those walks the registry backwards. Only a strictly greater local
# version is a release.
NEWEST=$(printf '%s\n%s\n' "$OLD" "$NEW" | sort -V | tail -1)
if [ "$OLD" != "$NEW" ] && [ "$NEWEST" = "$NEW" ]; then
echo "Version change: $PKG_NAME ($OLD -> $NEW)"
# The package NAME, because every downstream step spends this as a
# `pnpm --filter` argument and a directory basename matches nothing.
# An unmatched filter exits 0, so the build silently no-ops and the
# publish ships an unbuilt tarball — how 8.0.17 and 8.0.19 shipped broken.
CHANGED+=("$PKG_NAME")
fi
done
if [ ${#CHANGED[@]} -eq 0 ]; then
echo "packages=[]" >> $GITHUB_OUTPUT
else
JSON=$(printf '%s\n' "${CHANGED[@]}" | jq -R . | jq -s -c .)
echo "packages=$JSON" >> $GITHUB_OUTPUT
fi
publish:
needs: detect-changes
if: needs.detect-changes.outputs.packages != '[]'
runs-on: hanzo-build-linux-amd64
strategy:
# One package's broken build must not withhold every other package's
# release: the matrix legs are independent publishes, not one artifact.
fail-fast: false
matrix:
package: ${{ fromJson(needs.detect-changes.outputs.packages) }}
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: pnpm install
- name: Build package
# The trailing `...` is load-bearing: it builds the package's WORKSPACE
# DEPENDENCIES first, in topological order.
#
# Without it, `pnpm --filter @hanzo/ui build` runs alone against siblings
# that have no `dist/`. Those siblings type themselves through
# `"types": "dist/index.d.ts"`, so tsc reports them as missing modules and
# the errors do not name the real cause:
# src/product/SiteNav.tsx: Cannot find module '@hanzo/products'
# src/core/tokens.ts: Cannot find module '@hanzo/tokens'
# src/gitops.ts: Cannot find module '@hanzo/cd'
# plus a wave of implicit-any errors in the files that imported them,
# which read like a source problem in @hanzo/ui and are not.
run: pnpm --filter ${{ matrix.package }}... build
- name: Provision bun (only for a package whose test script asks for it)
# @hanzo/canvas declares `bun test` and this runner has no bun, so its
# suite has been `spawn ENOENT` on every run — which fails the leg before
# the publish and is why canvas has sat at 0.2.2 here against 0.2.1 on
# npmjs. The step that exists to stop an untested release was itself the
# thing stopping the release.
#
# Read from the package's OWN manifest, not from a list here, so a
# package that adopts or drops bun needs no edit to this file. A package
# that does not ask for bun does not pay for it.
run: |
set -euo pipefail
PKG_DIR=$(pnpm --filter ${{ matrix.package }} exec pwd | tail -1)
if ! jq -r '.scripts.test // ""' "$PKG_DIR/package.json" | grep -qw bun; then
echo "::notice::${{ matrix.package }} does not run bun"
exit 0
fi
if command -v bun >/dev/null 2>&1; then
echo "bun already on PATH: $(bun --version)"
exit 0
fi
# From npmjs, not bun.sh/install: this job already reaches the npm
# registry (it is what the job exists to publish to) and setup-node
# already put the global bin on PATH, so this adds no new host to
# trust and no new failure mode. The shell installer would also want
# unzip, which a minimal runner image need not carry.
npm install -g bun
bun --version
- name: Test package
# A published version is IMMUTABLE, so this is the last point at which a
# defect costs nothing. Nothing else runs a package's own tests before it
# ships: ci.yml's test job runs `cd pkgs/ui && …` — that one directory —
# and this workflow went checkout → build → publish. So @hanzo/event's
# suite, which pins the version the client stamps on every event, ran in
# no workflow that gates a release, and the stale stamp it was written to
# catch could still have reached npmjs.
#
# `--if-present` so a package with no test script is skipped rather than
# failing the release: the set of testable packages is read from each
# package.json, not listed here, so a new package is covered the day it
# adds a `test` script.
#
# This step needs `test` to mean the same thing in every package — the
# package's own suite, no browser, no network. @hanzo/ui briefly chained
# its two CONSUMER suites onto it (pack a tarball, npm-install it into a
# throwaway app, drive it in chromium), which is a real gate and belongs
# exactly where hanzo.yml already runs it: on the push, in its own
# `ui-consumer` job, next to the `playwright install` that gives it a
# browser. Here there is no such step, so a chained `test` could only
# fail — the last gate before an immutable version would have been one
# that cannot pass, which is the failure mode this repo has already paid
# for twice.
run: pnpm --filter ${{ matrix.package }} run --if-present test
- name: Verify the tarball carries what package.json promises
# `types` is a PROMISE to every consumer, and tsup writes dist/*.mjs in a
# separate pass from dist/*.d.ts — so a declaration failure yields a
# package whose JS is fine and whose types are absent. That is invisible
# here and surfaces in dependents as "Could not find a declaration file".
#
# Asserted from the package's OWN manifest, so it holds for every package
# this workflow publishes and needs no per-package list.
run: |
set -euo pipefail
PKG_DIR=$(pnpm --filter ${{ matrix.package }} exec pwd | tail -1)
TYPES=$(jq -r '.types // .typings // empty' "$PKG_DIR/package.json")
if [ -z "$TYPES" ]; then
echo "::notice::${{ matrix.package }} declares no types entrypoint — nothing to verify"
exit 0
fi
if [ ! -f "$PKG_DIR/$TYPES" ]; then
echo "::error::${{ matrix.package }} declares types at $TYPES but the build produced no such file."
echo "::error::Publishing would ship a package that silently has no types. Fix the declaration build."
exit 1
fi
echo "types present: $TYPES"
- name: Fetch the npm token from KMS
id: npmtok
# publish.yml used secrets.NPM_TOKEN, which exists on NEITHER the hanzoai
# org nor this repo on git.hanzo.ai. An absent secret interpolates to the
# empty string rather than failing, so every publish reached npmjs
# unauthenticated and 401'd -- the reason @hanzo/ui has never shipped from
# CI and each release was a hand publish.
#
# KMS is where a secret lives, so the token is read at run time from the
# same org path and with the same machine identity every other workflow
# here already uses. Nothing new to rotate, and no npm credential is
# stored on the forge.
env:
KMS_CLIENT_ID: ${{ secrets.KMS_CLIENT_ID }}
KMS_CLIENT_SECRET: ${{ secrets.KMS_CLIENT_SECRET }}
KMS_ENDPOINT: ${{ vars.KMS_ENDPOINT || 'https://kms.hanzo.ai' }}
KMS_ORG: ${{ vars.KMS_ORG || 'hanzo' }}
KMS_SECRET_ENV: ${{ vars.KMS_SECRET_ENV || 'prod' }}
# Where the token actually lives, MEASURED: NPM_TOKEN sits at the org
# root, not under deploy/ (which holds CLOUDFLARE_*, GIT_TOKEN,
# KUBECONFIG, UNIVERSE_PIN_TOKEN). Empty means root; set the
# KMS_NPM_PATH variable to move it without editing this file.
KMS_NPM_PATH: ${{ vars.KMS_NPM_PATH || '' }}
run: |
token=$(curl -fsS "${KMS_ENDPOINT}/v1/kms/auth/login" \
-H 'Content-Type: application/json' \
-d "{\"clientId\":\"${KMS_CLIENT_ID}\",\"clientSecret\":\"${KMS_CLIENT_SECRET}\"}" \
| jq -r '.accessToken // empty')
if [ -z "${token}" ]; then
echo "::error::KMS login failed at ${KMS_ENDPOINT}/v1/kms/auth/login (check KMS_CLIENT_ID/KMS_CLIENT_SECRET)."
exit 1
fi
# The read is org-scoped by the TOKEN's owner claim, not by a path
# segment: the flat form is the contract cloud's embedded KMS serves,
# and it answers {name, env, value}. The older
# /v1/kms/orgs/<org>/secrets/... form with .secret.value 404s here.
secret_url="${KMS_ENDPOINT}/v1/kms/secrets/${KMS_NPM_PATH:+${KMS_NPM_PATH}/}NPM_TOKEN?env=${KMS_SECRET_ENV}"
# No -f: a 404 must report WHICH path was empty, not just exit 22.
npm_token=$(curl -sS "${secret_url}" -H "Authorization: Bearer ${token}" | jq -r '.value // empty')
if [ -z "${npm_token}" ]; then
echo "::error::No NPM_TOKEN at ${secret_url}."
echo "::error::Put it there, or set the KMS_NPM_PATH variable to the path that has it."
exit 1
fi
# Masked before it is ever an output, so it cannot surface in a log.
echo "::add-mask::${npm_token}"
echo "token=${npm_token}" >> "$GITHUB_OUTPUT"
- name: Publish package
# Idempotent on purpose: the desired state is "npmjs holds this version", and
# a version already there satisfies it. Versions are immutable, so a conflict
# means the release already happened (a re-run, or a hand publish) — that is
# success, not a failure. Every other error still fails the job.
env:
NODE_AUTH_TOKEN: ${{ steps.npmtok.outputs.token }}
run: |
set -o pipefail
OUT=$(pnpm --filter ${{ matrix.package }} publish \
--no-git-checks --access public 2>&1) || true
echo "$OUT"
case "$OUT" in
*EPUBLISHCONFLICT*|*"cannot publish over"*|*"previously published versions"*)
echo "::notice::${{ matrix.package }} is already on npmjs at this version — nothing to do" ;;
*)
# Not a conflict: confirm the version really landed, else fail loudly.
#
# RETRIED, because npmjs is read-after-write eventually consistent
# and this asked it exactly once, about two seconds after the
# write. On run 26906 both @hanzo/og@1.0.0 and @hanzo/shop@1.0.0
# failed here having printed npm's own `+ @hanzo/og@1.0.0` success
# line moments earlier, and both are on the registry now: the
# publishes worked and the check was simply too early.
#
# A false red here is worse than a slow green. It reports that a
# release did not happen when it did, so the next person bumps the
# version to "fix" it and burns a number over a replication lag —
# and the real signal, a publish that genuinely failed, becomes
# indistinguishable from the noise.
PKG_DIR=$(pnpm --filter ${{ matrix.package }} exec pwd | tail -1)
NAME=$(jq -r '.name' "$PKG_DIR/package.json")
WANT=$(jq -r '.version' "$PKG_DIR/package.json")
landed=
for attempt in 1 2 3 4 5 6; do
if npm view "$NAME@$WANT" version >/dev/null 2>&1; then landed=1; break; fi
echo "$NAME@$WANT not visible to npmjs yet (attempt $attempt/6); waiting 10s"
sleep 10
done
[ -n "$landed" ] \
|| { echo "::error::$NAME@$WANT is not on npmjs 60s after publish"; exit 1; }
echo "published $NAME@$WANT" ;;
esac
- name: Publish to the Hanzo registry
# Second home for the same tarball: api.hanzo.ai/v1/packages/hanzo/npm, our
# own registry (hanzoai/git), so an install does not have to reach npmjs.
# Same artifact, same version — this republishes what the step above just
# built, it does not rebuild or re-version.
#
# BEST-EFFORT by construction. No token ⇒ skip with a notice, so this can
# land before the credential exists and starts working the moment it does.
# An already-published version is a success, not a failure: the registry
# holding it is the desired state either way. npmjs is authoritative and is
# never affected by anything here.
env:
# REGISTRY_TOKEN is the fleet-wide name (six other workflows use it and
# the hanzoai org carries it). This asked for HANZO_REGISTRY_TOKEN, which
# is defined nowhere, so it read as empty and the step below took its
# "not set" branch and exited 0 on every run -- the mirror to
# api.hanzo.ai has never happened, and said so only as a notice.
HANZO_REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: |
set -uo pipefail
if [ -z "${HANZO_REGISTRY_TOKEN:-}" ]; then
echo "::notice::HANZO_REGISTRY_TOKEN not set — package published to npmjs only."
echo "::notice::Add it (a Hanzo Git token with package:write) to mirror to api.hanzo.ai."
exit 0
fi
REG=https://api.hanzo.ai/v1/packages/hanzo/npm/
NPMRC="$RUNNER_TEMP/.npmrc-hanzo"
{
echo "@hanzo:registry=${REG}"
echo "//api.hanzo.ai/v1/packages/hanzo/npm/:_authToken=${HANZO_REGISTRY_TOKEN}"
} > "$NPMRC"
out=$(npm_config_userconfig="$NPMRC" \
pnpm --filter ${{ matrix.package }} publish \
--no-git-checks --access public --registry "$REG" 2>&1) || true
echo "$out"
case "$out" in
*EPUBLISHCONFLICT*|*"cannot publish over"*|*"already exists"*)
echo "::notice::already in the Hanzo registry at this version — nothing to do" ;;
*) echo "$out" | grep -qiE 'error|failed' \
&& echo "::warning::mirror to api.hanzo.ai failed (npmjs publish unaffected)" \
|| echo "mirrored to the Hanzo registry" ;;
esac
+91
View File
@@ -0,0 +1,91 @@
name: Validate Registries
on:
pull_request:
paths:
- "apps/v4/public/r/registries.json"
- "apps/v4/registry/directory.json"
push:
branches:
- main
paths:
- "apps/v4/public/r/registries.json"
- "apps/v4/registry/directory.json"
jobs:
validate:
runs-on: hanzo-build-linux-amd64
name: pnpm validate:registries
permissions:
contents: read
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 20
- name: Block reserved registry namespaces
env:
RESERVED_NAMESPACES: "@shadcn,@ui,@blocks,@components,@block,@component,@util,@utils,@registry,@lib,@hook,@hooks,@theme,@themes,@chart,@charts"
run: |
node <<'EOF'
const fs = require("node:fs")
const files = [
"apps/v4/public/r/registries.json",
"apps/v4/registry/directory.json",
]
const reservedNamespaces = new Set(
process.env.RESERVED_NAMESPACES.split(",").filter(Boolean)
)
function readNames(filePath) {
return JSON.parse(fs.readFileSync(filePath, "utf8")).map(
(entry) => entry.name
)
}
const violations = files.flatMap((filePath) => {
return readNames(filePath)
.filter((name) => reservedNamespaces.has(name))
.map((name) => `${filePath}: ${name}`)
})
if (violations.length > 0) {
console.error("Reserved registry namespaces are not allowed:")
for (const violation of violations) {
console.error(`- ${violation}`)
}
process.exit(1)
}
EOF
- uses: pnpm/action-setup@v4
name: Install pnpm
id: pnpm-install
with:
version: 9.0.6
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install
- name: Validate registries
run: pnpm --filter=v4 validate:registries
+1
View File
@@ -1,2 +1,3 @@
auto-install-peers=true
link-workspace-packages=true
puppeteer_skip_download=true
+14
View File
@@ -0,0 +1,14 @@
dist
node_modules
.next
build
.contentlayer
**/fixtures
deprecated
apps/v4/registry/styles/**/*.css
# Byte-coupled: hz.js carries the marked region of src/anon.js VERBATIM, and
# pkgs/event/src/anon.test.ts compares them byte for byte. Reformatting either
# one alone breaks the build; the same region is also vendored by hanzoai/cloud.
pkgs/event/src/anon.js
pkgs/event/hz.js
+3 -3
View File
@@ -4,8 +4,8 @@
"typescript.preferences.quoteStyle": "single",
"eslint.workingDirectories": [
{ "pattern": "apps/*/" },
{ "pattern": "packages/*/" },
{ "pattern": "pkg/*/" }
{ "pattern": "pkgs/*/" },
{ "pattern": "pkgs/*/" }
],
"tailwindCSS.classFunctions": ["cva", "cn"],
"vitest.debugExclude": [
@@ -19,6 +19,6 @@
"search.exclude": {
"apps/v4/registry/radix-*": true,
"apps/v4/public/r/*": true,
"packages/shadcn/test/fixtures/*": true
"pkgs/shadcn/test/fixtures/*": true
}
}
+190
View File
@@ -0,0 +1,190 @@
# Hanzo UI Consolidation — one library, all polish, no duplication
**Goal.** RIP the fragmentation (`@hanzo/data` vs `@hanzo/ui` vs in-console duplicates) into ONE canonical, cross-platform, presentational, clean-room library: **`@hanzo/ui`, built on `@hanzo/gui`**. Preserve every polished component + token; zero loss; no fork.
**Status.**
- **Step 1 — Audit:** complete (this document; four source trees read file-by-file).
- **Step 2 — Establish `@hanzo/ui` + migrate the stable foundation:** **DONE & GREEN** (`pkg/ui` is now a real package; `tsc --noEmit` = 0 errors, `vitest` = 12/12).
- **Step 3 — Console re-point:** **STAGED, not merged.** Gate is split (backend live, app lanes in-flight) — see [Step 3](#step-3--console-re-point-staged).
**One decision needs CTO sign-off before publish** — see [Naming / version](#naming--version--the-one-decision).
---
## Architecture — three layers, decomplected
```
@hanzo/gui (Tamagui/One) cross-platform primitives: Text, XStack/YStack, Button, Input,
│ Card, Popover, Select, Switch, Slider, Spinner, ScrollView …
@hanzo/data (pkg/data) the metadata-driven RECORD layer: fields → records → views
│ (RecordsView, DataTable, BoardView, RecordDetail, field editors)
@hanzo/ui (pkg/ui) ◄──── THE ONE LIBRARY. Two orthogonal concerns, one package:
• product/app layer → import { … } from '@hanzo/ui'
• record layer → import { … } from '@hanzo/ui/data' (re-exports @hanzo/data)
```
`@hanzo/data` stays the **source of truth** for the record layer; `@hanzo/ui` **composes** it (`export * from '@hanzo/data'` on the `./data` subpath) rather than copying — so the CMS/ERP/Help app lanes that consume `@hanzo/data` today keep working untouched, and there is exactly one home. This mirrors how the gui base wraps `hanzogui`.
Both layers own a `DataTable` (product = generic typed `<T>` list; data = field-driven record grid). Keeping the record layer on the `./data` subpath keeps each name unambiguous — no rename, no collision.
---
## The preserve-and-merge manifest
Legend: **✅ migrated** (in `pkg/ui`, GREEN) · **↪ re-exported** (composed from `@hanzo/data`) · **⏳ staged** (Step 3, after app lanes land) · **🔧 pending prop-injection** (app-coupled; decouple before it can live in a presentational lib) · **🏠 stays in console** (app glue, not a UI component).
### A. Record layer — `@hanzo/data` → `@hanzo/ui/data` ↪
Source: `pkg/data/src/*` (v1.2.0, clean-room, already published). Surfaced through `@hanzo/ui/data` and `@hanzo/ui/primitives/bases/data`. Nothing re-implemented.
| Component / module | Source | New home |
|---|---|---|
| `RecordsView` (flagship: toolbar over table⇆board, filter/sort/search/group, saved views) | `view/RecordsView.tsx`, `view/Toolbar.tsx` | `@hanzo/ui/data` |
| `DataTable` (field-driven record grid: sort headers, drag resize/reorder, row select, inline edit, pagination) | `table/DataTable.tsx` | `@hanzo/ui/data` |
| `BoardView` (kanban, optimistic drag-move) · `RecordCard` | `board/BoardView.tsx`, `table/RecordCard.tsx` | `@hanzo/ui/data` |
| `RecordDetail` / `RecordForm` | `record/RecordDetail.tsx` | `@hanzo/ui/data` |
| Field model + registry (`FieldType` ×24, `FieldDefinition`, `registerField`, routers) | `field/{types,registry,registerDefaults,FieldDisplay,FieldInput}` | `@hanzo/ui/data` |
| Field displays + **editors** (select/currency/date/relation/file/JSON/boolean/fullName/address…) | `field/{displays,inputs}.tsx` | `@hanzo/ui/data` |
| Composable primitives: `Menu`, `CheckBox`, `Toggle`, `Calendar` | `primitives/*` | `@hanzo/ui/data` |
| Pure logic: `applyView`, `sortRecords`, `filterRecords`, `searchRecords`, `paginate`, `boardColumns`, … | `{table,board,view}/logic.ts` | `@hanzo/ui/data` (+ `@hanzo/data/*/logic` subpaths for Node hosts) |
| Tokens: `tokens`, `TAG_TONES`, `tagTone` | `theme.ts` | **also top-level `@hanzo/ui`** (token surface) |
### B. Console `components/ui/*` product primitives → `@hanzo/ui/product` ✅ / 🔧
Source: `console2/src/components/ui/*`. The pure subset was ported **byte-identical** (verified by `diff`) — 100% of the polish preserved (Charts' hover tooltips/axis-ticks/honest-null, DataTable width/flex/hover-gating, Field's 6 variants, SlideOver's focus-trap + scroll-lock, ComboBox's ReDoS-safe filter).
| Component | Source (`console2/src/components/ui/`) | New home | Status |
|---|---|---|---|
| `DataTable<T>` (generic typed list) + `Column` | `DataTable.tsx` | `@hanzo/ui` (`product/DataTable`) | ✅ |
| `PageHeader` | `PageHeader.tsx` | `@hanzo/ui` | ✅ |
| `Field*` (`FieldRow/Text/TextArea/Switch/Select/Slider`) | `Field.tsx` | `@hanzo/ui` | ✅ |
| **Charts** (`Sparkline/LineChart/BarChart/Donut/BarRows`, `CHART_PALETTE`) | `Charts.tsx` | `@hanzo/ui` | ✅ |
| `Donut` (standalone dependency-free ring) | `Donut.tsx` | `@hanzo/ui` as **`DonutRing`** (alias — Charts.Donut is canonical) | ✅ |
| `StatusTag` (health verdict → pill) | `StatusTag.tsx` | `@hanzo/ui` | ✅ |
| `EmptyState` (DO/Vercel-class first-run) | `EmptyState.tsx` | `@hanzo/ui` | ✅ (**decoupled** — see §I) |
| `Metric*` (`MetricCard/MiniBars/UtilBar/LegendDot/Panel/HintButton`, `SERIES`) | `Metric.tsx` | `@hanzo/ui` (its `Sparkline` preserved as **`MetricSparkline`**) | ✅ |
| `ComboBox` + pure `filterOptions`/`isKnownOption` | `ComboBox.tsx`, `combobox/filter.ts` | `@hanzo/ui` | ✅ (+ `filter.test.ts`) |
| `PrimaryButton` · `HanzoMark` · `ProductIcon` · `ProviderLogo` | resp. files | `@hanzo/ui` | ✅ |
| `SlideOver` (a11y drawer) · `SelectMenu` · `Toast` (`useToast`) | resp. files | `@hanzo/ui` | ✅ (SlideOver import decoupled `~/…/color``./color`) |
| `Reorder<T>` (pointer DnD) · `FadeIn` · `ThemeToggle` · `color` (`asColor`/`IconLike`) | resp. files | `@hanzo/ui` | ✅ (+ `Reorder.test.ts`) |
| `DetailPane` (right-side pane over SlideOver) | `components/DetailPane.tsx` | `@hanzo/ui` | ⏳ (liftable; lands with Step 3 — depends only on SlideOver) |
| `ChunkGuard` (stale-deploy chunk-error reload) | `components/ChunkGuard.tsx` | `@hanzo/ui` | ⏳ (fully portable; lands with Step 3) |
### C. Design tokens + motion → `@hanzo/ui` ✅
The brief's premise (`globals.css` `t_dark`/`t_light` token blocks) is **corrected**: `t_dark`/`t_light` are `@hanzo/gui` (Tamagui) theme **class names**; the calm values live in three real places, all preserved:
| Polish | Source | New home |
|---|---|---|
| Calm dark-first tokens (surface, text, accent `#60a5fa`, tag tones) — raw hex, theme-config independent | `pkg/data/src/theme.ts` | `@hanzo/ui` (top-level `tokens`/`TAG_TONES`/`tagTone`) |
| OKLCH shadcn-compatible palette (soft-charcoal `oklch(0.145 0 0)`, `--radius: 0.5rem`, indigo sidebar accent) | `pkgs/ui/style/hanzo-default-colors.css`, `@hanzo/tokens` | unchanged (legacy shadcn line); the gui line uses the hex tokens above |
| **Motion vocabulary**`fade-up` (FadeIn), `collapse`/`slide`/`fade` (shell/drawer), `drag`, `skeleton`/`pulse`/`row-in`; all `prefers-reduced-motion`-guarded | `console2/app/globals.css` (motion section) | **`@hanzo/ui/styles/motion.css`** (verbatim) — `import '@hanzo/ui/styles/motion.css'` once at app root |
### D. Generic DocType renderer → `@hanzo/ui` ⏳ (Step 3, FINAL — active lane)
Source: `console2/src/components/doctype/*`. **Actively edited by the CMS/ERP/Help lanes (untracked).** Dependency-**injected** (`client: FrameworkClient` prop) — not `~/lib/api`-coupled — so the real move-blockers are `~/lib/framework/{types,fields}` + the `ui/*` primitives (now in `@hanzo/ui`) + `@hanzo/data` (now `@hanzo/ui/data`).
| File | Class | Step-3 disposition |
|---|---|---|
| `MediaGrid.tsx` | presentational | → `@hanzo/ui` (cleanest lift; props-only DAM gallery) |
| `DocTypeRecords.tsx` / `DocTypeDetail.tsx` | mixed (data-bound shell over `@hanzo/data` views) | shell **stays in console**; renders `@hanzo/ui/data` views (already true) |
| `CollectionsBrowser.tsx` | mixed (card grid + install/create orchestration) | presentational card-grid → `@hanzo/ui`; orchestration stays |
| `data.ts` | glue (relation loading) | stays in console |
### E. LivingOverview → `@hanzo/ui` ⏳ (Step 3 — active lane)
Source: `console2/src/components/products/overview/living/*` (untracked/modified). Pure/presentational parts portable; app-glue concentrated in `registry.ts` (live API clients).
| Part | Class | Step-3 disposition |
|---|---|---|
| `motion.ts`, `hooks.ts`, `logic.ts`, `config.ts` (types) | pure | → `@hanzo/ui` (unit-tested; count-up, poll clock, unit formatting) |
| `tiles.tsx`, `LivingOverview.tsx` | presentational (reuse `ui/Charts` verbatim) | → `@hanzo/ui` (once `config.ts`'s one `~/lib/products/registry` `ProductIcon` type is swapped for `@hanzo/ui`'s `IconLike`) |
| `adapters.ts`, `registry.ts` | glue (map REAL `/v1` sources → `OverviewData`) | **stay in console** |
### F. Framework / Base-data libs → stay in console 🏠
`src/lib/framework/*` (DocType wire contract + `FrameworkApi` client + pure `fields.ts` mapper to `@hanzo/data`) and `src/lib/base-data/*` (Base REST client + schema→field mapper) are **app data-access glue**, not UI. They stay in the console. Their pure mappers (`docTypeToFields`, `baseCollectionToFields`) could later publish as a small `@hanzo/base-data` adapter, but they are not UI components and are out of scope for `@hanzo/ui`.
### G. App-coupled console components → 🔧 pending prop-injection (NOT yet in the lib)
These import app `~/lib`/session/router/branding and must be made presentational (inject props) before they belong in a host-agnostic lib. Kept in console for now; the injection contract is specified so the eventual lift is mechanical.
| Component | Coupling | Inject to lift |
|---|---|---|
| `BackendStateCard` (`BackendState.tsx`) | `~/lib/api` `ApiError` | accept a numeric `status` (or shared error shape) instead of `ApiError` |
| `BrandLogo` | session + live IAM `organization()` + `~/config` + branding | `orgName`, `logoUrl` (or a `loadOrgLogo` loader), brand mark/name as props |
| `Breadcrumbs` | `next/navigation` + catalog lookups | `crumbs[]` (or `pathname` + resolver) + `onNavigate` |
| `Loader` / `BrandMark` | `~/config` + brand logo pkgs | `brand`/`brandName` + the animated-SVG getter |
| `States` (`ErrorState`, `OperatorAccessRequired`) | `~/lib/api` + session + branding | the error, the account identity, brand strings |
---
## What this pass built (Step 2)
New in **`pkg/ui`** (was a bare `src/` staging dir — no package):
- `package.json``@hanzo/ui` `8.0.0`, source-published (`files: ["src"]`, exports map to `.ts`), peers `@hanzo/gui >=7.2.2`, `@hanzo/data >=1.2.0`, `react >=19`; MIT (repo `LICENSE.md`). Exports: `.`, `./product`, `./data`, `./primitives/bases/data`, `./styles/motion.css`.
- `src/index.ts` — top-level barrel (product layer + tokens).
- `src/product/index.ts` — product barrel; **collision-free** (Charts `Sparkline`/`Donut` canonical; Metric's → `MetricSparkline`; standalone ring → `DonutRing`; `ComboOption` from the one filter module) — **zero loss**.
- `src/data.ts``export * from '@hanzo/data'` (the record layer, one home).
- `src/styles/hanzo-motion.css` — the motion vocabulary, verbatim.
- `tsconfig.json`, `vitest.config.ts`, `gui.config.ts`, `gui.d.ts`, `.npmignore`, `README.md` — mirror the proven `pkg/data` setup.
- **One decouple fix:** `product/EmptyState.tsx` no longer imports `~/lib/products/registry`; it uses the local `IconLike` type — the last host coupling in the product tree is gone.
**Green:** `tsc --noEmit` = 0 errors (strict); `vitest run` = 12/12 (`combobox/filter`, `Reorder`). Every product component is host-agnostic (imports only `react`, `@hanzo/gui`, `@hanzogui/*`, `@hanzo/data`, relative).
## Clean-room guarantee (preserved)
`pkg/data` audited for GPL/Twenty contamination: **none.** No GPL, no copied license headers/SPDX, no Twenty entity/decorator architecture. The field/record model is an independent `FieldType` union + `FieldDefinition` + runtime `Map` registry (records are plain `Record<string, unknown>`). "Twenty" appears **only as benchmark prose** ("Airtable/Twenty-class polish, clean-room"). License: MIT (repo `LICENSE.md`). `@hanzo/ui` inherits the same posture.
---
## Naming / version — DECIDED: `@hanzo/ui@8`, shadcn retired to `@hanzo/ui-shadcn`
**Decision:** the gui-based unified library takes the `@hanzo/ui` name **forward at `8.0.0`** (aligning with the "Hanzo Cloud 8.x" umbrella; major = breaking re-platform). The legacy shadcn/Radix line (`pkgs/ui`, v5.7.0) is **retired by renaming** to `@hanzo/ui-shadcn` (never hard-deleted) — it stays fully alive under the new name, freeing `@hanzo/ui` for v8. Precedent: `pkg/data@1.2.0` already superseded `pkgs/data@1.1.0` under the same name.
This is **DONE (repo-local):** `pkg/ui/package.json` is `@hanzo/ui@8.0.0`, GREEN. The rest is a coordinated, **sequenced** retire — because publishing `@hanzo/ui@8` (a different, gui-based API with no shadcn `Button/Card/Dialog`) under the name ~20 repos consume for shadcn primitives will BREAK any consumer that resolves to `@8`. So order matters:
**Blast radius (measured across `~/work/hanzo`).** Declared deps on `@hanzo/ui` in ~20 repos. Most pin `^5.x` (semver-safe from an `@8` bump): paas, chat, platform, app, hanzo.ai, o11y, docs, mdx, hanzobot, ui-repo `pkgs/checkout` `^5.3`, `pkgs/agent-ui` `^5.0`. **Would break on `@8`:** `hanzoai/identity/app` (`"latest"`), ui-repo `pkgs/commerce` (`>=5.0.0`); `app/` uses `workspace:^`. The `ui.hanzo.ai` docs app + `pkgs/{commerce,checkout,agent-ui}` import the shadcn line internally.
**Publish reality.** `.github/workflows/publish-on-tag.yml` publishes **`pkgs/ui`** (the shadcn line) as `@hanzo/ui` on a `v*` tag — it does not reference `pkg/ui`. So a tag push today publishes shadcn, not v8. Publishing v8 needs the workflow rewired to build/publish `pkg/ui`. `npm` is not authed locally (`npm whoami` empty) — per house rules, publish goes through **CI (self-hosted runners, canonical org `NPM_TOKEN`)**, not a local `npm publish`.
**Safe sequence to fully land v8 (each step reversible until the tag push):**
1.`pkg/ui` = `@hanzo/ui@8.0.0`, GREEN (done, committed to a branch).
2. Rename `pkgs/ui` name → `@hanzo/ui-shadcn`; update the internal ui-repo consumers (`app/`, `pkgs/{commerce,checkout,agent-ui}`) + `check-no-hanzogui`/registry scripts that reference it.
3. Migrate external consumers off `@hanzo/ui``@hanzo/ui-shadcn` (start with the break-risk ones: `identity` `latest`, `commerce` `>=5`; the `^5` pins are safe to migrate at leisure). ~20 repos — do as a tracked sweep or flag per-repo.
4. Rewire `publish-on-tag.yml` (and `release.yml`) to build + publish `pkg/ui` as `@hanzo/ui@8`, and `pkgs/ui-shadcn` as `@hanzo/ui-shadcn`.
5. `npm deprecate '@hanzo/ui@<8' 'moved to @hanzo/ui-shadcn; @hanzo/ui@8+ is the @hanzo/gui-based unified lib'`.
6. Tag `v8.0.0` → CI publishes `@hanzo/ui@8.0.0`. Verify `npm view @hanzo/ui@8.0.0`.
**Gated on the user's direct go-ahead** (irreversible / globally-visible / ecosystem-wide): steps 26 — the `pkgs/ui` rename, the ~20-repo consumer sweep, the CI rewire, `npm deprecate`, and the `v8.0.0` tag push that triggers publish. These were not executed autonomously.
---
## Step 3 — console re-point (STAGED)
**Gate (per the brief):** cloud `/v1/framework/modules` live **AND** console CMS/ERP/Help native.
- ✅ Backend: `/v1/framework/modules[/:module[/install]]` is **implemented + wired** (`cloud/clients/framework/framework.go:71-73`, HIP-0106 order 129, bound in `subsystems.go:130`, real tests).
- ⏳ Console: `CmsModule.tsx`, `ErpModule.tsx`, `HelpModule.tsx`, `components/doctype/` are **untracked/in-flight** on `feat/console-native-cms`.
→ Gate is **split**, so the re-point is **staged, not applied.** I deliberately did **not** touch `console2`'s working tree (it holds the lanes' uncommitted work — editing it would collide and risk loss, the opposite of the goal).
**Ready-to-apply re-point (mechanical, once the lanes land + merge):**
1. `console2` deps: keep `@hanzo/gui`, `@hanzo/data`; add `@hanzo/ui` (`^6.0.0`). (`@hanzo/data` may stay as a direct dep or be dropped in favor of `@hanzo/ui/data` — both resolve to the same source.)
2. Re-point imports (delete the now-duplicated in-console copies — extract, don't copy):
- `~/components/ui/{DataTable,PageHeader,Field,Charts,Donut,StatusTag,EmptyState,Metric,ComboBox,combobox/filter,PrimaryButton,HanzoMark,ProductIcon,ProviderLogo,SlideOver,SelectMenu,Toast,Reorder,FadeIn,ThemeToggle,color}`**`@hanzo/ui`** (or `@hanzo/ui/product`). Two spot-renames: Metric's `Sparkline``MetricSparkline`, standalone `Donut``DonutRing`.
- `@hanzo/data` (in `doctype/*`, `base-data/*`, `products/*Module`) → **`@hanzo/ui/data`** (identical surface).
- `~/components/{DetailPane,ChunkGuard}`**`@hanzo/ui`**.
- `app/globals.css` motion block → `import '@hanzo/ui/styles/motion.css'` (keep base resets local).
- Move the 5 app-coupled components (§G) into the lib only after applying their inject-props contract; until then they stay local.
3. Verify **identical render**: `tsc --noEmit` + `vitest` + `next build` green, then headless-Playwright the live pages pixel-same (the polish is byte-identical, so parity is expected).
**Report:** `@hanzo/ui` is the ONE unified library — product layer + record layer (`@hanzo/data`) + charts + calm tokens + motion, all on `@hanzo/gui`, presentational, cross-platform, clean-room, GREEN. Console re-point is **ready to apply once the CMS/ERP/Help lanes land**; it was not merged to avoid colliding with that in-flight work.
## Follow-ups (semver-minor, with visual e2e)
- Collapse the preserved Sparkline/Donut variants to one each (`MetricSparkline``Sparkline`, `DonutRing``Donut`) once call-sites are proven identical.
- Lift §G's five components after applying their prop-injection contracts.
- Consider publishing the pure `framework`/`base-data` mappers as a small `@hanzo/base-data` adapter (not UI).
+33 -49
View File
@@ -1,10 +1,10 @@
# Contributing
Thanks for your interest in contributing to ui.hanzo.com. We're happy to have you here.
Thanks for your interest in contributing to ui.shadcn.com. We're happy to have you here.
Please take a moment to review this document before submitting your first pull request. We also strongly recommend that you check for open issues and pull requests to see if someone else is working on something similar.
If you need any help, feel free to reach out to [@hanzo](https://x.com/hanzoai).
If you need any help, feel free to reach out to [@shadcn](https://twitter.com/shadcn).
## About this repository
@@ -12,7 +12,9 @@ This repository is a monorepo.
- We use [pnpm](https://pnpm.io) and [`workspaces`](https://pnpm.io/workspaces) for development.
- We use [Turborepo](https://turbo.build/repo) as our build system.
- We use [changesets](https://github.com/changesets/changesets) for managing releases.
- Releases are semver-driven: bump a package's `version` in its `package.json` and
merge to `main`. CI (`.github/workflows/publish.yml`) detects the version change
and publishes that package to npm. One step, one source of truth.
## Structure
@@ -20,28 +22,25 @@ This repository is structured as follows:
```
apps
└── www
└── v4
├── app
├── components
├── content
└── registry
── default
│ ├── example
│ └── ui
└── new-york
── new-york-v4
├── example
└── ui
packages
└── cli
└── shadcn
```
| Path | Description |
| --------------------- | ---------------------------------------- |
| `apps/www/app` | The Next.js application for the website. |
| `apps/www/components` | The React components for the website. |
| `apps/www/content` | The content for the website. |
| `apps/www/registry` | The registry for the components. |
| `packages/cli` | The `hanzo-ui` package. |
| Path | Description |
| -------------------- | ---------------------------------------- |
| `apps/v4/app` | The Next.js application for the website. |
| `apps/v4/components` | The React components for the website. |
| `apps/v4/content` | The content for the website. |
| `apps/v4/registry` | The registry for the components. |
| `packages/shadcn` | The `shadcn` package. |
## Development
@@ -79,76 +78,61 @@ You can use the `pnpm --filter=[WORKSPACE]` command to start the development pro
#### Examples
1. To run the `ui.hanzo.com` website:
1. To run the `ui.shadcn.com` website:
```bash
pnpm --filter=www dev
pnpm --filter=v4 dev
```
2. To run the `hanzo-ui` package:
2. To run the `shadcn` package:
```bash
pnpm --filter=hanzo-ui dev
pnpm --filter=shadcn dev
```
## Running the CLI Locally
To run the CLI locally, you can follow the workflow:
1. Start by running the registry (main site) to make sure the components are up to date:
1. Start by running the dev server:
```bash
pnpm v4:dev
pnpm dev
```
2. Run the development script for the CLI:
2. In another terminal tab, test the CLI by running:
```bash
pnpm hanzo:dev
```
3. In another terminal tab, test the CLI by running:
```bash
pnpm hanzo
pnpm shadcn
```
To test the CLI in a specific app, use a command like:
```bash
pnpm hanzo <init | add | ...> -c ~/Desktop/my-app
```
4. To run the tests for the CLI:
```bash
pnpm --filter=hanzo test
pnpm shadcn <init | add | ...> -c ~/Desktop/my-app
```
This workflow ensures that you are running the most recent version of the registry and testing the CLI properly in your local environment.
## Documentation
The documentation for this project is located in the `www` workspace. You can run the documentation locally by running the following command:
The documentation for this project is located in the `v4` workspace. You can run the documentation locally by running the following command:
```bash
pnpm --filter=www dev
pnpm --filter=v4 dev
```
Documentation is written using [MDX](https://mdxjs.com). You can find the documentation files in the `apps/www/content/docs` directory.
Documentation is written using [MDX](https://mdxjs.com). You can find the documentation files in the `apps/v4/content/docs` directory.
## Components
We use a registry system for developing components. You can find the source code for the components under `apps/www/registry`. The components are organized by styles.
We use a registry system for developing components. You can find the source code for the components under `apps/v4/registry`. The components are organized by styles.
```bash
apps
└── www
└── v4
└── registry
── default
│ ├── example
│ └── ui
└── new-york
── new-york-v4
├── example
└── ui
```
@@ -157,7 +141,7 @@ When adding or modifying components, please ensure that:
1. You make the changes for every style.
2. You update the documentation.
3. You run `pnpm build:registry` to update the registry.
3. You run `pnpm registry:build` to update the registry.
## Commit Convention
@@ -196,9 +180,9 @@ If you have a request for a new component, please open a discussion on GitHub. W
## CLI
The `hanzo-ui` package is a CLI for adding components to your project. You can find the documentation for the CLI [here](https://ui.hanzo.com/docs/cli).
The `shadcn` package is a CLI for adding components to your project. You can find the documentation for the CLI [here](https://ui.shadcn.com/docs/cli).
Any changes to the CLI should be made in the `packages/cli` directory. If you can, it would be great if you could add tests for your changes.
Any changes to the CLI should be made in the `packages/shadcn` directory. If you can, it would be great if you could add tests for your changes.
## Testing
+106
View File
@@ -0,0 +1,106 @@
# Hanzo Design System — Canonical Tokens
`@hanzo/ui` is the single source of truth for the shared Hanzo product look across
the **Tailwind** apps (hanzo.chat, hanzo.app, hanzo console, commerce, hanzo-desktop).
Change a value here; apps converge on it. This file is that source of truth for the
three things that must read as **one product**: typography, the sidebar/panel system,
and the dark-black palette.
> One library: **`@hanzo/ui@8`** (`pkg/ui`, on **`@hanzo/gui`**) IS the component
> library — the cross-platform product/record layer every surface consumes.
> **`@hanzo/ui-shadcn`** (`pkgs/ui`) is the legacy shadcn/Tailwind/Radix kit, kept
> only for existing v5 consumers (pin `@hanzo/ui-shadcn@^5`; no new adoptions).
> This file stays the source of truth for the *token values* (fonts, dark palette,
> the sidebar glyph) both render.
---
## 1. Typography — Basel Grotesk + Geist Mono
| Role | Family | Notes |
|------|--------|-------|
| UI / body / display / heading (`sans`) | **Basel Grotesk** | Self-hosted. Book = weight **400**, Medium = weight **500**. |
| code / data / mono (`mono`) | **Geist Mono** | `next/font/google` (`Geist_Mono`) or the geist CDN. |
| Arabic / Hebrew (`--font-ar` / `--font-he`) | unchanged | i18n only — keep. |
**Dropped as defaults:** Geist Sans, DM Sans, Figtree, Inter, PT Sans, Roboto Mono.
Basel is a **licensed, non-Google** face — **self-host** the woff2/woff, do NOT use
`next/font/google` for it. Canonical files (mirror lux.exchange):
`Basel-Grotesk-Book.woff2/.woff` (400), `Basel-Grotesk-Medium.woff2/.woff` (500).
`@font-face` (weights 400/500, `font-display: swap`, `font-style: normal`):
```css
@font-face {
font-family: 'Basel';
font-style: normal;
font-weight: 400; /* Book; 500 = Medium */
font-display: swap;
src: url('.../Basel-Grotesk-Book.woff2') format('woff2'),
url('.../Basel-Grotesk-Book.woff') format('woff');
}
```
Per-app adoption (converge the value, keep each app's own mechanism):
- **@hanzo/ui / Next apps** → `next/font/local` for Basel (`--font-basel-sans`) +
`next/font/google` `Geist_Mono` (`--font-geist-mono`). See `app/lib/fonts.ts`;
tailwind `sans → var(--font-basel-sans)`, `mono → var(--font-geist-mono)`.
- **Vite + Tailwind apps** (chat, launcher, desktop) → self-host Basel `@font-face`
+ geist-mono CDN import; tailwind `fontFamily.sans = ['Basel', …]`,
`mono = ['Geist Mono', …]`.
- **Tamagui (console)** → Basel `@font-face` in globals + override the Tamagui
`body`/`heading` font `family` to Basel; Geist Mono for `code`/`pre`.
---
## 2. Sidebar toggle icon — lucide `PanelLeft`
One glyph everywhere: lucide **`PanelLeft`** (the shadcn `SidebarTrigger` default).
Where a directional open/close affordance is wanted, use the pair
**`PanelLeftClose`** (expanded) / **`PanelLeft`** (collapsed). Never a hamburger,
a magnifier, a directional arrow, or a bespoke panel SVG for the sidebar toggle.
- Icon: stroke-2, ~1620px, `currentColor`.
- Button: ghost/outline, square (`size-7`/`h-6 w-6`), subtle hover.
---
## 3. Sidebar + panels
| Spec | Value |
|------|-------|
| Sidebar width (expanded) | **16rem / 256px** (`SIDEBAR_WIDTH`) |
| Sidebar width (collapsed / icon rail) | **3rem / 48px** (apps vary 4870px) |
| Sidebar / panel surface | resting **#0a0a0a** over the true-black page |
| Border / separation | `border-border` — subtle white-alpha ~10% in dark (`border-r` / `border-l`) |
| Item hover | subtle `white/5` |
| Item active | monochrome `white/10`**no colored accent** (the house style is monochrome) |
| Right panel rail | `border-l border-border`, same surface, collapsible |
Prefer the `@hanzo/ui` `Sidebar` primitive (`pkgs/ui/primitives/sidebar.tsx`,
`SidebarTrigger``PanelLeft`) where the app can consume it; otherwise match these
classes/tokens.
---
## 4. Dark-black palette (true-black OLED)
The house dark theme is a **true-black** canvas (matches hanzo.ai marketing +
hanzo.chat OLED), with a shallow surface-depth ladder for cards/panels and quiet
hairline borders — never harsh pure-white on pure-black.
| Token | Value | Use |
|-------|-------|-----|
| Page background | **#000000** (`oklch(0 0 0)`) | body / canvas |
| Surface / sidebar / panel (resting) | **#0a0a0a** | sidebars, panels, cards |
| Press | **#050505** | pressed surface |
| Elevated / hover | **#171717** | hover, raised card |
| Border / divider | **rgba(255,255,255,0.10)** (≈ `#171717` opaque on black) | hairlines |
| Foreground (primary text) | near-white **#ededf1** (`oklch(0.985)`) — not pure `#fff` | body text |
| Muted / secondary text | `white/70` (≈ `#a1a1aa`) | secondary |
Keep each app's theme-token engine (CSS vars / Tailwind tokens / Tamagui `$color*`);
converge the **values/usage** to the table above, don't rip the engine.
Reference: `pkgs/ui/style/hanzo-default-colors.css` (`.dark` / `.hanzo-ui-dark-theme`).
+97
View File
@@ -0,0 +1,97 @@
# ui.hanzo.ai — the @hanzo/ui docs + component registry, served by the house
# static server (ghcr.io/hanzoai/static, a Go binary), same as every other Hanzo
# static site. Built on our own runners, never on a laptop and never by a
# third-party builder.
#
# The app is a Next.js static export: `pnpm build` in app/ writes app/out, which
# is the entire site — docs, the registry JSON the CLI reads, and the static
# /api/registry/*.json index.
FROM node:22 AS builder
WORKDIR /src
ENV NEXT_TELEMETRY_DISABLED=1
# Publishable ingest key (pk-…), baked in at build because a static export has no
# server to read config at runtime. Write-only and HMAC-verified to one org, so it
# is safe in a public bundle; the deployment that builds decides which org the
# site reports as.
#
# ONE name, end to end: KMS holds `deploy/PUBLISHABLE_KEY`, hanzo.yml declares it
# as this image's build_secret, and the KMS name IS the build-arg name. NEXT_PUBLIC_
# is added HERE because that prefix is what makes Next inline it — the app reads
# process.env.NEXT_PUBLIC_PUBLISHABLE_KEY.
#
# Do NOT re-declare `ARG NEXT_PUBLIC_PUBLISHABLE_KEY` after the ENV below. A later
# ARG of the same name shadows the ENV with its own (empty) default, and the build
# stays green while the bundle ships blank — which is exactly how hanzo.chat 1.0.58
# shipped a keyless site from a fully green run.
#
# Fail CLOSED, on BOTH ways this goes wrong.
#
# empty — builds, serves and looks correct while cloud answers
# `401 ingest_key_required` for every anonymous pageview. The previous
# `ARG …=""` default made that the normal outcome of an unattended
# build, which is why no automated lane could ever publish a working
# image.
# `pk_…` — the OLDER key format. v5.7.6 shipped one, passed by hand on a local
# `docker build`, and it is now dead: api.hanzo.ai 401s it on both the
# fetch and beacon transports. A hand-passed key goes stale in silence,
# so requiring the current `pk-` shape refuses the stale one outright.
#
# Neither failure is visible from outside the artifact, so refuse the artifact.
ARG PUBLISHABLE_KEY
ENV NEXT_PUBLIC_PUBLISHABLE_KEY=$PUBLISHABLE_KEY
RUN case "$PUBLISHABLE_KEY" in \
pk-*) : ;; \
'') echo "PUBLISHABLE_KEY is empty - pass --build-arg PUBLISHABLE_KEY=<pk-...> (KMS deploy/PUBLISHABLE_KEY, env prod)" >&2; exit 1 ;; \
*) echo "PUBLISHABLE_KEY is not a publishable key (expected a pk- prefix)" >&2; exit 1 ;; \
esac
# 300+ prerendered pages; the default heap is not enough.
ENV NODE_OPTIONS=--max-old-space-size=8192
RUN corepack enable
COPY . .
# The lockfile is committed, so the build resolves exactly what was reviewed.
RUN pnpm install --frozen-lockfile
# Workspace packages the app imports must be built first: their package.json
# exports point at dist/. Only @hanzo/event qualifies -- .npmrc sets
# link-workspace-packages=true and the lockfile resolves it to link:../pkgs/event.
#
# There used to be a `cd pkgs/ui` line here. pkgs/ui was @hanzo/ui-shadcn, deleted
# in 5dbdb2943 when shadcn was consolidated to one home, and that commit did not
# touch this file -- so every build since has run `cd` into a directory that does
# not exist and died with exit code 2 before compiling anything. The app takes
# @hanzo/ui from the registry now (npm:@hanzo/ui-shadcn@^5), not the workspace.
RUN cd pkgs/event && pnpm build
# Builds the component registry, then the site (app/package.json build script).
#
# ...and then PROVES the key reached the client bundle. The gate above proves a
# key was PASSED; only this proves it was INLINED. Those are different failures:
# a rename on either side of `process.env.NEXT_PUBLIC_PUBLISHABLE_KEY` leaves the
# build-arg intact and the bundle keyless, and a static export cannot report that
# at runtime because there is no runtime.
#
# `&&`, never `;` — a `;` chain returns the LAST command's status, so a failed
# build followed by a passing grep exits 0 and the image is published.
RUN cd app && pnpm build && \
if [ -z "${NEXT_PUBLIC_PUBLISHABLE_KEY}" ]; then \
echo "ERROR: NEXT_PUBLIC_PUBLISHABLE_KEY is empty after a successful build." >&2; exit 1; \
elif grep -rqF "${NEXT_PUBLIC_PUBLISHABLE_KEY}" out; then \
echo "Build OK - ingest key inlined into app/out, verified"; \
else \
echo "ERROR: key supplied but NOT present in app/out - ui would ship unattributed" >&2; exit 1; \
fi
# 0.5.1 serves a directory's index.html in place. On 0.4.1 every page 301'd to
# an explicit /index.html, which leaks that filename into the address bar and
# into the URLs Next builds for its route prefetches.
FROM ghcr.io/hanzoai/static:0.5.2-amd64
COPY --from=builder /src/app/out /public
EXPOSE 3000
# No -spa: the export writes a real index.html per route (trailingSlash), so a
# missing path must 404 rather than silently render the home page.
ENTRYPOINT ["/static", "-port", "3000", "-root", "/public"]
+495 -26
View File
@@ -1,16 +1,313 @@
# Hanzo UI - LLM Context
# @hanzo/ui — LLM context
## Overview
**What this is.** The React component library for AI applications: 161+
components, 24+ blocks, two themes, and a single typed import surface, all on ONE
substrate (`@hanzo/gui`) so the same import runs on web, native and desktop.
Published as `@hanzo/ui` (v8) on npm. Docs at https://ui.hanzo.ai. Dev port: 3003.
React component library (shadcn/ui fork). 161 components, 24+ blocks, two themes, multi-framework. Published as `@hanzo/ui` on npm.
**Canonical role.** This is the canonical impl repo for Hanzo's web UI kit —
frontend components, not an SDK. It sits alongside the two SDK lines (full cloud
SDK generated from OpenAPI in `hanzo-<lang>/sdk` + wrapper in `hanzoai/<lang>-sdk`;
AI/agents lib `hanzo` in `hanzoai/python-sdk` flagship, `@hanzo/ai` in `hanzo-js/ai`).
`@hanzo/event` (telemetry, `POST /v1/event`) lives here in `pkgs/event`. DRY: one
impl, one place — link out, never duplicate.
**Docs**: https://ui.hanzo.ai | **Dev port**: 3003
**Brand rules (hard).**
- Never call Hanzo an "LLM gateway" or position it against LiteLLM — it is a full
AI SDK / AI cloud, not a proxy. Purge that framing on sight.
- Paths are `/v1/…` only — never an `/api/` prefix.
- Zen models are our own family — never name upstream models.
- Voice: "Hanzo — the Open AI Cloud." Developer-first, crisp, no emoji-spam.
**Install / run.**
```bash
pnpm add @hanzo/ui # consume
# dev:
pnpm install && pnpm build:registry && pnpm dev # registry MUST build before app
```
**Key entry points.** `pkg/ui/` (core lib + v8 subpaths: /product /data /canvas
/dashboard /usage /gitops) · `app/registry/{default,new-york}/`
(component SOURCE OF TRUTH) · `pkgs/*` (auto-published `@hanzo/*` packages) ·
`packages/shadcn/` (CLI) · `app/content/docs/` (MDX docs). Publish = bump a
package `version` + merge to main (`.github/workflows/publish.yml`).
**Spec / more context.** Canonical SDK + docs model: `~/work/hanzo/SDK-ARCHITECTURE.md`.
Detailed engineering notes (build order, import surface, telemetry, upstream sync,
gotchas) follow below.
---
## v8 — the canonical `@hanzo/ui` (`pkg/ui`)
`@hanzo/ui@8` (`pkg/ui`) is THE Hanzo component library, and there is ONE
substrate: every component renders through `@hanzo/gui` (Tamagui) primitives on
the `@hanzo/tokens` scale, so one import works on web, native (expo) and desktop
(Tauri). The Radix + Tailwind surface it used to ship alongside is gone — it
lives on as its own package, `@hanzo/shadcn`, and `@hanzo/ui` no longer depends
on it, on any `@radix-ui/*` package, on cva, cmdk or sonner.
`@hanzo/ui-shadcn` (`pkgs/ui`, v5.x) is the legacy standalone package —
superseded, being retired.
### Layout
```
pkg/ui/src/
core/ design core: cn.ts (clsx+tailwind-merge),
tokens.ts (re-export of @hanzo/tokens), fonts.ts (Geist vars)
root.tsx <Hanzo> — the root. Carries the gui config AND the stylesheet.
gallery.tsx EVERY component, once, in every variant. The one list.
theme.css SELF-CONTAINED token CSS vars + Geist Sans/Mono — the identity
backends/gui/ THE component surface on @hanzo/gui. index.ts is its manifest.
product/ the product/app layer (charts, PageHeader, ComboBox, …)
models/ the unified ModelSelector + catalog helpers
primitives/ GENERATED per-member entrypoints (scripts/gen-primitives.mjs)
index.ts root barrel = the component surface + cn
```
### Out of the box — the package carries its own config and its own CSS
```tsx
import { Hanzo, Button } from '@hanzo/ui'
<Hanzo><Button>Ship</Button></Hanzo>
```
That is the entire setup. No `gui.config.ts`, no CSS import, no generator script.
Three things used to be each app's job:
1. **The stylesheet.** gui compiles a style prop to an atomic class the first
time something RENDERS it, so the sheet does not exist until a render has
happened — which is why every app ran a `gen-gui-css.mjs` of its own. hanzo.app
never did: it shipped 103 `_bg-` classes and 26 `_dsp-` classes against a
stylesheet containing ZERO of either, every gui-styled element unstyled in
production, green build throughout. The render happens at OUR publish time now
(`scripts/gen-css.mjs` renders `src/gallery.tsx` in both themes and writes
`dist/styles.css` — 381 KB, 35 KB gzipped, 340 atomic selectors), and
`<Hanzo>` imports it. Styles gui generates at RUNTIME for props we could not
know at publish time still reach the document through `insertStyleRules`;
the shipped sheet is what makes the FIRST paint and every SSR/static render
correct.
2. **The config.** `<Hanzo>` passes `config` from `gui-config.ts` to
`GuiProvider` — as a VALUE, never a bare `import './gui-config'`. Vite 8
(rolldown) ignores package.json `sideEffects` ARRAYS outright: with any array
the registration is dropped and the first render dies on "Missing hanzogui
config"; only `sideEffects: true` keeps it, and that costs +63% bundle
(404 KB → 661 KB measured). Correctness does not live in bundler metadata.
3. **The theme.** gui throws `Missing theme.` for any component with no root
theme context, so a root is structurally required — there is no version of
this with no root at all. Forgetting `<Hanzo>` is therefore a hard crash on
first paint, never a silently unstyled page.
`theme.css` is dark-first at `:root` (it used to claim dark-first while shipping
LIGHT at `:root`, so an app that mounted the dark default and read `--background`
got white). `.light` retunes, and both answer to gui's own `.t_light`/`.t_dark`
that `<Hanzo>` stamps on the body — one theme, named the same by the CSS custom
properties and the component tokens.
### Three tests, one list of components
`src/gallery.tsx` is the specification of "what this package has to style", and
all three layers render THAT — a second copy of the list is how a component gets
styled by one and missed by another.
| Layer | Command | What it catches |
|---|---|---|
| `src/styles.test.tsx` | `pnpm test:unit` | every atomic class the gallery renders vs every class `dist/styles.css` defines a rule for. Not "the intersection is large" — TOTAL. Catches a stale sheet. |
| `src/backends/gui/render.test.tsx` | `pnpm test:unit` | the surface mounts under the real provider; a component that throws on first paint fails. |
| `test/consumer.spec.ts` | `pnpm test:consumer` | packs the tarball, installs it into a temp app OUTSIDE the repo (never a workspace link — that hides `files`/`exports`/`workspace:*` defects), builds, serves, and asserts COMPUTED styles + screenshots at 390 and 1280 in both themes. |
The consumer spec also fails on a solid-white border (the `@hanzo/design`
`border-card: var(--white…)` defect — borders are low-alpha hairlines) and on any
element that has a text child and a zero-height box.
### House rules for a component
- Style through gui props and theme tokens (`$background`, `$color12`,
`$borderColor`) — never a utility class string, never a hard-coded font.
- Touch targets meet the 44px floor via `hitSlop`, never via padding.
- Behaviour (focus, portalling, keyboard, a11y) comes from the matching
`@hanzogui/*` primitive; nothing reimplements it.
- Free-form text children go through `ink()`; `data-slot` markers through
`slot()`. One helper each, one place.
- Module scope stays side-effect free — `forwardRef`/`createContext` calls carry
`/* @__PURE__ */` and nothing assigns `displayName`, so importing one symbol
never drags a neighbour in. A one-symbol import bundles ~4.7KB against ~29KB
for the whole barrel.
- Utility classes carry `hz-`. This sheet used to claim `.row`, `.skeleton`,
`.fade`, `.mono`, `.drag` and `.tnum` at the document level, in a package an
app imports once at its root; an app with its own `.row` got no warning, it got
whichever rule the cascade preferred. The unprefixed selectors survive as
aliases on the same rules for one minor version and are **REMOVED IN 8.1.0**.
Nothing here emits them — `styles.test.tsx` scans every `className=` literal in
`src/` and fails on an unprefixed one. `glass`/`elevation-N` are the one family
still bare: they are an API VALUE (`glass(3).className`), not a typed literal,
so they move on their own change.
- A prop must actually arrive. gui is not the DOM, and two measured cases prove
it in both directions: **`name` is gui's OWN prop** (it names a styled component
and a theme) and is consumed before it reaches the element, so a `name` on a
field type-checks and renders nothing; and gui **drops `secureTextEntry` on
web**, which is why masking needs BOTH spellings via `masked()` in
`backends/gui/mask.ts` and why `<Input type="password">` rendered passwords in
PLAIN TEXT until 8.0.61 (the wrapper destructured `type` out and never
forwarded it, next to an eye offering to reveal what was already visible).
Render it and read the markup before you believe a prop works.
### Subpaths
| Subpath | What |
|---|---|
| `@hanzo/ui` | the component API: Button, Badge, Card*, Checkbox, Dialog*, DropdownMenu*, Input, Toaster, Avatar*, Tabs*, Select*, Tooltip*, Popover*, Command*, Collapsible*, Resizable*, ScrollArea, Slider, Switch, Progress, Separator, Label, Textarea, AspectRatio — + `cn` (the product layer is kept off root, at `/product`) |
| `@hanzo/ui/components` | alias of the root surface, for hosts that shim the package through a `declare module` |
| `@hanzo/ui/product` | the product/app layer: charts, metrics, PageHeader, StatusTag, EmptyState, ComboBox, SlideOver, Toast, Reorder, Field |
| `@hanzo/ui/models` | ModelSelector + fetchModelCatalog + catalog helpers |
| `@hanzo/ui/core` · `/tokens` | cn, Geist font vars, the @hanzo/tokens color/theme/radii/spacing scale |
| `@hanzo/ui/theme.css` | the design tokens alone (custom properties + Geist + touch/elevation) |
| `@hanzo/ui/styles.css` | the COMPLETE sheet — tokens + motion + the generated gui atomic/theme CSS. `<Hanzo>` imports it, so an app never has to |
| `@hanzo/ui/gallery` | every component, once — what the generator, the unit test and the consumer test all render |
| `@hanzo/ui/primitives/<Member>` | per-member entrypoints (for hosts that modularize `@hanzo/ui` imports) |
| `@hanzo/ui/data` | `@hanzo/data`: RecordsView, DataTable, typed field editors |
| `@hanzo/ui/{canvas,dashboard,usage,gitops}` | the optional-peer kits (each re-exports its home package) |
| `@hanzo/ui/product/*` · `/primitives/*` | deep imports — one module without its barrel |
| `@hanzo/ui/product/pure` | the product layer's RULES with none of the layer (below) |
| `@hanzo/ui/product/theme-toggle-next` | the `@hanzogui/next-theme` binding, off the barrel on purpose (below) |
| `@hanzo/ui/css` | `substitute()` — resolve a `var()` chain, for tests jsdom cannot answer (below) |
Everything ships COMPILED from `dist` — every `exports` target is a real file in
the tarball, including `theme.css` and all 90 `primitives/*` entrypoints.
`src/dist.test.ts` asserts that against the built output, wildcards included; a
subpath pointing at a file tsc never emitted is invisible from source.
### Three doors that exist because the barrel is not one
`@hanzo/ui/product` mounts the whole gui runtime to give you one component, and
for three kinds of caller that is not a cost — it is a wall.
**`@hanzo/ui/product/pure` — the rules, without the layer.** `pages()`,
`masked()`, `displayName()`, `tone()`, `orgScope`, `filterOptions`,
`resolveBrand` and the wordmark geometry. Every module it re-exports imports
NOTHING (`src/dist.test.ts` asserts the closure is empty) and is on
`postbuild.mjs`'s `DATA` list, so none is stamped `'use client'` — a stamped
module is a client REFERENCE on React's server layer, and calling `pages()`
through one in a server component throws instead of paging. It loads under a
bare `require()` with no transform and no DOM; the test proves it in a child
node process rather than under vitest, which has vite's transform already
installed and would prove nothing.
The components import these same modules, so there is one definition and not a
testable copy of a shipped one.
**`@hanzo/ui/product/theme-toggle-next` — Next, quarantined.**
`@hanzogui/next-theme`'s provider imports `next/script`, so the product barrel's
one `export { ThemeToggleNext }` line put Next in the graph of every Vite,
Express and Tauri host — the hosts this layer promises to run on. A barrel
re-export is a static edge no bundler can split. It is off the barrel; `<ThemeToggle />`
with no props still reaches it by dynamic import and degrades when next-theme is
absent, and `dist.test.ts` asserts BOTH — no static edge, and the dynamic one
still there, because a test that only asserted the absence would pass on a
deleted feature. `next` is now an OPTIONAL peer here: next-theme requires it and
nothing in this package admitted that.
**`@hanzo/ui/css``substitute(value, vars?)`.** jsdom does not resolve
`var()`; it hands a test the text verbatim. The theme rungs are
`var(--border, rgb(255 255 255 / .10))` on purpose (follow the live cascade
where design's sheet is mounted, keep the audited literal where it is not), so
every consumer trying to assert a border's contrast compared a colour to a
function call. With no `vars` map the answer is exact rather than approximate:
jsdom mounts no design sheet, so the fallback IS what a browser computes. It is
NOT part of `@hanzo/ui/core` — that subpath is ESM-only because @hanzo/design
publishes no `require` condition, and a jest consumer is the caller that needs
this. Importing nothing is what lets it ship both formats.
### One DropdownMenu
There is one `DropdownMenu`, with one API. It is the compound surface (Trigger,
Content, Item, CheckboxItem, RadioItem, Label, Separator, Shortcut, Group,
Portal, Sub*, RadioGroup) AND it accepts the declarative `trigger` + `items`
spec, which it renders through those very same parts. `@hanzo/ui` and
`@hanzo/ui/product` export the same component; there is no second shape.
### modularizeImports support
`scripts/gen-primitives.mjs` reads the gui backend barrel and emits one
`src/primitives/<Member>.tsx` per exported value (re-export from the backend).
This makes `@hanzo/ui/primitives/Button` etc. resolve, so a host whose
`next.config` rewrites `@hanzo/ui``@hanzo/ui/primitives/{{member}}` works
unchanged. Re-run `pnpm gen:primitives` after changing the surface.
### Build — plain `tsc`, one file in, one file out
There is no bundler. `pnpm build` is two `tsc` passes, `scripts/postbuild.mjs`,
then `scripts/gen-css.mjs` (which renders the gallery through vite's SSR pipeline
to harvest `config.getCSS()` into `dist/styles.css` — the slow step, ~2 min):
| Pass | Config | Emits |
|---|---|---|
| ESM + types | `tsconfig.build.json` | `dist/**/*.js`, `.d.ts`, `.js.map`, `.d.ts.map` |
| CJS | `tsconfig.cjs.json` (`--noCheck`) | `dist-cjs/**/*.js`, folded into `dist/**/*.cjs` |
`postbuild.mjs` does the only two things `tsc` will not: it resolves every
relative specifier to a fully-specified path (`./button``./button.js`,
`./x``./x/index.js`; `.cjs` on the CJS half) so Node ESM and strict bundlers
resolve, and it prepends `'use client'` to every emitted module — the whole
library is client-side @hanzo/gui UI and Next's flight-client loader wants the
directive first. It is prepended without a newline so source-map lines hold.
**`@hanzo/data` runs this same script**, with its package root and its own data
modules as arguments (`node ../ui/scripts/postbuild.mjs . theme,table/logic,…`).
It has the same two formats and the same two problems, and a second copy of the
file would be a second place to get the barrel rule wrong. That package used to
point its `exports` at `src/index.ts` and ship raw TSX — every consumer had to
transpile it, and `require('@hanzo/data')` could not load at all, which is one of
the two edges that broke `require('@hanzo/ui')`. It emits `dist/` now, both
formats, from 1.2.2.
The output is UNBUNDLED and mirrors `src/` one-for-one, so a consumer importing
one symbol pulls one module. A bundler here would be actively harmful: tsup's
code splitting emitted 11 shared `chunk-*.js`, and importing `Button` alone
dragged in `chunk-RCMDRI6V.js` (48K of source). Measured with esbuild, `import
{ Button } from '@hanzo/ui'` costs 4717 bytes bundled from tsup output vs 2021
from `tsc` output. Dropping `rollup-plugin-dts` (tsup's `dts` worker) is also
what makes the package build on TypeScript 7, which it cannot do otherwise.
Everything under `src/` is emitted, so every `exports` subpath resolves by
construction — no hand-maintained entry list to drift.
NOTE: `pkg/ui` (singular) sits OUTSIDE the `pkgs/*` pnpm workspace, installs
standalone, and publishes via the maintainer flow, not `publish.yml`. Because the
optional-peer kits (canvas/dashboard/gitops/usage) are not on the public
registry, a standalone install must skip auto-installing peers. `.npmrc` and
`pnpm-lock.yaml` are gitignored here, so create the `.npmrc` once:
```bash
cd pkg/ui
printf 'auto-install-peers=false\nstrict-peer-dependencies=false\n' > .npmrc
pnpm install --ignore-workspace # component-surface deps (radix, cmdk, sonner, …) + @hanzo/tokens
pnpm gen:primitives # refresh the per-member entrypoints
pnpm typecheck:ui # scoped typecheck of the component surface (green)
```
`@hanzo/tokens` (`pkgs/tokens`) must be built first (`pnpm --filter @hanzo/tokens build`)
so the `file:` link resolves. The scoped `typecheck:ui` excludes the optional-peer
subpaths, whose homes aren't installed standalone.
The **kits** = canvas, wallet, network, billing, dashboard, usage, gitops, data.
Add one by mirroring `src/gitops.ts` (a one-line `export *`) + a `./name` export
+ an optional peer/devDep. `pkg/*` is a pnpm workspace member (for `workspace:*`
dev links), but `pkg/ui` publishes via the maintainer flow, not `publish.yml`
(which auto-publishes only `pkgs/*` on a version bump — see PUBLISH_GUIDE.md). The
shared shell lives here too: `AppHeader` + `BrandMark` (@hanzo/logo) +
`OrgSwitcher` + `orgScope` (the console org-scope contract, hoisted per #36). Lux
surfaces use `@luxfi/web3` for wallet/login; `@hanzo/ui/wallet`+`/network` are the
Hanzo-branded equivalents.
## Repository Structure
```
ui/
app/ Documentation site (Next.js 15.3.1, React 19)
app/ Hanzo documentation site (Next.js 15.3.1, React 19)
registry/ Component registry (SOURCE OF TRUTH)
default/ui/ 150+ components
default/example/ Usage demos
@@ -18,13 +315,25 @@ ui/
new-york/ Alternative theme
content/docs/ MDX documentation
scripts/ Build scripts
apps/
v4/ Upstream shadcn v4 docs/registry app (port 4000)
packages/
shadcn/ shadcn CLI v4.1.0 (font system, chart colors, scaffold)
tests/ Integration tests for shadcn CLI
og/ OG image generation (Hanzo-only)
pkg/
ui/ Core library (npm)
auth/ Auth components (Firebase optional since v2.6.0)
auth-firebase/ Firebase auth (opt-in package)
commerce/ E-commerce components
react/ React primitives
brand/ Branding system
brands/ White-label configs (Zoo, Lux)
commerce/ E-commerce components
checkout/ Checkout flow
shop/ Shop components
agent-ui/ AI agent UI components
tokens/ Design tokens
skills/
shadcn/ AI skill definitions for shadcn CLI
templates/ Project templates (next, vite, astro, react-router, start + monorepo variants)
template/next/ Hanzo-customized Next.js template
```
## Critical: Build Order
@@ -46,14 +355,173 @@ pnpm lint # Lint all workspaces
pnpm typecheck # Type checking
pnpm test # Unit tests
pnpm test:e2e # Playwright E2E
pnpm changeset # Create changeset for publishing
```
## How this ships
One way, and it runs on our own stack:
push -> github.com/hanzoai/ui (a mirror)
.github/workflows/sync.yml carries refs onward
-> git.hanzo.ai/hanzoai/ui CANONICAL
.hanzo/workflows/ci.yml lint, typecheck, build, test
.hanzo/workflows/registries.yml the v4 registry check
.hanzo/workflows/publish.yml publishes every pkgs/* package
.hanzo/workflows/deploy.yml builds ghcr.io/hanzoai/ui
-> hanzoai/universe crs/ui.yaml names the tag that is live
-> hanzoai/operator reconciles the App
-> hanzoai/static behind hanzoai/ingress serves ui.hanzo.ai
**git.hanzo.ai is canonical; GitHub is a mirror.** `.github/workflows/` holds
exactly one file, `sync.yml`, and its only job is getting refs to the forge. Every
build, check, publish and deploy is a workflow under `.hanzo/workflows/`, which the
forge reads. `.hanzo/workflows` uses GitHub Actions syntax, so a workflow moves
between the two by changing directory and nothing else.
No Vercel. `ci.yml` used to end in a `deploy-preview` job that ran `vercel deploy`
on every PR; previews come from our own stack or not at all, so that job is gone.
Its `status` job never depended on it.
## Publishing
One way: bump a package's `version` in its `package.json` and merge to `main`.
`.hanzo/workflows/publish.yml` detects the changed `@hanzo/*` package and publishes
it to npm (needs `NPM_TOKEN` as a forge secret). No changesets, no version-PR bot
— the semver bump is the trigger.
It is the SOLE publisher of every non-private `@hanzo/*` in `pkgs/*`, and it
mirrors the same tarball to `api.hanzo.ai/v1/packages/hanzo/npm` when
`HANZO_REGISTRY_TOKEN` is present. That mirror is best-effort by construction: no
token means a notice, not a failure, and npmjs stays authoritative either way.
## Deploying the site
`app/` is a Next.js static export (`output: "export"`, `trailingSlash: true`);
`pnpm build` there writes `app/out`, which `Dockerfile` copies into
`ghcr.io/hanzoai/static`. ui.hanzo.ai has been served that way since 2026-07-25 —
no Cloudflare, no GitHub Pages.
What was missing until now is the build. `crs/ui.yaml` is live and promoted, but no
workflow ever produced the image it pins: every tag up to `v5.7.6` was pushed by
hand. `.hanzo/workflows/deploy.yml` is that step. It publishes
`ghcr.io/hanzoai/ui:<sha>` and stops there — a build never deploys itself. A human
sets `spec.image.tag` in `hanzoai/universe`
`infra/k8s/operator/crs/ui.yaml`, which is the one live thing that says which build
serves.
Coverage lives in `ci.yml`'s `test` job, which runs `pnpm test:coverage` so the
lcov it uploads to Codecov actually exists. A separate `coverage.yml` used to run
the same suite a second time for the same upload plus a PR comment through the
GitHub API; one workflow does it now.
`registries.yml` keeps the `apps/v4` registry honest: reserved namespaces are
rejected and `pnpm --filter=v4 validate:registries` must pass. Its other job
labelled and commented on pull requests with the `gh` CLI against the GitHub API,
which does not exist on the forge, so that job did not come along.
## Telemetry — `@hanzo/event` is the ONE client (`pkgs/event`)
`@hanzo/event` is the single canonical telemetry client for every Hanzo surface.
ONE API surface over **TWO** planes — the client never sends the org; the server
resolves the tenant.
1. **Event stream** — pageview/event/identify/group (and an error breadcrumb),
batched to `POST {host}/v1/event` with `{ batch: [Event, …] }`
`-> { accepted, dropped }`. Tenant from the session or a publishable `pk_` key.
2. **Error plane** — every captured exception is ALSO framed as a real **Sentry
envelope** and POSTed to `POST {dsn.origin}/v1/sentry/{projectId}/envelope/?sentry_key=…`.
This is the ONLY thing that reaches the Sentry error dashboard.
> **There is NO server-side fan-out from `/v1/event` into Sentry.** Versions
> ≤ 0.3.1 claimed there was ("lensed server-side into … error tracking"). There
> is not: cloud's handler folds the exception into `properties.$exception`,
> writes one row to the event warehouse (readable via `GET /v1/errors`), and
> stops. Because every property believed that claim, the whole fleet reported
> **zero** errors to Sentry until 0.3.2 added the envelope. Do not re-collapse
> these planes.
Configure the error plane with `dsn` (or `NEXT_PUBLIC_HANZO_EVENT_DSN`), minted
per property via `POST /v1/sentry/projects`. The DSN key is publishable and
write-only — safe in a bundle, same trust class as `pk_`. **No DSN => the error
plane is inert** (fail-safe: nothing sent, nothing thrown, event stream
unaffected); assert `client.errorPlaneEnabled` if you need to know.
There is NO third plane. Web analytics used to be one — `analytics.hanzo.ai/hz.js`
posting a bare JSON array of `{site, ts, type, …}` to a second collector behind an
identical path spelling — and 0.3.7 deleted both. `hz.js` now lives in this package
as the script-tag DISTRIBUTION of this client: same `WireEvent`, same
`{ batch: [ … ] }`, same `POST {host}/v1/event`. Point everything at the API host.
It could not authenticate until 0.3.12: it sent no `Authorization` and no
`?ingest_key=`, so a keyed static surface's writes were unattributed, the door
refused them (`401`), and nothing in the page said so. `data-ingest-key="pk-…"`.
Entries: `.` (framework-agnostic: `createAnalytics`, `EVENTS`, `GOALS`,
attribution + DSN/scrub helpers) and `./react` (`AnalyticsProvider`,
`useAnalytics`, `usePageview`, `ErrorBoundary`). Auto error capture
(window.onerror / unhandledrejection / React boundary) makes it the drop-in
error-tracking replacement. Secrets and PII are scrubbed client-side before an
error leaves the device. SSR-safe, fail-soft, beacon-on-unload.
Build is a tsup dual bundle: **CJS → `.cjs`, ESM → `.mjs`** (required under
`"type": "module"` — a CJS `.js` is parsed as ESM and crashes `require()` with
"exports is not defined"). Each `exports` condition carries its own types.
### Interaction analytics — `<Hanzo analytics>` is the one wiring
An app instruments nothing. `<Hanzo analytics>` (`pkg/ui/src/root.tsx`) is the
whole setup, and every click / change / submit / route change inside the tree
reaches `POST /v1/event` named by the component it happened on:
```tsx
<Hanzo analytics={{ product: 'console', ingestKey: process.env.NEXT_PUBLIC_PUBLISHABLE_KEY }}>
```
Four packages, one of each concern, no duplication:
| Concern | Where | Note |
|---|---|---|
| client + wire | `@hanzo/event` (`pkgs/event`) | one endpoint, one key |
| capture engine | `@hanzo/observe` (`pkgs/observe`) | delegated listeners, semantic annotation, redaction |
| provider + consent | `@hanzogui/telemetry` (`~/work/hanzo/gui`) | `<TelemetryProvider/>`; owns DNT/GPC + stored choice |
| curated events | `@hanzo/ui/product` `instrument.ts` | `emit({component, action})` — what autocapture cannot know |
**`analytics` is a prop, not a default.** Mounting a component library must not
start a network conversation the app did not ask for. Off, no provider renders.
**Component names are real in production.** Every primitive already carries a
`data-slot` (via `slot()`); `componentName()` in `pkgs/observe/src/annotate.ts`
reads it, ranked ABOVE the React fiber owner deliberately — the fiber name is
dev-only, so grouping on it silently empties the dashboard at deploy. Labels keep
the qualifier: `card/button[Save]`.
**It cannot double-count.** The engine installs *delegated* listeners on a root,
so two engines on one root report everything twice — which is what an app got by
mounting a library provider AND `<ObserveProvider/>`, both correct instructions.
Since observe 0.1.7 the first engine claims its root under a `Symbol.for`
registry (page-wide, so duplicate copies of the package still see each other) and
any later one stays inert (`engine.capturing === false`). Verified in Chromium,
not only in jsdom.
**Consent is decided in ONE layer.** The engine takes `enabled` as a value; the
provider resolves policy (GPC, DNT, stored `hz_consent`, build kill switch) and
passes the answer down. Do not add a second, partial copy to the engine — the two
then disagree about an explicit opt-in.
### One way — supersessions (no divergent telemetry client)
| Package | Status | Note |
|---|---|---|
| `@hanzo/event` | **canonical** | `pkgs/event`, posts `/v1/event` only |
| `@hanzo/capture` (npm) | **deprecated → `@hanzo/event`** | the old name of this package; `@hanzo/event` is a superset |
| `pkgs/capture` (`@hanzo/analytics@0.1.0` dup) | **deleted** | stale in-repo duplicate, removed |
| `hanzoai/analytics` `packages/event` (`@hanzo/event@0.2.0`) | **deleted** | An unpublished FORK of this package in another repo. It was the only copy that could actually reach Sentry, while the published one here could not — the fleet's error telemetry died in that gap. Its envelope + scrub implementation was merged here in 0.3.2. Never fork this package again; it publishes from `pkgs/event` only. |
## Three-Layer Architecture
1. **Components** (`registry/{style}/ui/`) -- Single primitives (Button, Card, Dialog). CLI-installable.
2. **Examples** (`registry/{style}/example/`) -- Usage demos for docs via `<ComponentPreview />`.
3. **Blocks** (`registry/{style}/blocks/`) -- Full-page sections (Dashboard, Login). NOT CLI-installable, docs only.
1. **Components** (`registry/{style}/ui/`) — single primitives (Button, Card, Dialog). CLI-installable.
2. **Examples** (`registry/{style}/example/`) — usage demos for docs via `<ComponentPreview />`.
3. **Blocks** (`registry/{style}/blocks/`) — full-page sections (Dashboard, Login). NOT CLI-installable, docs only.
## Import Path Transformation
@@ -78,28 +546,29 @@ import { cn } from '@hanzo/ui/lib/utils'
## Tech Stack
React 18.3.1 (19 experimental), Next.js 15.3.1, Tailwind CSS (OKLCH colors), Radix UI, Turborepo + pnpm, Fumadocs (MDX), class-variance-authority.
React 19, Next.js 15.3+, Tailwind CSS 4 (OKLCH colors), Radix UI, Turborepo + pnpm, Fumadocs (MDX), class-variance-authority.
## Upstream Sync
Remote `shadcn` points to a local clone of shadcn-ui/ui.
hanzoai/ui is NOT a GitHub fork — no shared object store, so large merges can fail on push.
Strategy: file-level checkout from shadcn/main for specific directories (not git merge).
- Take theirs: packages/shadcn/, packages/tests/, apps/, templates/, scripts/, skills/
- Keep ours: app/, pkg/, demo/, docs/, template/next/, pnpm-workspace.yaml, package.json
- Regenerate: pnpm-lock.yaml after sync
## Key Features
- **Page Builder** (`/builder`): Drag-drop block assembly with @dnd-kit, export to TSX
- **Page Builder** (`/builder`): drag-drop block assembly with @dnd-kit, export to TSX
- **White-Label**: Zoo/Lux forks via `brands/{BRAND}.brand.ts`
- **External Registries**: 35+ sources in `app/registries.json`, install via `npx @hanzo/ui add @aceternity/spotlight`
## Gotchas
- Registry index is `Index[style][name]`, NOT `Index[name]` -- caused silent block render failures
- Shiki `getHighlighter` incompatible with static export -- replaced with basic pre/code
- Registry index is `Index[style][name]`, NOT `Index[name]` caused silent block render failures
- Shiki `getHighlighter` incompatible with static export replaced with basic pre/code
- Some blocks (login-01, login-02, sidebar-02) have Server Component issues with event handlers
- Zod validation removed from `_getAllBlocks()`/`_getBlockCode()` -- we control generation
- Firebase split to optional `@hanzo/auth-firebase` package (Jan 2025)
- `@hanzo/auth` v2.6.0 uses pluggable provider registry: `registerAuthProvider('firebase', FirebaseAuthService)`
## Component Stats (2025-10-18)
- 161 total files, ~127 implemented, ~34 stubs
- Unique: 9 3D components, 12 AI components, 13 animation components, 15 nav variants
- 3x more components than upstream shadcn/ui (161 vs 58)
- `@hanzo/auth` v2.6.0 uses a pluggable provider registry: `registerAuthProvider('firebase', FirebaseAuthService)`
## Rules
+6
View File
@@ -0,0 +1,6 @@
ui
Copyright (c) 2023 Hanzo AI, Inc.
This product includes software from shadcn/ui (https://github.com/shadcn-ui/ui), licensed under MIT:
Copyright (c) 2023 shadcn
+24 -159
View File
@@ -1,174 +1,39 @@
# NPM Publishing Guide - React 19 Packages
# Publishing
## Current Package Versions
Two lanes, one trigger each. No changesets, no version-PR bot.
All packages updated to support **React 19.2.0**:
## 1. `pkgs/*` — auto-publish on version bump (`publish.yml`)
- `@hanzo/ui` - v5.1.1
- `@hanzo/auth` - Latest
- `@hanzo/commerce` - Latest
- `@hanzo/brand` - Latest
- `@hanzo/react` - v1.0.0
Bump a package's `version` in `pkgs/<name>/package.json` and merge to `main`.
`.github/workflows/publish.yml` detects the changed public `@hanzo/*` package,
builds it, and publishes to npm (repo secret `NPM_TOKEN`). Patch bumps only
(`x.y.z``x.y.z+1`).
## Publishing Methods
## 2. `pkg/ui` — `@hanzo/ui@8`, the v8 lane (maintainer flow)
### 1. Automatic Publishing (Tag-based)
When you push a git tag starting with `v` (typically matching @hanzo/ui version), the workflow automatically checks all packages and publishes any with new versions:
`pkg/ui` (with `pkg/data`) is the modern cross-platform library on `@hanzo/gui`.
It publishes from the package directory (`prepack` builds the `types/`):
```bash
# Tag with @hanzo/ui version (workflow checks all packages)
git tag v5.1.1
git push origin v5.1.1
```
**What happens:**
1. Tests run (pkg/ui and pkg/react)
2. All 5 packages build
3. **Automatic version detection:**
- Checks each package's current version in package.json
- Queries npm to see if that version already exists
- Only publishes packages with new versions not on npm
4. GitHub release created (only if packages were published)
**Example workflow output:**
```
📦 Checking @hanzo/ui@5.1.1
⏭️ Already published - skipping
📦 Checking @hanzo/auth@2.5.5
🚀 Publishing to npm...
✅ Successfully published @hanzo/auth@2.5.5
📊 Publishing Summary
✅ Published: 1 package(s)
⏭️ Skipped: 4 package(s)
```
This approach means you:
- Only need to tag once (with @hanzo/ui version)
- Don't need to track which packages need publishing
- Can bump any package version and it auto-publishes on next tag
- Similar to python-sdk monorepo publishing
**Workflow:** `.github/workflows/publish-on-tag.yml`
### 2. Manual Publishing (Workflow Dispatch)
Use GitHub Actions UI to manually publish specific packages:
1. Go to **Actions****NPM Publish**
2. Click **Run workflow**
3. Select package: `ui`, `auth`, `commerce`, `brand`, `react`, or `all`
4. Select version bump: `patch`, `minor`, or `major`
5. Click **Run workflow**
**What happens:**
- Selected package(s) build
- Version bumped automatically
- Package(s) published to npm
- PR created with version bump
**Workflow:** `.github/workflows/npm-publish.yml`
### 3. Local Publishing (Manual)
For quick patches or testing:
```bash
# Build and test
cd pkg/ui
pnpm build
pnpm test
# Bump version
npm version patch # or minor/major
# Publish
pnpm typecheck && pnpm test && pnpm build
# bump "version" in package.json (patch), commit to main, then:
npm publish --access public
```
`@hanzo/ui-shadcn` (`pkgs/ui`) is the legacy v5 kit — existing consumers pin
`@hanzo/ui-shadcn@^5`; it rides lane 1 like any other `pkgs/*` package.
> The old tag-driven flow (`publish-on-tag.yml`, `npm-publish.yml`, the
> `pkg/commerce|brand|react` paths) is gone — do not tag to publish here.
## Prerequisites
### NPM Authentication Token
- `NPM_TOKEN` repo secret (lane 1) / npm auth as a maintainer (lane 2)
- Every package carries `"publishConfig": { "access": "public" }`
The GitHub secret `NPM_AUTH_TOKEN` must be set:
## Checklist
1. Generate token at https://www.npmjs.com/settings/tokens
2. Add to GitHub: Settings → Secrets → Actions → `NPM_AUTH_TOKEN`
### Package Publish Configuration
All packages already configured with:
```json
{
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
}
}
```
## React 19 Compatibility
### Key Updates Made
1. **Type Declarations**: Added `hanzo-ui.d.ts` files for React 19 compatibility
2. **Peer Dependencies**: Force React 19.2.0 via pnpm overrides
3. **Test Environment**: Switched to happy-dom for better React 19 support
4. **Build Configuration**: All packages build successfully with React 19
### Test Status
- **pkg/ui**: 206/207 tests passing (99.5%)
- **pkg/react**: 10/10 tests passing (100%)
## Publishing Checklist
Before publishing:
- [ ] All packages build successfully: `pnpm build`
- [ ] Tests pass: `pnpm test`
- [ ] Types check: `cd app && pnpm typecheck`
- [ ] Lint passes: `cd app && pnpm lint`
- [ ] Update CHANGELOG.md
- [ ] Update version in package.json (if manual)
- [ ] Commit changes
## Troubleshooting
### Build Failures
```bash
# Clean install
rm -rf node_modules pnpm-lock.yaml
pnpm install
# Rebuild
pnpm build
```
### Test Failures
```bash
# Run specific package tests
cd pkg/ui && pnpm test
cd pkg/react && pnpm test
```
### Publish Failures
- Check NPM_AUTH_TOKEN is valid
- Ensure version is unique (not already published)
- Verify package builds: `cd pkg/<name> && pnpm build`
## Package URLs
- npm: https://www.npmjs.com/org/hanzo
- GitHub: https://github.com/hanzoai/ui
- Docs: https://ui.hanzo.ai
---
**Last Updated:** 2025-10-05
**React Version:** 19.2.0
- [ ] `pnpm typecheck` + `pnpm test` green in the package
- [ ] Patch version bump (check the last published patch first)
- [ ] Commit to `main`; lane 1 publishes on merge, lane 2 via `npm publish`
+93 -28
View File
@@ -1,35 +1,42 @@
<p align="center"><img src=".github/hero.svg" alt="@hanzo/ui" width="880"></p>
# @hanzo/ui
Accessible and customizable components for React, Vue, Svelte, and React Native. **Built on shadcn/ui with multi-framework support, 3D components, AI components, and advanced features.**
**The React component library for AI applications.** Accessible, customizable primitives for React, Vue, Svelte, and React Native — built on shadcn/ui, extended with AI, 3D, animation, and commerce components, and a single typed import surface.
<p align="center">
<a href="https://www.npmjs.com/package/@hanzo/ui"><img src="https://img.shields.io/npm/v/@hanzo/ui?color=black&label=%40hanzo%2Fui" alt="npm"></a>
<a href="./LICENSE.md"><img src="https://img.shields.io/badge/license-MIT-black" alt="MIT"></a>
<a href="https://ui.hanzo.ai"><img src="https://img.shields.io/badge/docs-ui.hanzo.ai-black" alt="docs"></a>
</p>
![hero](app/public/og.jpg)
## Features
- **161+ Components** - 3x more than shadcn/ui
- **Multi-Framework** - React, Vue, Svelte, React Native
- **Two Themes** - Default & New York variants
- **AI Components** - Chat, assistants, playground
- **3D Components** - Interactive 3D elements
- **Animations** - Advanced motion components
- **Page Builder** - Visual drag-drop interface
- **White-Label** - Fork and rebrand easily
- **Blocks** - 24+ production-ready templates
- **Accessible** - Built with Radix UI primitives
- **Customizable** - Tailwind CSS powered
- **TypeScript** - Fully typed
- **161+ components** 3x the surface of upstream shadcn/ui
- **Multi-framework** React, Vue, Svelte, React Native
- **Two themes** Default & New York variants
- **AI components** — chat, assistants, agent UI, playground
- **3D components** — interactive 3D elements
- **Animations** — advanced motion components
- **Page builder** — visual drag-and-drop assembly, export to TSX
- **Blocks** — 24+ production-ready full-page templates
- **White-label** — fork and rebrand by domain (Zoo, Lux, …)
- **Accessible** — built on Radix UI primitives
- **Customizable** Tailwind CSS 4 (OKLCH), fully typed TypeScript
## Quick Start
## Quick start
### Installation
### Install
```bash
npm install @hanzo/ui
# or
pnpm add @hanzo/ui
# or
npm install @hanzo/ui
```
### Usage
### Use
```tsx
import { Button, Card, Input } from '@hanzo/ui'
@@ -51,23 +58,59 @@ export function App() {
}
```
## Documentation
## One import surface (v8)
Visit **[ui.hanzo.ai](https://ui.hanzo.ai)** for full docs.
`@hanzo/ui@8` is the single entry point for the whole kit. Each capability is a
thin subpath that re-exports its home package — code lives once, and each home is
an optional peer, pulled only when you use its subpath.
| Import | What you get |
|---|---|
| `@hanzo/ui` · `/product` | charts, metrics, PageHeader, StatusTag, EmptyState, ComboBox, SlideOver, Toast |
| `@hanzo/ui/data` | RecordsView, DataTable, typed field editors |
| `@hanzo/ui/canvas` | ProjectCanvas, ServiceNode, DeployTimeline, EnvSwitcher |
| `@hanzo/ui/dashboard` | landing + deploy-pipeline + overview kit |
| `@hanzo/ui/usage` | UsageMeter, UsageProviderCard, UsageDashboard |
| `@hanzo/ui/gitops` | GitopsAppList, tree, diff, sync/rollback, HealthBadge |
Also available as granular imports:
```ts
import { Button, Card } from '@hanzo/ui/components'
import * as Dialog from '@hanzo/ui/primitives/dialog'
import { cn } from '@hanzo/ui/lib/utils'
```
## CLI
Add components straight into your project — the CLI copies source you own:
```bash
npx @hanzo/ui add button
npx @hanzo/ui add card dialog
```
Install from 35+ external registries too:
```bash
npx @hanzo/ui add @aceternity/spotlight
```
## Packages
- `@hanzo/ui` - Main UI library (161 components)
- `@hanzo/auth` - Authentication components
- `@hanzo/commerce` - E-commerce components
- `@hanzo/brand` - Branding system
The workspace publishes a family of scoped packages under `@hanzo/*`:
| Package | Purpose |
|---|---|
| `@hanzo/ui` | Core library + the v8 import surface (161+ components) |
| `@hanzo/react` | React primitives |
| `@hanzo/data` | Records, data tables, typed field editors |
| `@hanzo/canvas` | Service/deploy canvas components |
| `@hanzo/dashboard` | Dashboard + deploy-pipeline kit |
| `@hanzo/commerce` · `@hanzo/checkout` · `@hanzo/shop` | Commerce components |
| `@hanzo/agent-ui` | AI agent UI components |
| `@hanzo/brand` · `@hanzo/tokens` | Branding system & design tokens |
| `@hanzo/event` | Telemetry client (`POST /v1/event`) |
## Development
@@ -75,17 +118,39 @@ npx @hanzo/ui add card dialog
git clone https://github.com/hanzoai/ui.git
cd ui
pnpm install
pnpm dev
pnpm build:registry # generate the component registry FIRST
pnpm dev # docs site + registry (http://localhost:3003)
```
> The registry generates the JSON the CLI reads, so `build:registry` must run
> before `build`. Keep the Default and New York themes in sync when adding
> components. Use pnpm — not npm or yarn.
```bash
pnpm build # build the docs app
pnpm lint # lint all workspaces
pnpm typecheck # type check
pnpm test # unit tests
pnpm test:e2e # Playwright E2E
```
## Documentation
Full docs, live previews, and the component catalog: **[ui.hanzo.ai](https://ui.hanzo.ai)**.
## Contributing
Please read the [contributing guide](/CONTRIBUTING.md).
See the [contributing guide](/CONTRIBUTING.md).
## License
MIT - See [LICENSE.md](./LICENSE.md) for details.
MIT — see [LICENSE.md](./LICENSE.md).
---
Built by [Hanzo](https://hanzo.ai)
## Hanzo — the Open AI Cloud
Open source · every language · on-chain settlement. [hanzo.ai](https://hanzo.ai) · [docs.hanzo.ai](https://docs.hanzo.ai)
**SDKs in every language** — [Python](https://github.com/hanzoai/python-sdk) (flagship) · [TypeScript](https://github.com/hanzo-js/sdk) · [Go](https://github.com/hanzo-go/sdk) · [Rust](https://github.com/hanzo-rs/sdk) · [C++](https://github.com/hanzo-cpp/sdk) · [Swift](https://github.com/hanzo-swift/sdk) · [Kotlin](https://github.com/hanzo-kt/sdk) · [umbrella](https://github.com/hanzoai/sdk)
+1 -1
View File
@@ -6,4 +6,4 @@ We will investigate all legitimate reports and do our best to quickly fix the pr
Our preference is that you make use of GitHub's private vulnerability reporting feature to disclose potential security vulnerabilities in our Open Source Software.
To do this, please visit the security tab of the repository and click the "Report a vulnerability" button.
To do this, please visit the security tab of the repository and click the [Report a vulnerability](https://github.com/shadcn-ui/ui/security/advisories/new) button.
-12
View File
@@ -1,12 +0,0 @@
node_modules/
.next/
out/
build/
dist/
next-env.d.ts
__registry__/
.source/
*.config.js
*.config.mjs
.turbo/
coverage/
-40
View File
@@ -1,40 +0,0 @@
{
"env": {
"browser": true,
"es2021": true,
"node": true
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"plugins": ["@typescript-eslint", "react"],
"ignorePatterns": [
"__registry__/**",
".source/**",
".next/**",
"out/**",
"build/**",
"dist/**",
".turbo/**",
"node_modules/**"
],
"rules": {
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-empty-object-type": "warn",
"@typescript-eslint/ban-ts-comment": "warn",
"@typescript-eslint/no-unused-vars": "warn",
"@typescript-eslint/no-require-imports": "warn",
"@typescript-eslint/no-this-alias": "warn",
"@typescript-eslint/triple-slash-reference": "warn",
"@typescript-eslint/ban-types": "off",
"react/no-unescaped-entities": "warn",
"react/jsx-no-comment-textnodes": "warn",
"react/no-find-dom-node": "warn",
"prefer-const": "warn"
}
}
+3 -3
View File
@@ -94,7 +94,7 @@ forge script script/DeployIdentitySystem.s.sol:DeployIdentitySystem \
# For Lux Mainnet
forge script script/DeployIdentitySystem.s.sol:DeployIdentitySystem \
--rpc-url https://api.lux.network/ext/bc/C/rpc \
--rpc-url https://api.lux.network/v1/bc/C/rpc \
--broadcast
# For Zoo Mainnet
@@ -131,8 +131,8 @@ Visit `http://localhost:3333/identity` to access the identity registration page.
|---------|----------|---------|----------|
| Hanzo Mainnet | 36963 | https://rpc.hanzo.ai | https://explorer.hanzo.ai |
| Hanzo Testnet | 36962 | https://testnet-rpc.hanzo.ai | https://testnet-explorer.hanzo.ai |
| Lux Mainnet | 96369 | https://api.lux.network/ext/bc/C/rpc | https://explorer.lux.network |
| Lux Testnet | 96368 | https://testnet-api.lux.network/ext/bc/C/rpc | https://testnet-explorer.lux.network |
| Lux Mainnet | 96369 | https://api.lux.network/v1/bc/C/rpc | https://explorer.lux.network |
| Lux Testnet | 96368 | https://testnet-api.lux.network/v1/bc/C/rpc | https://testnet-explorer.lux.network |
| Zoo Mainnet | 200200 | https://rpc.zoo.network | https://explorer.zoo.network |
| Zoo Testnet | 200201 | https://testnet-rpc.zoo.network | https://testnet-explorer.zoo.network |
+2 -2
View File
@@ -42,8 +42,8 @@ describe("@hanzo/ui namespace imports - Type Resolution", () => {
* import { GridPattern } from '@hanzo/ui/pattern/grid'
* ```
*
* The package exports are properly configured in pkg/ui/package.json.
* Build artifacts exist in pkg/ui/dist/{code,3d,pattern}/.
* The package exports are properly configured in pkgs/ui/package.json.
* Build artifacts exist in pkgs/ui/dist/{code,3d,pattern}/.
*
* Vitest runtime resolution fails due to internal cross-package imports
* (e.g., CodeBlock imports from @hanzo/ui/lib/utils), which require
File diff suppressed because it is too large Load Diff
-427
View File
@@ -1,427 +0,0 @@
"use client"
import * as React from "react"
import dynamic from "next/dynamic"
import { Index } from "@/__registry__"
import {
closestCenter,
DndContext,
DragEndEvent,
DragOverlay,
DragStartEvent,
} from "@dnd-kit/core"
import {
arrayMove,
SortableContext,
useSortable,
verticalListSortingStrategy,
} from "@dnd-kit/sortable"
import { CSS } from "@dnd-kit/utilities"
import { Download, Eye, GripVertical, Plus, Trash2 } from "lucide-react"
import { Button } from "@/registry/new-york/ui/button"
import { Card } from "@/registry/new-york/ui/card"
import { Input } from "@/registry/new-york/ui/input"
import { ScrollArea } from "@/registry/new-york/ui/scroll-area"
import { Separator } from "@/registry/new-york/ui/separator"
// Dynamic block component loader
const DynamicBlock = ({ blockName, scale = 1 }: { blockName: string; scale?: number }) => {
const [BlockComponent, setBlockComponent] = React.useState<React.ComponentType | null>(null)
const [error, setError] = React.useState(false)
React.useEffect(() => {
import(`@/registry/default/block/${blockName}`)
.then((mod) => {
setBlockComponent(() => mod.default)
setError(false)
})
.catch((err) => {
console.error(`Failed to load block ${blockName}:`, err)
setError(true)
})
}, [blockName])
if (error) {
return (
<div className="flex h-full items-center justify-center bg-muted/50 p-4 text-center">
<p className="text-xs text-muted-foreground">Failed to load {blockName}</p>
</div>
)
}
if (!BlockComponent) {
return (
<div className="flex h-full items-center justify-center bg-muted/50">
<div className="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent" />
</div>
)
}
return (
<div style={{ transform: `scale(${scale})`, transformOrigin: "top left" }}>
<BlockComponent />
</div>
)
}
interface PageBlock {
id: string
blockName: string
}
export default function PageBuilder() {
const [blocks, setBlocks] = React.useState<string[]>([])
const [availableBlocks, setAvailableBlocks] = React.useState<string[]>([])
const [pageBlocks, setPageBlocks] = React.useState<PageBlock[]>([])
const [activeId, setActiveId] = React.useState<string | null>(null)
const [filter, setFilter] = React.useState("")
const [viewport, setViewport] = React.useState<"desktop" | "tablet" | "mobile">("desktop")
React.useEffect(() => {
// Get block IDs from the registry index
const blockIds = Object.keys(Index.default || {}).filter((key) => {
const item = Index.default[key]
return item?.type === "components:block"
})
setBlocks(blockIds)
setAvailableBlocks(blockIds)
}, [])
const filteredBlocks = availableBlocks.filter((block) =>
block.toLowerCase().includes(filter.toLowerCase())
)
const addBlock = (blockName: string) => {
setPageBlocks([...pageBlocks, { id: crypto.randomUUID(), blockName }])
}
const removeBlock = (id: string) => {
setPageBlocks(pageBlocks.filter((b) => b.id !== id))
}
const handleDragStart = (event: DragStartEvent) => {
setActiveId(event.active.id as string)
}
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event
setActiveId(null)
if (!over || active.id === over.id) return
setPageBlocks((items) => {
const oldIndex = items.findIndex((item) => item.id === active.id)
const newIndex = items.findIndex((item) => item.id === over.id)
return arrayMove(items, oldIndex, newIndex)
})
}
const generatePageCode = () => {
const imports = pageBlocks
.map((block) => `import ${toPascalCase(block.blockName)} from "@/registry/default/block/${block.blockName}"`)
.join("\n")
const components = pageBlocks
.map((block) => ` <${toPascalCase(block.blockName)} />`)
.join("\n")
return `"use client"
import * as React from "react"
${imports}
export default function CustomPage() {
return (
<div className="flex min-h-screen flex-col">
${components}
</div>
)
}
`
}
const copyCode = async () => {
const code = generatePageCode()
await navigator.clipboard.writeText(code)
// TODO: Show toast notification
}
const downloadCode = () => {
const code = generatePageCode()
const blob = new Blob([code], { type: "text/typescript" })
const url = URL.createObjectURL(blob)
const a = document.createElement("a")
a.href = url
a.download = "page.tsx"
a.click()
URL.revokeObjectURL(url)
}
const deployWithHanzo = () => {
// TODO: Integrate with Hanzo deployment API
const code = generatePageCode()
console.log("Deploying with Hanzo:", code)
// This would call hanzo deployment service
window.open("https://hanzo.ai/deploy", "_blank")
}
const toPascalCase = (str: string) => {
return str
.split("-")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join("")
}
const viewportWidths = {
desktop: "100%",
tablet: "768px",
mobile: "375px",
}
return (
<div className="flex h-screen max-h-screen gap-4 p-6">
{/* Left Sidebar - Block Library */}
<div className="w-64 space-y-4">
<div>
<h2 className="text-lg font-semibold">Block Library</h2>
<p className="text-sm text-muted-foreground">
Drag blocks to build your page
</p>
</div>
<Input
placeholder="Filter blocks..."
value={filter}
onChange={(e) => setFilter(e.target.value)}
/>
<ScrollArea className="h-[calc(100vh-200px)]">
<div className="space-y-4">
{filteredBlocks.map((block) => (
<Card
key={block}
className="cursor-grab overflow-hidden transition-colors hover:bg-muted"
onClick={() => addBlock(block)}
>
{/* 1/4 Scale Block Preview */}
<div className="relative h-32 overflow-hidden bg-muted/50">
<div className="pointer-events-none">
<DynamicBlock blockName={block} scale={0.25} />
</div>
{/* Overlay with block name and add button */}
<div className="absolute inset-0 flex items-center justify-center bg-background/0 opacity-0 transition-opacity hover:bg-background/80 hover:opacity-100">
<div className="flex items-center gap-2">
<Plus className="h-5 w-5" />
<span className="text-sm font-medium">Add to page</span>
</div>
</div>
</div>
<div className="border-t p-2">
<p className="truncate text-xs font-medium">{block}</p>
</div>
</Card>
))}
</div>
</ScrollArea>
</div>
<Separator orientation="vertical" />
{/* Center - Page Builder Canvas */}
<div className="flex-1 space-y-4">
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold">Page Builder</h2>
<p className="text-sm text-muted-foreground">
{pageBlocks.length} blocks in page
</p>
</div>
<div className="flex items-center gap-2">
{/* Viewport Controls */}
<div className="flex rounded-lg border">
<Button
variant={viewport === "mobile" ? "default" : "ghost"}
size="sm"
onClick={() => setViewport("mobile")}
className="rounded-r-none"
>
Mobile
</Button>
<Button
variant={viewport === "tablet" ? "default" : "ghost"}
size="sm"
onClick={() => setViewport("tablet")}
className="rounded-none border-x"
>
Tablet
</Button>
<Button
variant={viewport === "desktop" ? "default" : "ghost"}
size="sm"
onClick={() => setViewport("desktop")}
className="rounded-l-none"
>
Desktop
</Button>
</div>
<Separator orientation="vertical" className="h-8" />
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={copyCode}
disabled={pageBlocks.length === 0}
>
<svg
className="mr-2 h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<rect width="13" height="13" x="9" y="9" rx="2" ry="2" />
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
</svg>
Copy Code
</Button>
<Button
variant="outline"
size="sm"
onClick={downloadCode}
disabled={pageBlocks.length === 0}
>
<Download className="mr-2 h-4 w-4" />
Download
</Button>
<Button
variant="default"
size="sm"
onClick={deployWithHanzo}
disabled={pageBlocks.length === 0}
>
<svg
className="mr-2 h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
</svg>
Deploy with Hanzo
</Button>
</div>
</div>
</div>
<ScrollArea className="h-[calc(100vh-140px)] rounded-lg border bg-background">
<div className="flex min-h-full items-start justify-center p-4">
<div
style={{
width: viewportWidths[viewport],
maxWidth: "100%",
transition: "width 0.3s ease",
}}
>
<DndContext
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<SortableContext
items={pageBlocks.map((b) => b.id)}
strategy={verticalListSortingStrategy}
>
<div className="min-h-[600px] bg-background">
{pageBlocks.length === 0 ? (
<div className="flex h-96 items-center justify-center rounded-lg border border-dashed text-center">
<div className="space-y-2">
<p className="text-sm text-muted-foreground">
Your page is empty
</p>
<p className="text-xs text-muted-foreground">
Click blocks from the left to add them
</p>
</div>
</div>
) : (
pageBlocks.map((block) => (
<SortableBlock
key={block.id}
id={block.id}
blockName={block.blockName}
onRemove={() => removeBlock(block.id)}
/>
))
)}
</div>
</SortableContext>
<DragOverlay>
{activeId ? (
<div className="rounded-lg border bg-card p-4 shadow-lg">
<p className="text-sm font-medium">
{pageBlocks.find((b) => b.id === activeId)?.blockName}
</p>
</div>
) : null}
</DragOverlay>
</DndContext>
</div>
</div>
</ScrollArea>
</div>
</div>
)
}
function SortableBlock({
id,
blockName,
onRemove,
}: {
id: string
blockName: string
onRemove: () => void
}) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id })
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
}
return (
<div ref={setNodeRef} style={style} className="group relative">
<div className="absolute -left-12 top-2 z-10 flex flex-col items-center gap-2">
<button
{...attributes}
{...listeners}
className="cursor-grab rounded bg-card p-1 shadow-sm hover:shadow active:cursor-grabbing"
>
<GripVertical className="h-4 w-4 text-muted-foreground" />
</button>
<Button
variant="ghost"
size="icon"
onClick={onRemove}
className="h-6 w-6 bg-background/80 opacity-0 backdrop-blur transition-opacity group-hover:opacity-100"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
{/* Block Preview - No gaps between blocks */}
<div className="relative overflow-hidden border-b last:border-b-0">
<DynamicBlock blockName={blockName} scale={1} />
</div>
</div>
)
}
+2 -2
View File
@@ -1,7 +1,6 @@
"use client"
import { useEffect, useState } from "react"
import { ConnectButton } from "@rainbow-me/rainbowkit"
import { formatEther } from "viem"
import {
useAccount,
@@ -11,6 +10,7 @@ import {
useWriteContract,
} from "wagmi"
import { ConnectWallet } from "@/components/connect-wallet"
import {
AI_TOKEN_ABI,
CONTRACT_ADDRESSES,
@@ -208,7 +208,7 @@ export function IdentityForm() {
</CardDescription>
</CardHeader>
<CardContent>
<ConnectButton />
<ConnectWallet />
{isConnected && aiBalance !== undefined && (
<div className="mt-4">
+1 -1
View File
@@ -304,7 +304,7 @@ export default function MCPPage() {
</p>
<div className="flex gap-4">
<Button asChild>
<Link href="https://discord.gg/hanzo">Join Discord</Link>
<Link href="https://discord.gg/CJCyAsm9Vr">Join Discord</Link>
</Button>
<Button variant="outline" asChild>
<Link href="/docs">Read Documentation</Link>
+18
View File
@@ -0,0 +1,18 @@
/**
* POST /api/chat — AI chat about UI components.
*
* Uses Hanzo AI (zen-coder-flash) to answer questions about
* Hanzo UI components, props, patterns, and composition.
* Compatible with Vercel AI SDK useChat hook.
*/
import { NextResponse } from 'next/server'
export const dynamic = "force-static"
export async function GET() {
return NextResponse.json(
{ error: 'AI chat requires a server runtime. Use api.hanzo.ai/v1 directly.' },
{ status: 501 },
)
}
@@ -0,0 +1,38 @@
/**
* GET /api/registry/components/:name — Get component with full source.
*
* Returns the component JSON including embedded source code.
*/
import { NextRequest, NextResponse } from "next/server"
import { getComponent, getComponentMap } from "../../lib"
export const dynamic = "force-static"
export const dynamicParams = false
export function generateStaticParams() {
const map = getComponentMap()
return Array.from(map.keys()).map((name) => ({ name }))
}
export async function GET(
_req: NextRequest,
{ params }: { params: Promise<{ name: string }> }
) {
const { name } = await params
const component = getComponent(name)
if (!component) {
return NextResponse.json(
{ error: "Component not found", name },
{ status: 404 }
)
}
return NextResponse.json(component, {
headers: {
"Cache-Control": "public, s-maxage=900, stale-while-revalidate=1800",
"Access-Control-Allow-Origin": "*",
},
})
}
+22
View File
@@ -0,0 +1,22 @@
/**
* GET /api/registry/index — Full registry manifest.
*
* Returns ALL components with source in a single payload.
* MCP clients can hydrate their cache with one HTTP call.
*/
import { NextResponse } from "next/server"
import { getFullManifest } from "../lib"
export const dynamic = "force-static"
export async function GET() {
const manifest = getFullManifest()
return NextResponse.json(manifest, {
headers: {
"Cache-Control": "public, s-maxage=900, stale-while-revalidate=1800",
"Access-Control-Allow-Origin": "*",
},
})
}
+117
View File
@@ -0,0 +1,117 @@
/**
* Shared registry data loader for API routes.
*
* Reads from the built registry JSON files in public/registry/.
* These are generated at build time by scripts/build-registry.mts.
*/
import { readFileSync, readdirSync, existsSync } from "fs"
import path from "path"
const REGISTRY_DIR = path.join(process.cwd(), "public/registry")
const STYLES_DIR = path.join(REGISTRY_DIR, "styles/default")
export interface RegistryItem {
name: string
type: string
dependencies?: string[]
devDependencies?: string[]
registryDependencies?: string[]
files: Array<{ name: string; content: string } | string>
description?: string
category?: string
}
// In-memory cache (populated on first access, lives for the server lifetime)
let _index: RegistryItem[] | null = null
let _components: Map<string, RegistryItem> | null = null
function loadIndex(): RegistryItem[] {
if (_index) return _index
const indexPath = path.join(REGISTRY_DIR, "index.json")
if (!existsSync(indexPath)) return []
_index = JSON.parse(readFileSync(indexPath, "utf-8")) as RegistryItem[]
return _index
}
function loadComponents(): Map<string, RegistryItem> {
if (_components) return _components
_components = new Map()
if (!existsSync(STYLES_DIR)) return _components
const files = readdirSync(STYLES_DIR).filter((f) => f.endsWith(".json"))
for (const file of files) {
try {
const data = JSON.parse(
readFileSync(path.join(STYLES_DIR, file), "utf-8")
) as RegistryItem
_components.set(data.name, data)
} catch {
// skip malformed files
}
}
return _components
}
/** Get the full component index (names, types, deps — no source). */
export function getIndex(): RegistryItem[] {
return loadIndex()
}
/** Get all components with full source code. */
export function getComponentMap(): Map<string, RegistryItem> {
return loadComponents()
}
/** Get a single component by name (with source). */
export function getComponent(name: string): RegistryItem | undefined {
const map = loadComponents()
// Try exact match first
let item = map.get(name)
if (item) return item
// Try with -demo suffix stripped
item = map.get(`${name}-demo`)
return item
}
/** Search components by name/type. */
export function searchComponents(query: string): RegistryItem[] {
const q = query.toLowerCase()
const index = loadIndex()
return index.filter(
(item) =>
item.name.toLowerCase().includes(q) ||
item.type?.toLowerCase().includes(q) ||
item.description?.toLowerCase().includes(q) ||
item.category?.toLowerCase().includes(q)
)
}
/** List components filtered by type. */
export function listByType(type?: string): RegistryItem[] {
const index = loadIndex()
if (!type) return index
return index.filter((item) => item.type === type || item.type?.includes(type))
}
/** Get full registry manifest (all components with source — single payload). */
export function getFullManifest() {
const map = loadComponents()
const components: Record<string, any> = {}
for (const [name, item] of map) {
components[name] = item
}
return {
generated_at: Date.now(),
total: map.size,
components,
}
}
/** Invalidate the in-memory cache (call after registry:build). */
export function invalidateCache() {
_index = null
_components = null
}
+30
View File
@@ -0,0 +1,30 @@
/**
* GET /api/registry/search?q=button — Search components.
*/
import { NextResponse } from "next/server"
import { getIndex } from "../lib"
export const dynamic = "force-static"
export async function GET() {
// Static export: return full index (client-side filtering)
const items = getIndex()
return NextResponse.json(
{
total: items.length,
results: items.map((item) => ({
name: item.name,
type: item.type,
dependencies: item.dependencies,
})),
},
{
headers: {
"Cache-Control": "public, s-maxage=300, stale-while-revalidate=600",
"Access-Control-Allow-Origin": "*",
},
}
)
}
+17
View File
@@ -0,0 +1,17 @@
/**
* POST /api/search — Search UI components via Hanzo Cloud.
*
* Proxies to Hanzo Cloud search-docs API with the publishable key.
* Client-side code hits this route instead of Cloud directly.
*/
import { NextResponse } from 'next/server'
export const dynamic = "force-static"
export async function GET() {
return NextResponse.json(
{ error: 'Search requires a server runtime. Use client-side search.' },
{ status: 501 },
)
}
+16 -2
View File
@@ -1,7 +1,21 @@
"use client"
import { Analytics as VercelAnalytics } from "@vercel/analytics/react"
import { useEffect, useRef } from "react"
import { usePathname } from "next/navigation"
import { analytics } from "@/lib/analytics"
export function Analytics() {
return <VercelAnalytics />
const pathname = usePathname()
const started = useRef(false)
useEffect(() => {
if (!started.current) {
started.current = true
analytics.init()
}
analytics.pageview(pathname ?? undefined)
}, [pathname])
return null
}
+9 -1
View File
@@ -62,7 +62,15 @@ export function CommandMenu({ ...props }: DialogProps) {
const { recentSearches, addRecentSearch, clearRecentSearches } =
useRecentSearches()
const modKey = React.useMemo(() => (isMacOS() ? "⌘" : "Ctrl"), [])
// SSR renders without `window`, so the first client (hydration) render MUST
// also produce "Ctrl" — computing the platform-specific glyph during render
// makes the hydrated text ("⌘" on macOS) diverge from the server text
// ("Ctrl") → React #418 (hydration text mismatch). Start from the SSR-stable
// value and upgrade to the platform glyph after mount (client-only effect).
const [modKey, setModKey] = React.useState("Ctrl")
React.useEffect(() => {
if (isMacOS()) setModKey("⌘")
}, [])
React.useEffect(() => {
const down = (e: KeyboardEvent) => {
+40
View File
@@ -0,0 +1,40 @@
"use client"
import { useAccount, useConnect, useDisconnect } from "wagmi"
import { Button } from "@/registry/default/ui/button"
const short = (address: string) => `${address.slice(0, 6)}${address.slice(-4)}`
/**
* Connect the wallet the browser already has, over EIP-1193. There is no
* third-party modal and no bridge service in the page — the extension is the
* only party involved.
*/
export function ConnectWallet() {
const { address, isConnected } = useAccount()
const { connect, connectors, isPending } = useConnect()
const { disconnect } = useDisconnect()
if (isConnected && address) {
return (
<div className="flex items-center gap-3">
<span className="font-mono text-sm">{short(address)}</span>
<Button variant="outline" size="sm" onClick={() => disconnect()}>
Disconnect
</Button>
</div>
)
}
const injected = connectors[0]
return (
<Button
onClick={() => injected && connect({ connector: injected })}
disabled={!injected || isPending}
>
{isPending ? "Connecting…" : "Connect Wallet"}
</Button>
)
}
+6 -22
View File
@@ -1,34 +1,18 @@
"use client"
import "@rainbow-me/rainbowkit/styles.css"
import { useEffect, useState, type ReactNode } from "react"
import { RainbowKitProvider } from "@rainbow-me/rainbowkit"
import { type ReactNode } from "react"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { WagmiProvider, type Config } from "wagmi"
import { WagmiProvider } from "wagmi"
import { getConfig } from "@/lib/wagmi"
const queryClient = new QueryClient()
const config = getConfig()
export function Web3Provider({ children }: { children: ReactNode }) {
const [config, setConfig] = useState<Config | null>(null)
useEffect(() => {
// Dynamically import wagmi config only on client side
import("@/lib/wagmi").then((mod) => {
setConfig(mod.getConfig())
})
}, [])
// Don't render until config is loaded
if (!config) {
return <>{children}</>
}
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider modalSize="compact">{children}</RainbowKitProvider>
</QueryClientProvider>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</WagmiProvider>
)
}
+1 -1
View File
@@ -8,7 +8,7 @@ export const siteConfig = {
links: {
twitter: "https://x.com/hanzoai",
github: "https://github.com/hanzoai/ui",
discord: "https://discord.gg/hanzo",
discord: "https://discord.gg/CJCyAsm9Vr",
},
}
@@ -1,71 +0,0 @@
---
title: Stock Market
description: Display real-time stock market overview with indices and major stocks.
component: true
---
<ComponentPreview
name="market-overview"
description="Stock market overview widget"
/>
## Installation
<Tabs defaultValue="cli">
<TabsList>
<TabsTrigger value="cli">CLI</TabsTrigger>
<TabsTrigger value="manual">Manual</TabsTrigger>
</TabsList>
<TabsContent value="cli">
```bash
npx @hanzo/ui@latest add market-overview
```
</TabsContent>
<TabsContent value="manual">
<Steps>
<Step>Install the following dependencies:</Step>
```bash
npm install @hanzo/ui
```
<Step>Copy and paste the following code into your project.</Step>
<ComponentSource name="market-overview" />
<Step>Update the import paths to match your project setup.</Step>
</Steps>
</TabsContent>
</Tabs>
## Usage
```tsx
import { MarketOverview } from "@hanzo/ui/finance"
export default function Demo() {
return <MarketOverview colorTheme="dark" showFloatingTooltip />
}
```
## Props
| Prop | Type | Default | Description |
| ------------------- | ----------------- | ---------------------------------------- | ---------------------- |
| colorTheme | "light" \| "dark" | "dark" | Color theme |
| width | string \| number | "100%" | Widget width |
| height | string \| number | 400 | Widget height |
| showFloatingTooltip | boolean | false | Show tooltip on hover |
| locale | string | "en" | Language locale |
| isTransparent | boolean | false | Transparent background |
| tabs | string[] | ["indices", "futures", "bonds", "forex"] | Visible tabs |
+8 -19
View File
@@ -1,5 +1,5 @@
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import tsParser from "@typescript-eslint/parser";
export default [
{
@@ -12,14 +12,16 @@ export default [
"dist/**",
".turbo/**",
"node_modules/**",
"coverage/**",
"*.config.js",
"*.config.mjs",
],
},
// Use only base configs without rules
// JavaScript files
{
files: ["**/*.{js,jsx,mjs,cjs}"],
...js.configs.recommended,
rules: {
// Turn off all rules
"no-unused-vars": "off",
"no-undef": "off",
"no-empty": "off",
@@ -30,26 +32,13 @@ export default [
"prefer-const": "off",
},
},
// TypeScript files
// TypeScript files - parser only, all rules off (rely on TypeScript compiler)
{
files: ["**/*.{ts,tsx,mts}"],
languageOptions: {
parser: tseslint.parser,
},
plugins: {
"@typescript-eslint": tseslint.plugin,
},
rules: {
// Turn off ALL rules - rely on TypeScript compiler
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-object-type": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-require-imports": "off",
"@typescript-eslint/no-this-alias": "off",
"@typescript-eslint/no-unsafe-function-type": "off",
"@typescript-eslint/no-empty-interface": "off",
parser: tsParser,
},
rules: {},
},
// CommonJS files
{
Binary file not shown.
Binary file not shown.
+35
View File
@@ -0,0 +1,35 @@
import { createAnalytics } from "@hanzo/event"
// One client for the whole site. Pageviews (components/analytics.tsx) and
// product events (lib/events.ts) share it, so there is a single place that
// knows where telemetry goes — call sites only name what happened. Host
// defaults to the one edge (api.hanzo.ai), which is what a static site wants.
//
// /v1/event is authed, and deliberately does not trust the request Host — a Host
// header is spoofable, so a static page proves which org it belongs to by
// carrying a publishable key. `pk-` keys are write-only and HMAC-verified with no
// database hop, which is what makes them safe to ship inside a public bundle (the
// same reason a Sentry DSN is public). The build supplies it, so each deployment
// reports as the org that deployed it; without one cloud answers
// `401 ingest_key_required` and every logged-out pageview is dropped in silence.
//
// NEXT_PUBLIC_PUBLISHABLE_KEY is the ONE name, and it is the name the fleet
// already carries end to end: KMS holds `deploy/PUBLISHABLE_KEY`, the Dockerfile
// takes it as the PUBLISHABLE_KEY build-arg and re-exports it with the
// NEXT_PUBLIC_ prefix that makes Next inline it.
//
// This file used to read NEXT_PUBLIC_HANZO_INGEST_KEY — a spelling nothing in KMS
// or CI carries, so no builder could ever supply it. A key reached the live bundle
// exactly once, passed by hand on a local `docker build`, and that is the failure
// this rename removes: a name only a human can satisfy gets satisfied once and
// then goes stale silently. The key v5.7.6 shipped (`pk_…`, the older format) is
// now dead — api.hanzo.ai answers `401 ingest_key_required` for it on both
// transports, so ui.hanzo.ai has been dropping every logged-out pageview.
//
// Written as a full literal on purpose: Next replaces textual `process.env.
// NEXT_PUBLIC_*` occurrences at build time, so a name assembled at runtime
// resolves to undefined in a static export.
export const analytics = createAnalytics({
product: "site",
ingestKey: process.env.NEXT_PUBLIC_PUBLISHABLE_KEY,
})
+28 -7
View File
@@ -165,22 +165,42 @@ export const AI_TOKEN_ABI = [
// Contract addresses by chain ID
export const CONTRACT_ADDRESSES: Record<
number,
{ registry: `0x${string}`; token: `0x${string}` }
{
registry: `0x${string}`;
token: `0x${string}`;
chainConfig?: `0x${string}`;
husd?: `0x${string}`;
faucet?: `0x${string}`;
}
> = {
// Local Testnet (31337)
31337: {
registry: "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0",
token: "0x5FbDB2315678afecb367f032d93F642f64180aa3",
},
// Hanzo Mainnet (36963)
// Hanzo Mainnet (36963) — sovereign L1, deployed 2026-06
36963: {
registry: "0x0000000000000000000000000000000000000000", // Deploy and update
token: "0x0000000000000000000000000000000000000000", // Deploy and update
registry: "0xf3df584A4a996b5D215E740B2240886d42C7307a",
token: "0x799586e3637E68250449e840F22F8a1a01d6E934", // AIToken (AI)
chainConfig: "0x25C806e07bA1c7B5c3495a8C57E6b8fd346092E1",
husd: "0xe9e32EF8aaECB68794Da3E1E9191b0a64CeC2c83", // HUSD (LUSD)
faucet: "0xd27d8049A575A63b54aAbbC4C41dBa5963cedF56",
},
// Hanzo Testnet (36962)
// Hanzo Testnet (36962) — sovereign L1, deployed 2026-06
36962: {
registry: "0x0000000000000000000000000000000000000000", // Deploy and update
token: "0x0000000000000000000000000000000000000000", // Deploy and update
registry: "0x6EA9D7C669DAC51830219ff5d4391872a25AB147",
token: "0x9Adf4583DDB3aFF5fA08a6788fc203e9d9908F4F", // AIToken (AI)
chainConfig: "0x6162A52F71a1C8F0F1F86FE8D17d6DDedaEdaC3c",
husd: "0xc57b7eCE2Ce2E74ef3Bc08Cfd5f5Fb41B6Ad4D66", // HUSD (LUSD)
faucet: "0x88810C4F376aF0018641e98Fcc06f0b7Ba529937",
},
// Hanzo Devnet (36964) — sovereign L1, deployed 2026-06
36964: {
registry: "0xDeA8179dEc51eA55E03fbe257d51c6d0f5908E3F",
token: "0x486809dD1bac9A17f18a1a640cdEf014C7DD809a", // AIToken (AI)
chainConfig: "0xf911b6e4952781949Db84B478582Ca05817fECB4",
husd: "0xBf92c933774daDF112159Be4b29e6BDc3ffAa2B1", // HUSD (LUSD)
faucet: "0x0B2B0BF9f423C03151480e1bB63caCd4cB3B6343",
},
// Lux Mainnet (96369)
96369: {
@@ -208,6 +228,7 @@ export const NETWORK_NAMES: Record<number, string> = {
31337: "localhost",
36963: "hanzo",
36962: "hanzo-testnet",
36964: "hanzo-devnet",
96369: "lux",
96368: "lux-testnet",
200200: "zoo",
+3 -7
View File
@@ -1,6 +1,7 @@
import va from "@vercel/analytics"
import { z } from "zod"
import { analytics } from "@/lib/analytics"
const eventSchema = z.object({
name: z.enum([
"copy_npm_command",
@@ -27,11 +28,6 @@ export type Event = z.infer<typeof eventSchema>
export function trackEvent(input: Event): void {
const event = eventSchema.parse(input)
if (event) {
va.track(
event.name,
event.properties as
| Record<string, string | number | boolean | null>
| undefined
)
analytics.capture(event.name, event.properties)
}
}
+13 -2
View File
@@ -1,6 +1,17 @@
import { GeistMono } from "geist/font/mono"
import { GeistSans } from "geist/font/sans"
import localFont from "next/font/local"
export const fontSans = GeistSans
// Canonical Hanzo typography (single source of truth; see DESIGN.md):
// - UI / body / display / heading -> Basel Grotesk (self-hosted, Book 400 + Medium 500)
// - code / data / mono -> Geist Mono
// Basel replaces Geist Sans / DM Sans / Figtree / Inter as the default sans.
export const fontSans = localFont({
src: [
{ path: "../fonts/Basel-Grotesk-Book.woff2", weight: "400", style: "normal" },
{ path: "../fonts/Basel-Grotesk-Medium.woff2", weight: "500", style: "normal" },
],
variable: "--font-basel-sans",
display: "swap",
})
export const fontMono = GeistMono
+3 -4
View File
@@ -3,10 +3,9 @@ import { promises as fs } from "fs"
import path from "path"
import type { Highlighter } from "shiki"
// Enable syntax highlighting in development
// For static exports, we can use client-side highlighting instead
const highlightCodeEnabled =
process.env.NODE_ENV === "development" || !process.env.GITHUB_ACTIONS
// Highlight on the server while developing. A production build is a static
// export, which highlights on the client instead — so the build stays cheap.
const highlightCodeEnabled = process.env.NODE_ENV === "development"
// Singleton highlighter instance to prevent memory leaks
let highlighterInstance: Highlighter | null = null
+33
View File
@@ -0,0 +1,33 @@
/**
* Hanzo Cloud search configuration for ui.hanzo.ai
*
* Uses the same infrastructure as docs.hanzo.ai:
* - Meilisearch for fulltext search
* - Qdrant for vector/semantic search
* - Hanzo Cloud API for RAG chat
*/
export const searchBackend =
(process.env.NEXT_PUBLIC_HANZO_SEARCH_BACKEND as 'cloud' | 'meilisearch' | undefined) ??
'cloud'
export const searchEndpoint =
process.env.NEXT_PUBLIC_HANZO_SEARCH_ENDPOINT ??
'https://cloud-api.hanzo.ai/api/search-docs'
export const searchIndex =
process.env.NEXT_PUBLIC_HANZO_SEARCH_INDEX ?? 'app-ui-hanzo-ai'
export const indexEndpoint =
process.env.HANZO_SEARCH_INDEX_ENDPOINT ??
'https://cloud-api.hanzo.ai/api/index-docs'
export const chatEndpoint =
process.env.NEXT_PUBLIC_HANZO_CHAT_ENDPOINT ??
'https://cloud-api.hanzo.ai/api/chat-docs'
export const publishableKey =
process.env.NEXT_PUBLIC_HANZO_SEARCH_KEY ?? 'pk-hanzo-ui-search-2026'
export const adminKey =
process.env.HANZO_SEARCH_ADMIN_KEY ?? ''
+26 -27
View File
@@ -1,7 +1,8 @@
"use client"
import { getDefaultConfig } from "@rainbow-me/rainbowkit"
import { defineChain } from "viem"
import { createConfig, http } from "wagmi"
import { injected } from "wagmi/connectors"
// Localhost (Anvil)
export const localhost = defineChain({
@@ -76,7 +77,7 @@ export const luxMainnet = defineChain({
},
rpcUrls: {
default: {
http: ["https://api.lux.network/ext/bc/C/rpc"],
http: ["https://api.lux.network/v1/bc/C/rpc"],
},
},
blockExplorers: {
@@ -98,7 +99,7 @@ export const luxTestnet = defineChain({
},
rpcUrls: {
default: {
http: ["https://testnet-api.lux.network/ext/bc/C/rpc"],
http: ["https://testnet-api.lux.network/v1/bc/C/rpc"],
},
},
blockExplorers: {
@@ -155,28 +156,30 @@ export const zooTestnet = defineChain({
testnet: true,
})
let _config: ReturnType<typeof getDefaultConfig> | undefined
const chains = [
localhost,
hanzoMainnet,
hanzoTestnet,
luxMainnet,
luxTestnet,
zooMainnet,
zooTestnet,
] as const
export const getConfig = () => {
if (typeof window === "undefined") {
// Return a minimal config for SSR that won't be used
return {} as ReturnType<typeof getDefaultConfig>
}
type Config = ReturnType<typeof createConfig>
let _config: Config | undefined
export const getConfig = (): Config => {
if (!_config) {
_config = getDefaultConfig({
appName: "Hanzo Identity",
projectId:
process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID || "YOUR_PROJECT_ID",
chains: [
localhost,
hanzoMainnet,
hanzoTestnet,
luxMainnet,
luxTestnet,
zooMainnet,
zooTestnet,
],
_config = createConfig({
chains,
// The browser wallet speaks EIP-1193 directly. No third-party bridge, no
// vendor project id, and nothing phoning home from a docs page.
connectors: [injected()],
transports: Object.fromEntries(
chains.map((chain) => [chain.id, http()])
) as Record<(typeof chains)[number]["id"], ReturnType<typeof http>>,
ssr: true,
})
}
@@ -184,8 +187,4 @@ export const getConfig = () => {
return _config
}
// For backward compatibility
export const config =
typeof window !== "undefined"
? getConfig()
: ({} as ReturnType<typeof getDefaultConfig>)
export const config = getConfig()
+4 -6
View File
@@ -5,15 +5,13 @@ const nextConfig = {
// Transpile packages that might have issues with pnpm symlinks
transpilePackages: ["chrono-node", "@hanzo/ui"],
// Enable static export for GitHub Pages deployment (but not for E2E tests)
output: process.env.GITHUB_ACTIONS && !process.env.E2E_TEST ? "export" : undefined,
// The docs site is a static export, wherever it is built. It is served by
// hanzoai/static; `next dev` is unaffected by this.
output: "export",
// Use trailing slashes for GitHub Pages compatibility
// Directory-style URLs, so a static server resolves /docs/button/index.html.
trailingSlash: true,
// Base path for GitHub Pages (when deployed to github.io subdirectory)
basePath: process.env.GITHUB_PAGES ? "/react-sdk" : "",
// Asset prefix for proper loading on custom domain
assetPrefix: process.env.NEXT_PUBLIC_APP_URL || "",
+75 -72
View File
@@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"dev": "next dev --port 3333",
"build": "pnpm registry:build && (pnpm registry:capture || echo 'Skipping screenshots in CI') && next build",
"build": "pnpm registry:build && pnpm registry:api && (pnpm registry:capture || echo 'Skipping screenshots in CI') && next build",
"start": "next start --port 3001",
"lint": "eslint .",
"lint:fix": "eslint --fix .",
@@ -17,24 +17,28 @@
"format:write": "prettier --write \"**/*.{ts,tsx,mdx}\" --cache",
"format:check": "prettier --check \"**/*.{ts,tsx,mdx}\" --cache",
"registry:build": "tsx --tsconfig ./tsconfig.scripts.json ./scripts/build-registry.mts",
"registry:api": "tsx --tsconfig ./tsconfig.scripts.json ./scripts/build-registry-api.mts",
"registry:capture": "tsx --tsconfig ./tsconfig.scripts.json ./scripts/capture-registry.mts",
"validate:registries": "tsx --tsconfig ./tsconfig.scripts.json ./scripts/validate-registries.mts",
"postinstall": "docs-mdx source.config.ts .source"
},
"dependencies": {
"@ai-sdk/openai-compatible": "^0.2.0",
"ai": "^4.3.0",
"@ctrl/tinycolor": "^4.2.0",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@faker-js/faker": "^10.0.0",
"@hanzo/docs-core": "16.2.6",
"@hanzo/docs-docgen": "3.0.4",
"@faker-js/faker": "^10.4.0",
"@hanzo/event": "^0.3.3",
"@hanzo/docs-core": "16.5.3",
"@hanzo/docs-docgen": "3.0.7",
"@hanzo/docs-mdx": "14.3.0",
"@hanzo/docs-ui": "16.2.6",
"@hanzo/logo": "^1.0.3",
"@hanzo/ui": "workspace:^",
"@hookform/resolvers": "^5.2.2",
"@hanzo/docs-ui": "16.5.3",
"@hanzo/logo": "^1.0.5",
"@hanzo/ui": "npm:@hanzo/ui-shadcn@^5",
"@hookform/resolvers": "5.2.2",
"@monaco-editor/react": "^4.7.0",
"@radix-ui/react-accessible-icon": "^1.1.8",
"@radix-ui/react-accordion": "^1.2.12",
@@ -67,18 +71,16 @@
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
"@rainbow-me/rainbowkit": "^2.2.9",
"@rc-component/color-picker": "^3.0.2",
"@tabler/icons-react": "^3.35.0",
"@tanstack/react-query": "^5.90.7",
"@rc-component/color-picker": "^3.1.1",
"@tabler/icons-react": "^3.40.0",
"@tanstack/react-query": "^5.94.5",
"@tanstack/react-table": "^8.21.3",
"@tiptap/extension-placeholder": "^3.10.2",
"@tiptap/react": "^3.10.2",
"@tiptap/starter-kit": "^3.10.2",
"@tiptap/extension-placeholder": "^3.20.4",
"@tiptap/react": "^3.20.4",
"@tiptap/starter-kit": "^3.20.4",
"@types/react-syntax-highlighter": "^15.5.13",
"@vercel/analytics": "^1.5.0",
"@vercel/og": "^0.8.5",
"@xyflow/react": "^12.8.6",
"@vercel/og": "^0.11.1",
"@xyflow/react": "^12.10.1",
"chrono-node": "^2.9.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -87,82 +89,83 @@
"date-fns": "^4.1.0",
"embla-carousel-autoplay": "8.6.0",
"embla-carousel-react": "8.6.0",
"fumadocs-core": "^16.4.8",
"fumadocs-mdx": "^14.2.6",
"geist": "^1.5.1",
"fumadocs-core": "^16.7.4",
"fumadocs-mdx": "^14.2.11",
"geist": "^1.7.0",
"input-otp": "^1.4.2",
"jotai": "^2.15.0",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"lucide-react": "^0.544.0",
"monaco-editor": "^0.53.0",
"motion": "^12.34.0",
"jotai": "^2.18.1",
"js-yaml": "^4.1.1",
"lodash": "^4.17.23",
"lucide-react": "^0.577.0",
"monaco-editor": "^0.55.1",
"motion": "^12.38.0",
"next-themes": "0.4.6",
"qrcode.react": "^4.2.0",
"react": "19.2.0",
"react": "19.2.4",
"react-colorful": "^5.6.1",
"react-day-picker": "^9.11.0",
"react-dom": "19.2.0",
"react-dropzone": "^14.3.8",
"react-hook-form": "^7.64.0",
"react-day-picker": "^9.14.0",
"react-dom": "19.2.4",
"react-dropzone": "^15.0.0",
"react-hook-form": "^7.71.2",
"react-image-crop": "^11.0.10",
"react-medium-image-zoom": "^5.4.0",
"react-medium-image-zoom": "^5.4.1",
"react-resizable-panels": "^3.0.6",
"react-syntax-highlighter": "^15.6.6",
"react-syntax-highlighter": "^16.1.1",
"react-wrap-balancer": "^1.1.1",
"recharts": "3.3.0",
"recharts": "3.8.0",
"sonner": "^2.0.7",
"swr": "^2.3.6",
"tailwind-merge": "^3.3.1",
"swr": "^2.4.1",
"tailwind-merge": "^3.5.0",
"tailwindcss-animate": "^1.0.7",
"ts-morph": "27.0.2",
"vaul": "1.1.2",
"viem": "^2.38.6",
"wagmi": "^2.19.2",
"zod": "^4.1.11",
"zustand": "^5.0.8"
"viem": "^2.47.6",
"wagmi": "^3.5.0",
"zod": "^4.3.6",
"zustand": "^5.0.12"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@ianvs/prettier-plugin-sort-imports": "^4.7.0",
"@playwright/test": "^1.55.1",
"@tailwindcss/postcss": "^4.1.17",
"@eslint/eslintrc": "^3.3.5",
"@ianvs/prettier-plugin-sort-imports": "^4.7.1",
"@playwright/test": "^1.58.2",
"@tailwindcss/postcss": "^4.2.2",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.0",
"@testing-library/react": "^16.3.2",
"@types/js-yaml": "^4.0.9",
"@types/lodash": "^4.17.20",
"@types/lodash": "^4.17.24",
"@types/mdx": "^2.0.13",
"@types/node": "^24.6.2",
"@types/react": "19.2.2",
"@types/react-dom": "19.2.2",
"@types/node": "^25.5.0",
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"@types/unist": "^3.0.3",
"@typescript-eslint/parser": "^8.46.3",
"@vitejs/plugin-react": "^5.0.4",
"@vitest/ui": "^4.0.7",
"autoprefixer": "^10.4.21",
"@typescript-eslint/parser": "^8.57.1",
"@vitejs/plugin-react": "^6.0.1",
"@vitest/ui": "^4.1.0",
"autoprefixer": "^10.4.27",
"del-cli": "^7.0.0",
"eslint": "^9.39.1",
"eslint-config-next": "16.0.1",
"happy-dom": "^20.0.10",
"next": "^16.1.6",
"@eslint/js": "^9.28.0",
"eslint": "^10.1.0",
"eslint-config-next": "16.2.1",
"happy-dom": "^20.8.4",
"next": "^16.2.1",
"npm-run-all": "^4.1.5",
"playwright": "^1.55.1",
"playwright": "^1.58.2",
"png-to-ico": "^3.0.1",
"postcss": "^8.5.6",
"postcss-selector-parser": "^7.1.0",
"prettier": "^3.6.2",
"prettier-plugin-tailwindcss": "^0.6.14",
"puppeteer": "^23.6.0",
"rehype-pretty-code": "^0.14.1",
"rimraf": "^6.0.1",
"sharp": "^0.34.4",
"shiki": "^3.15.0",
"tailwindcss": "^4.1.17",
"tsx": "^4.20.6",
"postcss": "^8.5.8",
"postcss-selector-parser": "^7.1.1",
"prettier": "^3.8.1",
"prettier-plugin-tailwindcss": "^0.7.2",
"puppeteer": "^25.1.0",
"rehype-pretty-code": "^0.14.3",
"rimraf": "^6.1.3",
"sharp": "^0.34.5",
"shiki": "^4.0.2",
"tailwindcss": "^4.2.2",
"tsx": "^4.21.0",
"tw-animate-css": "^1.4.0",
"typescript": "^5.9.3",
"unist-builder": "4.0.0",
"unist-util-visit": "^5.0.0",
"vitest": "^4.0.7"
"unist-util-visit": "^5.1.0",
"vitest": "^4.1.0"
}
}
File diff suppressed because one or more lines are too long
-14
View File
@@ -1,14 +0,0 @@
import { NextApiRequest, NextApiResponse } from "next"
import components from "./components.json"
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== "GET") {
return res.status(405).end()
}
return res.status(200).json(components)
}
-1
View File
@@ -1 +0,0 @@
ui.hanzo.ai
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -76,6 +76,7 @@ export default function Component() {
strokeWidth={2}
dot={({ cx, cy, payload }) => {
const r = 24
if (cx == null || cy == null) return null
return (
<GitCommitVertical
key={payload.month}
+2 -2
View File
@@ -749,7 +749,7 @@ const AICode = React.forwardRef<HTMLDivElement, AICodeProps>(
// Setup hover providers for explanations
monaco.languages.registerHoverProvider(currentLanguage, {
provideHover: (model, position) => {
provideHover: (model: any, position: any) => {
const line = position.lineNumber
const explanation = explanations.find((e) => e.line === line)
@@ -768,7 +768,7 @@ const AICode = React.forwardRef<HTMLDivElement, AICodeProps>(
// Setup code actions for suggestions
monaco.languages.registerCodeActionProvider(currentLanguage, {
provideCodeActions: (model, range, context, token) => {
provideCodeActions: (model: any, range: any, context: any, token: any) => {
const actions = suggestions
.filter(
(s) =>
+67 -50
View File
@@ -34,57 +34,68 @@ import { Slider } from "@/registry/default/ui/slider"
import { Switch } from "@/registry/default/ui/switch"
import { Textarea } from "@/registry/default/ui/textarea"
declare global {
interface Window {
SpeechRecognition: typeof SpeechRecognition
webkitSpeechRecognition: typeof SpeechRecognition
webkitAudioContext: typeof AudioContext
}
// Web Speech API shapes, declared MODULE-LOCALLY and deliberately not in
// `declare global`.
//
// The Speech API is only partly standardised, so how much of it lib.dom ships
// varies by TypeScript version — and this file has now been broken from both
// directions by that. Declaring the names globally MERGES with whatever the lib
// provides instead of shadowing it, so every small difference became an error
// (`readonly` vs mutable → TS2687, a second `[index: number]` → TS2374,
// `error: string` against the real `SpeechRecognitionErrorCode` union → TS2717).
// Deleting the declarations instead broke the opposite way on a lib that does
// NOT carry them: TS2552 "Cannot find name 'SpeechRecognitionEvent'".
//
// Local names collide with neither. They describe exactly the surface this
// component touches, and the version of lib.dom in use stops mattering.
interface SpeechAlternative {
readonly transcript: string
}
interface SpeechRecognition extends EventTarget {
continuous: boolean
interimResults: boolean
lang: string
onresult: (event: SpeechRecognitionEvent) => void
onerror: (event: SpeechRecognitionErrorEvent) => void
onend: () => void
start(): void
stop(): void
abort(): void
}
interface SpeechResult {
readonly isFinal: boolean
readonly [index: number]: SpeechAlternative
}
interface SpeechRecognitionEvent extends Event {
results: SpeechRecognitionResultList
resultIndex: number
}
interface SpeechResultList {
readonly length: number
readonly [index: number]: SpeechResult
}
interface SpeechRecognitionErrorEvent extends Event {
error: string
message: string
}
interface SpeechRecognitionEventLike {
readonly resultIndex: number
readonly results: SpeechResultList
}
interface SpeechRecognitionResultList {
readonly length: number
item(index: number): SpeechRecognitionResult
[index: number]: SpeechRecognitionResult
}
interface SpeechRecognitionErrorEventLike {
readonly error: string
}
interface SpeechRecognitionResult {
readonly length: number
readonly isFinal: boolean
item(index: number): SpeechRecognitionAlternative
[index: number]: SpeechRecognitionAlternative
}
interface SpeechRecognizer {
continuous: boolean
interimResults: boolean
lang: string
onresult: (event: SpeechRecognitionEventLike) => void
onerror: (event: SpeechRecognitionErrorEventLike) => void
onend: () => void
start(): void
stop(): void
abort(): void
}
interface SpeechRecognitionAlternative {
readonly transcript: string
readonly confidence: number
}
// The constructor is read off `window` through a narrow cast rather than a
// `Window` augmentation, for the same reason: lib.dom may or may not already
// declare `SpeechRecognition` there, and re-declaring it with a different type
// is itself an error.
type SpeechRecognizerCtor = new () => SpeechRecognizer
const SpeechRecognition: {
prototype: SpeechRecognition
new (): SpeechRecognition
const getSpeechRecognizerCtor = (): SpeechRecognizerCtor | undefined => {
if (typeof window === "undefined") return undefined
const w = window as unknown as {
SpeechRecognition?: SpeechRecognizerCtor
webkitSpeechRecognition?: SpeechRecognizerCtor
}
return w.webkitSpeechRecognition ?? w.SpeechRecognition
}
interface VoiceProfile {
@@ -208,7 +219,7 @@ export function AIVoice({
const [error, setError] = useState<string | null>(null)
const [audioLevel, setAudioLevel] = useState(0)
const recognitionRef = useRef<SpeechRecognition | null>(null)
const recognitionRef = useRef<SpeechRecognizer | null>(null)
const synthRef = useRef<SpeechSynthesis | null>(null)
const audioContextRef = useRef<AudioContext | null>(null)
const analyserRef = useRef<AnalyserNode | null>(null)
@@ -218,10 +229,9 @@ export function AIVoice({
// Initialize speech recognition
useEffect(() => {
if (typeof window !== "undefined" && "webkitSpeechRecognition" in window) {
const SpeechRecognition =
window.webkitSpeechRecognition || window.SpeechRecognition
recognitionRef.current = new SpeechRecognition()
const Recognizer = getSpeechRecognizerCtor()
if (Recognizer) {
recognitionRef.current = new Recognizer()
if (recognitionRef.current) {
recognitionRef.current.continuous = true
@@ -299,8 +309,15 @@ export function AIVoice({
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
audioContextRef.current = new (window.AudioContext ||
window.webkitAudioContext)()
// `webkitAudioContext` is a vendor prefix lib.dom does not declare, so it
// is read through the same narrow cast as the recogniser rather than by
// augmenting Window (see the note at the top of this file).
const AudioCtx =
window.AudioContext ??
(window as unknown as { webkitAudioContext?: typeof AudioContext })
.webkitAudioContext
if (!AudioCtx) throw new Error("Web Audio API is not available")
audioContextRef.current = new AudioCtx()
analyserRef.current = audioContextRef.current.createAnalyser()
microphoneRef.current =
audioContextRef.current.createMediaStreamSource(stream)
+119
View File
@@ -0,0 +1,119 @@
/**
* Build static API responses from the registry data.
*
* Generates /public/api/registry/ files that work on CF Pages,
* GitHub Pages, or any static hosting. Run after build-registry.mts.
*
* Output:
* public/api/registry/index.json — full manifest (all components + source)
* public/api/registry/components.json — component list (no source)
* public/api/registry/components/{name}.json — individual component with source
* public/api/registry/search-index.json — lightweight search index
*/
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "fs"
import path from "path"
const REGISTRY_DIR = path.join(process.cwd(), "public/registry")
const STYLES_DIR = path.join(REGISTRY_DIR, "styles/default")
const API_DIR = path.join(process.cwd(), "public/api/registry")
const COMPONENTS_DIR = path.join(API_DIR, "components")
interface RegistryItem {
name: string
type: string
dependencies?: string[]
devDependencies?: string[]
registryDependencies?: string[]
files: Array<{ name: string; content: string } | string>
description?: string
category?: string
}
function main() {
console.log("Building static registry API files...")
// Read the index
const indexPath = path.join(REGISTRY_DIR, "index.json")
if (!existsSync(indexPath)) {
console.error("Registry index.json not found. Run registry:build first.")
process.exit(1)
}
const index: RegistryItem[] = JSON.parse(readFileSync(indexPath, "utf-8"))
// Read all component files
const components = new Map<string, RegistryItem>()
if (existsSync(STYLES_DIR)) {
const files = readdirSync(STYLES_DIR).filter((f) => f.endsWith(".json"))
for (const file of files) {
try {
const data: RegistryItem = JSON.parse(
readFileSync(path.join(STYLES_DIR, file), "utf-8")
)
components.set(data.name, data)
} catch {
// skip
}
}
}
// Create output directories
mkdirSync(COMPONENTS_DIR, { recursive: true })
// 1. Component list (no source — lightweight)
const componentList = index.map((item) => ({
name: item.name,
type: item.type,
dependencies: item.dependencies,
registryDependencies: item.registryDependencies,
}))
writeFileSync(
path.join(API_DIR, "components.json"),
JSON.stringify({ total: componentList.length, components: componentList })
)
console.log(` components.json: ${componentList.length} components`)
// 2. Individual component files (with source)
let written = 0
for (const [name, data] of components) {
writeFileSync(
path.join(COMPONENTS_DIR, `${name}.json`),
JSON.stringify(data)
)
written++
}
console.log(` components/*.json: ${written} files`)
// 3. Full manifest (single payload with all source)
const manifest: Record<string, any> = {}
for (const [name, data] of components) {
manifest[name] = data
}
writeFileSync(
path.join(API_DIR, "index.json"),
JSON.stringify({
generated_at: Date.now(),
total: components.size,
components: manifest,
})
)
console.log(` index.json: full manifest`)
// 4. Search index (lightweight — names + types for client-side search)
const searchIndex = index.map((item) => ({
n: item.name,
t: item.type,
d: (item.dependencies || []).join(","),
}))
writeFileSync(
path.join(API_DIR, "search-index.json"),
JSON.stringify(searchIndex)
)
console.log(` search-index.json: ${searchIndex.length} entries`)
console.log("Done! Static API files written to public/api/registry/")
}
main()
+1 -1
View File
@@ -23,7 +23,7 @@ const config = {
},
extend: {
fontFamily: {
sans: ["var(--font-geist-sans)", ...fontFamily.sans],
sans: ["var(--font-basel-sans)", ...fontFamily.sans],
mono: ["var(--font-geist-mono)", ...fontFamily.mono],
},
colors: {
+9 -1
View File
@@ -19,10 +19,18 @@
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"baseUrl": ".",
// `types/nav` and `types/unist` are imported as BARE specifiers, which only
// ever resolved because of an implicit project root. Under TypeScript 7
// `baseUrl` is gone outright — the compiler answers TS5102 "Option 'baseUrl'
// has been removed … use '\"paths\": {\"*\": [\"./*\"]}' instead" — so the
// mapping has to be explicit. `paths` entries resolve relative to this file,
// so no baseUrl is needed or accepted.
"paths": {
"@/*": [
"./*"
],
"types/*": [
"./types/*"
]
},
"plugins": [
+4
View File
@@ -0,0 +1,4 @@
node_modules
dist
e2e-shots
*.tsbuildinfo
+78
View File
@@ -0,0 +1,78 @@
# Hanzo CD — the dedicated deploy dashboard (cd.hanzo.ai)
A focused, mobile-first CD dashboard served at **cd.hanzo.ai**. It replaces the
ArgoCD React fork (`~/work/hanzo/deploy/ui`, webpack/`argo-cd-ui`) with the shared
Hanzo component packages over the native cloud CD plane. Its whole job: **see every
service deployed as an operator App CR — stats · sync · health · resource tree ·
logs · sync/rollback.**
## Stack + why
- **Vite + React 19 static SPA** → `dist/` (a static bundle, no runtime server).
- **`@hanzo/cd`** (workspace) is the ArgoCD-replacement UI — a clean-room,
framework-free port (plain React + scoped CSS, no Tamagui/Tailwind). We mount
`GitopsAppList` (the fleet), and compose `GitopsSyncPanel` + `GitopsAppTree` +
`GitopsNodeInfo` + `GitopsRollbackDialog` for the app detail (lazy per-node
`/resource` + `/logs` fetch).
- **`@hanzo/canvas/pure`** (workspace) for the pure helpers.
- The workspace packages are resolved by **Vite path alias** (`vite.config.ts`),
so the app builds off `@hanzo/cd/dist` + `@hanzo/canvas/src` **without a full
monorepo install** (the shared pnpm store lacks the registry deps of the heavier
packages). Run `pnpm --filter @hanzo/cd build` once if its `dist` is stale.
## Backend + auth (the contract this preserves)
- **API = the cloud binary at `/v1/deploy`** (SuperAdmin-gated, clean paths —
`applications`, `:name/tree`, `:name/resource/:ref`, `:name/logs`, `sync`,
`rollback`). The cd.hanzo.ai ingress peels `/v1/deploy/*` off to `cloud:8000`;
the SPA calls it **same-origin** with `credentials: 'include'`.
- **Auth = the `admin-console` PKCE gate** (`public/login.html`, ported verbatim
from the ArgoCD fork — self-contained, proven): `hanzo.id` → sets the non-httpOnly
`hanzo_iam_token` cookie the cloud binary validates (`SanitizeIdentity`
`c.IsAdmin()`). The SPA reads that cookie for its gate; a missing/expired session
(or an API 401/403) lands on the sign-in screen — never a fabricated row.
- `src/lib/adapt.ts` maps the `/v1/deploy` DTOs INTO the shared `@hanzo/cd`
view-models (reusing the package's own `foldHealth`/`foldSync`). Note: the cloud
wire uses the hyphenated `out-of-sync`, which `foldSync` (whitespace-only strip)
reads as Unknown — the adapter strips `[-_]` before folding.
## Build + serve
```bash
pnpm --filter cd build # tsc -b && vite build → dist/ (index.html + login.html + CNAME + assets)
pnpm --filter cd preview # serve dist locally
pnpm --filter cd dev # vite dev (proxies /v1 → CD_API, default https://cd.hanzo.ai)
```
Serving: the built `dist/` is a static bundle published to **`s3://cdn/cd`** — the
existing static plane cd.hanzo.ai already serves (ingress `staticFiles` +
`spaMode`, zero pods; see `universe/infra/k8s/operator/crs/static-sites.yaml`). No
ingress or backend change: publish the bundle, retire the old ArgoCD `ui/` + the
`deploy/dashboard.go` embed.
## Verify
- `tsc --noEmit -p tsconfig.app.json` — clean.
- `npx vitest run src/lib/adapt.test.ts` — the `/v1/deploy``@hanzo/cd`
mapping (incl. the `out-of-sync` fold fix, `parentRefs` tree linking, clean-semver
rollback).
- `npx playwright test` — builds + serves + proves the fleet renders, a row opens
the ArgoCD-grade detail (sync panel + resource topology), and **no horizontal body
scroll at 390px**. Screenshots in `e2e-shots/`.
## Follow-ons (registry-install / routing gated — flagged, not fabricated)
- **`@hanzo/canvas` fleet MAP** (the Railway board) needs `@hanzo/gui` (Tamagui),
which is an external npm dep not in the offline store here — so the map is a
registry-install follow-on. It already ships green in the console
(`hanzoai/console` `feat/cd-canvas-map`); lifting it in is an import once
`@hanzo/gui` installs.
- **`@hanzo/ui-shadcn` shared shell** (`HanzoHeader` + org/project switcher + ⌘K)
needs a full monorepo install (its Tailwind deps aren't in the offline store).
This app ships a lean self-contained topbar (Geist, small mark, env scope) in the
interim; swap to the shared shell once installed. The **org/project↔IAM switcher**
additionally needs `/v1/iam` routed on cd.hanzo.ai (today the SuperAdmin cookie
sees the whole fleet; env is the scope control).
- **Rollback targets** come from the app's `revisions` (forward-compat — the plane
doesn't expose prior tags yet; empty → the dialog's honest "no history"). Cloud
re-validates the clean semver.
+97
View File
@@ -0,0 +1,97 @@
/**
* e2e: Hanzo CD — mocked-network render + RESPONSIVE proof.
*
* Serves the built static SPA; mocks the cloud CD plane (`/v1/deploy/*`) with
* real-shaped `clients/deploy` rows + a session cookie so the app renders the
* fleet. Proves: the fleet renders on the shared @hanzo/cd `GitopsAppList`, a
* row opens the ArgoCD-grade detail (sync panel + resource topology), and — the
* CTO requirement — the body never scrolls horizontally at a 390px viewport.
* Screenshots at desktop (1440) and mobile (390).
*/
import { test, expect, type Route, type Page, type BrowserContext } from "@playwright/test"
import { mkdirSync } from "node:fs"
import { join } from "node:path"
const SHOTS = join(process.cwd(), "e2e-shots")
const FLEET = {
applications: [
{ name: "cloud", namespace: "hanzo", env: "main", repository: "ghcr.io/hanzoai/cloud", version: "v1.800.1", runningVersion: "v1.800.1", health: "healthy", sync: "synced", phase: "Running" },
{ name: "iam", namespace: "hanzo", env: "main", repository: "ghcr.io/hanzoai/iam", version: "v1.4.11", runningVersion: "v1.4.10", health: "progressing", healthMessage: "rolling update (1/2)", sync: "out-of-sync", phase: "Running", revisions: ["v1.4.10", "v1.4.9"] },
{ name: "gateway", namespace: "hanzo", env: "main", repository: "ghcr.io/hanzoai/gateway", version: "v2.16.4", runningVersion: "v2.16.4", health: "healthy", sync: "synced", phase: "Running" },
{ name: "o11y", namespace: "hanzo", env: "test", repository: "ghcr.io/hanzoai/o11y", version: "v1.5.12", runningVersion: "v1.5.10", health: "degraded", healthMessage: "CrashLoopBackOff", sync: "out-of-sync", phase: "Degraded" },
],
summary: { total: 4, healthy: 2, degraded: 1, outOfSync: 2 },
}
const TREE = {
application: FLEET.applications[1],
nodes: [
{ group: "hanzo.ai", version: "v1", kind: "App", namespace: "hanzo", name: "iam", ref: "hanzo.ai:App:hanzo:iam", uid: "u1", health: "progressing", parentRefs: [] },
{ group: "apps", version: "v1", kind: "Deployment", namespace: "hanzo", name: "iam", ref: "apps:Deployment:hanzo:iam", uid: "u2", health: "progressing", parentRefs: [{ ref: "hanzo.ai:App:hanzo:iam" }] },
{ group: "apps", version: "v1", kind: "ReplicaSet", namespace: "hanzo", name: "iam-6d8f", ref: "apps:ReplicaSet:hanzo:iam-6d8f", uid: "u3", health: "healthy", parentRefs: [{ ref: "apps:Deployment:hanzo:iam" }] },
{ group: "", version: "v1", kind: "Pod", namespace: "hanzo", name: "iam-6d8f-abc", ref: ":Pod:hanzo:iam-6d8f-abc", uid: "u4", health: "healthy", parentRefs: [{ ref: "apps:ReplicaSet:hanzo:iam-6d8f" }] },
],
}
const RESOURCE = { ref: "apps:Deployment:hanzo:iam", health: "healthy", liveManifest: { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "iam" }, spec: { replicas: 2 } }, desiredSource: "last-applied", diff: { modified: false } }
const LOGS = { application: "hanzo/iam", pod: "iam-6d8f-abc", logs: "listening on :8080\nready to serve\n" }
async function mock(route: Route) {
const p = new URL(route.request().url()).pathname
const json = (b: unknown) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(b) })
if (p === "/v1/deploy/applications") return json(FLEET)
if (/^\/v1\/deploy\/[^/]+\/tree$/.test(p)) return json(TREE)
if (/^\/v1\/deploy\/[^/]+\/resource\//.test(p)) return json(RESOURCE)
if (/^\/v1\/deploy\/[^/]+\/logs$/.test(p)) return json(LOGS)
if (p.startsWith("/v1/deploy/")) return json({ ok: true })
return route.continue()
}
async function open(ctx: BrowserContext): Promise<Page> {
// The PKCE login sets `hanzo_iam_token`; seed it so the SPA renders the dashboard.
await ctx.addCookies([{ name: "hanzo_iam_token", value: "e.y.j", url: "http://localhost:4173" }])
const page = await ctx.newPage()
await page.route("**/*", mock)
await page.goto("/", { waitUntil: "domcontentloaded" })
await page.locator("text=Fleet").first().waitFor({ timeout: 15_000 })
return page
}
test.beforeAll(() => mkdirSync(SHOTS, { recursive: true }))
test("renders the fleet, opens a row → ArgoCD-grade detail (desktop)", async ({ browser }) => {
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } })
const page = await open(ctx)
await expect(page.locator("text=Applications").first()).toBeVisible()
await expect(page.locator("tr", { hasText: "iam" }).first()).toBeVisible()
// sync fold: the hyphenated cloud verdict renders OutOfSync (not Unknown).
await expect(page.locator("text=OutOfSync").first()).toBeVisible()
await page.screenshot({ path: join(SHOTS, "cd-fleet-desktop.png"), fullPage: true })
// Tap the app's NAME cell (always visible; the row overflows on narrow tables).
await page.locator(".hz-gitops-row", { hasText: "iam" }).first().locator("td").first().click()
await expect(page.getByText("← Fleet").first()).toBeVisible({ timeout: 10_000 }) // detail-only breadcrumb
await expect(page.locator(".hz-gitops-tree-world").first()).toBeVisible({ timeout: 10_000 })
await expect(page.locator("text=Deployment").first()).toBeVisible()
await page.screenshot({ path: join(SHOTS, "cd-detail-desktop.png"), fullPage: true })
await ctx.close()
})
test("reflows with no horizontal body scroll at a narrow (mobile) viewport", async ({ browser }) => {
const ctx = await browser.newContext({ viewport: { width: 390, height: 844 } })
const page = await open(ctx)
await expect(page.locator("tr", { hasText: "iam" }).first()).toBeVisible()
const overflow = await page.evaluate(() => ({ sw: document.documentElement.scrollWidth, cw: document.documentElement.clientWidth }))
expect(overflow.sw, "no horizontal body scroll at 390px").toBeLessThanOrEqual(overflow.cw + 1)
await page.screenshot({ path: join(SHOTS, "cd-fleet-mobile.png"), fullPage: true })
// Tap-to-open must work on mobile: tap the name cell → the ArgoCD-grade detail.
await page.locator(".hz-gitops-row", { hasText: "iam" }).first().locator("td").first().click()
await expect(page.getByText("← Fleet").first()).toBeVisible({ timeout: 10_000 })
await expect(page.locator(".hz-gitops-tree-world").first()).toBeVisible({ timeout: 10_000 })
const overflow2 = await page.evaluate(() => ({ sw: document.documentElement.scrollWidth, cw: document.documentElement.clientWidth }))
expect(overflow2.sw, "no horizontal body scroll on the detail at 390px").toBeLessThanOrEqual(overflow2.cw + 1)
await page.screenshot({ path: join(SHOTS, "cd-detail-mobile.png"), fullPage: true })
await ctx.close()
})
+17
View File
@@ -0,0 +1,17 @@
<!doctype html>
<html lang="en" data-theme="dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Hanzo CD</title>
<link
rel="icon"
type="image/svg+xml"
href="data:image/svg+xml,%3Csvg viewBox='0 0 64 64' xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='64' height='64' rx='8' fill='%23000'/%3E%3Cg transform='translate(8,8) scale(0.716)'%3E%3Cpath d='M22.21 67V44.6369H0V67H22.21Z' fill='%23fff'/%3E%3Cpath d='M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z' fill='%23fff'/%3E%3Cpath d='M22.21 0H0V22.3184H22.21V0Z' fill='%23fff'/%3E%3Cpath d='M66.7198 0H44.5098V22.3184H66.7198V0Z' fill='%23fff'/%3E%3Cpath d='M66.7198 67V44.6369H44.5098V67H66.7198Z' fill='%23fff'/%3E%3C/g%3E%3C/svg%3E"
/>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+26
View File
@@ -0,0 +1,26 @@
{
"name": "cd",
"private": true,
"version": "0.1.0",
"type": "module",
"description": "Hanzo CD — the dedicated deploy dashboard served at cd.hanzo.ai (a @hanzo/cd + @hanzo/canvas SPA over /v1/deploy). Replaces the ArgoCD UI fork.",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"typecheck": "tsc --noEmit -p tsconfig.app.json",
"preview": "vite preview --port 4173"
},
"dependencies": {
"@hanzo/canvas": "workspace:*",
"@hanzo/cd": "workspace:*",
"react": "^19.2.4",
"react-dom": "^19.2.4"
},
"devDependencies": {
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.3",
"typescript": "~6.0.2",
"vite": "^8.1.4"
}
}
+28
View File
@@ -0,0 +1,28 @@
import { defineConfig, devices } from "@playwright/test"
/**
* Hanzo CD render + responsive proof. Builds the static SPA and serves it (vite
* preview), then the spec mocks `/v1/deploy/*` + a session cookie and asserts the
* fleet renders, a row opens the ArgoCD-grade detail (sync panel + resource tree),
* and the body never scrolls horizontally at 390px. Screenshots per width.
*
* pnpm --filter cd exec playwright test # builds + serves + runs
* BASE_URL=https://cd.hanzo.ai pnpm … test # against a live deploy
*/
export default defineConfig({
testDir: "./e2e",
timeout: 60_000,
retries: 1,
workers: 1,
reporter: "list",
use: { baseURL: process.env.BASE_URL ?? "http://localhost:4173", headless: true },
webServer: process.env.BASE_URL
? undefined
: {
command: "npx vite build && npx vite preview --port 4173 --strictPort",
url: "http://localhost:4173",
reuseExistingServer: true,
timeout: 120_000,
},
projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }],
})
+1
View File
@@ -0,0 +1 @@
cd.hanzo.ai
+143
View File
@@ -0,0 +1,143 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Hanzo CD — Sign in</title>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg viewBox='0 0 64 64' xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='64' height='64' rx='8' fill='%23000'/%3E%3Cg transform='translate(8,8) scale(0.716)'%3E%3Cpath d='M22.21 67V44.6369H0V67H22.21Z' fill='%23fff'/%3E%3Cpath d='M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z' fill='%23fff'/%3E%3Cpath d='M22.21 0H0V22.3184H22.21V0Z' fill='%23fff'/%3E%3Cpath d='M66.7198 0H44.5098V22.3184H66.7198V0Z' fill='%23fff'/%3E%3Cpath d='M66.7198 67V44.6369H44.5098V67H66.7198Z' fill='%23fff'/%3E%3C/g%3E%3C/svg%3E">
<style>
:root { color-scheme: light dark; }
* { box-sizing: border-box; }
html,body { height:100%; margin:0; }
body {
font-family: "Geist", "Geist Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
background:#0a0a0a; color:#e7e7e7;
display:grid; grid-template-columns: 1.1fr 1fr; min-height:100vh;
}
@media (max-width: 820px){ body{ grid-template-columns:1fr; } .pitch{ display:none; } }
.pitch { padding:64px 56px; display:flex; flex-direction:column; justify-content:center; gap:22px;
border-right:1px solid #1c1c1c; }
.brand { display:flex; align-items:center; gap:12px; font-size:22px; font-weight:700; letter-spacing:-.02em; }
.brand .mark { width:26px; height:26px; display:block; }
h1 { font-size:40px; line-height:1.08; margin:6px 0 0; letter-spacing:-.03em; font-weight:800; }
.sub { color:#9a9a9a; font-size:16px; max-width:38ch; line-height:1.5; }
ul.feat { list-style:none; padding:0; margin:8px 0 0; display:grid; gap:12px; }
ul.feat li { display:flex; gap:12px; align-items:flex-start; color:#cfcfcf; font-size:14.5px; }
ul.feat li b { color:#fff; font-weight:600; }
ul.feat .dot { width:6px; height:6px; border-radius:50%; background:#fff; margin-top:7px; flex:none; }
.auth { display:flex; align-items:center; justify-content:center; padding:40px; }
.card { width:100%; max-width:360px; text-align:center; }
.card .mark2 { width:52px; height:52px; display:block; margin:0 auto 18px; }
.card h2 { font-size:22px; margin:0 0 6px; font-weight:700; }
.card p.hint { color:#8f8f8f; font-size:14px; margin:0 0 26px; }
button#login {
width:100%; padding:14px 18px; font-size:15px; font-weight:600; cursor:pointer;
background:#fff; color:#000; border:0; border-radius:10px; display:flex; align-items:center;
justify-content:center; gap:10px; transition:opacity .15s;
}
button#login:hover { opacity:.88; }
button#login:disabled { opacity:.5; cursor:default; }
.status { margin-top:16px; font-size:13.5px; color:#9a9a9a; min-height:20px; }
.status.err { color:#ff7a7a; }
.foot { margin-top:28px; font-size:12px; color:#5c5c5c; }
a { color:#bdbdbd; }
</style>
</head>
<body>
<section class="pitch">
<div class="brand"><svg class="mark" viewBox="0 0 67 67" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M22.21 67V44.6369H0V67H22.21Z" fill="#fff"/><path d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z" fill="#fff"/><path d="M22.21 0H0V22.3184H22.21V0Z" fill="#fff"/><path d="M66.7198 0H44.5098V22.3184H66.7198V0Z" fill="#fff"/><path d="M66.7198 67V44.6369H44.5098V67H66.7198Z" fill="#fff"/></svg> Hanzo CD</div>
<h1>Continuous delivery for your whole fleet.</h1>
<p class="sub">GitOps for every Hanzo App. Declarative, versioned, and auto-synced from git to your clusters — one control plane across hanzo, lux, zoo &amp; pars.</p>
<ul class="feat">
<li><span class="dot"></span><span><b>Live fleet view</b> — health &amp; sync status for every App across all namespaces.</span></li>
<li><span class="dot"></span><span><b>Git-to-cluster</b> — desired state lives in <code>infra/k8s/operator/crs</code>, reconciled continuously.</span></li>
<li><span class="dot"></span><span><b>Sync &amp; rollback</b> — one click to reconcile or roll back any application.</span></li>
<li><span class="dot"></span><span><b>Resource trees &amp; logs</b> — drill into any workload the operator manages.</span></li>
</ul>
</section>
<section class="auth">
<div class="card">
<svg class="mark2" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><rect width="64" height="64" rx="14" fill="#fff"/><g transform="translate(8,8) scale(0.716)"><path d="M22.21 67V44.6369H0V67H22.21Z" fill="#000"/><path d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z" fill="#000"/><path d="M22.21 0H0V22.3184H22.21V0Z" fill="#000"/><path d="M66.7198 0H44.5098V22.3184H66.7198V0Z" fill="#000"/><path d="M66.7198 67V44.6369H44.5098V67H66.7198Z" fill="#000"/></g></svg>
<h2>Hanzo CD</h2>
<p class="hint">Sign in with your Hanzo account to manage the fleet.</p>
<button id="login">
<svg width="17" height="17" viewBox="0 0 67 67" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M22.21 67V44.6369H0V67H22.21Z" fill="currentColor"/><path d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z" fill="currentColor"/><path d="M22.21 0H0V22.3184H22.21V0Z" fill="currentColor"/><path d="M66.7198 0H44.5098V22.3184H66.7198V0Z" fill="currentColor"/><path d="M66.7198 67V44.6369H44.5098V67H66.7198Z" fill="currentColor"/></svg>
Sign in with Hanzo
</button>
<div class="status" id="status"></div>
<div class="foot">Powered by <a href="https://hanzo.id" rel="noreferrer">Hanzo IAM</a> · SuperAdmin access</div>
</div>
</section>
<script>
(function () {
var IAM = 'https://hanzo.id';
var CLIENT = 'admin-console'; // the IAM app whose org is `admin`
var REDIRECT = location.origin + '/login.html'; // must be registered on admin-console
var COOKIE = 'hanzo_iam_token'; // the cookie hanzoai/cloud validates
var statusEl = document.getElementById('status');
var btn = document.getElementById('login');
function setStatus(m, err){ statusEl.textContent = m || ''; statusEl.className = 'status' + (err ? ' err' : ''); }
function b64url(bytes){
var s = btoa(String.fromCharCode.apply(null, new Uint8Array(bytes)));
return s.replace(/\+/g,'-').replace(/\//g,'_').replace(/=+$/,'');
}
function randB64(n){ return b64url(crypto.getRandomValues(new Uint8Array(n))); }
async function sha256b64(str){
var d = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(str));
return b64url(d);
}
async function startLogin(){
btn.disabled = true; setStatus('Redirecting to Hanzo…');
var verifier = randB64(48);
var state = randB64(16);
sessionStorage.setItem('cd_pkce_verifier', verifier);
sessionStorage.setItem('cd_oauth_state', state);
var challenge = await sha256b64(verifier);
var u = new URL(IAM + '/login/oauth/authorize');
u.search = new URLSearchParams({
client_id: CLIENT, response_type: 'code', redirect_uri: REDIRECT,
scope: 'openid profile email', state: state,
code_challenge: challenge, code_challenge_method: 'S256'
}).toString();
location.href = u.toString();
}
async function finishLogin(code, state){
btn.disabled = true; setStatus('Signing in…');
if (state !== sessionStorage.getItem('cd_oauth_state')) { setStatus('Sign-in state mismatch — please try again.', true); btn.disabled = false; return; }
var verifier = sessionStorage.getItem('cd_pkce_verifier');
if (!verifier) { setStatus('Sign-in session lost — please try again.', true); btn.disabled = false; return; }
try {
var r = await fetch(IAM + '/v1/iam/oauth/access_token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code', client_id: CLIENT, code: code,
redirect_uri: REDIRECT, code_verifier: verifier
})
});
var j = await r.json();
if (!j.access_token) { setStatus('Sign-in failed: ' + (j.error_description || j.error || 'no token'), true); btn.disabled = false; return; }
// set the session cookie cloud reads (non-httpOnly by design so the page can set it; still Secure)
document.cookie = COOKIE + '=' + j.access_token + '; path=/; secure; samesite=lax; max-age=86400';
sessionStorage.removeItem('cd_pkce_verifier'); sessionStorage.removeItem('cd_oauth_state');
setStatus('Signed in. Loading the dashboard…');
location.replace('/');
} catch (e) {
setStatus('Sign-in error: ' + (e && e.message ? e.message : e), true); btn.disabled = false;
}
}
var q = new URLSearchParams(location.search);
if (q.get('error')) { setStatus('Hanzo returned: ' + (q.get('error_description') || q.get('error')), true); }
else if (q.get('code')) { finishLogin(q.get('code'), q.get('state')); }
btn.onclick = startLogin;
})();
</script>
</body>
</html>
+171
View File
@@ -0,0 +1,171 @@
/**
* Hanzo CD the dedicated deploy dashboard (cd.hanzo.ai). A static SPA over the
* cloud CD plane (`/v1/deploy`): the fleet of operator App CRs with health, sync,
* resource topology, logs, and sync/rollback, built on the shared @hanzo/cd
* components. Auth is the first-party `hanzo_iam_token` cookie (set by the PKCE
* login); a missing/expired session lands on the sign-in screen never a fake row.
*/
import { useCallback, useEffect, useMemo, useState } from "react"
import { DeployApi, hasSession, isAuthError } from "./lib/deploy"
import type { DeployApp } from "./lib/adapt"
import { Topbar, type EnvOption } from "./shell/Topbar"
import { FleetView } from "./views/FleetView"
import { AppView } from "./views/AppView"
const POLL_MS = 20_000
type Phase = "signin" | "loading" | "error" | "ready"
const parseHash = (): string => {
const m = /^#\/app\/(.+)$/.exec(location.hash)
return m ? decodeURIComponent(m[1]) : ""
}
function SignIn() {
return (
<div style={{ minHeight: "100vh", display: "grid", placeItems: "center", padding: 24 }}>
<div style={{ textAlign: "center", maxWidth: 360 }}>
<svg width={52} height={52} viewBox="0 0 64 64" style={{ marginBottom: 18 }} xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<rect width="64" height="64" rx="14" fill="#fff" />
<g transform="translate(8,8) scale(0.716)">
<path d="M22.21 67V44.6369H0V67H22.21Z" fill="#000" />
<path d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z" fill="#000" />
<path d="M22.21 0H0V22.3184H22.21V0Z" fill="#000" />
<path d="M66.7198 0H44.5098V22.3184H66.7198V0Z" fill="#000" />
<path d="M66.7198 67V44.6369H44.5098V67H66.7198Z" fill="#000" />
</g>
</svg>
<h2 style={{ fontSize: 22, margin: "0 0 6px", fontWeight: 700 }}>Hanzo CD</h2>
<p className="cd-muted" style={{ margin: "0 0 26px", fontSize: 14 }}>
Sign in with your Hanzo account to manage the fleet.
</p>
<button type="button" className="cd-btn cd-btn--primary" style={{ width: "100%", justifyContent: "center", padding: "13px 18px" }} onClick={() => (location.href = "/login.html")}>
Sign in with Hanzo
</button>
<div className="cd-muted" style={{ marginTop: 28, fontSize: 12 }}>
Powered by Hanzo IAM · SuperAdmin access
</div>
</div>
</div>
)
}
export function App() {
const [phase, setPhase] = useState<Phase>(hasSession() ? "loading" : "signin")
const [apps, setApps] = useState<DeployApp[]>([])
const [errMsg, setErrMsg] = useState("")
const [env, setEnv] = useState("")
const [selected, setSelected] = useState<string>(parseHash())
const [refreshing, setRefreshing] = useState(false)
const [toast, setToast] = useState<{ msg: string; err?: boolean } | null>(null)
const load = useCallback(() => {
if (!hasSession()) {
setPhase("signin")
return
}
setRefreshing(true)
DeployApi.applications()
.then((rows) => {
setApps(rows)
setPhase("ready")
})
.catch((e) => {
if (isAuthError(e)) setPhase("signin")
else {
setPhase("error")
setErrMsg(e instanceof Error ? e.message : "failed to load")
}
})
.finally(() => setRefreshing(false))
}, [])
useEffect(() => {
load()
const t = setInterval(load, POLL_MS)
const onHash = () => setSelected(parseHash())
window.addEventListener("hashchange", onHash)
return () => {
clearInterval(t)
window.removeEventListener("hashchange", onHash)
}
}, [load])
const notify = useCallback((msg: string, err?: boolean) => {
setToast({ msg, err })
setTimeout(() => setToast(null), 4000)
}, [])
const envs: EnvOption[] = useMemo(() => {
const counts = new Map<string, number>()
for (const a of apps) if (a.env) counts.set(a.env, (counts.get(a.env) ?? 0) + 1)
return Array.from(counts, ([id, count]) => ({ id, label: id, count })).sort((x, y) => x.id.localeCompare(y.id))
}, [apps])
const visibleApps = useMemo(() => (env ? apps.filter((a) => a.env === env) : apps), [apps, env])
const selectedApp = selected ? apps.find((a) => a.name === selected) ?? null : null
const openApp = (name: string) => {
location.hash = `#/app/${encodeURIComponent(name)}`
}
const backToFleet = () => {
location.hash = ""
}
const signOut = () => {
document.cookie = "hanzo_iam_token=; path=/; max-age=0"
location.href = "/login.html"
}
if (phase === "signin") return <SignIn />
return (
<div className="cd-app">
<Topbar envs={envs} env={env} onEnv={setEnv} onRefresh={load} refreshing={refreshing} onSignOut={signOut} />
<main className="cd-main">
{phase === "loading" ? (
<div className="cd-muted" style={{ padding: 40 }}>
Loading the fleet
</div>
) : phase === "error" ? (
<div style={{ padding: 24, border: "1px solid var(--cd-border-strong)", borderRadius: 10, background: "var(--cd-surface)" }}>
<div style={{ fontWeight: 700, marginBottom: 6 }}>Could not reach the deploy plane</div>
<div className="cd-muted" style={{ fontSize: 14, marginBottom: 14 }}>
{errMsg || "The CD read (GET /v1/deploy/applications) failed."}
</div>
<button type="button" className="cd-btn" onClick={load}>
Retry
</button>
</div>
) : selectedApp ? (
<AppView app={selectedApp} onBack={backToFleet} onChanged={load} notify={notify} />
) : (
<FleetView apps={visibleApps} onOpen={openApp} />
)}
</main>
{toast ? (
<div
role="status"
style={{
position: "fixed",
bottom: 18,
left: "50%",
transform: "translateX(-50%)",
zIndex: 1000,
maxWidth: "calc(100vw - 32px)",
padding: "11px 16px",
borderRadius: 10,
background: toast.err ? "#3d1418" : "var(--cd-surface-2)",
border: `1px solid ${toast.err ? "#E96D76" : "var(--cd-border-strong)"}`,
color: "var(--cd-fg-strong)",
fontSize: 13.5,
boxShadow: "0 8px 30px rgba(0,0,0,0.4)",
}}
>
{toast.msg}
</div>
) : null}
</div>
)
}
+261
View File
@@ -0,0 +1,261 @@
/**
* RED adversarial suite feeds hostile / malformed /v1/deploy JSON to the adapter
* and the shared @hanzo/cd folds, asserting the view degrades honestly and
* NEVER (a) crashes, (b) wedges the tree builder into a hang, or (c) mislabels a
* bad state as Healthy/Synced. Written by Red; not part of Blue's suite.
*/
import { describe, expect, it } from "vitest"
import { buildResourceGraph, foldHealth, foldSync } from "@hanzo/cd"
import {
manifestText,
normalizeDeployApp,
parseApplications,
toGitopsApp,
toLogLines,
toManagedResource,
toResourceTree,
toRollbackHistory,
} from "./adapt"
// A wall-clock guard: proves a call TERMINATES (no infinite loop) fast.
function within<T>(ms: number, fn: () => T): T {
const t0 = Date.now()
const out = fn()
const dt = Date.now() - t0
expect(dt).toBeLessThan(ms)
return out
}
// ── A. Robustness: hostile top-level inputs never throw ──────────────────────
describe("RED robustness — malformed payloads never crash", () => {
const junk: unknown[] = [null, undefined, 42, "str", true, [], {}, { applications: null }, { apps: 7 }, NaN]
it("parseApplications tolerates any junk → array, never throws", () => {
for (const j of junk) expect(Array.isArray(parseApplications(j))).toBe(true)
})
it("normalizeDeployApp of junk → safe defaults", () => {
for (const j of junk) {
const a = normalizeDeployApp(j)
expect(typeof a.name).toBe("string")
expect(a.namespace).toBe("hanzo") // default
expect(Array.isArray(a.endpoints)).toBe(true)
}
})
it("toResourceTree / toManagedResource / toLogLines tolerate junk", () => {
for (const j of junk) {
expect(Array.isArray(toResourceTree(j).nodes)).toBe(true)
expect(() => toManagedResource(j)).not.toThrow()
expect(Array.isArray(toLogLines(j))).toBe(true)
}
})
it("wrong-typed fields (health object, numeric name) degrade to empty, dropped", () => {
const rows = parseApplications({ applications: [{ name: 123, health: { evil: 1 }, sync: ["x"] }, { name: "ok" }] })
expect(rows.map((r) => r.name)).toEqual(["ok"]) // numeric-name row dropped
const g = toGitopsApp(normalizeDeployApp({ name: "z", health: { nested: true } }))
expect(g.health).toBe("Unknown") // object health → '' → Unknown, NOT guessed up
})
})
// ── B. CYCLE SAFETY — the headline #3 case ───────────────────────────────────
// Adversarial parentRefs must not wedge buildResourceGraph into an infinite loop.
const treeJSON = (nodes: object[]) => toResourceTree({ nodes })
const node = (ref: string, kind: string, parents: string[] = []) => ({
ref,
kind,
name: ref,
namespace: "demo",
parentRefs: parents.map((p) => ({ ref: p })),
})
describe("RED cycle safety — buildResourceGraph terminates on hostile parentRefs", () => {
it("self-referencing parent (A→A) terminates; node survives as a root", () => {
const g = within(1000, () => buildResourceGraph(treeJSON([node("A", "Pod", ["A"])])))
expect(g.nodes.map((n) => n.id)).toContain("A")
expect(g.nodes.find((n) => n.id === "A")!.depth).toBe(0)
expect(g.edges).toHaveLength(0) // self-edge dropped
})
// ── HIGH-1 (FIXED): buildResourceGraph used to THROW whenever a node's parent
// appeared LATER in the array — `children` was seeded lazily per-node, so
// `children.get(pid)!.push(id)` dereferenced undefined. That is EVERY cycle AND
// any tree listing a child before its parent (K8s lists are not topologically
// sorted), and with no error boundary the whole SPA white-screened. `children`
// is now pre-seeded for every id, so order and cycles are both safe. These are
// the regression tests: each MUST build a graph, never throw. ───────────────
it("BENIGN acyclic tree that lists a child before its parent builds", () => {
// Deployment listed before the Application that owns it — a totally normal,
// non-adversarial ordering a backend may emit.
const g = within(1000, () =>
buildResourceGraph(treeJSON([node("dep", "Deployment", ["app"]), node("app", "Application")])),
)
expect(g.nodes.map((n) => n.id).sort()).toEqual(["app", "dep"])
expect(g.nodes.find((n) => n.id === "app")!.depth).toBe(0)
expect(g.nodes.find((n) => n.id === "dep")!.depth).toBe(1)
})
it("2-cycle with NO external root (A↔B) does not crash", () => {
const g = within(1000, () =>
buildResourceGraph(treeJSON([node("A", "Pod", ["B"]), node("B", "Pod", ["A"])])),
)
expect(Array.isArray(g.nodes)).toBe(true) // unreachable-from-a-root nodes may drop; must not throw
})
it("2-cycle WITH an external root (R→A, A↔B) renders R, A, B", () => {
const g = within(1000, () =>
buildResourceGraph(treeJSON([node("R", "App"), node("A", "Pod", ["R", "B"]), node("B", "Pod", ["A"])])),
)
expect(g.nodes.map((n) => n.id).sort()).toEqual(["A", "B", "R"])
})
it("3-cycle (A→B→C→A) with a root renders 4 nodes", () => {
const g = within(1000, () =>
buildResourceGraph(
treeJSON([node("R", "App"), node("A", "Pod", ["R", "C"]), node("B", "Pod", ["A"]), node("C", "Pod", ["B"])]),
),
)
expect(g.nodes.map((n) => n.id).sort()).toEqual(["A", "B", "C", "R"])
})
it("dangling parent (points at a non-existent uid) → node is a root, no crash", () => {
const g = within(1000, () => buildResourceGraph(treeJSON([node("A", "Pod", ["ghost-does-not-exist"])])))
expect(g.nodes.find((n) => n.id === "A")!.depth).toBe(0)
})
it("large deep chain (2000 nodes) terminates quickly", () => {
const chain = Array.from({ length: 2000 }, (_, i) => node(`n${i}`, "Pod", i ? [`n${i - 1}`] : []))
const g = within(6000, () => buildResourceGraph(treeJSON(chain)))
expect(g.nodes.length).toBe(2000)
})
it("every node self-parents (all self-cycles) → all render as roots, terminates", () => {
const many = Array.from({ length: 500 }, (_, i) => node(`s${i}`, "Pod", [`s${i}`]))
const g = within(4000, () => buildResourceGraph(treeJSON(many)))
expect(g.nodes.length).toBe(500)
})
})
// ── C. MISLABEL — the fold's one dangerous direction: bad → Healthy/Synced ────
describe("RED mislabel (FIXED) — a BAD state is never up-guessed to a GOOD one", () => {
it("health words that MEAN broken never fold to Healthy", () => {
// The positive substring up-guess is gone: unrecognized → honest Unknown.
expect(foldHealth("Broken")).toBe("Unknown") // contains 'ok'
expect(foldHealth("revoked")).toBe("Unknown") // cert/token revoked
expect(foldHealth("invoked")).toBe("Unknown")
expect(foldHealth("NotReady")).toBe("Unknown") // contains 'ready'
expect(foldHealth("unavailable")).toBe("Unknown") // contains 'available'
expect(foldHealth("MinimumReplicasUnavailable")).toBe("Unknown")
// The canonical vocabulary cloud actually emits still folds exactly:
expect(foldHealth("healthy")).toBe("Healthy")
expect(foldHealth("")).toBe("Unknown")
expect(foldHealth("degraded")).toBe("Degraded")
expect(foldHealth("CrashLoopBackOff")).toBe("Degraded") // bad-substring — safe direction
})
it("sync words that MEAN not-synced never fold to Synced", () => {
expect(foldSync("notsynced")).toBe("Unknown") // means NOT synced
expect(foldSync("unsynced")).toBe("Unknown")
expect(foldSync("NotSynced")).toBe("Unknown")
// Canonical + separator-normalized cases still fold exactly:
expect(foldSync("synced")).toBe("Synced")
expect(foldSync("out-of-sync")).toBe("OutOfSync") // hyphens normalized at the source
expect(foldSync("out-of-sync".replace(/[-_]/g, ""))).toBe("OutOfSync")
expect(foldSync("")).toBe("Unknown")
})
it("SAFE: a Degraded app is never shown Healthy through the adapter", () => {
const g = toGitopsApp(normalizeDeployApp({ name: "x", health: "degraded" }))
expect(g.health).toBe("Degraded")
})
})
// ── D. Honest sync derivation (no fabricated Synced) ─────────────────────────
describe("RED honest sync — derived state does not fabricate Synced", () => {
it("missing sync + version drift → OutOfSync", () => {
expect(toGitopsApp(normalizeDeployApp({ name: "a", version: "v2", runningVersion: "v1" })).sync).toBe("OutOfSync")
})
it("missing sync + equal version → Synced (declared==running proxy)", () => {
expect(toGitopsApp(normalizeDeployApp({ name: "a", version: "v1", runningVersion: "v1" })).sync).toBe("Synced")
})
it("missing sync + BOTH versions empty → Synced (''==''), a benign over-report", () => {
// documents: an app the plane reports with no versions folds to Synced, not Unknown.
expect(toGitopsApp(normalizeDeployApp({ name: "a" })).sync).toBe("Synced")
})
})
// ── E. Manifest handling — object/string/circular ────────────────────────────
describe("RED manifest — object/string/circular never throw", () => {
it("circular object manifest → '' (no throw)", () => {
const circular: Record<string, unknown> = { kind: "Deployment" }
circular.self = circular
expect(manifestText(circular)).toBe("")
const m = toManagedResource({ ref: "r", liveManifest: circular })
expect(m.liveState).toBe("")
})
it("object manifest → pretty JSON; string manifest → verbatim; null → ''", () => {
expect(toManagedResource({ ref: "r", liveManifest: { a: 1 } }).liveState).toContain('"a": 1')
expect(toManagedResource({ ref: "r", liveManifest: "raw yaml here" }).liveState).toBe("raw yaml here")
expect(toManagedResource({ ref: "r", liveManifest: null }).liveState).toBe("")
})
it("a manifest string carrying HTML is passed through as TEXT (React escapes on render)", () => {
const evil = "<img src=x onerror=alert(document.cookie)>"
// The adapter must NOT interpret it; it stays a plain string (rendered via {text}).
expect(toManagedResource({ ref: "r", liveManifest: evil }).liveState).toBe(evil)
})
})
// ── F. Logs — array gap + unbounded blob (no client cap) ─────────────────────
describe("RED logs (FIXED) — fidelity + bound", () => {
it("logs delivered as an ARRAY yield lines (blob AND array shapes)", () => {
expect(toLogLines({ pod: "p", logs: ["line-1", "line-2"] })).toEqual([
{ content: "line-1", podName: "p" },
{ content: "line-2", podName: "p" },
])
// the blob shape still works
expect(toLogLines({ pod: "p", logs: "a\nb" }).map((l) => l.content)).toEqual(["a", "b"])
})
it("a huge blob is line-capped client-side (keeps the most recent lines)", () => {
const blob = Array.from({ length: 50_000 }, (_, i) => `l${i}`).join("\n")
const out = toLogLines({ pod: "p", logs: blob })
expect(out.length).toBe(2000) // MAX_LOG_LINES
expect(out[out.length - 1].content).toBe("l49999") // tail kept, not head
})
it("a single enormous line is clamped (no one-megabyte text node)", () => {
const huge = "x".repeat(50_000)
const out = toLogLines({ pod: "p", logs: huge })
expect(out[0].content.length).toBeLessThanOrEqual(4001) // MAX_LOG_LINE + ellipsis
})
})
// ── G. Rollback semver filter — injection + hygiene ──────────────────────────
describe("RED rollback — only clean semver, current excluded, no injection", () => {
it("drops non-semver + injection-shaped tags, excludes current, newest-first, dedupes", () => {
const h = toRollbackHistory("v1.800.1", [
"v1.800.0",
"latest",
"main",
"deadbeef",
"v1.800.1", // == current → excluded
"v1.799.15",
"v1.799.15", // dup
"v1.2.3; rm -rf /", // injection shape → not semver → dropped
"'; DROP TABLE apps;--",
])
expect(h.map((r) => r.revision)).toEqual(["v1.800.0", "v1.799.15"])
expect(h[0].id).toBeGreaterThan(h[1].id)
})
it("pre-release semver IS offered as a rollback target (documents the accepted grammar)", () => {
const h = toRollbackHistory("v2.0.0", ["v1.9.9-rc.1", "v1.9.9"])
expect(h.map((r) => r.revision)).toContain("v1.9.9-rc.1")
})
it("empty history → []", () => {
expect(toRollbackHistory("v1.0.0", [])).toEqual([])
})
})
+149
View File
@@ -0,0 +1,149 @@
/**
* The /v1/deploy @hanzo/cd adapter pins the console CD app against cloud's
* REAL clients/deploy shapes (the hyphenated sync verdict, object manifests,
* parentRefs tree edges, the logs blob, clean-semver rollback).
*/
import { describe, expect, it } from "vitest"
import {
normalizeDeployApp,
parseApplications,
toGitopsApp,
toLogLines,
toManagedResource,
toResourceTree,
toRollbackHistory,
} from "./adapt"
describe("toGitopsApp", () => {
it("folds the hyphenated cloud sync verdict (the foldSync hyphen bug)", () => {
const a = parseApplications({
applications: [
{ name: "iam", namespace: "hanzo", env: "main", repository: "ghcr.io/hanzoai/iam", version: "v1.4.11", runningVersion: "v1.4.10", health: "progressing", healthMessage: "rolling", sync: "out-of-sync" },
],
})[0]
const g = toGitopsApp(a)
expect(g.sync).toBe("OutOfSync") // 'out-of-sync' must NOT fall through to Unknown
expect(g.health).toBe("Progressing")
expect(g.revision).toBe("v1.4.11")
expect(g.source).toEqual({ repoURL: "ghcr.io/hanzoai/iam" })
expect(g.message).toBe("rolling")
expect(g.project).toBe("main")
})
it("derives sync from declared-vs-running when the plane omits it", () => {
const [drift, synced] = parseApplications([
{ name: "a", repository: "r", version: "v2", runningVersion: "v1" },
{ name: "b", repository: "r", version: "v1", runningVersion: "v1" },
])
expect(toGitopsApp(drift).sync).toBe("OutOfSync")
expect(toGitopsApp(synced).sync).toBe("Synced")
})
})
describe("parseApplications", () => {
it("tolerates a bare array and drops nameless rows", () => {
expect(parseApplications([{ name: "cloud" }, { name: "" }]).map((a) => a.name)).toEqual(["cloud"])
})
})
describe("toResourceTree", () => {
it("maps parentRefs[].ref into linking AppTreeNodes (uid == ref token)", () => {
const tree = toResourceTree({
nodes: [
{ group: "hanzo.ai", version: "v1", kind: "App", namespace: "hanzo", name: "iam", ref: "hanzo.ai:App:hanzo:iam", parentRefs: [] },
{ group: "apps", version: "v1", kind: "Deployment", namespace: "hanzo", name: "iam", ref: "apps:Deployment:hanzo:iam", health: "progressing", parentRefs: [{ ref: "hanzo.ai:App:hanzo:iam" }] },
],
})
expect(tree.nodes).toHaveLength(2)
const dep = tree.nodes.find((n) => n.kind === "Deployment")!
expect(dep.uid).toBe("apps:Deployment:hanzo:iam")
expect(dep.parentRefs?.[0].uid).toBe("hanzo.ai:App:hanzo:iam") // matches the App node's uid → the tree links
expect(dep.health?.status).toBe("Progressing")
})
})
describe("toManagedResource", () => {
it("stringifies object manifests + reads the nested desired manifest", () => {
const m = toManagedResource({
ref: { group: "apps", version: "v1", kind: "Deployment", namespace: "hanzo", name: "iam", ref: "apps:Deployment:hanzo:iam" },
liveManifest: { kind: "Deployment", spec: { replicas: 1 } },
diff: { modified: true, desiredManifest: { kind: "Deployment", spec: { replicas: 2 } } },
})
expect(m.uid).toBe("apps:Deployment:hanzo:iam")
expect(m.liveState).toContain('"replicas": 1')
expect(m.targetState).toContain('"replicas": 2')
})
})
describe("toLogLines", () => {
it("splits the /v1/deploy logs blob and tags the pod", () => {
const lines = toLogLines({ pod: "iam-abc", logs: "first\nsecond\n" })
expect(lines).toHaveLength(2)
expect(lines[0]).toEqual({ content: "first", podName: "iam-abc" })
})
it("honest empty when no logs", () => {
expect(toLogLines({ pod: "", logs: "" })).toEqual([])
})
})
describe("toRollbackHistory", () => {
it("offers only clean-semver releases, current excluded, newest first", () => {
const h = toRollbackHistory("v1.800.1", ["v1.800.0", "latest", "v1.799.15", "v1.800.1", "main"])
expect(h.map((r) => r.revision)).toEqual(["v1.800.0", "v1.799.15"])
expect(h[0].id).toBeGreaterThan(h[1].id) // monotonic ids for the dialog
})
})
// ── the ACTUAL argoproj shape /v1/deploy/applications serves in production ────
// Captured live (cloud v1.801.109). The first cut read only flat keys, so the
// fleet bound to zero rows against real data; this pins the real contract.
describe("live argoproj-shaped application", () => {
const LIVE = {
apiVersion: "argoproj.io/v1alpha1",
kind: "Application",
metadata: {
name: "admin-guard",
namespace: "hanzo",
uid: "257fcb88-974d-46fc-8ac2-6f8b86a1f15f",
creationTimestamp: "2026-07-15T05:31:10Z",
labels: { "hanzo.ai/env": "main", "hanzo.ai/instance": "admin-guard" },
},
spec: {
source: { repoURL: "https://git.hanzo.ai/hanzoai/universe", path: "infra/k8s/operator/crs", targetRevision: "main" },
destination: { server: "https://kubernetes.default.svc", namespace: "hanzo" },
project: "default",
},
status: {
sync: { status: "Synced", revision: "v0.1.4" },
health: { status: "Healthy", message: "Running: available" },
resources: [],
summary: { images: ["ghcr.io/hanzoai/admin-guard:v0.1.4"] },
},
}
it("binds name/env/health/sync/revision/repository from the nested shape", () => {
const a = normalizeDeployApp(LIVE)
expect(a.name).toBe("admin-guard")
expect(a.namespace).toBe("hanzo")
expect(a.env).toBe("main")
expect(a.health).toBe("Healthy")
expect(a.sync).toBe("Synced")
expect(a.version).toBe("v0.1.4")
expect(a.repository).toBe("ghcr.io/hanzoai/admin-guard")
expect(a.runningVersion).toBe("v0.1.4")
})
it("folds to a Healthy/Synced gitops app (the fleet row renders)", () => {
const g = toGitopsApp(normalizeDeployApp(LIVE))
expect(g.name).toBe("admin-guard")
expect(g.health).toBe("Healthy")
expect(g.sync).toBe("Synced")
})
it("still reads the flat native shape (both wires supported)", () => {
const a = normalizeDeployApp({ name: "x", health: "degraded", sync: "out-of-sync", version: "v1.2.3" })
expect(a.name).toBe("x")
expect(a.health).toBe("degraded")
expect(toGitopsApp(a).sync).toBe("OutOfSync")
})
})
+271
View File
@@ -0,0 +1,271 @@
/**
* The CD data adapter maps cloud's `/v1/deploy` DTOs INTO the shared view-models
* of `@hanzo/cd` (GitopsApplication / ResourceTree / ManagedResource / LogLine
* / RevisionHistory). Reuses each package's OWN pure folds (foldHealth/foldSync) so
* status vocabulary is one way. No React, no I/O unit-testable.
*
* Contract (cloud clients/deploy):
* applications: [{name,namespace,env,role,repository,version,runningVersion,
* health,healthMessage,sync,phase,endpoints}]
* {name}/tree: {application, nodes:[{group,version,kind,namespace,name,ref,uid,
* createdAt,health,healthMessage,sync,version,parentRefs:[{,ref}]}]}
* {name}/resource/{ref}: {ref, health, healthMessage, liveManifest:{},
* desiredSource, diff:{modified,desiredManifest:{}}}
* {name}/logs: {application, pod, container, logs:"…", note?}
*/
import {
foldHealth,
foldSync,
type AppTreeNode,
type GitopsApplication,
type LogLine,
type ManagedResource,
type ResourceRef,
type ResourceTree,
type RevisionHistory,
} from "@hanzo/cd"
// ── optional-safe helpers (snake_case + camelCase tolerant) ──────────────────
const str = (v: unknown): string => (typeof v === "string" ? v : "")
const arr = (v: unknown): unknown[] => (Array.isArray(v) ? v : [])
const rec = (v: unknown): Record<string, unknown> =>
v && typeof v === "object" ? (v as Record<string, unknown>) : {}
const pick = (r: Record<string, unknown>, ...keys: string[]): unknown => {
for (const k of keys) if (r[k] !== undefined && r[k] !== null) return r[k]
return undefined
}
const strList = (v: unknown): string[] => arr(v).map(str).filter(Boolean)
const epochMs = (v: unknown): number | undefined => {
const s = str(v)
if (!s) return undefined
const t = Date.parse(s)
return Number.isNaN(t) ? undefined : t
}
// The cloud wire uses hyphenated verdicts (`out-of-sync`); @hanzo/cd's folds
// strip whitespace but not hyphens, so normalize `[-_]` out before folding.
const fHealth = (raw: string) => foldHealth(raw.replace(/[-_]/g, ""))
const fSync = (raw: string) => foldSync(raw.replace(/[-_]/g, ""))
/** A manifest object → pretty JSON text; a string verbatim; else ''. */
export function manifestText(v: unknown): string {
if (typeof v === "string") return v
if (v && typeof v === "object") {
try {
return JSON.stringify(v, null, 2)
} catch {
return ""
}
}
return ""
}
/** repo basename of an image repository: `ghcr.io/hanzoai/iam` → `iam`. */
export function repoBaseName(repository: string): string {
const s = (repository || "").trim().replace(/:.*/, "")
return (s.split("/").filter(Boolean).pop() ?? "").toLowerCase()
}
// ── Application ──────────────────────────────────────────────────────────────
export interface DeployApp {
name: string
namespace: string
env: string
role: string
repository: string
version: string
runningVersion: string
health: string
healthMessage: string
sync: string
phase: string
endpoints: string[]
/** Prior release tags the plane records (forward-compat rollback source); [] today. */
revisions: string[]
}
/** Normalize a raw /v1/deploy application row to a stable internal shape. */
/**
* Normalize one application from EITHER wire shape:
*
* - argoproj (what `/v1/deploy/applications` actually serves): the projection
* nests everything `metadata.{name,namespace,labels}`, `spec.source.*`,
* `spec.project`, `status.{sync,health}.status`, `status.summary.images[]`.
* - flat native: `{name,namespace,env,repository,version,health,sync,…}`.
*
* Both are read here so the app binds regardless of which the plane returns
* flat keys win when present, then the nested argo fields fill in.
*/
export function normalizeDeployApp(raw: unknown): DeployApp {
const r = rec(raw)
const meta = rec(pick(r, "metadata"))
const spec = rec(pick(r, "spec"))
const status = rec(pick(r, "status"))
const source = rec(pick(spec, "source"))
const dest = rec(pick(spec, "destination"))
const sync = rec(pick(status, "sync"))
const health = rec(pick(status, "health"))
const labels = rec(pick(meta, "labels"))
const summary = rec(pick(status, "summary"))
// `status.summary.images: ["ghcr.io/hanzoai/x:v1.2.3"]` → repository + tag.
const image = strList(pick(summary, "images"))[0] ?? ""
const cut = image.lastIndexOf(":")
const imageRepo = cut > 0 ? image.slice(0, cut) : image
const imageTag = cut > 0 ? image.slice(cut + 1) : ""
return {
name: str(pick(r, "name")) || str(pick(meta, "name")),
namespace:
str(pick(r, "namespace", "ns")) || str(pick(meta, "namespace")) || str(pick(dest, "namespace")) || "hanzo",
env: str(pick(r, "env", "environment")) || str(pick(labels, "hanzo.ai/env")) || str(pick(source, "targetRevision")),
role: str(pick(r, "role")),
// Prefer the deployed image repository; fall back to the manifest repo URL.
repository: str(pick(r, "repository", "repo")) || imageRepo || str(pick(source, "repoURL")),
version: str(pick(r, "version", "tag")) || str(pick(sync, "revision")) || imageTag,
runningVersion: str(pick(r, "runningVersion", "running_version")) || imageTag,
health: str(pick(r, "health")) || str(pick(health, "status")),
healthMessage:
str(pick(r, "healthMessage", "health_message", "message")) || str(pick(health, "message")),
sync: str(pick(r, "sync", "syncStatus", "sync_status")) || str(pick(sync, "status")),
phase: str(pick(r, "phase")) || str(pick(health, "status")),
endpoints: strList(pick(r, "endpoints", "urls")),
revisions: strList(pick(r, "revisions", "history", "tags")),
}
}
/** Parse the applications list payload (array or {applications|apps|items}). */
export function parseApplications(data: unknown): DeployApp[] {
const rows = Array.isArray(data) ? data : arr(pick(rec(data), "applications", "apps", "items", "services"))
return rows.map(normalizeDeployApp).filter((a) => a.name)
}
/** Fold a normalized app into the @hanzo/cd application view-model. */
export function toGitopsApp(a: DeployApp): GitopsApplication {
return {
name: a.name,
namespace: a.namespace,
project: a.env || undefined,
health: fHealth(a.health),
// desired (declared version) vs live (runningVersion): equal ⇒ Synced.
sync: a.sync ? fSync(a.sync) : fSync(a.version === a.runningVersion ? "synced" : "outofsync"),
revision: a.version || undefined,
source: a.repository ? { repoURL: a.repository } : undefined,
message: a.healthMessage || undefined,
}
}
// ── tree → ResourceTree (AppTreeNode with linking parentRefs) ────────────────
/** Map a /v1/deploy ResourceRef DTO (carrying its `ref` token) to a linking ref.
* The `ref` token is the stable id shared by a node and its children's
* parentRefs, so it drives resourceId() the tree links on it. */
function toRef(raw: unknown): ResourceRef {
const r = rec(raw)
const token = str(pick(r, "ref"))
return {
uid: token || str(pick(r, "uid")),
group: str(pick(r, "group")),
version: str(pick(r, "version")),
kind: str(pick(r, "kind")),
namespace: str(pick(r, "namespace", "ns")),
name: str(pick(r, "name")),
}
}
function toTreeNode(raw: unknown): AppTreeNode {
const r = rec(raw)
const self = toRef(r)
const version = str(pick(r, "version"))
return {
...self,
parentRefs: arr(pick(r, "parentRefs", "parent_refs")).map(toRef),
health: { status: fHealth(str(pick(r, "health"))), message: str(pick(r, "healthMessage")) || undefined },
sync: pick(r, "sync") ? fSync(str(pick(r, "sync"))) : undefined,
images: version ? [version] : [],
createdAt: epochMs(pick(r, "createdAt", "created_at", "creationTimestamp")),
}
}
export function toResourceTree(raw: unknown): ResourceTree {
const r = rec(raw)
const nodes = arr(pick(r, "nodes", "resources"))
.map(toTreeNode)
.filter((n) => n.name && n.kind)
// Defense-in-depth: never render a Secret node (its live manifest carries
// base64 data). Cloud already excludes Secrets from the tree; drop any that
// slip through so the client never surfaces one.
.filter((n) => n.kind !== "Secret")
return { nodes }
}
// ── resource → ManagedResource (live + desired for the node drawer/diff) ─────
export function toManagedResource(raw: unknown): ManagedResource {
const r = rec(raw)
const refField = pick(r, "ref")
const self: ResourceRef =
typeof refField === "string"
? { uid: refField, group: "", version: "", kind: "", namespace: "", name: "" }
: toRef(refField)
const diffObj = rec(pick(r, "diff"))
const desired = pick(r, "desiredManifest", "desired") ?? pick(diffObj, "desiredManifest", "desired")
return {
...self,
liveState: manifestText(pick(r, "liveManifest", "live_manifest", "live", "manifest")),
targetState: manifestText(desired),
}
}
// ── logs (blob OR array) → LogLine[], bounded ────────────────────────────────
// Client-side caps so one giant log payload can't bloat the DOM even if the
// server's `?tail` is absent or ignored: keep the last MAX_LOG_LINES, and clamp
// any single line (a newline-free megabyte otherwise renders as one huge node).
const MAX_LOG_LINES = 2000
const MAX_LOG_LINE = 4000
export function toLogLines(raw: unknown): LogLine[] {
const r = rec(raw)
const pod = str(pick(r, "pod")) || undefined
// Tolerate both shapes: a newline blob (`logs:"…"`) and a line array
// (`logs:[…]` / `lines:[…]`), which the blob-only path silently dropped to [].
const listed = arr(pick(r, "logs", "lines", "log", "output"))
const lines = listed.length
? listed.map(str)
: str(pick(r, "logs", "log", "output")).split("\n")
const kept = lines.filter((l) => l.length > 0).slice(-MAX_LOG_LINES)
return kept.map((l) => ({ content: l.length > MAX_LOG_LINE ? l.slice(0, MAX_LOG_LINE) + "…" : l, podName: pod }))
}
// ── git tags → RevisionHistory[] (rollback targets; cloud takes a clean semver) ─
const SEMVER = /^v\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/
export const isReleaseTag = (t: string): boolean => SEMVER.test(t.trim())
/** Build rollback history from an app's real git tags (clean semver, newest first,
* current excluded). `revision` carries the tag cloud's rollback endpoint accepts. */
export function toRollbackHistory(currentTag: string, tags: string[]): RevisionHistory[] {
const seen = new Set<string>([currentTag.trim()])
const clean: string[] = []
for (const raw of tags) {
const t = raw.trim()
if (!t || seen.has(t) || !isReleaseTag(t)) continue
seen.add(t)
clean.push(t)
}
clean.sort(compareSemverDesc)
return clean.map((revision, i) => ({ id: clean.length - i, revision }))
}
export function compareSemverDesc(a: string, b: string): number {
const pa = semverParts(a)
const pb = semverParts(b)
for (let i = 0; i < 3; i++) if (pa[i] !== pb[i]) return pb[i] - pa[i]
return b.localeCompare(a)
}
function semverParts(tag: string): [number, number, number] {
const m = /^v?(\d+)\.(\d+)\.(\d+)/.exec(tag.trim())
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : [-1, -1, -1]
}
+90
View File
@@ -0,0 +1,90 @@
/**
* The typed client for cloud's native CD plane (`/v1/deploy`). Same-origin: the
* cd.hanzo.ai ingress peels `/v1/deploy/*` off to the cloud binary, so the SPA
* calls it with `credentials: 'include'` and the first-party `hanzo_iam_token`
* cookie (set by the PKCE login) rides cloud validates it (SuperAdmin gate). A
* 401/403 surfaces as an `ApiError` so the app can send the user back to sign in.
*
* Responses are mapped INTO the shared `@hanzo/cd` view-models by `./adapt`.
*/
import type { LogLine, ManagedResource, ResourceTree } from "@hanzo/cd"
import { parseApplications, toLogLines, toManagedResource, toResourceTree, type DeployApp } from "./adapt"
export class ApiError extends Error {
status: number
constructor(message: string, status: number) {
super(message)
this.name = "ApiError"
this.status = status
}
}
/** True when the error means "not signed in / not authorized" (→ sign-in screen). */
export const isAuthError = (e: unknown): boolean => e instanceof ApiError && (e.status === 401 || e.status === 403)
const url = (path: string): string => `/v1/deploy/${path.replace(/^\/+/, "")}`
async function request<T>(method: "GET" | "POST", path: string, body?: unknown): Promise<T> {
let res: Response
try {
res = await fetch(url(path), {
method,
credentials: "include",
headers: body !== undefined ? { "Content-Type": "application/json" } : undefined,
body: body !== undefined ? JSON.stringify(body) : undefined,
})
} catch (e) {
throw new ApiError(e instanceof Error ? e.message : "network error", 0)
}
if (!res.ok) {
let msg = `${res.status} ${res.statusText}`
try {
const j = (await res.json()) as { error?: string; message?: string }
msg = j.error || j.message || msg
} catch {
/* non-JSON body */
}
throw new ApiError(msg, res.status)
}
if (res.status === 204) return undefined as T
return (await res.json()) as T
}
export const DeployApi = {
/** The fleet: every operator App CR (`GET /v1/deploy/applications`). */
applications: async (): Promise<DeployApp[]> => parseApplications(await request<unknown>("GET", "applications")),
/**
* One application's owned-resource tree
* (`GET /v1/deploy/applications/:name/resource-tree`).
*
* The wired route lives under `applications/` the shorter `:name/tree` form
* belongs to an unregistered handler, so it 404s against the live plane.
*/
tree: async (name: string): Promise<ResourceTree> =>
toResourceTree(await request<unknown>("GET", `applications/${encodeURIComponent(name)}/resource-tree`)),
/** One tree node's live manifest + desired-vs-live (`GET /v1/deploy/:name/resource/:ref`). */
resource: async (name: string, ref: string): Promise<ManagedResource> =>
toManagedResource(await request<unknown>("GET", `${encodeURIComponent(name)}/resource/${encodeURIComponent(ref)}`)),
/** The newest pod's logs (`GET /v1/deploy/:name/logs`). */
logs: async (name: string, tail = 300): Promise<LogLine[]> =>
toLogLines(await request<unknown>("GET", `${encodeURIComponent(name)}/logs?tail=${tail}`)),
/** Pin the CR image to a prior clean-semver release (`POST /v1/deploy/:name/rollback`). */
rollback: async (name: string, tag: string): Promise<void> => {
await request<unknown>("POST", `${encodeURIComponent(name)}/rollback`, { tag })
},
/** Request an operator reconcile now (`POST /v1/deploy/:name/sync`). */
sync: async (name: string): Promise<void> => {
await request<unknown>("POST", `${encodeURIComponent(name)}/sync`, {})
},
}
/** Read the `hanzo_iam_token` cookie the PKCE login sets (non-httpOnly by design). */
export function hasSession(): boolean {
return document.cookie.split(";").some((c) => c.trim().startsWith("hanzo_iam_token="))
}
+14
View File
@@ -0,0 +1,14 @@
import { StrictMode } from "react"
import { createRoot } from "react-dom/client"
import { App } from "./App"
import { ErrorBoundary } from "./shell/ErrorBoundary"
import "./styles.css"
createRoot(document.getElementById("root")!).render(
<StrictMode>
<ErrorBoundary>
<App />
</ErrorBoundary>
</StrictMode>,
)
+51
View File
@@ -0,0 +1,51 @@
import { Component, type ErrorInfo, type ReactNode } from "react"
/**
* Top-level boundary so a single component render throw can never white-screen
* the whole dashboard. A caught error shows a recover panel (back to the fleet /
* reload) instead of an unmounted root. Deliberately dependency-free.
*/
export class ErrorBoundary extends Component<{ children: ReactNode }, { error: Error | null }> {
state = { error: null as Error | null }
static getDerivedStateFromError(error: Error) {
return { error }
}
componentDidCatch(error: Error, info: ErrorInfo) {
// Surface to the console for the operator; never log identity/token.
console.error("[cd] render error", error, info.componentStack)
}
private reset = () => {
this.setState({ error: null })
if (location.hash && location.hash !== "#/") location.hash = "#/"
}
render() {
if (!this.state.error) return this.props.children
return (
<div style={{ maxWidth: 520, margin: "12vh auto", padding: "0 24px", textAlign: "center", fontFamily: "'Geist', ui-sans-serif, system-ui, sans-serif" }}>
<h1 style={{ fontSize: 20, fontWeight: 700, margin: "0 0 8px" }}>Something went wrong</h1>
<p style={{ color: "#8a8a8a", fontSize: 14, margin: "0 0 20px" }}>
A view failed to render. Your fleet is unaffected this is only the dashboard.
</p>
<div style={{ display: "flex", gap: 10, justifyContent: "center" }}>
<button onClick={this.reset} style={btn(true)}> Back to fleet</button>
<button onClick={() => location.reload()} style={btn(false)}>Reload</button>
</div>
</div>
)
}
}
const btn = (primary: boolean): React.CSSProperties => ({
padding: "9px 16px",
fontSize: 14,
fontWeight: 600,
cursor: "pointer",
borderRadius: 8,
border: primary ? "0" : "1px solid #2a2a2a",
background: primary ? "#fff" : "transparent",
color: primary ? "#000" : "inherit",
})
+102
View File
@@ -0,0 +1,102 @@
/**
* The Hanzo CD topbar the shared shell chrome for the dedicated dashboard: the
* small Hanzo mark + wordmark (top-left), the environment scope switcher (the CD
* "project" dimension main/test/dev, the operator namespaces), a refresh, and
* sign-out. Lean + self-contained (dark, Geist) so the static SPA has no heavy
* shell dependency; the org/projectIAM switcher lights up when /v1/iam is routed
* on cd.hanzo.ai (today the SuperAdmin cookie sees the whole fleet).
*/
const MARK = (size: number, fill: string) => (
<svg width={size} height={size} viewBox="0 0 67 67" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M22.21 67V44.6369H0V67H22.21Z" fill={fill} />
<path d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z" fill={fill} />
<path d="M22.21 0H0V22.3184H22.21V0Z" fill={fill} />
<path d="M66.7198 0H44.5098V22.3184H66.7198V0Z" fill={fill} />
<path d="M66.7198 67V44.6369H44.5098V67H66.7198Z" fill={fill} />
</svg>
)
export interface EnvOption {
id: string
label: string
count: number
}
export function Topbar({
envs,
env,
onEnv,
onRefresh,
refreshing,
onSignOut,
}: {
envs: EnvOption[]
env: string
onEnv: (id: string) => void
onRefresh: () => void
refreshing?: boolean
onSignOut: () => void
}) {
return (
<header
style={{
display: "flex",
alignItems: "center",
gap: 12,
padding: "10px 16px",
borderBottom: "1px solid var(--cd-border)",
background: "var(--cd-surface)",
position: "sticky",
top: 0,
zIndex: 20,
flexWrap: "wrap",
}}
>
<a href="/" style={{ display: "flex", alignItems: "center", gap: 9, textDecoration: "none", color: "var(--cd-fg-strong)" }}>
<span style={{ display: "inline-flex" }}>{MARK(18, "#fff")}</span>
<span style={{ fontWeight: 800, letterSpacing: "-0.02em", fontSize: 15 }}>Hanzo CD</span>
</a>
{envs.length > 1 ? (
<div style={{ display: "inline-flex", gap: 2, background: "var(--cd-surface-2)", borderRadius: 8, padding: 2, border: "1px solid var(--cd-border)" }}>
{[{ id: "", label: "All", count: envs.reduce((n, e) => n + e.count, 0) }, ...envs].map((o) => {
const on = o.id === env
return (
<button
key={o.id || "all"}
type="button"
onClick={() => onEnv(o.id)}
className="cd-mono"
style={{
border: 0,
cursor: "pointer",
fontSize: 12,
fontWeight: 600,
padding: "5px 10px",
borderRadius: 6,
background: on ? "var(--cd-border-strong)" : "transparent",
color: on ? "var(--cd-fg-strong)" : "var(--cd-fg-muted)",
}}
>
{o.label}
<span style={{ opacity: 0.6, marginLeft: 5 }}>{o.count}</span>
</button>
)
})}
</div>
) : null}
<span style={{ flex: 1 }} />
<button type="button" className="cd-btn" onClick={onRefresh} disabled={refreshing}>
{refreshing ? "Refreshing…" : "Refresh"}
</button>
<button type="button" className="cd-btn" onClick={onSignOut}>
Sign out
</button>
</header>
)
}
export { MARK }
+90
View File
@@ -0,0 +1,90 @@
/* Hanzo CD app chrome. The @hanzo/gitops components bring their OWN scoped CSS
(GitopsStyles + THEME_VARS); this is only the shell + page shell, dark-first,
Geist. Mobile-first: nothing scrolls the body horizontally. */
:root {
color-scheme: dark;
--cd-bg: #0a0a0a;
--cd-surface: #0d1117;
--cd-surface-2: #161b22;
--cd-border: #21262d;
--cd-border-strong: #30363d;
--cd-fg: #e7e7e7;
--cd-fg-muted: #8b949e;
--cd-fg-strong: #f0f6fc;
--cd-accent: #2f81f7;
--cd-sans: "Geist", "Geist Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--cd-mono: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
* {
box-sizing: border-box;
}
html,
body,
#root {
height: 100%;
margin: 0;
}
html {
overflow-x: hidden;
}
body {
font-family: var(--cd-sans);
background: var(--cd-bg);
color: var(--cd-fg);
-webkit-font-smoothing: antialiased;
}
.cd-app {
display: flex;
flex-direction: column;
min-height: 100%;
}
.cd-main {
flex: 1;
min-width: 0;
padding: 20px;
max-width: 1400px;
width: 100%;
margin: 0 auto;
}
@media (max-width: 640px) {
.cd-main {
padding: 14px;
}
}
.cd-btn {
font-family: inherit;
font-size: 13px;
font-weight: 600;
border: 1px solid var(--cd-border-strong);
background: var(--cd-surface-2);
color: var(--cd-fg);
border-radius: 8px;
padding: 8px 12px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 6px;
min-height: 36px;
}
.cd-btn:hover {
border-color: var(--cd-fg-muted);
}
.cd-btn--primary {
background: #fff;
color: #000;
border-color: #fff;
}
.cd-btn:disabled {
opacity: 0.5;
cursor: default;
}
.cd-muted {
color: var(--cd-fg-muted);
}
.cd-mono {
font-family: var(--cd-mono);
}

Some files were not shown because too many files have changed in this diff Show More