mirror of
https://github.com/hanzoai/gui.git
synced 2026-10-25 02:31:41 +00:00
gui converges on one canonical history, here
git.hanzo.ai and github.com/hanzoai/gui held two UNRELATED histories under one name: no shared commit, no merge-base, and no way to tell from either side that the other existed. The forge line ran 375 commits to 7.6.5 with the Tenant*/AppSwitcher/HanzoAppBar naming; the GitHub line ran to 8.1.x with the Org*/intent/rove refactor. Both were active this month. The tie is broken by what CONSUMERS resolve, which is not a matter of taste: every app in the fleet pins `@hanzogui/shell ^8.1.x`, and npm's latest is 8.1.5, whose source is byte-identical to the GitHub line. The forge line publishes 7.6.x — semver BELOW what every consumer asks for — so nothing it shipped could reach an app even when it built. That is why archiving the GitHub copy froze the shared shell for five surfaces: it archived the only lineage anyone runs. So this commit carries the 8.1.x TREE and BOTH PARENTS. Nothing is discarded and nothing is force-pushed: the 375 commits stay in main's own ancestry, and the tag `archive/gui-7x` names their tip so they are reachable without archaeology. Being a descendant of the previous main, it is an ordinary fast-forward — the `can_force_push = false` rule on this branch stays exactly as it was, which is the point. Replacing a history is not a thing this repo should permit, and it did not have to. Anything still wanted from the 7.6.x line is cherry-picked forward onto this.
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@hanzogui/shell": patch
|
||||
---
|
||||
|
||||
Tight app-first shell defaults: 14px base type scale (linear.app/vercel register, still CSS-var overridable), H-only header lockup (drop product wordmark), and Community hub (remove Showcase; Community → hanzo.app/community).
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
name: git-safety
|
||||
description: Git safety rules. INVOKE WHEN: git push, force push, git reset, git clean, destructive git, push force, reset hard. NEVER force push or do destructive git operations.
|
||||
---
|
||||
|
||||
# git safety
|
||||
|
||||
rules for git operations in this repo.
|
||||
|
||||
## rules
|
||||
|
||||
- NEVER force push (`git push --force` or `git push -f`)
|
||||
- NEVER `git reset --hard` without explicit permission
|
||||
- NEVER `git clean -fd` without explicit permission
|
||||
- prefer `git stash` over `git reset`
|
||||
- always `git pull` before `git push`
|
||||
|
||||
## forbidden commands
|
||||
|
||||
these require EXPLICIT user permission:
|
||||
- `git push --force`
|
||||
- `git push -f`
|
||||
- `git push --force-with-lease`
|
||||
- `git reset --hard`
|
||||
- `git clean -fd`
|
||||
- `git checkout -- .` (discards all changes)
|
||||
|
||||
## safe alternatives
|
||||
|
||||
instead of `git reset --hard`:
|
||||
- `git stash` to save changes
|
||||
- `git stash pop` to restore
|
||||
|
||||
instead of force push:
|
||||
- `git pull --rebase` then normal push
|
||||
- ask user how to resolve conflicts
|
||||
|
||||
## workflow
|
||||
|
||||
1. `git pull` before making changes
|
||||
2. make changes
|
||||
3. `git add` relevant files
|
||||
4. `git commit`
|
||||
5. `git pull` again before push
|
||||
6. `git push` (no force flags)
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
name: release-safety
|
||||
description: Release safety rules. INVOKE WHEN: yarn release, npm publish, release canary, release packages, publishing, skip checks, skip tests. NEVER skip checks or tests without explicit permission.
|
||||
---
|
||||
|
||||
# release safety
|
||||
|
||||
rules for running releases in this monorepo.
|
||||
|
||||
## rules
|
||||
|
||||
- NEVER add `--skip-checks` unless user explicitly says "skip checks"
|
||||
- NEVER add `--skip-tests` unless user explicitly says "skip tests"
|
||||
- if checks or tests fail, FIX THE ISSUES instead of skipping
|
||||
- ask user before adding any skip flags
|
||||
- the point of checks is to catch problems before publishing
|
||||
|
||||
## when things fail
|
||||
|
||||
1. read the error output carefully
|
||||
2. fix the underlying issue
|
||||
3. re-run the release
|
||||
4. do NOT just skip the failing step
|
||||
|
||||
## common flags
|
||||
|
||||
safe flags:
|
||||
- `--canary` - publish canary version
|
||||
- `--ci` - run in ci mode
|
||||
- `--dirty` - allow dirty working directory
|
||||
- `--build-fast` - faster builds
|
||||
|
||||
dangerous flags (require explicit permission):
|
||||
- `--skip-checks` - skips type checking
|
||||
- `--skip-tests` - skips test suite
|
||||
- `--skip-build` - skips building packages
|
||||
+1
-1
@@ -7,7 +7,7 @@ node_modules
|
||||
**/*/node_modules
|
||||
**/*/dist
|
||||
**/*/.turbo
|
||||
**/*/.gui
|
||||
**/*/.hanzogui
|
||||
|
||||
# expo
|
||||
**/*/.expo
|
||||
|
||||
@@ -16,7 +16,7 @@ body:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Hanzo GUI Version
|
||||
label: Gui Version
|
||||
render: markdown
|
||||
validations:
|
||||
required: true
|
||||
@@ -32,9 +32,9 @@ body:
|
||||
render: markdown
|
||||
description: |
|
||||
- If you don't include reproduction, we will close the issue.
|
||||
- The easiest way to reproduce is using the starter free repo: https://github.com/hanzoai/starter-free
|
||||
- The easiest way to reproduce is using the starter free repo: https://github.com/hanzogui/starter-free
|
||||
- Clone that repo and push your reproduction as a new commit.
|
||||
- For simpler bugs you can fork [this CodeSandbox](https://codesandbox.io/p/github/gui/starter-free) to reproduce the issue.
|
||||
- For simpler bugs you can fork [this CodeSandbox](https://codesandbox.io/p/github/hanzogui/starter-free) to reproduce the issue.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
||||
@@ -10,13 +10,13 @@ runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
with:
|
||||
bun-version-file: 'package.json'
|
||||
|
||||
@@ -28,7 +28,7 @@ runs:
|
||||
echo "path=$HOME" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache Bun
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: bun_cache
|
||||
with:
|
||||
@@ -39,9 +39,9 @@ runs:
|
||||
|
||||
- name: Install Dependencies
|
||||
shell: bash
|
||||
run: bun install
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Re-link workspace bins
|
||||
shell: bash
|
||||
# Re-run install to ensure bin symlinks are created for workspace packages after build
|
||||
run: bun install
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="640" viewBox="0 0 1280 640" role="img" aria-label="gui">
|
||||
<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">gui</text>
|
||||
<text x="378" y="322" font-family="Inter,system-ui,sans-serif" font-size="30" fill="#ffffff" opacity=".66">Hanzo GUI — cross-platform UI primitives, tenant shell…</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>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,95 @@
|
||||
import { readdirSync, readFileSync } from 'node:fs'
|
||||
import { basename, join } from 'node:path'
|
||||
|
||||
const repoRoot = process.cwd()
|
||||
const workflowPath = join(repoRoot, '.github/workflows/test-native.yml')
|
||||
const e2eDir = join(repoRoot, 'apps/kitchen-sink/e2e')
|
||||
|
||||
const explicitlyExcludedTests = new Map([
|
||||
// android-only: every test skips on iOS (device.getPlatform() === 'ios'), so running
|
||||
// it on the iOS sim produced zero coverage while burning ~6.6min. it still runs in the
|
||||
// android Detox job, which executes all e2e files unsharded.
|
||||
[
|
||||
'SelectAndroidOnPress.test.ts',
|
||||
'android-only (skips on iOS); runs in the android job',
|
||||
],
|
||||
])
|
||||
|
||||
const workflow = readFileSync(workflowPath, 'utf8')
|
||||
const shardMatches = [...workflow.matchAll(/test_files:\s*'([^']*)'/g)]
|
||||
|
||||
if (shardMatches.length === 0) {
|
||||
console.error(`No iOS Detox shard definitions found in ${workflowPath}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const assignedTestFiles = shardMatches.flatMap((match) =>
|
||||
match[1]
|
||||
.split(/\s+/)
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean)
|
||||
)
|
||||
|
||||
const assignedTestBasenames = assignedTestFiles
|
||||
.filter((entry) => entry.endsWith('.test.ts'))
|
||||
.map((entry) => basename(entry))
|
||||
|
||||
const allDetoxTests = readdirSync(e2eDir)
|
||||
.filter((entry) => entry.endsWith('.test.ts'))
|
||||
.sort()
|
||||
|
||||
const counts = new Map()
|
||||
for (const testName of assignedTestBasenames) {
|
||||
counts.set(testName, (counts.get(testName) ?? 0) + 1)
|
||||
}
|
||||
|
||||
const duplicateAssignments = [...counts.entries()]
|
||||
.filter(([, count]) => count > 1)
|
||||
.map(([testName, count]) => `${testName} (${count}x)`)
|
||||
|
||||
const missingAssignments = allDetoxTests.filter(
|
||||
(testName) => !counts.has(testName) && !explicitlyExcludedTests.has(testName)
|
||||
)
|
||||
|
||||
const staleExclusions = [...explicitlyExcludedTests.keys()].filter(
|
||||
(testName) => !allDetoxTests.includes(testName)
|
||||
)
|
||||
|
||||
if (
|
||||
duplicateAssignments.length === 0 &&
|
||||
missingAssignments.length === 0 &&
|
||||
staleExclusions.length === 0
|
||||
) {
|
||||
console.info('iOS Detox shard coverage is valid.')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
if (duplicateAssignments.length > 0) {
|
||||
console.error('Duplicate iOS Detox shard assignments:')
|
||||
for (const entry of duplicateAssignments) {
|
||||
console.error(`- ${entry}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (missingAssignments.length > 0) {
|
||||
console.error('Missing iOS Detox tests from shard matrix:')
|
||||
for (const testName of missingAssignments) {
|
||||
console.error(`- ${testName}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (staleExclusions.length > 0) {
|
||||
console.error('Stale explicit shard exclusions:')
|
||||
for (const testName of staleExclusions) {
|
||||
console.error(`- ${testName}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (explicitlyExcludedTests.size > 0) {
|
||||
console.error('Explicitly excluded iOS Detox tests:')
|
||||
for (const [testName, reason] of explicitlyExcludedTests.entries()) {
|
||||
console.error(`- ${testName}: ${reason}`)
|
||||
}
|
||||
}
|
||||
|
||||
process.exit(1)
|
||||
Executable
+132
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# run each maestro flow as its own invocation, retrying ONLY the flow that
|
||||
# fails. the previous harness re-ran the WHOLE suite on any failure and only
|
||||
# passed on a single fully-clean attempt, so a transient flake landing on a
|
||||
# DIFFERENT flow each attempt could never go green even though every flow
|
||||
# passed at least once (observed on iOS Native: attempt 1 failed `Select`,
|
||||
# attempt 2 failed `OpenApp`, each passed on the other attempt -> suite red).
|
||||
#
|
||||
# per-flow retry isolates a flake to its own flow and keeps the other flows'
|
||||
# passes, so the suite goes green as long as every flow passes within its
|
||||
# own attempt budget.
|
||||
#
|
||||
# configuration (env vars, all optional):
|
||||
# FLOWS_DIR directory of flow .yaml files (default: ./flows)
|
||||
# BUNDLE_ID app to terminate between attempts (default: com.hanzogui.hanzoguikitchensink)
|
||||
# MAX_ATTEMPTS attempts per flow before giving up (default: 3)
|
||||
# SKIP_FLOWS space-separated basenames to skip; these are util
|
||||
# sub-flows / warm-ups, not standalone tests
|
||||
# (default: "OpenApp.yaml WarmUp.yaml")
|
||||
# METRO_PID_FILE if the pid in this file is dead, abort (default: /tmp/metro-pid)
|
||||
# FLOW_TIMEOUT hard timeout (seconds) per flow attempt; a wedged xcuitest
|
||||
# driver makes `maestro test` hang with no exit, so without it
|
||||
# the loop never advances and the job sits idle until the
|
||||
# runner's 45-min cap (default: 360)
|
||||
#
|
||||
# NOT using `set -e`: a failing/timed-out `maestro test` is expected and handled inline.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
FLOWS_DIR="${FLOWS_DIR:-./flows}"
|
||||
BUNDLE_ID="${BUNDLE_ID:-com.hanzogui.hanzoguikitchensink}"
|
||||
MAX_ATTEMPTS="${MAX_ATTEMPTS:-3}"
|
||||
SKIP_FLOWS="${SKIP_FLOWS:-OpenApp.yaml WarmUp.yaml}"
|
||||
METRO_PID_FILE="${METRO_PID_FILE:-/tmp/metro-pid}"
|
||||
FLOW_TIMEOUT="${FLOW_TIMEOUT:-360}"
|
||||
|
||||
metro_alive() {
|
||||
local pid
|
||||
pid="$(cat "$METRO_PID_FILE" 2>/dev/null || true)"
|
||||
[ -z "$pid" ] && return 0 # no tracked pid -> nothing to check
|
||||
kill -0 "$pid" 2>/dev/null
|
||||
}
|
||||
|
||||
recover_sim() {
|
||||
echo "recovering simulator state (terminate app, keep Hermes bytecode cache)..."
|
||||
xcrun simctl terminate booted "$BUNDLE_ID" 2>/dev/null || true
|
||||
sleep 2
|
||||
if ! metro_alive; then
|
||||
echo "::error::Metro died during test run — aborting (every retry would fail)"
|
||||
tail -50 /tmp/metro.log 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# run `maestro test <flow>` with a hard per-attempt timeout. when the iOS
|
||||
# xcuitest driver wedges (e.g. "Request for viewHierarchy failed, code: 500"),
|
||||
# maestro hangs with no exit — without a timeout the retry loop below never
|
||||
# advances and the whole job sits idle until the runner's 45-min cap (observed:
|
||||
# a 28-min hang on one flow that then got force-cancelled = red CI for a flake).
|
||||
# on timeout we SIGTERM then SIGKILL maestro's PROCESS GROUP — `set -m` gives the
|
||||
# backgrounded job its own pgid, so killing -$pid also reaps the child
|
||||
# xcodebuild/driver processes — then recover_sim resets state and the next
|
||||
# attempt re-establishes a fresh driver. returns maestro's exit code, or 124.
|
||||
run_flow_with_timeout() {
|
||||
local flow="$1"
|
||||
set -m
|
||||
maestro test "$flow" --no-ansi &
|
||||
local pid=$!
|
||||
set +m
|
||||
local waited=0
|
||||
while kill -0 "$pid" 2>/dev/null; do
|
||||
if [ "$waited" -ge "$FLOW_TIMEOUT" ]; then
|
||||
echo "::warning::flow exceeded ${FLOW_TIMEOUT}s — killing maestro (wedged xcuitest driver?)"
|
||||
kill -TERM -"$pid" 2>/dev/null || kill -TERM "$pid" 2>/dev/null
|
||||
sleep 10
|
||||
kill -KILL -"$pid" 2>/dev/null || kill -KILL "$pid" 2>/dev/null
|
||||
wait "$pid" 2>/dev/null
|
||||
return 124
|
||||
fi
|
||||
sleep 5
|
||||
waited=$((waited + 5))
|
||||
done
|
||||
wait "$pid"
|
||||
}
|
||||
|
||||
shopt -s nullglob
|
||||
flows=("$FLOWS_DIR"/*.yaml "$FLOWS_DIR"/*.yml)
|
||||
shopt -u nullglob
|
||||
|
||||
if [ "${#flows[@]}" -eq 0 ]; then
|
||||
echo "::error::no flow files found in $FLOWS_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
failed=""
|
||||
for flow in "${flows[@]}"; do
|
||||
base="$(basename "$flow")"
|
||||
case " $SKIP_FLOWS " in
|
||||
*" $base "*)
|
||||
echo "skipping util sub-flow: $base"
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
|
||||
name="${base%.*}"
|
||||
passed=false
|
||||
for attempt in $(seq 1 "$MAX_ATTEMPTS"); do
|
||||
echo "::group::$name (attempt $attempt/$MAX_ATTEMPTS)"
|
||||
if run_flow_with_timeout "$flow"; then
|
||||
passed=true
|
||||
echo "::endgroup::"
|
||||
echo "✓ $name passed (attempt $attempt/$MAX_ATTEMPTS)"
|
||||
break
|
||||
fi
|
||||
echo "::endgroup::"
|
||||
echo "::warning::$name failed (attempt $attempt/$MAX_ATTEMPTS)"
|
||||
if [ "$attempt" -lt "$MAX_ATTEMPTS" ]; then
|
||||
recover_sim
|
||||
fi
|
||||
done
|
||||
if [ "$passed" = false ]; then
|
||||
failed="$failed $name"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$failed" ]; then
|
||||
echo "::error::flows failed after $MAX_ATTEMPTS attempts:$failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "all flows passed"
|
||||
@@ -0,0 +1,285 @@
|
||||
name: Build iOS Test Container App
|
||||
env:
|
||||
# Relative path from the monorepo root to the app
|
||||
test_container_app_path: apps/kitchen-sink
|
||||
# Package name in the monorepo
|
||||
test_app_package_name: '@hanzogui/kitchen-sink'
|
||||
# Should match the name of "ios/<app_name>.xcworkspace"
|
||||
ios_app_name: hanzoguikitchensink
|
||||
# A unique ID to identify the app on GitHub Actions, this is used as part of cache keys and artifact names, must be unique among all workflows
|
||||
app_id: ios-hanzoguikitchensink
|
||||
# The command used to prebuild the app
|
||||
prebuild_command: bunx expo prebuild --platform ios --no-install # --no-install is used to skip installing dependencies, specifically `pod install` as we want to do it after the Cache Pods step
|
||||
|
||||
# These should be set in the repository secrets
|
||||
# Redis database used for caching and remembering things between runs, such as the last build number
|
||||
# KV_STORE_REDIS_REST_URL:
|
||||
# KV_STORE_REDIS_REST_TOKEN:
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
configuration:
|
||||
required: true
|
||||
type: string
|
||||
description: "Either 'Debug' or 'Release'."
|
||||
outputs:
|
||||
build-hash:
|
||||
description: 'A hash to identify the build (expo fingerprint).'
|
||||
value: ${{ jobs.build-ios.outputs.build-hash }}
|
||||
built-app-cache-key:
|
||||
description: 'The GitHub Actions cache key of the built .app, can be used in subsequent workflows to get the built app from cache using this key.'
|
||||
value: ${{ jobs.build-ios.outputs.built-app-cache-key }}
|
||||
built-app-path:
|
||||
description: 'The path to the built .app relative to the repository root.'
|
||||
value: ${{ jobs.build-ios.outputs.built-app-path }}
|
||||
|
||||
jobs:
|
||||
build-ios:
|
||||
name: Build
|
||||
runs-on: macos-15
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
timeout-minutes: 60
|
||||
outputs:
|
||||
built-app-cache-key: ${{ steps.check-has-build.outputs.cache-primary-key || steps.pre-check-has-build.outputs.cache-primary-key }}
|
||||
build-hash: ${{ steps.calculate-fingerprint.outputs.fingerprint || steps.get-fingerprint-from-cache.outputs.fingerprint }}
|
||||
built-app-path: ${{ steps.get-built-app-path.outputs.built_app_path }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ${{ env.test_container_app_path }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Set Xcode version
|
||||
# Xcode 26 (Swift 6.2) is required for expo-modules-core 55.x which uses
|
||||
# conformance isolation syntax (@MainActor in protocol inheritance clause, SE-0434)
|
||||
run: sudo xcode-select -s /Applications/Xcode_26.0.1.app
|
||||
|
||||
- name: Get Built App Path
|
||||
id: get-built-app-path
|
||||
env:
|
||||
BUILT_APP_PATH: ${{ env.test_container_app_path }}/build/Build/Products/${{ inputs.configuration }}-iphonesimulator/${{ env.ios_app_name }}.app
|
||||
run: |
|
||||
echo "Built app path: $BUILT_APP_PATH"
|
||||
echo "built_app_path=$BUILT_APP_PATH" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Calculate Pre-Fingerprint Hash
|
||||
id: calculate-pre-fingerprint
|
||||
env:
|
||||
# A hash that represents bun.lock + app.json - if these haven't changed,
|
||||
# the fingerprint likely hasn't changed either. This lets us skip bun install
|
||||
# and prebuild if we have a cached fingerprint for this pre-fingerprint hash.
|
||||
PRE_FINGERPRINT_HASH: ${{ hashFiles('bun.lock', format('{0}/app.json', env.test_container_app_path), format('{0}/package.json', env.test_container_app_path), 'packages/vxrn/expo-plugin.cjs') }}
|
||||
run: |
|
||||
if [ -z "$PRE_FINGERPRINT_HASH" ]; then
|
||||
echo '[ERROR] Failed to calculate pre-fingerprint hash.'
|
||||
fi
|
||||
echo "Pre-fingerprint hash: $PRE_FINGERPRINT_HASH"
|
||||
echo "pre_fingerprint_hash=$PRE_FINGERPRINT_HASH" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Read Cached Fingerprint
|
||||
id: get-fingerprint-from-cache
|
||||
env:
|
||||
KV_STORE_REDIS_REST_URL: ${{ secrets.KV_STORE_REDIS_REST_URL }}
|
||||
KV_STORE_REDIS_REST_TOKEN: ${{ secrets.KV_STORE_REDIS_REST_TOKEN }}
|
||||
run: |
|
||||
FINGERPRINT_FROM_CACHE=$(curl "$KV_STORE_REDIS_REST_URL/get/${{ env.app_id }}-fingerprint-from-pre-hash-${{ steps.calculate-pre-fingerprint.outputs.pre_fingerprint_hash }}" -H "Authorization: Bearer $KV_STORE_REDIS_REST_TOKEN" | jq -r '.result')
|
||||
|
||||
if [ "$FINGERPRINT_FROM_CACHE" != "null" ]; then
|
||||
curl -X POST "$KV_STORE_REDIS_REST_URL/EXPIRE/${{ env.app_id }}-fingerprint-from-pre-hash-${{ steps.calculate-pre-fingerprint.outputs.pre_fingerprint_hash }}/2592000" -H "Authorization: Bearer $KV_STORE_REDIS_REST_TOKEN" # Reset TTL to 30 days
|
||||
echo "Fingerprint from cache: $FINGERPRINT_FROM_CACHE"
|
||||
echo "fingerprint=$FINGERPRINT_FROM_CACHE" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo 'No cached fingerprint found.'
|
||||
echo "fingerprint=null" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Check If Build Already Exists
|
||||
uses: actions/cache/restore@v4
|
||||
id: pre-check-has-build
|
||||
if: ${{ steps.get-fingerprint-from-cache.outputs.fingerprint != 'null' }}
|
||||
with:
|
||||
key: ${{ env.app_id }}-${{ inputs.configuration }}-${{ steps.get-fingerprint-from-cache.outputs.fingerprint }}
|
||||
lookup-only: true
|
||||
path: ${{ steps.get-built-app-path.outputs.built_app_path }}
|
||||
|
||||
# The steps below are skipped if we have a cache hit
|
||||
|
||||
- name: Install
|
||||
if: ${{ !steps.pre-check-has-build.outputs.cache-hit }}
|
||||
uses: ./.github/actions/install
|
||||
with:
|
||||
workspace-focus: ${{ env.test_app_package_name }}
|
||||
|
||||
- name: Prebuild
|
||||
if: ${{ !steps.pre-check-has-build.outputs.cache-hit }}
|
||||
run: ${{ env.prebuild_command }}
|
||||
|
||||
# workaround for https://github.com/expo/expo/issues/42525
|
||||
# remove once expo-modules-core ships a fix or expo SDK 56+ resolves it
|
||||
- name: Patch Podfile for ExpoModulesCore Swift 6 and ContextMenuAuxiliaryPreview
|
||||
if: ${{ !steps.pre-check-has-build.outputs.cache-hit }}
|
||||
run: |
|
||||
# workaround: expo-modules-core 55.x uses @MainActor in class inheritance
|
||||
# clauses which requires Swift 6 mode. SWIFT_STRICT_CONCURRENCY=minimal
|
||||
# suppresses concurrency warnings in swift 6 mode.
|
||||
# workaround 2: ContextMenuAuxiliaryPreview uses deprecated transform: .default
|
||||
# which is an error in Xcode 26 / Swift 6.2 - disable warnings-as-errors for it.
|
||||
PODFILE="ios/Podfile"
|
||||
if grep -q "ContextMenuAuxiliaryPreview" "$PODFILE" 2>/dev/null; then
|
||||
echo "Podfile already patched (both ExpoModulesCore + ContextMenuAuxiliaryPreview), skipping"
|
||||
elif [ -f "$PODFILE" ]; then
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const podfile = fs.readFileSync('$PODFILE', 'utf8');
|
||||
const workaround = \`
|
||||
# workaround: expo-modules-core 55.x requires Swift 6 mode with isolated
|
||||
# conformances (SE-0470) for @MainActor in protocol conformance syntax.
|
||||
installer.pods_project.targets.each do |target|
|
||||
if target.name == 'ExpoModulesCore'
|
||||
target.build_configurations.each do |build_config|
|
||||
build_config.build_settings['SWIFT_VERSION'] = '6'
|
||||
build_config.build_settings['SWIFT_STRICT_CONCURRENCY'] = 'minimal'
|
||||
flags = build_config.build_settings['OTHER_SWIFT_FLAGS'] || '$(inherited)'
|
||||
unless flags.include?('IsolatedConformances')
|
||||
build_config.build_settings['OTHER_SWIFT_FLAGS'] = "\#{flags} -enable-upcoming-feature IsolatedConformances"
|
||||
end
|
||||
end
|
||||
end
|
||||
# workaround: ContextMenuAuxiliaryPreview uses deprecated transform: .default
|
||||
# which becomes a build error in Xcode 26 / Swift 6.2 strict mode.
|
||||
if target.name == 'ContextMenuAuxiliaryPreview'
|
||||
target.build_configurations.each do |build_config|
|
||||
build_config.build_settings['SWIFT_TREAT_WARNINGS_AS_ERRORS'] = 'NO'
|
||||
build_config.build_settings['GCC_TREAT_WARNINGS_AS_ERRORS'] = 'NO'
|
||||
end
|
||||
end
|
||||
end
|
||||
\`;
|
||||
const updated = podfile.replace(
|
||||
/(post_install do \|installer\|.*?)(^\s+end\s*\nend)/ms,
|
||||
'\$1' + workaround + '\$2'
|
||||
);
|
||||
if (updated === podfile) {
|
||||
console.error('ERROR: could not find post_install block in Podfile');
|
||||
process.exit(1);
|
||||
}
|
||||
fs.writeFileSync('$PODFILE', updated);
|
||||
console.log('Patched Podfile with ExpoModulesCore Swift 6 and ContextMenuAuxiliaryPreview workarounds');
|
||||
"
|
||||
else
|
||||
echo "WARNING: Podfile not found at $PODFILE"
|
||||
fi
|
||||
|
||||
- name: Cache Pods
|
||||
if: ${{ !steps.pre-check-has-build.outputs.cache-hit }}
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: ${{ env.app_id }}-pods
|
||||
with:
|
||||
path: ${{ env.test_container_app_path }}/ios/Pods
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles(format('{0}/ios/Podfile', env.test_container_app_path)) }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ env.cache-name }}-
|
||||
|
||||
- name: Pod Install
|
||||
if: ${{ !steps.pre-check-has-build.outputs.cache-hit }}
|
||||
run: |
|
||||
set -o pipefail
|
||||
pod install --project-directory=ios 2>&1 | tee pod-install.log || {
|
||||
echo "::error::Pod install failed. Check pod-install.log artifact for details."
|
||||
echo "=== Last 50 lines of pod install output ==="
|
||||
tail -50 pod-install.log
|
||||
exit 1
|
||||
}
|
||||
|
||||
- name: Upload Pod Install Log on Failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: pod-install-log
|
||||
path: ${{ env.test_container_app_path }}/pod-install.log
|
||||
retention-days: 7
|
||||
|
||||
- name: Calculate Fingerprint
|
||||
if: ${{ !steps.pre-check-has-build.outputs.cache-hit }}
|
||||
id: calculate-fingerprint
|
||||
run: |
|
||||
# Use expo fingerprint for accurate native dependency detection
|
||||
FINGERPRINT=$(bunx @expo/fingerprint fingerprint:generate --platform ios | jq -r '.hash')
|
||||
if [ -z "$FINGERPRINT" ]; then
|
||||
echo '[ERROR] Failed to calculate fingerprint.'
|
||||
exit 1
|
||||
fi
|
||||
echo "Fingerprint: $FINGERPRINT"
|
||||
echo "fingerprint=$FINGERPRINT" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Write Fingerprint to Cache
|
||||
if: ${{ !steps.pre-check-has-build.outputs.cache-hit }}
|
||||
env:
|
||||
KV_STORE_REDIS_REST_URL: ${{ secrets.KV_STORE_REDIS_REST_URL }}
|
||||
KV_STORE_REDIS_REST_TOKEN: ${{ secrets.KV_STORE_REDIS_REST_TOKEN }}
|
||||
run: |
|
||||
curl -X POST "$KV_STORE_REDIS_REST_URL/SETEX/${{ env.app_id }}-fingerprint-from-pre-hash-${{ steps.calculate-pre-fingerprint.outputs.pre_fingerprint_hash }}/2592000/${{ steps.calculate-fingerprint.outputs.fingerprint }}" -H "Authorization: Bearer $KV_STORE_REDIS_REST_TOKEN"
|
||||
|
||||
- name: Check If Build Already Exists
|
||||
if: ${{ !steps.pre-check-has-build.outputs.cache-hit }}
|
||||
uses: actions/cache/restore@v4
|
||||
id: check-has-build
|
||||
with:
|
||||
key: ${{ env.app_id }}-${{ inputs.configuration }}-${{ steps.calculate-fingerprint.outputs.fingerprint }}
|
||||
lookup-only: true
|
||||
path: ${{ steps.get-built-app-path.outputs.built_app_path }}
|
||||
|
||||
- name: Restore Build Cache
|
||||
if: ${{ !steps.pre-check-has-build.outputs.cache-hit && !steps.check-has-build.outputs.cache-hit }}
|
||||
id: restore-build-cache
|
||||
uses: actions/cache/restore@v4
|
||||
env:
|
||||
cache-name: ${{ env.app_id }}-build
|
||||
with:
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}
|
||||
path: |
|
||||
${{ env.test_container_app_path }}/build
|
||||
|
||||
- name: Build
|
||||
if: ${{ !steps.pre-check-has-build.outputs.cache-hit && !steps.check-has-build.outputs.cache-hit }}
|
||||
run: |
|
||||
set -o pipefail
|
||||
xcrun xcodebuild -scheme '${{ env.ios_app_name }}' \
|
||||
-workspace 'ios/${{ env.ios_app_name }}.xcworkspace' \
|
||||
-configuration ${{ inputs.configuration }} \
|
||||
-sdk 'iphonesimulator' \
|
||||
-destination 'generic/platform=iOS Simulator' \
|
||||
-derivedDataPath build \
|
||||
OTHER_SWIFT_FLAGS='$(inherited) -enable-upcoming-feature IsolatedConformances' \
|
||||
SWIFT_STRICT_CONCURRENCY=minimal \
|
||||
RCT_REMOVE_LEGACY_ARCH=1 \
|
||||
| tee xcodebuild.log | xcpretty
|
||||
|
||||
- name: Upload Built App to Cache
|
||||
if: ${{ !steps.pre-check-has-build.outputs.cache-hit && !steps.check-has-build.outputs.cache-hit }}
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
key: ${{ steps.check-has-build.outputs.cache-primary-key }}
|
||||
path: ${{ steps.get-built-app-path.outputs.built_app_path }}
|
||||
|
||||
- name: Upload Build Log
|
||||
uses: actions/[email protected]
|
||||
if: ${{ always() && !steps.pre-check-has-build.outputs.cache-hit && !steps.check-has-build.outputs.cache-hit }}
|
||||
with:
|
||||
name: xcodebuild-${{ env.app_id }}-${{ inputs.configuration }}.log
|
||||
path: |
|
||||
${{ env.test_container_app_path }}/xcodebuild.log
|
||||
|
||||
- name: Save Build Cache
|
||||
uses: actions/cache/save@v4
|
||||
if: ${{ always() && !steps.pre-check-has-build.outputs.cache-hit && !steps.check-has-build.outputs.cache-hit }}
|
||||
with:
|
||||
key: ${{ steps.restore-build-cache.outputs.cache-primary-key }}
|
||||
path: |
|
||||
${{ env.test_container_app_path }}/build
|
||||
@@ -0,0 +1,23 @@
|
||||
name: Changelog
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
tagged-release:
|
||||
name: Changelog
|
||||
runs-on: ubuntu-latest
|
||||
# needs: release
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: marvinpinto/action-automatic-releases@d68defdd11f9dcc7f52f35c1b7c236ee7513bcc1 # latest (archived action - consider replacing with softprops/action-gh-release)
|
||||
with:
|
||||
repo_token: '${{ github.token }}'
|
||||
prerelease: false
|
||||
@@ -0,0 +1,213 @@
|
||||
name: Checks
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
push:
|
||||
paths-ignore:
|
||||
- 'assets/**'
|
||||
- '.vscode/**'
|
||||
branches:
|
||||
- main
|
||||
- v2
|
||||
- 'v2-*'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# cancel in-progress runs on the same branch (avoids duplicate PR + push runs)
|
||||
concurrency:
|
||||
group: checks-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
checks:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Install
|
||||
uses: ./.github/actions/install
|
||||
|
||||
# replaces dependabot. all critical advisories are pinned to patched
|
||||
# versions via root package.json "overrides", so this gates cleanly with
|
||||
# no ignore list. remaining high/moderate advisories require cross-major
|
||||
# bumps of example/tooling deps (vite, undici, tar, react-router, etc.) -
|
||||
# run `bun audit` locally to review them.
|
||||
- name: Security audit
|
||||
run: bun audit --audit-level critical
|
||||
|
||||
- name: Check
|
||||
run: bun run check
|
||||
|
||||
# Every package's declarations, which this gate reads and nothing else
|
||||
# here emits. `<pkg>/types/*.d.ts` used to be committed — 2,885 of them,
|
||||
# until d1f5ae7fc0 correctly reclassified them as build artifacts and
|
||||
# gitignored them. A fresh checkout then has no types/ at all, so every
|
||||
# workspace import resolved to an untyped module and Typecheck went red
|
||||
# with 139 errors that are nowhere in the source ("@hanzogui/themes/v5 has
|
||||
# no exported member 'themes'" and friends).
|
||||
#
|
||||
# Install's pre-build is the JS half — postinstall runs build:js, which is
|
||||
# SKIP_TYPES=1. This is the other half. Both halves are one turbo `build`
|
||||
# task, so the second pass reuses the first's cache for everything except
|
||||
# the declarations it adds.
|
||||
- name: Build declarations
|
||||
run: bun run build:types
|
||||
|
||||
- name: Typecheck
|
||||
run: bun run typecheck
|
||||
|
||||
- name: Lint
|
||||
run: bun run lint
|
||||
|
||||
unit-tests:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=6144'
|
||||
# NO SKIP_TYPES. It used to be set here, reasoning that "types are the
|
||||
# checks job's business, not ours" — but two of these suites are
|
||||
# `vitest --typecheck` (@hanzogui/web, @hanzogui/input) and they read
|
||||
# their dependencies' .d.ts. Those live in <pkg>/types/, gitignored and
|
||||
# emitted only by a types build, so skipping types made ^build emit JS
|
||||
# alone, @hanzogui/native resolve as an untyped module, and
|
||||
# React.useContext(NativeMenuContext) collapse to `unknown`. The job then
|
||||
# failed in createComponent.tsx on a type error that is not in the source
|
||||
# and cannot be fixed there. Let ^build emit what the gate reads.
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Install
|
||||
uses: ./.github/actions/install
|
||||
|
||||
- name: Get Playwright version
|
||||
id: pw-version
|
||||
run: echo "version=$(bunx playwright --version | awk '{print $2}')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache Playwright browsers
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-${{ runner.os }}-${{ steps.pw-version.outputs.version }}
|
||||
|
||||
- name: Install playwright
|
||||
run: bunx playwright install
|
||||
|
||||
- name: Restore Turbo cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .turbo/cache
|
||||
key: turbo-${{ runner.os }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
turbo-${{ runner.os }}-
|
||||
|
||||
- name: Run unit tests
|
||||
run: bun turbo run test:web --filter='!@hanzogui/kitchen-sink' --concurrency=1
|
||||
|
||||
- name: Run native unit tests
|
||||
run: bun turbo run test:native --filter=@hanzogui/static-tests --filter=@hanzogui/core-test --filter=@hanzogui/components-test --concurrency=1
|
||||
|
||||
changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
integration-relevant: ${{ steps.turbo-check.outputs.relevant }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
with:
|
||||
bun-version-file: 'package.json'
|
||||
|
||||
# turbo reads workspace graph from package.json files — no bun install needed
|
||||
# TODO: turbo ls --affected is broken in <=2.8.20 (ignores git refs, false positives
|
||||
# on root config changes — vercel/turborepo#11144, #10869). fix landed in 2.8.21
|
||||
# canaries (PR #12424). once stable, simplify this to: turbo ls --affected
|
||||
- name: Install turbo
|
||||
run: npm install -g turbo@2
|
||||
|
||||
- name: Check if kitchen-sink dependency tree changed
|
||||
id: turbo-check
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
||||
BASE="${{ github.event.pull_request.base.sha }}"
|
||||
else
|
||||
BASE="HEAD~1"
|
||||
fi
|
||||
|
||||
# get all package directories in kitchen-sink's transitive dependency tree
|
||||
# turbo ls resolves the real workspace dependency graph — no manual list needed
|
||||
DEP_DIRS=$(turbo ls --filter='@hanzogui/kitchen-sink...' 2>/dev/null | awk 'NR>1 && NF>0 {print $2}' | grep '/')
|
||||
|
||||
if [ -z "$DEP_DIRS" ]; then
|
||||
echo "relevant=true" >> $GITHUB_OUTPUT
|
||||
echo "::warning::Could not resolve dependency tree, running tests as fallback"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# build a single regex from all dependency directories + root config files
|
||||
PATTERN=$(echo "$DEP_DIRS" | paste -sd'|' -)
|
||||
FULL_PATTERN="^($PATTERN)/|^(package\.json|bun\.lock|turbo\.json)$"
|
||||
|
||||
CHANGED=$(git diff --name-only "$BASE" HEAD)
|
||||
|
||||
if echo "$CHANGED" | grep -qE "$FULL_PATTERN"; then
|
||||
MATCHED=$(echo "$CHANGED" | grep -E "$FULL_PATTERN" | head -5)
|
||||
echo "relevant=true" >> $GITHUB_OUTPUT
|
||||
echo "::notice::Integration tests needed. Changed dependency files: $(echo "$MATCHED" | tr '\n' ', ')"
|
||||
else
|
||||
echo "relevant=false" >> $GITHUB_OUTPUT
|
||||
echo "::notice::Integration tests skipped — no changes in kitchen-sink dependency tree"
|
||||
fi
|
||||
|
||||
integration-tests:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.integration-relevant == 'true'
|
||||
# gate is turbo's actual workspace dependency graph — no manual path list to maintain
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1/3, 2/3, 3/3]
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=6144'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Install
|
||||
uses: ./.github/actions/install
|
||||
|
||||
- name: Get Playwright version
|
||||
id: pw-version
|
||||
run: echo "version=$(bunx playwright --version | awk '{print $2}')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache Playwright browsers
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-${{ runner.os }}-${{ steps.pw-version.outputs.version }}
|
||||
|
||||
- name: Install playwright
|
||||
run: bunx playwright install --with-deps
|
||||
|
||||
- name: Restore Turbo cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .turbo/cache
|
||||
key: turbo-${{ runner.os }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
turbo-${{ runner.os }}-
|
||||
|
||||
- name: Run integration tests (shard ${{ matrix.shard }})
|
||||
run: cd apps/kitchen-sink && NODE_ENV=test npx playwright test --shard=${{ matrix.shard }}
|
||||
timeout-minutes: 20
|
||||
@@ -0,0 +1,308 @@
|
||||
name: Release
|
||||
|
||||
# github evaluates workflow_run from the default branch. keep this workflow on
|
||||
# main as well as v3-beta so successful v3-beta CI can publish automatically.
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [Checks]
|
||||
types: [completed]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release:
|
||||
description: Version to publish from main
|
||||
required: true
|
||||
default: canary
|
||||
type: choice
|
||||
options:
|
||||
- canary
|
||||
- patch
|
||||
- minor
|
||||
- major
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: npm-release
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
beta:
|
||||
name: Publish v3 beta
|
||||
if: >-
|
||||
github.event_name == 'workflow_run' &&
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.event == 'push' &&
|
||||
github.event.workflow_run.head_branch == 'v3-beta'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout tested v3-beta source
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.workflow_run.head_sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Select latest v3-beta build
|
||||
id: candidate
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git fetch --no-tags origin v3-beta
|
||||
if [[ "$(git rev-parse HEAD)" != "$(git rev-parse origin/v3-beta)" ]]; then
|
||||
echo "A newer v3-beta commit exists; its Checks run will publish instead."
|
||||
echo "release=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "release=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Setup Node.js
|
||||
if: steps.candidate.outputs.release == 'true'
|
||||
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
|
||||
with:
|
||||
node-version: '24'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
package-manager-cache: false
|
||||
|
||||
- name: Setup Bun
|
||||
if: steps.candidate.outputs.release == 'true'
|
||||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
||||
with:
|
||||
bun-version-file: 'package.json'
|
||||
|
||||
- name: Install npm with trusted publishing support
|
||||
if: steps.candidate.outputs.release == 'true'
|
||||
run: npm install --global [email protected]
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.candidate.outputs.release == 'true'
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Get Playwright version
|
||||
if: steps.candidate.outputs.release == 'true'
|
||||
id: beta-pw-version
|
||||
run: echo "version=$(bunx playwright --version | awk '{print $2}')" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Cache Playwright browsers
|
||||
if: steps.candidate.outputs.release == 'true'
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-${{ runner.os }}-${{ steps.beta-pw-version.outputs.version }}
|
||||
|
||||
- name: Install Playwright Chromium
|
||||
if: steps.candidate.outputs.release == 'true'
|
||||
run: bunx playwright install --with-deps chromium
|
||||
|
||||
- name: Publish tested source to the beta channel
|
||||
if: steps.candidate.outputs.release == 'true'
|
||||
env:
|
||||
BETA_VERSION: 3.0.0-beta.${{ github.run_number }}.${{ github.run_attempt }}
|
||||
run: >-
|
||||
bun scripts/release.ts
|
||||
--beta
|
||||
--version "$BETA_VERSION"
|
||||
--ci
|
||||
--dirty
|
||||
--skip-finish
|
||||
--skip-tests
|
||||
--skip-native-tests
|
||||
--skip-checks
|
||||
|
||||
ensure-ci:
|
||||
name: Approve main release source
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
steps:
|
||||
# a canary is a throwaway version on its own dist-tag that nothing installs
|
||||
# unless asked for by name, so it is cuttable from any branch and does not
|
||||
# wait on a full CI run. requiring current main meant merging a change just
|
||||
# to test it. a stable release still needs both.
|
||||
- name: Require current main and successful full CI
|
||||
if: inputs.release != 'canary'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TARGET_SHA: ${{ github.sha }}
|
||||
WORKFLOW_FILE: checks.yaml
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[[ "$GITHUB_REF" == "refs/heads/main" ]]
|
||||
|
||||
main_sha=$(gh api "repos/$GITHUB_REPOSITORY/git/ref/heads/main" --jq .object.sha)
|
||||
[[ "$TARGET_SHA" == "$main_sha" ]]
|
||||
|
||||
get_runs() {
|
||||
gh api --method GET "repos/$GITHUB_REPOSITORY/actions/workflows/$WORKFLOW_FILE/runs" \
|
||||
-f head_sha="$TARGET_SHA" -f per_page=20
|
||||
}
|
||||
|
||||
wait_for_run() {
|
||||
local run_id="$1"
|
||||
for ((attempt = 1; attempt <= 40; attempt++)); do
|
||||
read -r status conclusion < <(
|
||||
gh api "repos/$GITHUB_REPOSITORY/actions/runs/$run_id" \
|
||||
--jq '[.status, (.conclusion // "pending")] | @tsv'
|
||||
)
|
||||
if [[ "$status" == "completed" ]]; then
|
||||
[[ "$conclusion" == "success" ]]
|
||||
return
|
||||
fi
|
||||
sleep 120
|
||||
done
|
||||
echo "Timed out waiting for CI run $run_id" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
runs=$(get_runs)
|
||||
run_id=$(jq -r '.workflow_runs | sort_by(.created_at) | last | .id // empty' <<< "$runs")
|
||||
if [[ -n "$run_id" ]]; then
|
||||
status=$(jq -r --argjson id "$run_id" '.workflow_runs[] | select(.id == $id) | .status' <<< "$runs")
|
||||
conclusion=$(jq -r --argjson id "$run_id" '.workflow_runs[] | select(.id == $id) | .conclusion // "pending"' <<< "$runs")
|
||||
if [[ "$status" == "completed" && "$conclusion" == "success" ]]; then
|
||||
echo "Full CI already passed for $TARGET_SHA."
|
||||
exit 0
|
||||
fi
|
||||
if [[ "$status" != "completed" ]] && wait_for_run "$run_id"; then
|
||||
echo "Existing full CI passed for $TARGET_SHA."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
started=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
|
||||
gh api --method POST \
|
||||
"repos/$GITHUB_REPOSITORY/actions/workflows/$WORKFLOW_FILE/dispatches" \
|
||||
-f ref=main
|
||||
|
||||
run_id=''
|
||||
for ((attempt = 1; attempt <= 12; attempt++)); do
|
||||
sleep 10
|
||||
runs=$(get_runs)
|
||||
run_id=$(jq -r --arg started "$started" \
|
||||
'[.workflow_runs[] | select(.event == "workflow_dispatch" and .created_at >= $started)] | sort_by(.created_at) | last | .id // empty' \
|
||||
<<< "$runs")
|
||||
[[ -n "$run_id" ]] && break
|
||||
done
|
||||
[[ -n "$run_id" ]]
|
||||
wait_for_run "$run_id"
|
||||
|
||||
publish:
|
||||
name: Publish approved main release
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
needs: ensure-ci
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout release source
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Validate release source and select baseline
|
||||
id: source
|
||||
env:
|
||||
RELEASE_KIND: ${{ inputs.release }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git fetch --no-tags origin main
|
||||
# baseline selection below is needed for every release kind; only the
|
||||
# must-be-current-main assertion is stable-only (see ensure-ci).
|
||||
if [[ "$RELEASE_KIND" != 'canary' ]]; then
|
||||
[[ "$GITHUB_REF" == "refs/heads/main" ]]
|
||||
[[ "$(git rev-parse HEAD)" == "$(git rev-parse origin/main)" ]]
|
||||
fi
|
||||
|
||||
tag_ref=$(git describe --tags --match 'v*' --abbrev=0 2>/dev/null || true)
|
||||
canary_ref=$(git log --grep='^canary' --format='%H' -1)
|
||||
if [[ -z "$tag_ref" ]]; then
|
||||
base_ref="$canary_ref"
|
||||
elif [[ -z "$canary_ref" ]]; then
|
||||
base_ref="$tag_ref"
|
||||
elif (( $(git log -1 --format=%ct "$canary_ref") > $(git log -1 --format=%ct "$tag_ref") )); then
|
||||
base_ref="$canary_ref"
|
||||
else
|
||||
base_ref="$tag_ref"
|
||||
fi
|
||||
[[ -n "$base_ref" ]]
|
||||
echo "base-ref=$base_ref" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
|
||||
with:
|
||||
node-version: '24'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
package-manager-cache: false
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
||||
with:
|
||||
bun-version-file: 'package.json'
|
||||
|
||||
- name: Install npm with trusted publishing support
|
||||
run: npm install --global [email protected]
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Prepare release commit
|
||||
env:
|
||||
RELEASE_KIND: ${{ inputs.release }}
|
||||
RELEASE_BASE_REF: ${{ steps.source.outputs.base-ref }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git config user.name 'Gui Release'
|
||||
git config user.email '[email protected]'
|
||||
bun scripts/release.ts "--$RELEASE_KIND" --ci --dirty --skip-publish --skip-push --skip-tests --skip-native-tests --skip-checks
|
||||
|
||||
# a canary mutates no git at all, matching orez and takeout. pushing a
|
||||
# throwaway version commit to main is what wedged the one repo: main
|
||||
# requires a pull request and a merge queue there, so the push is rejected
|
||||
# with GH006 and the publish step below never runs. the bump only has to
|
||||
# exist in the tree for npm.
|
||||
- name: Push release commit
|
||||
if: inputs.release != 'canary'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TARGET_SHA: ${{ github.sha }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git fetch --no-tags origin main
|
||||
[[ "$(git rev-parse origin/main)" == "$TARGET_SHA" ]]
|
||||
auth=$(printf 'x-access-token:%s' "$GH_TOKEN" | base64 | tr -d '\n')
|
||||
trap 'git config --local --unset-all http.https://github.com/.extraheader || true' EXIT
|
||||
git config --local http.https://github.com/.extraheader "AUTHORIZATION: basic $auth"
|
||||
git push origin HEAD:main
|
||||
|
||||
- name: Publish prepared version
|
||||
env:
|
||||
RELEASE_BASE_REF: ${{ steps.source.outputs.base-ref }}
|
||||
run: bun scripts/release.ts --republish --ci --dirty --skip-finish
|
||||
|
||||
- name: Push stable tag
|
||||
if: inputs.release != 'canary'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
version=$(node -p "require('./pkgs/ui/hanzogui/package.json').version")
|
||||
[[ "$(git rev-list -n 1 "v$version")" == "$(git rev-parse HEAD)" ]]
|
||||
auth=$(printf 'x-access-token:%s' "$GH_TOKEN" | base64 | tr -d '\n')
|
||||
trap 'git config --local --unset-all http.https://github.com/.extraheader || true' EXIT
|
||||
git config --local http.https://github.com/.extraheader "AUTHORIZATION: basic $auth"
|
||||
git push origin "v$version"
|
||||
gh release create "v$version" --verify-tag --generate-notes --title "v$version"
|
||||
@@ -0,0 +1,132 @@
|
||||
name: Test iOS Kitchen Sink Go (Maestro)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, 'v2-*']
|
||||
pull_request:
|
||||
branches: [main, 'v*']
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
test_container_app_path: apps/kitchen-sink-go
|
||||
|
||||
jobs:
|
||||
test:
|
||||
# TODO: re-enable once Sheet demo is fixed on Expo Go CI
|
||||
if: false
|
||||
name: Run Maestro Tests (exports=${{ matrix.package-exports }})
|
||||
runs-on: macos-14
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
package-exports: [true, false]
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ${{ env.test_container_app_path }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Set Xcode version
|
||||
run: sudo xcode-select -s /Applications/Xcode_16.2.app
|
||||
|
||||
- name: Install Maestro
|
||||
env:
|
||||
MAESTRO_VERSION: cli-2.3.0
|
||||
run: |
|
||||
curl -fsSL "https://github.com/mobile-dev-inc/maestro/releases/download/${MAESTRO_VERSION}/maestro.zip" -o maestro.zip
|
||||
unzip -q maestro.zip
|
||||
mv maestro "$HOME/.maestro"
|
||||
echo "$HOME/.maestro/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Install Dependencies
|
||||
uses: ./.github/actions/install
|
||||
|
||||
- name: Boot iOS Simulator
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
DEVICE_ID=$(xcrun simctl list devices available -j | jq -r '.devices | to_entries[] | select(.key | contains("iOS")) | .value[] | select(.name == "iPhone 16") | .udid' | head -1)
|
||||
if [ -z "$DEVICE_ID" ]; then
|
||||
echo "iPhone 16 not found, using first available iPhone"
|
||||
DEVICE_ID=$(xcrun simctl list devices available -j | jq -r '.devices | to_entries[] | select(.key | contains("iOS")) | .value[] | select(.name | contains("iPhone")) | .udid' | head -1)
|
||||
fi
|
||||
echo "Booting simulator: $DEVICE_ID"
|
||||
xcrun simctl boot "$DEVICE_ID" || true
|
||||
xcrun simctl bootstatus "$DEVICE_ID" -b
|
||||
|
||||
echo "Waiting for simulator to be fully ready for Maestro..."
|
||||
for i in {1..30}; do
|
||||
if $HOME/.maestro/bin/maestro --device "$DEVICE_ID" test --help > /dev/null 2>&1; then
|
||||
echo "Maestro can communicate with device"
|
||||
break
|
||||
fi
|
||||
echo "Waiting for Maestro device detection... ($i/30)"
|
||||
sleep 2
|
||||
done
|
||||
sleep 5
|
||||
|
||||
- name: Install Expo Go
|
||||
run: |
|
||||
# Install Expo Go on simulator (no custom build needed)
|
||||
bunx expo start --ios &
|
||||
sleep 30
|
||||
# Kill expo after it installs Expo Go
|
||||
kill %1 || true
|
||||
|
||||
- name: Start Metro Bundler
|
||||
run: |
|
||||
EXPO_NO_TELEMETRY=true GUI_PACKAGE_EXPORTS=${{ matrix.package-exports }} bunx expo start --offline &
|
||||
echo "Waiting for Metro to start..."
|
||||
for i in {1..30}; do
|
||||
if curl -s "http://127.0.0.1:8081/" -H "Expo-Platform: ios" > /dev/null 2>&1; then
|
||||
echo "Metro is responding!"
|
||||
break
|
||||
fi
|
||||
echo "Waiting for Metro... ($i/30)"
|
||||
sleep 2
|
||||
done
|
||||
|
||||
- name: Pre-warm Bundle
|
||||
run: |
|
||||
echo "Pre-warming bundle..."
|
||||
MANIFEST=$(curl -s "http://127.0.0.1:8081/" -H "Expo-Platform: ios")
|
||||
BUNDLE_URL=$(echo "$MANIFEST" | jq -r '.launchAsset.url')
|
||||
echo "Fetching bundle from: $BUNDLE_URL"
|
||||
curl -s --max-time 900 "$BUNDLE_URL" > /dev/null 2>&1 || echo "Bundle fetch completed (or timed out)"
|
||||
echo "Bundle pre-warm complete"
|
||||
|
||||
- name: Verify Maestro Device Connection
|
||||
run: |
|
||||
echo "Checking Maestro device connection..."
|
||||
for i in {1..10}; do
|
||||
if xcrun simctl list devices booted | grep -q "Booted"; then
|
||||
echo "Simulator is booted"
|
||||
sleep 3
|
||||
break
|
||||
fi
|
||||
echo "Waiting for device... ($i/10)"
|
||||
sleep 3
|
||||
done
|
||||
xcrun simctl list devices booted
|
||||
|
||||
# per-flow retry (shared with test-ios-native): a flake in one flow is
|
||||
# retried in isolation instead of re-running the whole suite. runs Expo
|
||||
# Go (host.exp.Exponent), so terminate that between attempts.
|
||||
- name: Run Maestro Tests
|
||||
env:
|
||||
BUNDLE_ID: host.exp.Exponent
|
||||
run: bash "${{ github.workspace }}/.github/scripts/run-maestro-flows.sh"
|
||||
|
||||
- name: Upload Maestro Artifacts on Failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: maestro-artifacts-exports-${{ matrix.package-exports }}
|
||||
path: |
|
||||
~/.maestro/tests/**/*
|
||||
@@ -0,0 +1,238 @@
|
||||
name: Test iOS Native (Maestro)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, 'v2-*', rn82]
|
||||
pull_request:
|
||||
branches: [main, 'v*']
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
# required: this workflow calls the reusable build-ios-kitchensink-app.yml,
|
||||
# whose build-ios job declares pull-requests: read (caller must grant >= callee)
|
||||
pull-requests: read
|
||||
|
||||
env:
|
||||
test_container_app_path: apps/kitchen-sink
|
||||
MAESTRO_DRIVER_STARTUP_TIMEOUT: 360000
|
||||
|
||||
jobs:
|
||||
build:
|
||||
# don't forward secrets (KV_STORE_REDIS_REST_*) to fork PR builds running on macOS runners.
|
||||
# fork PRs skip the native build entirely; same-repo refs still build with secrets.
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
uses: ./.github/workflows/build-ios-kitchensink-app.yml
|
||||
with:
|
||||
configuration: Debug
|
||||
secrets: inherit
|
||||
|
||||
test:
|
||||
name: Run Maestro Tests (exports=${{ matrix.package-exports }})
|
||||
needs: build
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: macos-15
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
package-exports: [true, false]
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ${{ env.test_container_app_path }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Set Xcode version
|
||||
run: sudo xcode-select -s /Applications/Xcode_26.0.1.app
|
||||
|
||||
- name: Restore Built App from Cache
|
||||
id: restore-app-cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
key: ${{ needs.build.outputs.built-app-cache-key }}
|
||||
path: ${{ needs.build.outputs.built-app-path }}
|
||||
|
||||
- name: Check Cache Restore
|
||||
if: steps.restore-app-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
echo "::warning::iOS cache not restored (key: ${{ needs.build.outputs.built-app-cache-key }}). This may happen on first run after cache key changes."
|
||||
echo "SKIP_MAESTRO_TESTS=true" >> $GITHUB_ENV
|
||||
|
||||
- name: Install Maestro
|
||||
if: env.SKIP_MAESTRO_TESTS != 'true'
|
||||
env:
|
||||
MAESTRO_VERSION: cli-2.3.0
|
||||
run: |
|
||||
# Pin to specific version via GitHub releases (avoids broken get.maestro.mobile.dev)
|
||||
curl -fsSL "https://github.com/mobile-dev-inc/maestro/releases/download/${MAESTRO_VERSION}/maestro.zip" -o maestro.zip
|
||||
unzip -q maestro.zip
|
||||
mv maestro "$HOME/.maestro"
|
||||
echo "$HOME/.maestro/bin" >> $GITHUB_PATH
|
||||
|
||||
# Install dependencies BEFORE booting simulator to avoid simulator timeout
|
||||
# during potentially long bun install + build:js steps
|
||||
- name: Install Dependencies
|
||||
if: env.SKIP_MAESTRO_TESTS != 'true'
|
||||
uses: ./.github/actions/install
|
||||
|
||||
- name: Boot Simulator & Start Metro (parallel)
|
||||
if: env.SKIP_MAESTRO_TESTS != 'true'
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
# start Metro in background, capture output for debugging
|
||||
cd "${{ env.test_container_app_path }}"
|
||||
EXPO_NO_TELEMETRY=true GUI_PACKAGE_EXPORTS=${{ matrix.package-exports }} bunx expo start --dev-client --offline --reset-cache > /tmp/metro.log 2>&1 &
|
||||
METRO_PID=$!
|
||||
echo "$METRO_PID" > /tmp/metro-pid
|
||||
cd "${{ github.workspace }}"
|
||||
|
||||
# boot simulator
|
||||
DEVICE_ID=$(xcrun simctl list devices available -j | jq -r '.devices | to_entries[] | select(.key | contains("iOS")) | .value[] | select(.name == "iPhone 16") | .udid' | head -1)
|
||||
if [ -z "$DEVICE_ID" ]; then
|
||||
echo "iPhone 16 not found, using first available iPhone"
|
||||
DEVICE_ID=$(xcrun simctl list devices available -j | jq -r '.devices | to_entries[] | select(.key | contains("iOS")) | .value[] | select(.name | contains("iPhone")) | .udid' | head -1)
|
||||
fi
|
||||
echo "Booting simulator: $DEVICE_ID"
|
||||
xcrun simctl boot "$DEVICE_ID" || true
|
||||
xcrun simctl bootstatus "$DEVICE_ID" -b
|
||||
|
||||
# install app and verify FrontBoard registration
|
||||
APP_PATH="${{ needs.build.outputs.built-app-path }}"
|
||||
BUNDLE_ID="com.hanzogui.hanzoguikitchensink"
|
||||
for attempt in 1 2 3; do
|
||||
xcrun simctl install booted "$APP_PATH"
|
||||
sleep 3
|
||||
if xcrun simctl get_app_container booted "$BUNDLE_ID" 2>/dev/null; then
|
||||
echo "App installed and registered (attempt $attempt)"
|
||||
break
|
||||
fi
|
||||
echo "::warning::App not registered after install (attempt $attempt)"
|
||||
xcrun simctl uninstall booted "$BUNDLE_ID" 2>/dev/null || true
|
||||
sleep 3
|
||||
if [ $attempt -eq 3 ]; then
|
||||
echo "::error::App failed to register after 3 attempts"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# wait for Metro to be ready, fail if it never responds
|
||||
echo "Waiting for Metro..."
|
||||
METRO_READY=false
|
||||
for i in {1..90}; do
|
||||
# check Metro process is still alive
|
||||
if ! kill -0 "$METRO_PID" 2>/dev/null; then
|
||||
echo "::error::Metro process died unexpectedly"
|
||||
echo "=== Metro output ==="
|
||||
cat /tmp/metro.log
|
||||
exit 1
|
||||
fi
|
||||
if curl -s "http://127.0.0.1:8081/" -H "Expo-Platform: ios" > /dev/null 2>&1; then
|
||||
echo "Metro is responding after ~$((i*2))s"
|
||||
METRO_READY=true
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
if [ "$METRO_READY" != "true" ]; then
|
||||
echo "::error::Metro failed to respond within 180s"
|
||||
echo "=== Metro output ==="
|
||||
cat /tmp/metro.log
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Pre-warm Bundle
|
||||
if: env.SKIP_MAESTRO_TESTS != 'true'
|
||||
run: |
|
||||
echo "Pre-warming JS bundle (Metro build + cache)..."
|
||||
MANIFEST=$(curl -s "http://127.0.0.1:8081/" -H "Expo-Platform: ios")
|
||||
BUNDLE_URL=$(echo "$MANIFEST" | jq -r '.launchAsset.url')
|
||||
if [ -z "$BUNDLE_URL" ] || [ "$BUNDLE_URL" = "null" ]; then
|
||||
echo "::error::Failed to get bundle URL from manifest"
|
||||
echo "Manifest response: $MANIFEST"
|
||||
exit 1
|
||||
fi
|
||||
echo "Fetching bundle from: $BUNDLE_URL"
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 900 "$BUNDLE_URL")
|
||||
if [ "$HTTP_CODE" != "200" ]; then
|
||||
echo "::error::Bundle fetch failed with HTTP $HTTP_CODE"
|
||||
exit 1
|
||||
fi
|
||||
echo "Bundle pre-warm complete (HTTP $HTTP_CODE)"
|
||||
|
||||
- name: Start Simulator Log Collection
|
||||
if: env.SKIP_MAESTRO_TESTS != 'true'
|
||||
run: |
|
||||
# capture simulator device logs in background for debugging app crashes
|
||||
xcrun simctl spawn booted log stream --predicate 'process == "hanzoguikitchensink" OR subsystem == "com.hanzogui.hanzoguikitchensink"' --level debug > /tmp/simulator-app.log 2>&1 &
|
||||
echo $! > /tmp/simlog-pid
|
||||
# also capture broader crash/error logs
|
||||
xcrun simctl spawn booted log stream --predicate 'eventMessage CONTAINS "crash" OR eventMessage CONTAINS "fatal" OR eventMessage CONTAINS "error" OR process == "ReportCrash"' --level error > /tmp/simulator-errors.log 2>&1 &
|
||||
echo $! > /tmp/simlog-errors-pid
|
||||
|
||||
- name: Warm up App (cold start + Hermes bytecode compile)
|
||||
if: env.SKIP_MAESTRO_TESTS != 'true'
|
||||
run: |
|
||||
# WarmUp.yaml does clearState:true for a clean first launch.
|
||||
# Subsequent test flows use OpenApp.yaml (no clearState) so Hermes
|
||||
# bytecode cache persists across launches = much faster restarts.
|
||||
# if WarmUp fails, the Maestro xcuitest driver (port 7001) is usually
|
||||
# dead — every subsequent flow will fail with "Failed to connect to
|
||||
# /127.0.0.1:7001", so fail fast rather than burn 15+ min on doomed retries.
|
||||
if ! maestro test ./flows/WarmUp.yaml --no-ansi; then
|
||||
echo "::error::WarmUp failed — Maestro driver is likely dead, aborting"
|
||||
echo "=== Recent app logs ==="
|
||||
tail -50 /tmp/simulator-app.log 2>/dev/null || echo "No app logs"
|
||||
echo "=== Metro output ==="
|
||||
tail -50 /tmp/metro.log 2>/dev/null || echo "No metro logs"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# run each flow independently and retry ONLY the flow that flakes, so a
|
||||
# transient flake in one flow doesn't discard the passes of the others.
|
||||
# the OpenApp/WarmUp util sub-flows are skipped (WarmUp ran in the step
|
||||
# above; OpenApp is invoked via runFlow by every component flow).
|
||||
- name: Run Maestro Tests
|
||||
if: env.SKIP_MAESTRO_TESTS != 'true'
|
||||
run: bash "${{ github.workspace }}/.github/scripts/run-maestro-flows.sh"
|
||||
|
||||
- name: Dump Logs on Failure
|
||||
if: failure() && env.SKIP_MAESTRO_TESTS != 'true'
|
||||
run: |
|
||||
# stop log collection
|
||||
kill "$(cat /tmp/simlog-pid 2>/dev/null)" 2>/dev/null || true
|
||||
kill "$(cat /tmp/simlog-errors-pid 2>/dev/null)" 2>/dev/null || true
|
||||
echo "::group::Simulator App Logs (last 200 lines)"
|
||||
tail -200 /tmp/simulator-app.log 2>/dev/null || echo "No app logs captured"
|
||||
echo "::endgroup::"
|
||||
echo "::group::Simulator Error Logs (last 100 lines)"
|
||||
tail -100 /tmp/simulator-errors.log 2>/dev/null || echo "No error logs captured"
|
||||
echo "::endgroup::"
|
||||
echo "::group::Metro Bundler Output (last 100 lines)"
|
||||
tail -100 /tmp/metro.log 2>/dev/null || echo "No metro logs captured"
|
||||
echo "::endgroup::"
|
||||
echo "::group::Crash Reports"
|
||||
find ~/Library/Logs/DiagnosticReports -name "*hanzoguikitchensink*" -newer /tmp/simulator-app.log 2>/dev/null | while read f; do
|
||||
echo "=== $f ==="
|
||||
head -50 "$f"
|
||||
done || echo "No crash reports found"
|
||||
echo "::endgroup::"
|
||||
|
||||
- name: Report Test Status
|
||||
if: always() && env.SKIP_MAESTRO_TESTS == 'true'
|
||||
run: |
|
||||
echo "::warning::Maestro tests skipped - cache was not available (will be available on next run)"
|
||||
|
||||
- name: Upload Maestro Artifacts on Failure
|
||||
if: failure() && env.SKIP_MAESTRO_TESTS != 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: maestro-artifacts-exports-${{ matrix.package-exports }}
|
||||
path: |
|
||||
~/.maestro/tests/**/*
|
||||
/tmp/simulator-app.log
|
||||
/tmp/simulator-errors.log
|
||||
/tmp/metro.log
|
||||
@@ -0,0 +1,669 @@
|
||||
name: Native Tests (Detox)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- 'native-*'
|
||||
- 'v*'
|
||||
- rn82
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
# required: this workflow calls the reusable build-ios-kitchensink-app.yml,
|
||||
# whose build-ios job declares pull-requests: read (caller must grant >= callee)
|
||||
pull-requests: read
|
||||
|
||||
# cancel in-progress runs on feature branches, but let main finish for full history
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
env:
|
||||
test_container_app_path: apps/kitchen-sink
|
||||
# Increment this to force a fresh Android build (invalidates Redis KV cache)
|
||||
android_cache_version: v4
|
||||
|
||||
jobs:
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# iOS Build & Test
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
validate-ios-detox-shards:
|
||||
name: Validate iOS Detox Shards
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Validate shard coverage
|
||||
run: node .github/scripts/check-ios-detox-shards.mjs
|
||||
|
||||
build-ios:
|
||||
name: Build iOS App
|
||||
uses: ./.github/workflows/build-ios-kitchensink-app.yml
|
||||
with:
|
||||
configuration: Debug
|
||||
secrets: inherit
|
||||
|
||||
test-ios:
|
||||
name: iOS Detox Tests (${{ matrix.shard_name }})
|
||||
needs:
|
||||
- build-ios
|
||||
- validate-ios-detox-shards
|
||||
runs-on: macos-15
|
||||
timeout-minutes: 90
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# 4 shards balanced by test execution time. the old ~7min/shard ts-jest
|
||||
# type-check tax is gone (e2e now transpiles via e2e/tsconfig.json), so
|
||||
# startup is small and these fit well under the native-ci runner's
|
||||
# 45-minute process timeout. CompilerExtraction runs an in-test
|
||||
# `npx hanzogui build` (~12-19min, variable) so it gets its OWN shard
|
||||
# (with the iOS-skipped SafeArea/SheetKeyboardDrag, which cost ~0, plus
|
||||
# the small iOS-running SheetFitKeyboardSafeArea, ~1 launch); the
|
||||
# remaining files are bin-packed to ~14.5min test-exec each (~25min wall).
|
||||
# SelectAndroidOnPress is android-only (excluded here via
|
||||
# check-ios-detox-shards.mjs, still runs in the android job). Coverage
|
||||
# validated by that script (every e2e test assigned exactly once).
|
||||
- shard_name: '1/4'
|
||||
test_files: 'e2e/Accordion.test.ts e2e/CompilerExtraction.test.ts e2e/SafeArea.test.ts e2e/SheetKeyboardDrag.test.ts e2e/SheetFitKeyboardSafeArea.test.ts e2e/SheetPressResponderSheetRngh.test.ts'
|
||||
- shard_name: '2/4'
|
||||
test_files: 'e2e/PressStyleNative.noRngh.test.ts e2e/CompilerTernaryActive.test.ts e2e/TabsOnInteraction.test.ts e2e/PointerEvents.test.ts e2e/GroupPressNative.test.ts e2e/ShorthandVariables.test.ts e2e/check-rngh-status.test.ts'
|
||||
- shard_name: '3/4'
|
||||
test_files: 'e2e/SheetScrollableDrag.test.ts e2e/ThemeMutation.test.ts e2e/NativePortal.test.ts e2e/GroupPressTransitionMatrix.test.ts e2e/MenuRadioGroup.test.ts e2e/PressStyleScrollStuck.test.ts e2e/NativeMixedDriver.test.ts'
|
||||
- shard_name: '4/4'
|
||||
test_files: 'e2e/PressStyleNative.test.ts e2e/SheetPressRegression.test.ts e2e/SheetKeyboardFitContent.test.ts e2e/SelectRemount.test.ts e2e/SheetDragResist.test.ts e2e/ThemeChangeBasic.test.ts e2e/NestedPressExclusive.test.ts e2e/MediaQueryGtMd.test.ts'
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ${{ env.test_container_app_path }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Set Xcode version
|
||||
run: sudo xcode-select -s /Applications/Xcode_26.0.1.app
|
||||
|
||||
- name: Restore Built App from Cache
|
||||
id: restore-ios-cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
key: ${{ needs.build-ios.outputs.built-app-cache-key }}
|
||||
path: ${{ needs.build-ios.outputs.built-app-path }}
|
||||
|
||||
- name: Check iOS Cache Restore
|
||||
if: steps.restore-ios-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
echo "::warning::iOS cache not restored (key: ${{ needs.build-ios.outputs.built-app-cache-key }}). This may happen on first run after cache key changes."
|
||||
echo "SKIP_IOS_TESTS=true" >> $GITHUB_ENV
|
||||
|
||||
- name: Boot iOS Simulator (background)
|
||||
if: env.SKIP_IOS_TESTS != 'true'
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
# start simulator boot in background while deps install
|
||||
xcrun simctl shutdown all 2>/dev/null || true
|
||||
sleep 1
|
||||
|
||||
# find iPhone 16 simulator
|
||||
DEVICE_ID=$(xcrun simctl list devices available -j | jq -r '
|
||||
.devices | to_entries[]
|
||||
| select(.key | contains("iOS"))
|
||||
| .value[]
|
||||
| select(.name == "iPhone 16")
|
||||
| .udid' | head -1)
|
||||
|
||||
if [ -z "$DEVICE_ID" ]; then
|
||||
DEVICE_ID=$(xcrun simctl list devices available -j | jq -r '
|
||||
.devices | to_entries[]
|
||||
| select(.key | contains("iOS"))
|
||||
| .value[]
|
||||
| select(.name | contains("iPhone"))
|
||||
| .udid' | head -1)
|
||||
fi
|
||||
|
||||
echo "Booting simulator: $DEVICE_ID"
|
||||
xcrun simctl boot "$DEVICE_ID" || true
|
||||
echo "SIM_DEVICE_ID=$DEVICE_ID" >> $GITHUB_ENV
|
||||
|
||||
- name: Install Dependencies
|
||||
if: env.SKIP_IOS_TESTS != 'true'
|
||||
uses: ./.github/actions/install
|
||||
with:
|
||||
workspace-focus: '@hanzogui/kitchen-sink'
|
||||
|
||||
- name: Install tooling
|
||||
if: env.SKIP_IOS_TESTS != 'true'
|
||||
run: |
|
||||
npm install -g detox-cli &
|
||||
detox_pid=$!
|
||||
|
||||
# install by fully-qualified name so homebrew auto-trusts only this
|
||||
# formula (homebrew 6 refuses untrusted third-party taps otherwise)
|
||||
brew install wix/brew/applesimutils &
|
||||
applesimutils_pid=$!
|
||||
|
||||
wait "$detox_pid"
|
||||
wait "$applesimutils_pid"
|
||||
|
||||
brew_bin="$(brew --prefix)/bin"
|
||||
echo "$brew_bin" >> "$GITHUB_PATH"
|
||||
export PATH="$brew_bin:$PATH"
|
||||
|
||||
command -v detox
|
||||
command -v applesimutils
|
||||
|
||||
- name: Rebuild Detox Framework Cache
|
||||
if: env.SKIP_IOS_TESTS != 'true'
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: npx detox rebuild-framework-cache
|
||||
|
||||
- name: Wait for Simulator Boot
|
||||
if: env.SKIP_IOS_TESTS != 'true'
|
||||
working-directory: ${{ github.workspace }}
|
||||
timeout-minutes: 5
|
||||
run: |
|
||||
echo "Waiting for simulator $SIM_DEVICE_ID to boot..."
|
||||
# macOS doesn't have `timeout` — use perl one-liner as fallback
|
||||
perl -e 'alarm 300; exec @ARGV' xcrun simctl bootstatus "$SIM_DEVICE_ID" -b || {
|
||||
if xcrun simctl list devices booted | grep -q "Booted"; then
|
||||
echo "Simulator appears booted despite timeout"
|
||||
else
|
||||
echo "Simulator failed to boot"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
sleep 5
|
||||
|
||||
- name: Set Simulator to Light Mode
|
||||
if: env.SKIP_IOS_TESTS != 'true'
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
xcrun simctl ui booted appearance light
|
||||
|
||||
# wait for SpringBoard to be fully ready (FrontBoard app registration depends on it)
|
||||
echo "Waiting for SpringBoard readiness..."
|
||||
for i in {1..30}; do
|
||||
if xcrun simctl spawn booted launchctl list 2>/dev/null | grep -q SpringBoard; then
|
||||
echo "SpringBoard ready after ~${i}s"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
- name: Install App on Simulator
|
||||
if: env.SKIP_IOS_TESTS != 'true'
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
APP_PATH="${{ needs.build-ios.outputs.built-app-path }}"
|
||||
BUNDLE_ID="com.hanzogui.hanzoguikitchensink"
|
||||
|
||||
for attempt in 1 2 3; do
|
||||
echo "Install attempt $attempt/3..."
|
||||
xcrun simctl install booted "$APP_PATH"
|
||||
sleep 3
|
||||
|
||||
# verify FrontBoard registered the app
|
||||
if xcrun simctl get_app_container booted "$BUNDLE_ID" 2>/dev/null; then
|
||||
echo "App installed and registered on attempt $attempt"
|
||||
|
||||
# launch once to prime FrontBoard, then terminate
|
||||
xcrun simctl launch booted "$BUNDLE_ID" 2>/dev/null || true
|
||||
sleep 2
|
||||
xcrun simctl terminate booted "$BUNDLE_ID" 2>/dev/null || true
|
||||
break
|
||||
fi
|
||||
|
||||
echo "::warning::App not registered with FrontBoard after install (attempt $attempt)"
|
||||
if [ $attempt -lt 3 ]; then
|
||||
xcrun simctl uninstall booted "$BUNDLE_ID" 2>/dev/null || true
|
||||
sleep 5
|
||||
else
|
||||
echo "::error::App failed to register after 3 install attempts"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Setup Bun
|
||||
if: env.SKIP_IOS_TESTS != 'true'
|
||||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
with:
|
||||
bun-version-file: 'package.json'
|
||||
|
||||
- name: Run iOS Detox Tests
|
||||
if: env.SKIP_IOS_TESTS != 'true'
|
||||
env:
|
||||
DETOX_IOS_APP_PATH: build/Build/Products/Debug-iphonesimulator/hanzoguikitchensink.app
|
||||
run: |
|
||||
# --retries 0: no whole-file retry (that re-ran beforeAll + every test of a
|
||||
# flaky file, the 2x wall-time variance). individual flaky tests retry
|
||||
# in-place via jest.retryTimes (e2e/jest.setup.ts).
|
||||
bun run ../native-ci/src/run-detox-ios.ts --project-root "$PWD" --record-logs failing --retries 0 ${{ matrix.test_files }}
|
||||
|
||||
- name: Report iOS Test Status
|
||||
if: always() && env.SKIP_IOS_TESTS == 'true'
|
||||
run: |
|
||||
echo "::warning::iOS Detox tests skipped - cache was not available (will be available on next run)"
|
||||
|
||||
- name: Upload Detox Artifacts on Failure
|
||||
if: failure() && env.SKIP_IOS_TESTS != 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: detox-artifacts-ios-${{ strategy.job-index }}
|
||||
path: ${{ env.test_container_app_path }}/e2e/artifacts/
|
||||
retention-days: 7
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Android Build & Test
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
build-android:
|
||||
name: Build Android App
|
||||
# Only run on main/v* branches (not PRs)
|
||||
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/v') || github.ref == 'refs/heads/rn82'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
outputs:
|
||||
android-cache-key: ${{ steps.final-cache-key.outputs.cache_key }}
|
||||
android-path: apps/kitchen-sink/android
|
||||
fingerprint: ${{ steps.calculate-fingerprint.outputs.fingerprint || steps.get-fingerprint-from-cache.outputs.fingerprint }}
|
||||
steps:
|
||||
- name: Free Disk Space
|
||||
run: |
|
||||
# Android builds with debug symbols need significant disk space
|
||||
# Remove unused tools to free ~10GB+
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /usr/local/lib/android/sdk/ndk
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf /opt/hostedtoolcache/CodeQL
|
||||
df -h /
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Calculate Pre-Fingerprint Hash
|
||||
id: calculate-pre-fingerprint
|
||||
env:
|
||||
# Include cache version to allow manual cache busting
|
||||
PRE_FINGERPRINT_HASH: ${{ hashFiles('bun.lock', 'apps/kitchen-sink/app.json', 'packages/vxrn/expo-plugin.cjs') }}-${{ env.android_cache_version }}
|
||||
run: |
|
||||
echo "Pre-fingerprint hash: $PRE_FINGERPRINT_HASH"
|
||||
echo "pre_fingerprint_hash=$PRE_FINGERPRINT_HASH" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Read Cached Fingerprint
|
||||
id: get-fingerprint-from-cache
|
||||
env:
|
||||
KV_STORE_REDIS_REST_URL: ${{ secrets.KV_STORE_REDIS_REST_URL }}
|
||||
KV_STORE_REDIS_REST_TOKEN: ${{ secrets.KV_STORE_REDIS_REST_TOKEN }}
|
||||
run: |
|
||||
FINGERPRINT_FROM_CACHE=$(curl -s "$KV_STORE_REDIS_REST_URL/get/android-hanzoguikitchensink-fingerprint-from-pre-hash-${{ steps.calculate-pre-fingerprint.outputs.pre_fingerprint_hash }}" -H "Authorization: Bearer $KV_STORE_REDIS_REST_TOKEN" | jq -r '.result')
|
||||
|
||||
if [ "$FINGERPRINT_FROM_CACHE" != "null" ]; then
|
||||
curl -s -X POST "$KV_STORE_REDIS_REST_URL/EXPIRE/android-hanzoguikitchensink-fingerprint-from-pre-hash-${{ steps.calculate-pre-fingerprint.outputs.pre_fingerprint_hash }}/2592000" -H "Authorization: Bearer $KV_STORE_REDIS_REST_TOKEN"
|
||||
echo "Fingerprint from cache: $FINGERPRINT_FROM_CACHE"
|
||||
echo "fingerprint=$FINGERPRINT_FROM_CACHE" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo 'No cached fingerprint found.'
|
||||
echo "fingerprint=null" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Check if Android folder is already cached
|
||||
id: android-cache-check
|
||||
if: ${{ steps.get-fingerprint-from-cache.outputs.fingerprint != 'null' }}
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: apps/kitchen-sink/android
|
||||
key: android-detox-${{ env.android_cache_version }}-${{ steps.get-fingerprint-from-cache.outputs.fingerprint }}
|
||||
lookup-only: true
|
||||
|
||||
- name: Install Dependencies
|
||||
if: ${{ !steps.android-cache-check.outputs.cache-hit }}
|
||||
uses: ./.github/actions/install
|
||||
with:
|
||||
workspace-focus: '@hanzogui/kitchen-sink'
|
||||
|
||||
- name: Setup Java
|
||||
if: ${{ !steps.android-cache-check.outputs.cache-hit }}
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: Setup Android SDK
|
||||
if: ${{ !steps.android-cache-check.outputs.cache-hit }}
|
||||
uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3
|
||||
|
||||
- name: Prebuild Android
|
||||
if: ${{ !steps.android-cache-check.outputs.cache-hit }}
|
||||
working-directory: apps/kitchen-sink
|
||||
run: |
|
||||
bunx expo prebuild --platform android --clean
|
||||
|
||||
- name: Setup Detox Test Infrastructure
|
||||
if: ${{ !steps.android-cache-check.outputs.cache-hit }}
|
||||
working-directory: apps/kitchen-sink
|
||||
run: |
|
||||
# Create Gradle init script for JNI library conflicts
|
||||
cat > android/init.gradle << 'INITGRADLE'
|
||||
// Gradle init script to fix JNI library conflicts for Detox tests
|
||||
allprojects {
|
||||
afterEvaluate {
|
||||
if (it.hasProperty('android')) {
|
||||
android {
|
||||
packagingOptions {
|
||||
jniLibs {
|
||||
pickFirsts += ['**/libfbjni.so']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
INITGRADLE
|
||||
# Remove leading whitespace from heredoc
|
||||
sed -i 's/^ //' android/init.gradle
|
||||
|
||||
# Add Detox repository to build.gradle (using node for reliable file editing)
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
let content = fs.readFileSync('android/build.gradle', 'utf8');
|
||||
content = content.replace(
|
||||
\"maven { url 'https://www.jitpack.io' }\",
|
||||
\"maven { url 'https://www.jitpack.io' }\\n // Detox repository\\n maven { url \\\"\\\$rootDir/../../../node_modules/detox/Detox-android\\\" }\"
|
||||
);
|
||||
fs.writeFileSync('android/build.gradle', content);
|
||||
"
|
||||
|
||||
# Add Detox test runner config and dependencies to app/build.gradle
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
let content = fs.readFileSync('android/app/build.gradle', 'utf8');
|
||||
|
||||
// Add testInstrumentationRunner after versionName
|
||||
content = content.replace(
|
||||
'versionName \"1.0.0\"',
|
||||
'versionName \"1.0.0\"\\n\\n // Detox instrumentation test runner\\n testBuildType System.getProperty(\\'testBuildType\\', \\'debug\\')\\n testInstrumentationRunner \\'androidx.test.runner.AndroidJUnitRunner\\''
|
||||
);
|
||||
|
||||
// Add Detox dependencies before the final closing brace of dependencies block
|
||||
content = content.replace(
|
||||
/(implementation jscFlavor\s*\n\s*}\s*\n})/,
|
||||
'implementation jscFlavor\\n }\\n\\n // Detox dependencies\\n androidTestImplementation(\\'com.wix:detox:+\\')\\n androidTestImplementation \\'junit:junit:4.13.2\\'\\n}'
|
||||
);
|
||||
|
||||
fs.writeFileSync('android/app/build.gradle', content);
|
||||
"
|
||||
|
||||
# Create androidTest directory structure
|
||||
mkdir -p android/app/src/androidTest/java/com/hanzogui/hanzoguikitchensink
|
||||
|
||||
# Create DetoxTest.java
|
||||
cat > android/app/src/androidTest/java/com/hanzogui/hanzoguikitchensink/DetoxTest.java << 'DETOXTEST'
|
||||
package com.hanzogui.hanzoguikitchensink;
|
||||
|
||||
import com.wix.detox.Detox;
|
||||
import com.wix.detox.config.DetoxConfig;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.rule.ActivityTestRule;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@LargeTest
|
||||
public class DetoxTest {
|
||||
@Rule
|
||||
public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(MainActivity.class, false, false);
|
||||
|
||||
@Test
|
||||
public void runDetoxTests() {
|
||||
DetoxConfig detoxConfig = new DetoxConfig();
|
||||
detoxConfig.idlePolicyConfig.masterTimeoutSec = 90;
|
||||
detoxConfig.idlePolicyConfig.idleResourceTimeoutSec = 60;
|
||||
detoxConfig.rnContextLoadTimeoutSec = 180;
|
||||
|
||||
Detox.runTests(mActivityRule, detoxConfig);
|
||||
}
|
||||
}
|
||||
DETOXTEST
|
||||
|
||||
# Create AndroidManifest.xml for androidTest (fixes Android 12+ exported requirement)
|
||||
cat > android/app/src/androidTest/AndroidManifest.xml << 'MANIFEST'
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<!-- Fix for Android 12+ requiring explicit android:exported -->
|
||||
<application>
|
||||
<activity
|
||||
android:name="androidx.test.core.app.InstrumentationActivityInvoker$BootstrapActivity"
|
||||
android:exported="true"
|
||||
tools:node="merge" />
|
||||
<activity
|
||||
android:name="androidx.test.core.app.InstrumentationActivityInvoker$EmptyActivity"
|
||||
android:exported="true"
|
||||
tools:node="merge" />
|
||||
<activity
|
||||
android:name="androidx.test.core.app.InstrumentationActivityInvoker$EmptyFloatingActivity"
|
||||
android:exported="true"
|
||||
tools:node="merge" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
MANIFEST
|
||||
|
||||
- name: Calculate Fingerprint
|
||||
if: ${{ !steps.android-cache-check.outputs.cache-hit }}
|
||||
id: calculate-fingerprint
|
||||
working-directory: apps/kitchen-sink
|
||||
run: |
|
||||
FINGERPRINT=$(bunx @expo/fingerprint fingerprint:generate --platform android | jq -r '.hash')
|
||||
if [ -z "$FINGERPRINT" ]; then
|
||||
echo '[ERROR] Failed to calculate fingerprint.'
|
||||
exit 1
|
||||
fi
|
||||
echo "Fingerprint: $FINGERPRINT"
|
||||
echo "fingerprint=$FINGERPRINT" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Write Fingerprint to Cache
|
||||
if: ${{ !steps.android-cache-check.outputs.cache-hit }}
|
||||
env:
|
||||
KV_STORE_REDIS_REST_URL: ${{ secrets.KV_STORE_REDIS_REST_URL }}
|
||||
KV_STORE_REDIS_REST_TOKEN: ${{ secrets.KV_STORE_REDIS_REST_TOKEN }}
|
||||
run: |
|
||||
curl -s -X POST "$KV_STORE_REDIS_REST_URL/SETEX/android-hanzoguikitchensink-fingerprint-from-pre-hash-${{ steps.calculate-pre-fingerprint.outputs.pre_fingerprint_hash }}/2592000/${{ steps.calculate-fingerprint.outputs.fingerprint }}" -H "Authorization: Bearer $KV_STORE_REDIS_REST_TOKEN"
|
||||
|
||||
- name: Check If Build Already Exists (with new fingerprint)
|
||||
if: ${{ !steps.android-cache-check.outputs.cache-hit }}
|
||||
id: android-cache-check-new
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: apps/kitchen-sink/android
|
||||
key: android-detox-${{ env.android_cache_version }}-${{ steps.calculate-fingerprint.outputs.fingerprint }}
|
||||
lookup-only: true
|
||||
|
||||
- name: Cache Gradle
|
||||
if: ${{ !steps.android-cache-check.outputs.cache-hit && !steps.android-cache-check-new.outputs.cache-hit }}
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-
|
||||
|
||||
- name: Build Android App (Debug + Test APKs)
|
||||
if: ${{ !steps.android-cache-check.outputs.cache-hit && !steps.android-cache-check-new.outputs.cache-hit }}
|
||||
working-directory: apps/kitchen-sink
|
||||
env:
|
||||
# Limit Gradle memory to avoid OOM on free runners
|
||||
GRADLE_OPTS: '-Dorg.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError'
|
||||
run: |
|
||||
cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug --init-script init.gradle --no-daemon --max-workers=2
|
||||
|
||||
- name: Cache Android Folder
|
||||
if: ${{ !steps.android-cache-check.outputs.cache-hit && !steps.android-cache-check-new.outputs.cache-hit }}
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: apps/kitchen-sink/android
|
||||
key: android-detox-${{ env.android_cache_version }}-${{ steps.calculate-fingerprint.outputs.fingerprint }}
|
||||
|
||||
- name: Set Final Cache Key
|
||||
id: final-cache-key
|
||||
run: |
|
||||
if [ "${{ steps.android-cache-check.outputs.cache-hit }}" = "true" ]; then
|
||||
echo "cache_key=android-detox-${{ env.android_cache_version }}-${{ steps.get-fingerprint-from-cache.outputs.fingerprint }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "cache_key=android-detox-${{ env.android_cache_version }}-${{ steps.calculate-fingerprint.outputs.fingerprint }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
test-android:
|
||||
name: Android Detox Tests
|
||||
needs: build-android
|
||||
# Only run on main/v* branches (not PRs)
|
||||
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/v') || github.ref == 'refs/heads/rn82'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
# Android emulator in CI is flaky with window focus issues - don't block on failures
|
||||
continue-on-error: true
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ${{ env.test_container_app_path }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Enable KVM
|
||||
run: |
|
||||
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger --name-match=kvm
|
||||
|
||||
- name: Restore Android Folder from Cache
|
||||
id: restore-android-cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
key: ${{ needs.build-android.outputs.android-cache-key }}
|
||||
path: ${{ needs.build-android.outputs.android-path }}
|
||||
|
||||
- name: Check Cache Restore
|
||||
if: steps.restore-android-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
echo "::warning::Android cache not restored (key: ${{ needs.build-android.outputs.android-cache-key }}). This may happen on first run after cache key changes."
|
||||
echo "SKIP_ANDROID_TESTS=true" >> $GITHUB_ENV
|
||||
|
||||
- name: Free Disk Space
|
||||
if: env.SKIP_ANDROID_TESTS != 'true'
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /usr/local/lib/android/sdk/ndk
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf /opt/hostedtoolcache/CodeQL
|
||||
df -h /
|
||||
|
||||
- name: Install Dependencies
|
||||
if: env.SKIP_ANDROID_TESTS != 'true'
|
||||
uses: ./.github/actions/install
|
||||
with:
|
||||
workspace-focus: '@hanzogui/kitchen-sink'
|
||||
|
||||
- name: Setup Java
|
||||
if: env.SKIP_ANDROID_TESTS != 'true'
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: Install Detox CLI
|
||||
if: env.SKIP_ANDROID_TESTS != 'true'
|
||||
run: npm install -g detox-cli
|
||||
|
||||
- name: Setup Bun
|
||||
if: env.SKIP_ANDROID_TESTS != 'true'
|
||||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
with:
|
||||
bun-version-file: 'package.json'
|
||||
|
||||
- name: Start Android Emulator and Run Tests
|
||||
if: env.SKIP_ANDROID_TESTS != 'true'
|
||||
id: android-tests
|
||||
uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a # v2.37.0
|
||||
with:
|
||||
api-level: 31
|
||||
target: google_apis
|
||||
arch: x86_64
|
||||
profile: pixel_4
|
||||
avd-name: test
|
||||
# 4G ram + 1G heap (was 2G/512M) - the kitchen-sink JS bundle eagerly imports
|
||||
# all 192 test cases, so first launch under software graphics needs more headroom
|
||||
# to let the main looper idle within AndroidX's 45s startActivitySync timeout.
|
||||
ram-size: 4096M
|
||||
heap-size: 1024M
|
||||
disk-size: 4G
|
||||
force-avd-creation: false
|
||||
emulator-boot-timeout: 600
|
||||
disable-animations: true
|
||||
emulator-options: -no-snapshot -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -camera-front none
|
||||
working-directory: apps/kitchen-sink
|
||||
script: |
|
||||
# Free up memory before tests
|
||||
sudo swapoff -a || true
|
||||
sudo swapon -a || true
|
||||
|
||||
# Verify emulator is ready
|
||||
adb devices
|
||||
adb wait-for-device
|
||||
|
||||
# Wait for boot to complete
|
||||
adb shell 'while [ -z "$(getprop sys.boot_completed)" ]; do sleep 1; done'
|
||||
|
||||
# More robust screen unlock and focus sequence
|
||||
adb shell wm dismiss-keyguard
|
||||
adb shell input keyevent 82 # KEYEVENT_MENU - wake/unlock
|
||||
adb shell input keyevent 3 # KEYEVENT_HOME - go home first
|
||||
sleep 1
|
||||
adb shell input keyevent 82 # Unlock again
|
||||
adb shell input keyevent 4 # Back button to dismiss any dialogs
|
||||
sleep 3
|
||||
|
||||
# Verify screen is on and unlocked
|
||||
adb shell dumpsys window | grep -E "mAwake|mScreenOn" || true
|
||||
|
||||
# Let Detox handle APK installation and ADB reverse setup via reversePorts config
|
||||
# Android emulator is flaky - capture exit code but don't fail the job
|
||||
# --retries 0: no whole-file retry; individual tests retry via jest.retryTimes (e2e/jest.setup.ts)
|
||||
bun run ../native-ci/src/run-detox-android.ts --headless --project-root "$PWD" --record-logs failing --retries 0 || echo "ANDROID_TESTS_FAILED=true" >> $GITHUB_ENV
|
||||
|
||||
- name: Report Android Test Status
|
||||
if: always()
|
||||
run: |
|
||||
if [ "$SKIP_ANDROID_TESTS" = "true" ]; then
|
||||
echo "::warning::Android Detox tests skipped - cache was not available (will be available on next run)"
|
||||
elif [ "$ANDROID_TESTS_FAILED" = "true" ]; then
|
||||
echo "::warning::Android Detox tests failed - this is expected flakiness and does not block CI"
|
||||
else
|
||||
echo "Android Detox tests passed"
|
||||
fi
|
||||
|
||||
- name: Upload Detox Artifacts on Failure
|
||||
if: env.ANDROID_TESTS_FAILED == 'true' && env.SKIP_ANDROID_TESTS != 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: detox-artifacts-android
|
||||
path: ${{ env.test_container_app_path }}/e2e/artifacts/
|
||||
retention-days: 7
|
||||
+32
-15
@@ -1,11 +1,19 @@
|
||||
**/test-results/*
|
||||
**/.maestro-debug
|
||||
**/.gui/
|
||||
|
||||
# keep types in repo to track them
|
||||
# **/types/**/*.d.ts
|
||||
# but map can be ignored and just published on npm
|
||||
**/types/**/*.d.ts.map
|
||||
# generated declarations are build artifacts: hanzogui-build emits every
|
||||
# package's into <pkg>/types/, the build fixtures emit to their package root.
|
||||
# they still ship on npm - every package lists "types" in its files[].
|
||||
# hand-authored .d.ts stay tracked (re-export shims like pkgs/core/config/v5.d.ts,
|
||||
# ambient stubs like apps/gui.hanzo.ai/types/bento.d.ts): none live under pkgs/*/types/.
|
||||
pkgs/**/types/**/*.d.ts
|
||||
pkgs/**/types/**/*.d.ts.map
|
||||
pkgs/build/__tests__/fixtures/**/*.d.ts
|
||||
pkgs/build/__tests__/fixtures/**/*.d.ts.map
|
||||
pkgs/**/vitest.config.d.ts
|
||||
pkgs/**/vitest.config.d.ts.map
|
||||
pkgs/**/*.test.d.ts
|
||||
pkgs/**/*.test.d.ts.map
|
||||
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
@@ -32,20 +40,31 @@ node_modules
|
||||
|
||||
.vercel
|
||||
|
||||
code/kitchen-sink/ios
|
||||
apps/kitchen-sink/ios
|
||||
|
||||
.gui
|
||||
.hanzogui
|
||||
|
||||
.idea
|
||||
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.localp
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
code/kitchen-sink/ios
|
||||
code/kitchen-sink/android
|
||||
# key material / credentials - never commit
|
||||
*.pem
|
||||
*.key
|
||||
*.p12
|
||||
*.pfx
|
||||
*.keystore
|
||||
*.jks
|
||||
*.p8
|
||||
*.mobileprovision
|
||||
**/serviceAccount*.json
|
||||
|
||||
apps/kitchen-sink/ios
|
||||
apps/kitchen-sink/android
|
||||
|
||||
code/studio/types/**
|
||||
|
||||
@@ -57,9 +76,7 @@ code/studio/types/**
|
||||
.playwright-report/
|
||||
.playwright-results/
|
||||
test-results/
|
||||
.gui/
|
||||
# Wed Mar 25 04:08:49 PDT 2026
|
||||
*.tgz
|
||||
|
||||
# tauri build output only — no source lives here (2.1G)
|
||||
apps/desktop/
|
||||
# Rust build artifacts — never commit; a debug .a is 272MB and GitHub caps at 100MB.
|
||||
apps/desktop/src-tauri/target/
|
||||
**/src-tauri/target/
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
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
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"XcodeBuildMCP": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "xcodebuildmcp@latest"]
|
||||
},
|
||||
"native-devtools": {
|
||||
"command": "/Users/n8/.local/bin/native-devtools-mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,2 +1,2 @@
|
||||
code/tests/next-turbopack/src/TestComponent.tsx
|
||||
**/.gui/
|
||||
apps/tests/next-turbopack/src/TestComponent.tsx
|
||||
**/.react-router
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@
|
||||
"**/*.lock",
|
||||
"**/*.generated.*",
|
||||
"**/generated-*",
|
||||
"**/.gui/**",
|
||||
"**/.hanzogui/**",
|
||||
"**/node_modules/**",
|
||||
],
|
||||
}
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"**/scripts/**",
|
||||
"**/.gui/**",
|
||||
"**/.hanzogui/**",
|
||||
"**/tmp-data/**",
|
||||
"**/.build/**",
|
||||
"**/.sst/**",
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
**/*.d.ts
|
||||
**/generated-*.ts
|
||||
code/kitchen-sink/src/usecases/CompilerExtraction.native.tsx
|
||||
apps/kitchen-sink/src/usecases/CompilerExtraction.native.tsx
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
# Gui — agent operating contract
|
||||
|
||||
`CLAUDE.md` is a symlink to this file, so Claude, Codex, and every other agent
|
||||
read the same rules. Put durable, repo-wide agent guidance here.
|
||||
|
||||
## Finish what you start, then merge it back
|
||||
|
||||
The most common failure here is stopping early: a fix left on a branch, a
|
||||
migration written but never applied, a "critical, will do next" item dropped.
|
||||
Don't do that. If you were asked to do something, it is not done until:
|
||||
|
||||
- it is committed, pushed, validated, and **merged back to `main`** (or a PR is
|
||||
opened and driven to merge) — "I left it on a branch" / "I prepared the SQL"
|
||||
is not done;
|
||||
- it is **validated at the layer it changes**: typecheck/build for code, a real
|
||||
request or Playwright run for site behavior, an applied-and-verified query for
|
||||
a DB/RLS change. "Should work" is not validation;
|
||||
- any **migration is actually applied and the live state verified** — this repo's
|
||||
prod migration history is NOT auto-applied on deploy (see
|
||||
`apps/gui.hanzo.ai/supabase/README.md`); a committed migration that was never
|
||||
run is an open gap, not a fix;
|
||||
- for security/payments work especially, the loop is closed end to end — a
|
||||
documented finding with no shipped, deployed fix counts as no fix.
|
||||
|
||||
Keep going until the requested outcome is real. Continuing beats reporting. The
|
||||
only hard stops are the externally-irreversible ones — publishing/releasing to
|
||||
npm, force-pushing, rotating credentials, or changing prod infra (Cloudflare,
|
||||
DNS, Railway settings): pause and confirm for those, and hand them back with the
|
||||
exact steps when they block you. Everything else, finish it.
|
||||
|
||||
---
|
||||
|
||||
Please read ./CONTRIBUTING.md as well
|
||||
|
||||
Note you need to re-build packages (`bun run build` in the package directory) as you change them, unless you or someone is running a `bun run watch` at root.
|
||||
|
||||
FOR LONG RUNNNING DEBUGGING run `bun run watch` in the background its faster and rebuilds all packages.
|
||||
|
||||
keep commits to one line, add a trailing "Fixes #" if associated with a GH issue, and start with a convential commit style - UNLESS its a change that shouldn't go into the changelog, in those cases you can do things like `docs: ` or `site: `.
|
||||
|
||||
# Gui Testing Guide
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Kitchen Sink Tests
|
||||
|
||||
The kitchen-sink package contains the main integration tests for Gui components. To run these tests:
|
||||
|
||||
1. **Start the web server** (in the background):
|
||||
|
||||
```bash
|
||||
cd apps/kitchen-sink
|
||||
bun run start:web
|
||||
```
|
||||
|
||||
To open a specific test case in the browser:
|
||||
|
||||
```bash
|
||||
open "http://localhost:7979/?test=YourTestCaseName"
|
||||
```
|
||||
|
||||
Test case names match the file names in `apps/kitchen-sink/src/usecases/` (e.g., `SelectFocusScopeCase`).
|
||||
|
||||
To open a component demo:
|
||||
|
||||
```bash
|
||||
open "http://localhost:7979/?demo=Select"
|
||||
```
|
||||
|
||||
Demo names match files in `apps/demos/src/` without the `Demo` suffix (e.g., `Select` for `SelectDemo.tsx`).
|
||||
|
||||
2. **Run all web tests** with different animation drivers:
|
||||
|
||||
```bash
|
||||
bun run test:web
|
||||
```
|
||||
|
||||
This uses `run-tests-parallel.ts` which first runs `default` + `webkit` projects sequentially, then runs all four animated driver projects (`css`, `native`, `reanimated`, `motion`) in parallel against a single shared dev server.
|
||||
|
||||
3. **Run tests with a specific animation driver**:
|
||||
|
||||
```bash
|
||||
# Using env var + playwright --project flag
|
||||
cd apps/kitchen-sink
|
||||
NODE_ENV=test GUI_TEST_ANIMATION_DRIVER=css npx playwright test --project=animated-css
|
||||
|
||||
# Available projects: animated-css, animated-native, animated-reanimated, animated-motion
|
||||
```
|
||||
|
||||
4. **Run a specific test file**:
|
||||
|
||||
```bash
|
||||
# Using playwright directly
|
||||
cd apps/kitchen-sink
|
||||
npx playwright test tests/PopoverFocusScope.test.tsx
|
||||
|
||||
# Or with a specific driver
|
||||
NODE_ENV=test GUI_TEST_ANIMATION_DRIVER=css npx playwright test tests/YourTest.animated.test.tsx --project=animated-css
|
||||
```
|
||||
|
||||
5. **Debug tests**:
|
||||
```bash
|
||||
bun run test:web:debug
|
||||
# or
|
||||
npx playwright test --debug
|
||||
```
|
||||
|
||||
### Test Structure
|
||||
|
||||
Tests are located in `apps/kitchen-sink/tests/` and follow these naming conventions:
|
||||
|
||||
- `ComponentName.test.tsx` - Standard tests that run ONCE with the default animation driver
|
||||
- `ComponentName.animated.test.tsx` - Animation-dependent tests that run with ALL animation drivers (css, native, reanimated, motion)
|
||||
|
||||
This separation significantly speeds up the test suite since most tests don't need to run 4x across all animation drivers. Only use `.animated.test.tsx` for tests that specifically verify animation behavior across different drivers.
|
||||
|
||||
### Writing Tests
|
||||
|
||||
When writing tests for focus behavior or component interactions:
|
||||
|
||||
1. Use appropriate wait times for animations and focus changes
|
||||
2. Be aware that `trapFocus` behavior depends on the component's open state
|
||||
3. Test both trapped and non-trapped focus scenarios
|
||||
4. Consider browser focus behavior when `trapFocus` is false
|
||||
|
||||
### Common Issues
|
||||
|
||||
- If tests fail due to timing, add appropriate `waitForTimeout` calls
|
||||
- For focus tests, ensure elements are visible before testing focus state
|
||||
- When testing popover/dialog components, wait for animations to complete
|
||||
|
||||
## Releasing: the workspace has one version, and right now the tree disagrees
|
||||
|
||||
`scripts/release.ts` carries a single `version` for all 169 published packages,
|
||||
read from `pkgs/ui/hanzogui/package.json`. Every path assumes it — the bump, the
|
||||
`npm view name@version` already-published check, and the tarball name
|
||||
`getPublishArtifactPaths` expects `npm pack` to produce.
|
||||
|
||||
The tree no longer holds to it. `@hanzogui/shell` was hand-bumped to 8.0.7 while
|
||||
`@hanzo/gui` sits at 8.0.2 and `@hanzogui/core` at 8.0.1, so each dispatch of
|
||||
`Release` fails a different way, and none of them says so:
|
||||
|
||||
- **`patch`** computes 8.0.3 and writes it to every package.json — rewriting
|
||||
shell *backward* from 8.0.7, over versions 8.0.3–8.0.6 that are already on
|
||||
npm. The publish then skips it as "already published" and pushes the
|
||||
regression to `main`.
|
||||
- **`republish`** (`--republish` skips the version write) packs shell at 8.0.7
|
||||
but looks for `hanzogui-shell-8.0.2.tgz`, because the artifact path is built
|
||||
from the workspace `version`. The run dies untarring a file `npm pack` never
|
||||
named.
|
||||
- **`minor`** is the one that works: 8.1.0 clears every published version, so
|
||||
the tree reconverges on one number and all 169 publish cleanly.
|
||||
|
||||
Verify with `bun scripts/release.ts --<kind> --ci --dirty --skip-publish
|
||||
--skip-push --skip-tests --skip-native-tests --skip-checks --dry-run`, which
|
||||
prints the computed version and the full publish plan and writes nothing.
|
||||
|
||||
Releasing one package alone is `--only <name>` locally; there is no dispatch for
|
||||
it, so a single-package release cannot carry npm provenance today — provenance
|
||||
needs the GitHub `Release` workflow's `id-token: write`, and that workflow only
|
||||
ships the whole workspace.
|
||||
|
||||
## Commit Message Conventions
|
||||
|
||||
- Use `site:` prefix (not `fix(site):`) for hanzogui.dev changes since they don't go in the changelog
|
||||
- Use `ci:` prefix (not `fix(ci):`) for CI/workflow changes since they don't go in the changelog
|
||||
- Keep commit messages to a single line
|
||||
|
||||
## iOS Development
|
||||
|
||||
See [docs/using-ios.md](./docs/using-ios.md) for iOS native development and Detox testing tips.
|
||||
|
||||
## hanzogui.dev API Authentication
|
||||
|
||||
When making authenticated API calls from the client side in hanzogui.dev, always use the `authFetch` helper:
|
||||
|
||||
```ts
|
||||
import { authFetch } from '~/features/api/authFetch'
|
||||
|
||||
const response = await authFetch('/api/some-endpoint', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ ... }),
|
||||
})
|
||||
```
|
||||
|
||||
**Why this matters:** Cookies alone are not reliable for auth in production due to cross-origin/SameSite issues. The `authFetch` helper automatically includes the Authorization header with the user's access token. All payment/subscription endpoints require this.
|
||||
+6
-6
@@ -1,10 +1,10 @@
|
||||
# Contributing to Hanzo GUI
|
||||
# Contributing to `hanzogui`
|
||||
|
||||
To set up your development environment you'll need [bun](https://bun.sh) installed, then clone and install:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/hanzoai/gui
|
||||
cd gui
|
||||
cd hanzogui
|
||||
bun install
|
||||
```
|
||||
|
||||
@@ -18,13 +18,13 @@ bun run watch
|
||||
|
||||
We compile out js to `dist` for "0-setup" installs by compiling a .native version of every file, and then in every web file we replace react-native with react-native-web. We also build our types out to ./types/\*.d.ts for a variety of reasons - it means we can't cause type issues for people with stricter configurations using our packages, it means we can track type regressions (the styled() type setup is impossible delicate to explosions of complexity), and it's generally faster for end users.
|
||||
|
||||
### Linking hanzo-gui into your existing project
|
||||
### Linking hanzogui into your existing project
|
||||
|
||||
Most package managers have a `link` command that lets you link in the local hanzo-gui to your project, we've built a package we found useful that works with `bun` or `yarn` depending on your configuration:
|
||||
Most package managers have a `link` command that lets you link in the local hanzogui to your project, we've built a package we found useful that works with `bun` or `yarn` depending on your configuration:
|
||||
|
||||
```bash
|
||||
npm i -g lllink
|
||||
lllink ~/path/to/gui
|
||||
lllink ~/path/to/hanzogui
|
||||
```
|
||||
|
||||
### Running native apps
|
||||
@@ -55,7 +55,7 @@ bun run kitchen-sink:build:ios
|
||||
|
||||
### Running web
|
||||
|
||||
You can run `bun run sandbox` or `bun run dev` (the Hanzo GUI website).
|
||||
You can run `bun run sandbox` or `bun run dev` (the hanzogui website).
|
||||
|
||||
### Local Testing Setup
|
||||
|
||||
|
||||
+6
-6
@@ -17,10 +17,10 @@ ARG GITHUB_APP_CLIENT_ID
|
||||
ARG GITHUB_APP_SECRET
|
||||
ARG GITHUB_SPONSOR_WEBHOOK_SECRET
|
||||
ARG GITHUB_TOKEN
|
||||
ARG IS_HANZO_GUI_DEV
|
||||
ARG IS_GUI_DEV
|
||||
ARG NEXT_PUBLIC_GITHUB_APP_ID
|
||||
ARG NEXT_PUBLIC_GITHUB_AUTH_CLIENT_ID
|
||||
ARG NEXT_PUBLIC_IS_HANZO_GUI_DEV
|
||||
ARG NEXT_PUBLIC_IS_GUI_DEV
|
||||
ARG NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
|
||||
ARG NEXT_PUBLIC_SUPABASE_ANON_KEY
|
||||
ARG NEXT_PUBLIC_SUPABASE_URL
|
||||
@@ -36,7 +36,7 @@ ARG TAKEOUT_RENEWAL_COUPON_ID
|
||||
ARG URL
|
||||
ARG ONE_SERVER_URL
|
||||
ARG APP_NAME
|
||||
ARG HANZO_GUI_PRO_SECRET
|
||||
ARG GUI_PRO_SECRET
|
||||
ARG DEEPSEEK_API_KEY
|
||||
ARG BENTO_GITHUB_TOKEN
|
||||
ARG BENTO_BRANCH
|
||||
@@ -44,7 +44,7 @@ ARG BENTO_BRANCH
|
||||
# install dependencies (sharp needs libvips for image processing)
|
||||
RUN apt-get update && apt-get install -y git bsdmainutils vim-common gh libvips-dev
|
||||
|
||||
WORKDIR /root/hanzo-gui
|
||||
WORKDIR /root/hanzogui
|
||||
COPY . .
|
||||
|
||||
# init git (allow empty commit if nothing to commit)
|
||||
@@ -58,7 +58,7 @@ RUN if [ -n "$BENTO_GITHUB_TOKEN" ]; then \
|
||||
echo "Cloning bento repository (branch: $BRANCH)..."; \
|
||||
unset GITHUB_TOKEN && \
|
||||
echo "$BENTO_GITHUB_TOKEN" | gh auth login --with-token && \
|
||||
gh repo clone hanzoai/bento -- --branch "$BRANCH" && \
|
||||
gh repo clone hanzogui/bento -- --branch "$BRANCH" && \
|
||||
gh auth logout --hostname github.com && \
|
||||
echo "✅ Bento repository cloned (branch: $BRANCH)" && \
|
||||
echo "REQUIRE_BENTO=true" > /tmp/bento_status; \
|
||||
@@ -67,7 +67,7 @@ RUN if [ -n "$BENTO_GITHUB_TOKEN" ]; then \
|
||||
echo "REQUIRE_BENTO=false" > /tmp/bento_status; \
|
||||
fi
|
||||
|
||||
WORKDIR /root/hanzo-gui
|
||||
WORKDIR /root/hanzogui
|
||||
|
||||
# Set REQUIRE_BENTO based on whether bento was cloned
|
||||
RUN export $(cat /tmp/bento_status)
|
||||
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
# local build dockerfile - uses bento from ../bento
|
||||
# build from PARENT directory: docker build -f gui5/Dockerfile.local -t gui-site .
|
||||
# build from PARENT directory: docker build -f hanzogui5/Dockerfile.local -t hanzogui-site .
|
||||
FROM node:22
|
||||
|
||||
# install bun
|
||||
@@ -46,14 +46,14 @@ WORKDIR /root
|
||||
|
||||
# copy both repos
|
||||
COPY bento /root/bento
|
||||
COPY gui5 /root/gui
|
||||
COPY hanzogui5 /root/hanzogui
|
||||
|
||||
WORKDIR /root/gui
|
||||
WORKDIR /root/hanzogui
|
||||
|
||||
# init git (allow empty commit if nothing to commit)
|
||||
RUN git config --global user.email "[email protected]" && git config --global user.name "Docker Build" && git init . && git add -A && (git commit -m 'add' > /dev/null || true)
|
||||
|
||||
# install gui deps first
|
||||
# install hanzogui deps first
|
||||
RUN bun install
|
||||
|
||||
# setup bento - merge deps and symlink node_modules
|
||||
|
||||
@@ -1,42 +1,26 @@
|
||||
BSD 3-Clause License
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026-present, Hanzo AI, Inc.
|
||||
Copyright (c) 2020 Nate Wienert
|
||||
Copyright (c) 2015-present, Nicolas Gallagher.
|
||||
Copyright (c) 2015-present, Facebook, Inc.
|
||||
Copyright (c) 2021 Radix
|
||||
Copyright (c) 2017 Carmelo Pullara
|
||||
Copyright (c) 2018 Framer B.V.
|
||||
|
||||
Portions of this software are derived from upstream code originally licensed under
|
||||
the MIT License, with the following copyright notices retained per its terms:
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
Copyright (c) 2020 Nate Wienert
|
||||
Copyright (c) 2015-present, Nicolas Gallagher.
|
||||
Copyright (c) 2015-present, Facebook, Inc.
|
||||
Copyright (c) 2021 Radix
|
||||
Copyright (c) 2017 Carmelo Pullara
|
||||
Copyright (c) 2018 Framer B.V.
|
||||
Copyright (c) 2022 WorkOS
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
@@ -1,288 +1,47 @@
|
||||
Please read ./CONTRIBUTING.md as well
|
||||
# @hanzo/gui — notes for whoever is here next
|
||||
|
||||
Note you need to re-build packages (`bun run build` in the package directory) as you change them, unless you or someone is running a `bun run watch` at root.
|
||||
## A targeted publish (`--only`) currently dies in the test gate
|
||||
|
||||
FOR LONG RUNNNING DEBUGGING run `bun run watch` in the background its faster and rebuilds all packages.
|
||||
`bun ./scripts/release.ts --only @hanzo/gui` fails before it publishes:
|
||||
|
||||
keep commits to one line, add a trailing "Fixes #" if associated with a GH issue, and start with a convential commit style - UNLESS its a change that shouldn't go into the changelog, in those cases you can do things like `docs: ` or `site: `.
|
||||
|
||||
# How this ships
|
||||
|
||||
One way, and it runs on our own stack:
|
||||
|
||||
push -> github.com/hanzoai/gui (a mirror)
|
||||
.github/workflows/sync.yml carries refs onward
|
||||
-> git.hanzo.ai/hanzoai/gui CANONICAL
|
||||
.hanzo/workflows/checks.yaml check, lint, unit tests
|
||||
.hanzo/workflows/publish-gui.yml hanzogui + @hanzo/gui
|
||||
.hanzo/workflows/publish-gui-all.yml every @hanzogui/* primitive
|
||||
.hanzo/workflows/build.yml ghcr.io/hanzoai/gui
|
||||
|
||||
**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 and publish 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. `.github/actions/install` stays
|
||||
where it is — a moved workflow's `uses: ./.github/actions/install` still resolves.
|
||||
|
||||
## What publishes what
|
||||
|
||||
Each publisher is the only producer of its packages, and a tag is the trigger in
|
||||
every case — a version bump alone never ships.
|
||||
|
||||
| tag | workflow | publishes |
|
||||
| --- | --- | --- |
|
||||
| `release/gui-v*` | `publish-gui.yml` | `hanzogui` + the `@hanzo/gui` alias |
|
||||
| `release/gui-all-v*` (or dispatch) | `publish-gui-all.yml` | every `@hanzogui/*` primitive + both umbrellas |
|
||||
|
||||
`NPM_TOKEN` comes from KMS with a repo-secret fallback in both `publish-gui*`
|
||||
workflows.
|
||||
|
||||
**This repo publishes primitives only.** The AI app that used to live at
|
||||
`pkgs/ai` — `@hanzo/app`, plus the seven `@hanzo_network/*` libraries it is
|
||||
built from — moved to `hanzo-js/app` on 2026-07-28, and publishes itself from
|
||||
there. An application inside the primitives repo is the layering mistake the
|
||||
owner named: `@hanzo/gui` (primitives) -> `@hanzo/ui` (widgets) -> apps.
|
||||
|
||||
## Known dead ends
|
||||
|
||||
- `build.yml` delegates to `hanzoai/.github/.github/workflows/docker-build.yml@main`
|
||||
and **that file does not exist** in `hanzoai/.github` (which carries
|
||||
`platform-build.yml` and `promote.yml`). The workflow has therefore never
|
||||
completed a run and `ghcr.io/hanzoai/gui` is not in the registry. It moved to
|
||||
`.hanzo/workflows/` unchanged rather than being deleted, because it is the only
|
||||
declaration of the image path; making it real means either inlining a build or
|
||||
pointing it at `platform-build.yml`.
|
||||
- `gui.hanzo.ai` is **not served from this repo.** It answers from Cloudflare Pages
|
||||
out of `hanzoai/docs` (`apps/gui-docs`). This repo's own `apps/gui.hanzo.ai` is
|
||||
`one serve` — a long-running server, not a static export — so `hanzoai/static`
|
||||
cannot serve it and there is no App CR for it. Either point a CR at the docs
|
||||
image or convert that app to a static export; both are their own change.
|
||||
- `checks.yaml` lost its `changes` + `integration-tests` jobs on the way over. They
|
||||
filtered on `code/core/**`, `code/kitchen-sink/**` and friends — an upstream
|
||||
Tamagui layout this repo does not have (the real paths are `apps/` and `pkgs/`) —
|
||||
so the filter never matched and the shard matrix never ran. The Playwright suite
|
||||
they were meant to run is at `apps/kitchen-sink/playwright.config.ts` and is
|
||||
currently wired to nothing.
|
||||
- Three iOS workflows were deleted outright: `build-ios-kitchensink-app.yml`,
|
||||
`test-ios-native.yml` and `test-ios-kitchensink-go.yml`. All three wanted
|
||||
`macos-14`/`macos-15` runners, which the forge does not have, and all three ran
|
||||
in `code/kitchen-sink`, which does not exist. One was already `if: false`. Native
|
||||
iOS testing needs macOS capacity first; these files could not have provided it.
|
||||
|
||||
# Hanzo GUI Testing Guide
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Kitchen Sink Tests
|
||||
|
||||
The kitchen-sink package contains the main integration tests for Hanzo GUI components. To run these tests:
|
||||
|
||||
1. **Start the web server** (in the background):
|
||||
|
||||
```bash
|
||||
cd apps/kitchen-sink
|
||||
bun run start:web
|
||||
```
|
||||
|
||||
To open a specific test case in the browser:
|
||||
|
||||
```bash
|
||||
open "http://localhost:9000/?test=YourTestCaseName"
|
||||
```
|
||||
|
||||
Test case names match the file names in `apps/kitchen-sink/src/usecases/` (e.g., `SelectFocusScopeCase`).
|
||||
|
||||
To open a component demo:
|
||||
|
||||
```bash
|
||||
open "http://localhost:9000/?demo=Select"
|
||||
```
|
||||
|
||||
Demo names match files in `apps/demos/src/` without the `Demo` suffix (e.g., `Select` for `SelectDemo.tsx`).
|
||||
|
||||
2. **Run all web tests** with different animation drivers:
|
||||
|
||||
```bash
|
||||
bun run test:web
|
||||
```
|
||||
|
||||
This uses `run-tests-parallel.ts` which first runs `default` + `webkit` projects sequentially, then runs all four animated driver projects (`css`, `native`, `reanimated`, `motion`) in parallel against a single shared dev server.
|
||||
|
||||
3. **Run tests with a specific animation driver**:
|
||||
|
||||
```bash
|
||||
# Using env var + playwright --project flag
|
||||
cd apps/kitchen-sink
|
||||
NODE_ENV=test HANZO_GUI_TEST_ANIMATION_DRIVER=css npx playwright test --project=animated-css
|
||||
|
||||
# Available projects: animated-css, animated-native, animated-reanimated, animated-motion
|
||||
```
|
||||
|
||||
4. **Run a specific test file**:
|
||||
|
||||
```bash
|
||||
# Using playwright directly
|
||||
cd apps/kitchen-sink
|
||||
npx playwright test tests/PopoverFocusScope.test.tsx
|
||||
|
||||
# Or with a specific driver
|
||||
NODE_ENV=test HANZO_GUI_TEST_ANIMATION_DRIVER=css npx playwright test tests/YourTest.animated.test.tsx --project=animated-css
|
||||
```
|
||||
|
||||
5. **Debug tests**:
|
||||
```bash
|
||||
bun run test:web:debug
|
||||
# or
|
||||
npx playwright test --debug
|
||||
```
|
||||
|
||||
### Test Structure
|
||||
|
||||
Tests are located in `apps/kitchen-sink/tests/` and follow these naming conventions:
|
||||
|
||||
- `ComponentName.test.tsx` - Standard tests that run ONCE with the default animation driver
|
||||
- `ComponentName.animated.test.tsx` - Animation-dependent tests that run with ALL animation drivers (css, native, reanimated, motion)
|
||||
|
||||
This separation significantly speeds up the test suite since most tests don't need to run 4x across all animation drivers. Only use `.animated.test.tsx` for tests that specifically verify animation behavior across different drivers.
|
||||
|
||||
### Writing Tests
|
||||
|
||||
When writing tests for focus behavior or component interactions:
|
||||
|
||||
1. Use appropriate wait times for animations and focus changes
|
||||
2. Be aware that `trapFocus` behavior depends on the component's open state
|
||||
3. Test both trapped and non-trapped focus scenarios
|
||||
4. Consider browser focus behavior when `trapFocus` is false
|
||||
|
||||
### Common Issues
|
||||
|
||||
- If tests fail due to timing, add appropriate `waitForTimeout` calls
|
||||
- For focus tests, ensure elements are visible before testing focus state
|
||||
- When testing popover/dialog components, wait for animations to complete
|
||||
|
||||
## Commit Message Conventions
|
||||
|
||||
- Use `site:` prefix (not `fix(site):`) for gui.hanzo.ai changes since they don't go in the changelog
|
||||
- Use `ci:` prefix (not `fix(ci):`) for CI/workflow changes since they don't go in the changelog
|
||||
- Keep commit messages to a single line
|
||||
|
||||
## iOS Development
|
||||
|
||||
See [docs/using-ios.md](./docs/using-ios.md) for iOS native development and Detox testing tips.
|
||||
|
||||
## gui.hanzo.ai API Authentication
|
||||
|
||||
When making authenticated API calls from the client side in gui.hanzo.ai, always use the `authFetch` helper:
|
||||
|
||||
```ts
|
||||
import { authFetch } from '~/features/api/authFetch'
|
||||
|
||||
const response = await authFetch('/api/some-endpoint', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ ... }),
|
||||
})
|
||||
```
|
||||
@hanzogui/kitchen-sink#test -> 9 failed, 576 passed, 19 skipped
|
||||
[webkit] tests/RemoveScroll.test.tsx (scroll position restored after close,
|
||||
scroll lock prevents body scroll, edge cases, mobile touch device)
|
||||
```
|
||||
|
||||
**Why this matters:** Cookies alone are not reliable for auth in production due to cross-origin/SameSite issues. The `authFetch` helper automatically includes the Authorization header with the user's access token. All payment/subscription endpoints require this.
|
||||
**These predate the change you are trying to ship.** They are webkit-only
|
||||
scroll-restoration failures in `apps/kitchen-sink`, and that file was last
|
||||
touched by `95c64deb76 transplant onto tamagui v2.6.0`. Nothing in `pkgs/ui`
|
||||
reaches them.
|
||||
|
||||
So a one-file fix to, say, `pkgs/ui/hanzogui/css-check.mjs` cannot pass the gate
|
||||
on its own merits, and the honest options are:
|
||||
|
||||
## Tailwind → gui migration (marketing surfaces)
|
||||
- let the fleet release carry it (this is what happened for 8.1.0 — a normal
|
||||
release from `main` picked up a pending css-check fix and shipped it), or
|
||||
- `--skip-test`, having first confirmed your change cannot reach kitchen-sink,
|
||||
or
|
||||
- fix the webkit failures, which is a real piece of work and nobody's yet.
|
||||
|
||||
`@hanzogui/chrome` is the shared public-site chrome (`HanzoNav`, `HanzoFooter`,
|
||||
`ChatHero`, `HanzoWidget`) in Tamagui `styled()` + monochrome tokens. It compiles
|
||||
and ships `dist/{esm,cjs,jsx}` + `types/`. It is the bridgehead: a surface adopts
|
||||
the chrome first, then its page bodies.
|
||||
Do not read a green `--only` run as "tests passed" — the script's exit code is
|
||||
easy to mask behind a shell pipeline. Check the log for the turbo summary.
|
||||
|
||||
**Read `pkgs/ui/chrome/src/styles.tsx`'s header before touching a chrome file.**
|
||||
It states the contract this package is built under — no host config augmentation —
|
||||
and every build break so far has been a violation of it: element type is `render`
|
||||
(not `tag`); hover is DOM `onMouseEnter`/`onMouseLeave` (not `onHoverIn`); only
|
||||
`Text` takes `color`; style props are LONGHAND (`textAlign`, `paddingHorizontal` —
|
||||
the `text`/`px` shorthands live in augmentation that is absent here); anchors
|
||||
forward `href` through the `linkable` `.styleable` wrapper. Two type consequences:
|
||||
`GetProps<F>` requires `F extends StylableComponent`, and a raw-hex token handed to
|
||||
a strict RN colour prop needs `as ColorTokens`.
|
||||
## `pkgs/ui/hanzogui/css-check.mjs` ships as the `gui-css-check` bin
|
||||
|
||||
### What the migration actually is
|
||||
It is a real published surface, not a dev script: `files[]` includes it and
|
||||
`bin` points at it. Its `DEFAULT_ALLOW` list names the classes `@hanzo/ui`
|
||||
stamps as identity markers (`btn`, `btn-*`, `badge`, `badge-*`) and it is
|
||||
ENUMERATED, not wildcarded — `btn-` is a name anyone might type, so a glob would
|
||||
wave through a hand-written `btn-cta` that has no rule behind it, which is the
|
||||
exact failure the checker exists to catch. When `@hanzo/ui` adds a Button or
|
||||
Badge variant, add it here too; drift fails loudly rather than silently.
|
||||
|
||||
Measured on hanzo.ai (the largest surface): **126 marketing pages, 6,720
|
||||
`className=` sites, 29,845 utility tokens, 671 distinct utilities**, and **zero**
|
||||
imports of `@hanzo/ui` or `@hanzogui/*` in the page bodies. So this is not a
|
||||
shadcn component swap — the pages are raw Tailwind. The vocabulary is regular
|
||||
though: 30 utilities cover **51.4%** of all tokens, and colours are already
|
||||
semantic (`text-foreground`, not hex), so the colour layer is a rename.
|
||||
## `HanzoFooter` (pkgs/ui/chrome) is deprecated
|
||||
|
||||
### Primitive map
|
||||
|
||||
| Tailwind (count) | gui equivalent |
|
||||
|---|---|
|
||||
| `text-foreground` 1091 · `text-muted-foreground` 859 · `text-foreground/80` 244 | `Txt` + `color={c.fg / c.fgMuted / c.fgDim}` (`chrome/src/tokens.ts`) |
|
||||
| `border-border` 829 · `border` 795 | `borderColor={c.line}` · `borderWidth={1}` |
|
||||
| `flex` 760 · `items-center` 887 · `justify-center` 512 | `XStack`/`YStack` + `alignItems` / `justifyContent` |
|
||||
| `inline-flex` 484 | `XStack display="inline-flex"` |
|
||||
| `grid` 220 | `YStack` + explicit rows, or `View display="grid"` |
|
||||
| `mx-auto` 705 | `marginHorizontal="auto"` |
|
||||
| `px-4` 542 · `py-3` 347 | `paddingHorizontal={16}` · `paddingVertical={12}` (longhand) |
|
||||
| `gap-2` 509 · `gap-4` 239 | `gap={8}` · `gap={16}` |
|
||||
| `mb-4` 507 | `marginBottom={16}` |
|
||||
| `text-sm` 693 · `text-xl` 287 · `text-2xl` 323 | `Txt kind=` — the type scale in `chrome/src/styles.tsx` |
|
||||
| `font-medium` 676 · `font-bold` 560 | `fontWeight="500"` / `"700"` |
|
||||
| `text-center` 410 | `textAlign="center"` (never `text=`) |
|
||||
| `rounded-full` 625 · `rounded-xl` 391 | `borderRadius={999}` · `borderRadius={12}` |
|
||||
| `transition-colors` 372 | `useHover()` + explicit `color` on the child `Text` |
|
||||
| `relative` 268 · `absolute` 256 · `overflow-hidden` 251 | `position` / `overflow` props |
|
||||
| `h-4 w-4` 357/352 | icon `size={16}` (lucide props, unchanged) |
|
||||
|
||||
### Order
|
||||
|
||||
1. Adopt `@hanzogui/chrome` on a surface (nav/footer/hero) — the page bodies keep
|
||||
working untouched, so this is independently shippable.
|
||||
2. Extract the repeated page shapes. 17 hanzo.ai pages already open with a
|
||||
byte-identical hero block; those become one primitive, not 17 rewrites.
|
||||
3. Migrate bodies shape-by-shape, not page-by-page. Tailwind leaves as a
|
||||
consequence of the last shape moving, which is the only point it can be
|
||||
removed from the build.
|
||||
|
||||
Do not sed 6,720 class strings. The tail (671 − 30 utilities) is where the layout
|
||||
bugs hide, and a marketing site is visually regression-tested by eye.
|
||||
|
||||
---
|
||||
|
||||
## Additional notes (merged from LLM.md)
|
||||
|
||||
# gui — AI Assistant Context
|
||||
|
||||
# Hanzo GUI
|
||||
|
||||
<h3 align="center">
|
||||
Style library, design system, composable components, and more.
|
||||
</h3>
|
||||
|
||||
---
|
||||
|
||||
## Telemetry — `@hanzogui/telemetry` (pkgs/telemetry)
|
||||
|
||||
The ONE zero-config telemetry surface. `<TelemetryProvider>{children}</TelemetryProvider>`
|
||||
with no props wires all three planes; `@hanzo/ui/telemetry` re-exports it unchanged
|
||||
so the component layer needs no second definition.
|
||||
|
||||
- **One front door.** Everything (pageviews, product events, exceptions,
|
||||
interaction capture) is POSTed to `https://api.hanzo.ai/v1/event`. Cloud lenses
|
||||
that one stream into `sentry.hanzo.ai` (errors + session capture),
|
||||
`analytics.hanzo.ai` (web analytics) and `insights.hanzo.ai` (product insights,
|
||||
incl. `analytics_errors`). Those three hosts are DASHBOARDS — never ingest
|
||||
endpoints, never configured in a client.
|
||||
- **Layering.** `@hanzo/event` = the client (batching, attribution, the wire).
|
||||
`@hanzo/observe` = the capture engine (semantics, redaction, playback), loaded
|
||||
with a dynamic `import()` inside an idle callback so it cannot cost LCP.
|
||||
`@hanzogui/telemetry` = the zero-config policy that composes them. Mechanism
|
||||
below, policy here, one of each.
|
||||
- **Privacy.** DNT/GPC honored by default with no app code; an explicit
|
||||
`setConsent()` choice outranks the browser in both directions; nothing is
|
||||
written to storage while telemetry is refused.
|
||||
- **Guarantees.** SSR-safe (a DOM is required before anything is collected),
|
||||
fail-soft (every method swallows its own errors), no CDN script, ESM,
|
||||
`sideEffects: false`.
|
||||
- Tests: `cd pkgs/telemetry && bun run test` (24 tests — one-door URL, three-lens
|
||||
wire, DNT/GPC, consent precedence, SPA route counting, error boundary, SSR).
|
||||
`SiteFooter` in `@hanzo/ui/product` is the one footer. It renders the FOOTER
|
||||
model from `@hanzo/products`, so its links are shared data instead of a
|
||||
per-caller prop. `HanzoFooter` is NOT a re-export of it, deliberately: this
|
||||
package lives inside `@hanzo/gui` and `@hanzo/ui` peer-depends on `@hanzo/gui`,
|
||||
so re-exporting would close a `gui -> ui -> gui` cycle. Its 8 callers keep
|
||||
working and migrate on their own schedule.
|
||||
|
||||
-141
@@ -1,141 +0,0 @@
|
||||
# Fleet migration: shadcn `@hanzo/ui` → `@hanzo/gui`
|
||||
|
||||
The repeatable recipe for taking any Hanzo web surface OFF the shadcn/Radix+Tailwind
|
||||
`@hanzo/ui` library and ONTO the canonical `@hanzo/gui` design system, so every
|
||||
surface shares one look and feel. Written from increment 1 (hanzo.ai + hanzo.chat);
|
||||
follow it to roll the rest of the fleet (docs sites, marketing sites, apps) mechanically.
|
||||
|
||||
There are **two layers**. They are independent — do them in either order, per surface.
|
||||
|
||||
| Layer | What | Package | Substrate | Works on |
|
||||
|---|---|---|---|---|
|
||||
| **Chrome** | Header, footer, hero, AI-widget mount (the shared frame) | `@hanzogui/chrome` | plain React + Tailwind + framer-motion + lucide | React 18 **and** 19 · Next **and** Vite |
|
||||
| **Primitives** | Button/Input/Card/Badge/… inside pages | `@hanzo/gui` | Tamagui (styled + tokens + RN-web) | React ≥19 (Next proven; Vite via `@hanzogui/vite-plugin`) |
|
||||
|
||||
Why two: the cross-app chrome must run on React-18 Vite surfaces (hanzo.chat) and
|
||||
React-19 static-export Next surfaces alike, so it is authored as plain React +
|
||||
Tailwind — the SAME convention `@hanzogui/shell` (TenantHeader/HanzoAppBar) already
|
||||
uses. The Tamagui primitive layer needs React ≥19; it is proven to render, SSR-style-
|
||||
extracted with no FOUC, in a Next `output:'export'` static build (see Proof).
|
||||
|
||||
`@hanzogui/chrome` = PUBLIC marketing chrome. `@hanzogui/shell` = AUTHENTICATED tenant
|
||||
chrome (org switcher, app launcher). Pick per surface; don't conflate.
|
||||
|
||||
---
|
||||
|
||||
## Recipe A — adopt the shared chrome (`@hanzogui/chrome`)
|
||||
|
||||
Exports: `HanzoNav`, `HanzoFooter`, `ChatHero`, `HanzoWidget` (+ types `NavItem`,
|
||||
`NavLink`, `NavColumn`). All content and effects are **props** — the library is
|
||||
presentational and host-agnostic (no analytics, no router, no config baked in).
|
||||
|
||||
1. **Link the package (no publish needed).** Add to the app's `package.json`:
|
||||
```jsonc
|
||||
"@hanzogui/chrome": "link:<relpath>/gui/pkgs/ui/chrome"
|
||||
```
|
||||
Install (`pnpm install` / `npm install`). It ships as SOURCE.
|
||||
|
||||
2. **Transpile it as first-party.**
|
||||
- **Next**: `transpilePackages: ['@hanzogui/chrome']` in `next.config`. This ALSO
|
||||
dedupes React for the linked package — do **NOT** alias `react`/`react-dom` in
|
||||
webpack; a global react alias breaks Next's server/RSC React and crashes static
|
||||
export with `Cannot read properties of null (reading 'useContext')`.
|
||||
- **Vite**: `optimizeDeps.exclude: ['@hanzogui/chrome']` and ensure
|
||||
`server.fs.allow` includes the gui checkout. Vite transpiles linked TS by default.
|
||||
|
||||
3. **Let Tailwind see it** (its classes live outside your project root):
|
||||
- **Tailwind v4** (CSS-first): `@source '<relpath>/gui/pkgs/ui/chrome/src';` in globals.css.
|
||||
- **Tailwind v3**: add `'<relpath>/gui/pkgs/ui/chrome/src/**/*.{js,jsx,ts,tsx}'` to `content`.
|
||||
|
||||
4. **Write thin adapters** that inject the surface's content + effects. Keep analytics
|
||||
IN the app (the library takes callbacks):
|
||||
```tsx
|
||||
<HanzoNav items={NAV} logo={<HanzoLogo variant="white" size={22} />} brand="Hanzo AI"
|
||||
login={{ links: LOGIN_LINKS }} primary={{ label: 'Try Hanzo', href: CHAT, links: TRY_LINKS }}
|
||||
onPrimary={() => analytics.capture(EVENTS.CHAT_STARTED, { source: 'nav' })} />
|
||||
```
|
||||
`ChatHero` forwards on submit: pass `onSubmit={(q) => { analytics…; goTo(q) }}` (or a
|
||||
plain `href` to auto-append `?q=`). Cross-origin surfaces use absolute hrefs in nav data;
|
||||
a same-app landing forwards INTO itself (e.g. `navigate('/c/new?q='+q)`).
|
||||
|
||||
5. **AI widget** (optional): `<HanzoWidget repo="org/repo" />` injects the
|
||||
`<meta name="hanzo:repo">` convention + loads `hanzo.app/edit.js` once. Mount-only;
|
||||
edit.js behaviour is a separate workstream.
|
||||
|
||||
Gotcha — analytics stays put: never move `@hanzo/event` / `hz.js` into the library.
|
||||
The chrome is effect-free; wire telemetry in the app via the callback props.
|
||||
|
||||
---
|
||||
|
||||
## Recipe B — migrate page primitives (`@hanzo/ui` shadcn → `@hanzo/gui` Tamagui)
|
||||
|
||||
Proven to build + render in a Next 15 `output:'export'` static export. Steps mirror the
|
||||
one working fleet integration (`hanzoai/app/next.config.js`).
|
||||
|
||||
1. **Deps** (published npm, pin to the same versions the fleet uses, currently 7.3.0):
|
||||
`@hanzo/gui @hanzogui/config @hanzogui/lucide-icons-2 react-native-web`.
|
||||
|
||||
2. **Next config** — transpile the whole gui graph + map RN→RN-web + prefer `.web.*`:
|
||||
```js
|
||||
transpilePackages: ['@hanzo/gui','react-native-web', ...fs.readdirSync('node_modules/@hanzogui').map(n=>`@hanzogui/${n}`)]
|
||||
webpack: (c) => { c.resolve.alias['react-native$']='react-native-web';
|
||||
c.resolve.extensions=['.web.tsx','.web.ts','.web.jsx','.web.js',...c.resolve.extensions];
|
||||
c.resolve.fallback={...c.resolve.fallback, fs:false, '@react-native-async-storage/async-storage':false, 'pino-pretty':false}; return c }
|
||||
```
|
||||
No `@tamagui/next-plugin` — its dep is broken and unnecessary; `transpilePackages` +
|
||||
Tamagui's runtime SSR-extracts the atomic CSS into the static HTML (styled on first paint).
|
||||
(Vite surfaces: use `@hanzogui/vite-plugin` instead + the RN-web alias.)
|
||||
|
||||
3. **Provider** — wrap the tree once (context-only; additive, leaves other UI intact):
|
||||
```tsx
|
||||
import { createGui } from '@hanzo/gui'; import { defaultConfig } from '@hanzogui/config/v5'
|
||||
const config = createGui(defaultConfig)
|
||||
<GuiProvider config={config} defaultTheme="dark">{children}</GuiProvider>
|
||||
```
|
||||
|
||||
4. **Swap primitives.** Build a local `components/ui/*` shim layer that exposes the
|
||||
shadcn API but is backed by gui, then repoint imports (or alias `@hanzo/ui` → the shim
|
||||
to flip every importer at once). Component map (hanzo.ai surface, frequency-ranked):
|
||||
|
||||
| shadcn `@hanzo/ui` | files | `@hanzo/gui` |
|
||||
|---|---|---|
|
||||
| `Button` (+`buttonVariants`) | 121 | `Button` (map `variant`/`size` → theme/size props) |
|
||||
| `Input` | 12 | `Input` |
|
||||
| `Badge` | 10 | compose `XStack`+`Text` (no Tamagui Badge) |
|
||||
| `Label` | 6 | `Label` |
|
||||
| `Card`+family | 5 | `Card` + `Card.Header`/`Card.Footer` |
|
||||
| `Avatar`+family | 4 | `Avatar` (`Avatar.Image`/`Avatar.Fallback`) |
|
||||
| `Tabs`+family | 4 | `Tabs` |
|
||||
| `Textarea` | 4 | `TextArea` |
|
||||
| `Table`+family | 3 | compose from `styled` stacks (no Tamagui Table) |
|
||||
| `Progress`,`Accordion`,`Dialog`,`DropdownMenu`,`Select`,`Separator`,`Sheet`,`Slider`,`Checkbox` | ≤3 ea | `Progress`,`Accordion`,`Dialog`,`Menu`,`Select`,`Separator`,`Sheet`,`Slider`,`Checkbox` |
|
||||
| `Toaster` | 1 | `Toast`/`ToastProvider` |
|
||||
|
||||
Do the top-5 first — they cover the overwhelming majority of files. If a gui primitive
|
||||
is genuinely missing (Badge, Table), ADD it to `@hanzo/gui` — never reach back to shadcn.
|
||||
|
||||
5. **Cleanup during cutover**:
|
||||
- Repoint any Tailwind `content` glob off `@hanzo/ui/dist/**` (it purges nothing once shadcn is gone).
|
||||
- Collapse duplicate primitives to one (hanzo.ai has TWO Buttons: raw `@hanzo/ui` in 121
|
||||
files + a bug-fix wrapper `components/ui/button.tsx` in 17 — pick one).
|
||||
- Fix dangling type-only imports (`ToastActionElement`/`ToastProps` don't exist in `@hanzo/ui@5`).
|
||||
|
||||
---
|
||||
|
||||
## Proof (increment 1)
|
||||
|
||||
- **Spike** — a real `@hanzo/gui` component (`YStack`/`Text`/`Button` + live theme tokens)
|
||||
built and rendered STYLED in hanzo.ai's Next 15 `output:'export'` static export; the
|
||||
atomic CSS is SSR-extracted into the prerendered HTML (no FOUC). Toolchain GREEN.
|
||||
- **Chrome on hanzo.ai** — apex nav/footer/hero migrated to `@hanzogui/chrome`; static
|
||||
build passes (711 pages), chrome CSS generated by Tailwind `@source`, pixel-identical to
|
||||
pre-migration, hz.js + `@hanzo/event` untouched.
|
||||
- **Chrome on hanzo.chat** — new public landing (React 18 + Vite + Tailwind 3) on the SAME
|
||||
`@hanzogui/chrome`; composer forwards into the chat app.
|
||||
|
||||
## Per-surface rollout checklist
|
||||
|
||||
- [ ] Chrome: link `@hanzogui/chrome` · transpile/optimizeDeps · Tailwind source · adapters (nav data + analytics callbacks).
|
||||
- [ ] Primitives: add gui deps · next/vite config (transpile + RN-web alias) · `GuiProvider` · shim + swap top-5 · flip the rest.
|
||||
- [ ] Cleanup: repoint Tailwind glob · collapse duplicates · fix dangling types · drop `@hanzo/ui`.
|
||||
- [ ] Prove: build + Playwright screenshot (desktop + mobile), no horizontal scroll, console clean.
|
||||
@@ -1,38 +0,0 @@
|
||||
Hanzo GUI
|
||||
Copyright (c) 2026-present, Hanzo AI, Inc.
|
||||
|
||||
This product includes software derived from Tamagui.
|
||||
|
||||
Tamagui
|
||||
https://github.com/tamagui/tamagui
|
||||
SPDX-License-Identifier: MIT
|
||||
|
||||
Tamagui is distributed under the MIT License. Its original copyright and
|
||||
permission notices are reproduced below, as that license requires.
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Nate Wienert
|
||||
Copyright (c) 2015-present, Nicolas Gallagher.
|
||||
Copyright (c) 2015-present, Facebook, Inc.
|
||||
Copyright (c) 2021 Radix
|
||||
Copyright (c) 2017 Carmelo Pullara
|
||||
Copyright (c) 2018 Framer B.V.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,51 +1,57 @@
|
||||
<p align="center"><img src=".github/hero.svg" alt="gui" width="880"></p>
|
||||
|
||||
# Hanzo GUI
|
||||
|
||||
Forked from [tamagui](https://github.com/tamagui/tamagui) (MIT).
|
||||
<div align="center">
|
||||
<picture width="572px">
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/hanzoai/gui/raw/main/apps/gui.hanzo.ai/public/logo-black.png">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://github.com/hanzoai/gui/raw/main/apps/gui.hanzo.ai/public/logo-white.png">
|
||||
<img alt="Shows a black logo in light color mode and a white one in dark color mode." src="https://github.com/hanzoai/gui/raw/main/apps/gui.hanzo.ai/public/social.png">
|
||||
</picture>
|
||||
</div>
|
||||
|
||||
<h3 align="center">
|
||||
Style library, design system, composable components, and more.
|
||||
</h3>
|
||||
|
||||
<h4 align="center">
|
||||
Hanzo GUI is a bunch of libraries for building UIs that share code across React and React Native.
|
||||
Gui is a bunch of libraries for building UIs that share code across React and React Native.
|
||||
</h4>
|
||||
|
||||
<br />
|
||||
|
||||
<div align="center">
|
||||
<img alt="NPM downloads" src="https://img.shields.io/npm/dw/@hanzogui/core?logo=npm&label=NPM%20downloads&cacheSeconds=3600"/>
|
||||
<img alt="Commits per month" src="https://img.shields.io/github/commit-activity/m/hanzoai/gui?label=Commits&logo=git" />
|
||||
<img alt="Commits per month" src="https://img.shields.io/github/commit-activity/m/hanzogui/hanzogui?label=Commits&logo=git" />
|
||||
<img alt="Discord users online" src="https://img.shields.io/discord/909986013848412191?logo=discord&label=Discord&cacheSeconds=3600" />
|
||||
<a href="https://gurubase.io/g/hanzogui">
|
||||
<img alt="Gurubase" src="https://img.shields.io/badge/Gurubase-Ask%20Gui%20Guru-006BFF" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
- `@hanzogui/web` - Universal style library for React.
|
||||
- `gui` - UI kit that adapts to every platform.
|
||||
- `@hanzogui/static` - Optimizing compiler that works with `@hanzogui/web` and `gui`.
|
||||
- `@hanzogui/core` - Universal style library for React.
|
||||
- `@hanzogui/static` - Optimizing compiler that works with `core` and `hanzogui`.
|
||||
- `hanzogui` - UI kit that adapts to every platform.
|
||||
|
||||
<br />
|
||||
|
||||
**See [gui.hanzo.ai](https://gui.hanzo.ai) for documentation.**
|
||||
**See [hanzogui.dev](https://hanzogui.dev) for documentation.**
|
||||
|
||||
Hanzo GUI lets you share more code between web and native apps without sacrificing the two things that typically suffer when you do: performance and code quality.
|
||||
Gui lets you share more code between web and native apps without sacrificing the two things that typically suffer when you do: performance and code quality.
|
||||
|
||||
It does this with an optimizing compiler that outputs platform-specific optimizations - it turns styled components, even with complex logic or cross-module imports, into a simple `div` alongside atomic CSS on the web, or a View with its style objects hoisted on native.
|
||||
|
||||
The entirety of Hanzo GUI works at compile time and runtime, and can be set up gradually, with initial usage as simple as importing it and using the base views and styled function.
|
||||
The entirety of Gui works at compile time and runtime, and can be set up gradually, with initial usage as simple as importing it and using the base views and styled function.
|
||||
|
||||
We recommend checking out the starters with `npm create gui@latest`, they range from a simple learning example to a production-ready monorepo.
|
||||
We recommend checking out the starters with `npm create hanzogui@latest`, they range from a simple learning example to a production-ready monorepo.
|
||||
|
||||
The compiler optimizes most styled components by flattening them to plain `div`s (web) or `View`s (native) at build time. Real-world apps see double-digit Lighthouse improvements.
|
||||
The compiler optimizes most and ultimately flattens a majority of styled components. In the [~500px² responsive browser section](https://hanzogui.dev) of the Gui website, 49 of the 55 or so [inline styled components](https://github.com/hanzoai/gui/blob/main/apps/gui.hanzo.ai/components/HeroResponsive.tsx) are flattened to a `div`. The homepage gains nearly 15% on Lighthouse with the compiler on.
|
||||
|
||||
[Learn more on the website](https://gui.hanzo.ai/docs/intro/introduction).
|
||||
[Learn more on the website](https://hanzogui.dev/docs/intro/introduction).
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
To contribute to Hanzo GUI reference the [contributing guide](https://github.com/hanzoai/gui/blob/main/CONTRIBUTING.md).
|
||||
To contribute to Gui reference the [contributing guide](https://github.com/hanzoai/gui/blob/main/CONTRIBUTING.md).
|
||||
|
||||
To contribute to documentation, see the docs source in `code/gui.hanzo.ai/data/docs/`.
|
||||
To contribute to documentation, see the docs source in `apps/gui.hanzo.ai/data/docs/`.
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
# Security Policy
|
||||
|
||||
Thanks for helping keep Gui and its users safe.
|
||||
|
||||
## Supported versions
|
||||
|
||||
Security fixes are made against the latest published major (`hanzogui` v2 and the
|
||||
current `@hanzogui/*` line). Please make sure you can reproduce on the latest
|
||||
release before reporting.
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
**Please do not open a public issue for security problems.**
|
||||
|
||||
Report privately using **GitHub's private vulnerability reporting**:
|
||||
|
||||
1. Go to the repo's **Security** tab → **Report a vulnerability**
|
||||
(https://github.com/hanzoai/gui/security/advisories/new)
|
||||
2. Describe the issue, affected versions, and a proof-of-concept if you have one.
|
||||
|
||||
If you can't use GitHub advisories, email **[email protected]** (or
|
||||
natewienert@gmail.com) with the details.
|
||||
|
||||
We aim to acknowledge reports within 3 business days and to ship a fix or
|
||||
mitigation for confirmed high/critical issues as quickly as we reasonably can.
|
||||
We're happy to credit you in the advisory unless you'd prefer to stay anonymous.
|
||||
|
||||
## Scope
|
||||
|
||||
In scope:
|
||||
|
||||
- The published `@hanzogui/*` / `hanzogui` npm packages.
|
||||
- The hanzogui.dev web application and its API endpoints (auth, payments,
|
||||
account/team management).
|
||||
- This repository's CI/CD and release/publish pipeline.
|
||||
|
||||
Out of scope:
|
||||
|
||||
- Vulnerabilities in third-party dependencies that are already publicly known
|
||||
(please report those upstream; Dependabot tracks them here).
|
||||
- Findings that require a compromised developer machine, social engineering, or
|
||||
physical access.
|
||||
- Volumetric / denial-of-service testing against hanzogui.dev.
|
||||
|
||||
## Handling secrets
|
||||
|
||||
If you believe a credential has been committed to this repo (current tree or
|
||||
history), treat it as compromised: report it privately as above and **do not**
|
||||
post the value publicly. Maintainers: rotate the credential at its source first;
|
||||
history scrubbing is secondary.
|
||||
@@ -1,42 +0,0 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2026-present, Hanzo AI, Inc.
|
||||
|
||||
Portions of this software are derived from upstream code originally licensed under
|
||||
the MIT License, with the following copyright notices retained per its terms:
|
||||
|
||||
Copyright (c) 2020 Nate Wienert
|
||||
Copyright (c) 2015-present, Nicolas Gallagher.
|
||||
Copyright (c) 2015-present, Facebook, Inc.
|
||||
Copyright (c) 2021 Radix
|
||||
Copyright (c) 2017 Carmelo Pullara
|
||||
Copyright (c) 2018 Framer B.V.
|
||||
Copyright (c) 2022 WorkOS
|
||||
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"config": "./src/gui.config.ts",
|
||||
"components": ["hanzogui"]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"config": "./src/hanzogui.config.ts",
|
||||
"components": ["hanzogui"]
|
||||
}
|
||||
+8
-11
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hanzogui/demos",
|
||||
"version": "7.3.0",
|
||||
"version": "8.1.1",
|
||||
"source": "demos/src/index.tsx",
|
||||
"files": [
|
||||
"src",
|
||||
@@ -40,10 +40,10 @@
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "gui-build --skip-types",
|
||||
"watch": "gui-build --skip-types --watch",
|
||||
"clean": "gui-build clean",
|
||||
"clean:build": "gui-build clean:build"
|
||||
"build": "hanzogui-build --skip-types",
|
||||
"watch": "hanzogui-build --skip-types --watch",
|
||||
"clean": "hanzogui-build clean",
|
||||
"clean:build": "hanzogui-build clean:build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hanzogui/animate-presence": "workspace:*",
|
||||
@@ -78,7 +78,7 @@
|
||||
"@hanzogui/dev-config": "workspace:*",
|
||||
"@hanzogui/theme": "workspace:*",
|
||||
"@hanzogui/toast": "workspace:*",
|
||||
"hanzogui": "workspace:*"
|
||||
"@hanzo/gui": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hanzogui/build": "workspace:*",
|
||||
@@ -86,9 +86,6 @@
|
||||
"react-native": "0.83.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=19",
|
||||
"react-native": "*"
|
||||
},
|
||||
"license": "BSD-3-Clause",
|
||||
"author": "Hanzo AI <[email protected]>"
|
||||
"react": ">=19"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChevronDown } from '@hanzogui/lucide-icons-2'
|
||||
import { Accordion, Paragraph, Square } from 'hanzogui'
|
||||
import { Accordion, Paragraph, Square } from '@hanzo/gui'
|
||||
|
||||
export function AccordionDemo() {
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { addTheme } from '@hanzogui/theme'
|
||||
import React from 'react'
|
||||
|
||||
import { Button, H3, Theme, View, YStack } from 'hanzogui'
|
||||
import { Button, H3, Theme, View, YStack } from '@hanzo/gui'
|
||||
|
||||
export function AddThemeDemo() {
|
||||
const [theme, setTheme] = React.useState<any>()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AlertDialog, Button, XStack, YStack } from 'hanzogui'
|
||||
import { AlertDialog, Button, XStack, YStack } from '@hanzo/gui'
|
||||
|
||||
export function AlertDialogDemo() {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { AnimatePresence, Button, Square, XStack, YStack, isWeb } from 'hanzogui'
|
||||
import { AnimatePresence, Button, Square, XStack, YStack, isWeb } from '@hanzo/gui'
|
||||
|
||||
import { useIsIntersecting } from './useOnIntersecting'
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { LogoIcon } from '@hanzogui/logo'
|
||||
import { Play } from '@hanzogui/lucide-icons-2'
|
||||
import { Button, isWeb, Square, useControllableState, Image, useEvent } from 'hanzogui'
|
||||
import { Button, isWeb, Square, useControllableState, Image, useEvent } from '@hanzo/gui'
|
||||
|
||||
export function AnimationsDemo(props) {
|
||||
const [positionI, setPositionI] = useControllableState({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import { LogoIcon } from '@hanzogui/logo'
|
||||
|
||||
import { Button, Square, YStack, isWeb } from 'hanzogui'
|
||||
import { Button, Square, YStack, isWeb } from '@hanzo/gui'
|
||||
|
||||
import { useIsIntersecting } from './useOnIntersecting'
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LogoIcon } from '@hanzogui/logo'
|
||||
import { Square } from 'hanzogui'
|
||||
import { Square } from '@hanzo/gui'
|
||||
|
||||
export function AnimationsHoverDemo() {
|
||||
return (
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react'
|
||||
import { AnimatePresence } from '@hanzogui/animate-presence'
|
||||
import { ArrowLeft, ArrowRight } from '@hanzogui/lucide-icons-2'
|
||||
|
||||
import { Button, Image, XStack, YStack, styled } from 'hanzogui'
|
||||
import { Button, Image, XStack, YStack, styled } from '@hanzo/gui'
|
||||
|
||||
// @ts-ignore
|
||||
import photo1 from '../../public/photo1.jpg'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LogoIcon } from '@hanzogui/logo'
|
||||
import { Square } from 'hanzogui'
|
||||
import { Square } from '@hanzo/gui'
|
||||
|
||||
export function AnimationsTimingDemo() {
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Avatar, XStack } from 'hanzogui'
|
||||
import { Avatar, XStack } from '@hanzo/gui'
|
||||
|
||||
export function AvatarDemo() {
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { styled, View, Text, YStack } from 'hanzogui'
|
||||
import { styled, View, Text, YStack } from '@hanzo/gui'
|
||||
|
||||
// styled() with boxShadow containing embedded $token
|
||||
// if web imports are picked up on native, this resolves to var(--shadowColor)
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import { Button, Card, H2, Paragraph, Text, Theme, XStack, YStack } from 'hanzogui'
|
||||
|
||||
// Proof surface for the token layer: true-black neutral ramp, one accent per
|
||||
// white-label brand (read straight from each brand's generated theme — the
|
||||
// $color9 accent slot), Geist type. Open with ?demo=BrandTokens.
|
||||
// Brand accent SoT: pkgs/core/themes/src/brands.ts.
|
||||
|
||||
const BRANDS = ['hanzo', 'lux', 'zoo', 'pars'] as const
|
||||
|
||||
const SURFACES = [
|
||||
['background', '$background', '#000 canvas'],
|
||||
['color2', '$color2', '#0a0a0a surface'],
|
||||
['color3', '$color3', '#111 raised'],
|
||||
['color4', '$color4', '#171717 elevated'],
|
||||
] as const
|
||||
|
||||
export function BrandTokensDemo() {
|
||||
return (
|
||||
<Theme name="dark">
|
||||
<YStack backgroundColor="$background" padding="$6" gap="$5" minHeight={640}>
|
||||
<YStack gap="$2">
|
||||
<H2 fontFamily="$heading" color="$color12">
|
||||
Hanzo GUI — design tokens
|
||||
</H2>
|
||||
<Paragraph color="$color11" fontFamily="$body">
|
||||
True-black canvas · one accent per brand · Geist type system
|
||||
</Paragraph>
|
||||
</YStack>
|
||||
|
||||
<YStack gap="$2">
|
||||
<Text color="$color11" fontFamily="$mono" fontSize="$2">
|
||||
neutral surface ramp
|
||||
</Text>
|
||||
<XStack gap="$3" flexWrap="wrap">
|
||||
{SURFACES.map(([name, tok, label]) => (
|
||||
<YStack
|
||||
key={name}
|
||||
backgroundColor={tok}
|
||||
width={160}
|
||||
height={96}
|
||||
borderRadius={12}
|
||||
borderWidth={1}
|
||||
borderColor="$borderColor"
|
||||
padding="$3"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Text color="$color12" fontFamily="$mono" fontSize="$3">
|
||||
{name}
|
||||
</Text>
|
||||
<Text color="$color10" fontSize="$2">
|
||||
{label}
|
||||
</Text>
|
||||
</YStack>
|
||||
))}
|
||||
</XStack>
|
||||
</YStack>
|
||||
|
||||
<YStack gap="$2">
|
||||
<Text color="$color11" fontFamily="$mono" fontSize="$2">
|
||||
per-brand accent (applied sparingly over the shared canvas)
|
||||
</Text>
|
||||
<XStack gap="$4" flexWrap="wrap">
|
||||
{BRANDS.map((b) => (
|
||||
// brand themes come from the host app's config (@hanzogui/themes/v4),
|
||||
// not from the @hanzogui/dev-config this package is typed against
|
||||
// @ts-expect-error
|
||||
<Theme key={b} name={b}>
|
||||
<Card
|
||||
backgroundColor="$color2"
|
||||
borderWidth={1}
|
||||
borderColor="$borderColor"
|
||||
borderRadius={12}
|
||||
padding="$4"
|
||||
width={210}
|
||||
gap="$3"
|
||||
>
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<YStack
|
||||
width={14}
|
||||
height={14}
|
||||
borderRadius={999}
|
||||
backgroundColor="$color9"
|
||||
/>
|
||||
<Text color="$color12" fontFamily="$heading" fontSize="$5">
|
||||
{b}
|
||||
</Text>
|
||||
</XStack>
|
||||
<YStack
|
||||
height={44}
|
||||
borderRadius={8}
|
||||
backgroundColor="$color9"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Text color="$color1" fontSize="$3" fontWeight="700">
|
||||
Primary CTA
|
||||
</Text>
|
||||
</YStack>
|
||||
<Button>Themed button</Button>
|
||||
</Card>
|
||||
</Theme>
|
||||
))}
|
||||
</XStack>
|
||||
</YStack>
|
||||
</YStack>
|
||||
</Theme>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Drumstick, Hand } from '@hanzogui/lucide-icons-2'
|
||||
import type { ButtonProps, ThemeName } from 'hanzogui'
|
||||
import { Button, XStack, YStack } from 'hanzogui'
|
||||
import type { ButtonProps, ThemeName } from '@hanzo/gui'
|
||||
import { Button, XStack, YStack } from '@hanzo/gui'
|
||||
|
||||
const outlined = {
|
||||
bg: 'transparent',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Button } from '@hanzogui/button'
|
||||
import { Activity, Airplay } from '@hanzogui/lucide-icons-2'
|
||||
import { Theme, XGroup, XStack, YStack } from 'hanzogui'
|
||||
import { Theme, XGroup, XStack, YStack } from '@hanzo/gui'
|
||||
|
||||
export function ButtonDemo() {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { CardProps } from 'hanzogui'
|
||||
import { Button, Card, H2, Image, Paragraph, Theme, XStack } from 'hanzogui'
|
||||
import type { CardProps } from '@hanzo/gui'
|
||||
import { Button, Card, H2, Image, Paragraph, Theme, XStack } from '@hanzo/gui'
|
||||
|
||||
export function CardDemo() {
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Check as CheckIcon } from '@hanzogui/lucide-icons-2'
|
||||
import type { CheckboxProps } from 'hanzogui'
|
||||
import { Checkbox, Label, Theme, XStack, YStack } from 'hanzogui'
|
||||
import type { CheckboxProps } from '@hanzo/gui'
|
||||
import { Checkbox, Label, Theme, XStack, YStack } from '@hanzo/gui'
|
||||
|
||||
export function CheckboxDemo() {
|
||||
return (
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Check, Minus } from '@hanzogui/lucide-icons-2'
|
||||
|
||||
import type { View } from 'react-native'
|
||||
import { Pressable } from 'react-native'
|
||||
import { Label, XStack, YStack } from 'hanzogui'
|
||||
import { Label, XStack, YStack } from '@hanzo/gui'
|
||||
|
||||
export function CheckboxHeadlessDemo() {
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createCheckbox } from '@hanzogui/checkbox'
|
||||
import { View, styled } from '@hanzogui/core'
|
||||
import { Check } from '@hanzogui/lucide-icons-2'
|
||||
import { Label, XStack, YStack } from 'hanzogui'
|
||||
import { Label, XStack, YStack } from '@hanzo/gui'
|
||||
|
||||
const Frame = styled(View, {
|
||||
borderWidth: 1,
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
yellow,
|
||||
yellowDark,
|
||||
} from '@hanzogui/colors'
|
||||
import { H2, Paragraph, Separator, Square, XStack, YStack } from 'hanzogui'
|
||||
import { H2, Paragraph, Separator, Square, XStack, YStack } from '@hanzo/gui'
|
||||
|
||||
const colorGroups = ['gray', 'blue', 'green', 'yellow', 'red'] as const
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ContextMenu } from '@hanzogui/context-menu'
|
||||
import { Calendar, Check, ChevronRight } from '@hanzogui/lucide-icons-2'
|
||||
import React from 'react'
|
||||
import { Text, View, YStack } from 'hanzogui'
|
||||
import { Text, View, YStack } from '@hanzo/gui'
|
||||
|
||||
/**
|
||||
* Note: you'll want to use createMenu() to customize further.
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
View,
|
||||
XGroup,
|
||||
XStack,
|
||||
} from 'hanzogui'
|
||||
} from '@hanzo/gui'
|
||||
import { SelectDemoContents } from './SelectDemo'
|
||||
|
||||
type DialogMode = 'plain' | 'adapt' | 'keepMounted'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import type { SizeTokens } from 'hanzogui'
|
||||
import type { SizeTokens } from '@hanzo/gui'
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
XGroup,
|
||||
Label,
|
||||
YStack,
|
||||
} from 'hanzogui'
|
||||
} from '@hanzo/gui'
|
||||
|
||||
export function FormsDemo(props: { size: SizeTokens }) {
|
||||
const [status, setStatus] = React.useState<'off' | 'submitting' | 'submitted'>('off')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Activity, Airplay } from '@hanzogui/lucide-icons-2'
|
||||
import { Button, Group, ListItem, Separator, XGroup, YGroup, YStack } from 'hanzogui'
|
||||
import { Button, Group, ListItem, Separator, XGroup, YGroup, YStack } from '@hanzo/gui'
|
||||
|
||||
export function GroupDemo() {
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { H1, H2, H3, H4, H5, H6, YStack } from 'hanzogui'
|
||||
import { H1, H2, H3, H4, H5, H6, YStack } from '@hanzo/gui'
|
||||
|
||||
export function HeadingsDemo() {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { SizeTokens } from 'hanzogui'
|
||||
import { Button, Input, TextArea, XStack, YStack } from 'hanzogui'
|
||||
import type { SizeTokens } from '@hanzo/gui'
|
||||
import { Button, Input, TextArea, XStack, YStack } from '@hanzo/gui'
|
||||
|
||||
export function InputsDemo() {
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Input, Label, Switch, XStack, YStack } from 'hanzogui'
|
||||
import { Input, Label, Switch, XStack, YStack } from '@hanzo/gui'
|
||||
|
||||
export function LabelDemo() {
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { XStack } from 'hanzogui'
|
||||
import { XStack } from '@hanzo/gui'
|
||||
import { LinearGradient } from '@hanzogui/linear-gradient'
|
||||
|
||||
export function LinearGradientDemo() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChevronRight, Cloud, Moon, Star, Sun } from '@hanzogui/lucide-icons-2'
|
||||
import { ListItem, Separator, Theme, XStack, YGroup } from 'hanzogui'
|
||||
import { ListItem, Separator, Theme, XStack, YGroup } from '@hanzo/gui'
|
||||
|
||||
export function ListItemDemo() {
|
||||
return (
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react'
|
||||
import * as LucideIcons from '@hanzogui/lucide-icons-2'
|
||||
|
||||
import { ScrollView } from 'react-native'
|
||||
import { Input, Paragraph, Spacer, YStack, useDebounceValue } from 'hanzogui'
|
||||
import { Input, Paragraph, Spacer, YStack, useDebounceValue } from '@hanzo/gui'
|
||||
|
||||
import { Grid } from './Grid'
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from '@hanzogui/lucide-icons-2'
|
||||
import { Menu } from '@hanzogui/menu'
|
||||
import React from 'react'
|
||||
import { Button } from 'hanzogui'
|
||||
import { Button } from '@hanzo/gui'
|
||||
|
||||
/**
|
||||
* Menu Demo using Gui Menu component.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { SizeTokens } from 'hanzogui'
|
||||
import { Button, Theme, XStack, YStack } from 'hanzogui'
|
||||
import type { SizeTokens } from '@hanzo/gui'
|
||||
import { Button, Theme, XStack, YStack } from '@hanzo/gui'
|
||||
import { Input, TextArea } from '@hanzogui/input'
|
||||
|
||||
export function NewInputsDemo() {
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
ChevronUp,
|
||||
} from '@hanzogui/lucide-icons-2'
|
||||
import { useState } from 'react'
|
||||
import type { PopoverProps } from 'hanzogui'
|
||||
import type { PopoverProps } from '@hanzo/gui'
|
||||
import {
|
||||
Adapt,
|
||||
Button,
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
styled,
|
||||
XStack,
|
||||
YStack,
|
||||
} from 'hanzogui'
|
||||
} from '@hanzo/gui'
|
||||
|
||||
export function PopoverDemo() {
|
||||
const [shouldAdapt, setShouldAdapt] = useState(true)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { Button, Paragraph, Progress, Slider, Theme, XStack, YStack } from 'hanzogui'
|
||||
import { Button, Paragraph, Progress, Slider, Theme, XStack, YStack } from '@hanzo/gui'
|
||||
import { Play, RotateCcw } from '@hanzogui/lucide-icons-2'
|
||||
|
||||
export function ProgressDemo() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { SizeTokens } from 'hanzogui'
|
||||
import { Label, RadioGroup, Theme, XStack, YStack } from 'hanzogui'
|
||||
import type { SizeTokens } from '@hanzo/gui'
|
||||
import { Label, RadioGroup, Theme, XStack, YStack } from '@hanzo/gui'
|
||||
|
||||
export function RadioGroupDemo() {
|
||||
return (
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import { RovingFocusGroup } from '@hanzogui/roving-focus'
|
||||
import React from 'react'
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
||||
import { isWeb, useTheme } from 'hanzogui'
|
||||
import { isWeb, useTheme } from '@hanzo/gui'
|
||||
|
||||
const RadioGroupContext = React.createContext<RadioGroupContextValue>({})
|
||||
const RadioGroupItemContext = React.createContext<RadioGroupItemContextValue>({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Label, XStack, YStack, styled } from 'hanzogui'
|
||||
import { Label, XStack, YStack, styled } from '@hanzo/gui'
|
||||
|
||||
import { createRadioGroup } from '@hanzogui/radio-group'
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
YStack,
|
||||
useForceUpdate,
|
||||
useIsomorphicLayoutEffect,
|
||||
} from 'hanzogui'
|
||||
} from '@hanzo/gui'
|
||||
|
||||
export function ReplaceThemeDemo() {
|
||||
const [theme, setTheme] = React.useState<any>()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Circle, ScrollView, Square, XStack } from 'hanzogui'
|
||||
import { Circle, ScrollView, Square, XStack } from '@hanzo/gui'
|
||||
|
||||
export function ScrollViewDemo() {
|
||||
return (
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import { Check, ChevronDown, ChevronUp } from '@hanzogui/lucide-icons-2'
|
||||
import React from 'react'
|
||||
|
||||
import type { FontSizeTokens, SelectProps } from 'hanzogui'
|
||||
import { Adapt, Label, Select, Sheet, Theme, XStack, YStack, getFontSize } from 'hanzogui'
|
||||
import { LinearGradient } from 'hanzogui/linear-gradient'
|
||||
import type { FontSizeTokens, SelectProps } from '@hanzo/gui'
|
||||
import {
|
||||
Adapt,
|
||||
Label,
|
||||
Select,
|
||||
Sheet,
|
||||
Theme,
|
||||
XStack,
|
||||
YStack,
|
||||
getFontSize,
|
||||
} from '@hanzo/gui'
|
||||
import { LinearGradient } from '@hanzo/gui/linear-gradient'
|
||||
|
||||
export function SelectDemo() {
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Paragraph, Separator, XStack, YStack } from 'hanzogui'
|
||||
import { Paragraph, Separator, XStack, YStack } from '@hanzo/gui'
|
||||
|
||||
export function SeparatorDemo() {
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Circle, Square, XStack } from 'hanzogui'
|
||||
import { Circle, Square, XStack } from '@hanzo/gui'
|
||||
|
||||
export function ShapesDemo() {
|
||||
return (
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ChevronDown, ChevronUp } from '@hanzogui/lucide-icons-2'
|
||||
import type { SheetProps } from '@hanzogui/sheet'
|
||||
import { Sheet } from '@hanzogui/sheet'
|
||||
import React, { memo } from 'react'
|
||||
import { Button, H2, Input, Paragraph, XStack, YStack } from 'hanzogui'
|
||||
import { Button, H2, Input, Paragraph, XStack, YStack } from '@hanzo/gui'
|
||||
|
||||
const spModes = ['percent', 'constant', 'fit', 'mixed'] as const
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { SliderProps } from 'hanzogui'
|
||||
import { Slider, Theme, XStack } from 'hanzogui'
|
||||
import type { SliderProps } from '@hanzo/gui'
|
||||
import { Slider, Theme, XStack } from '@hanzo/gui'
|
||||
|
||||
export function SliderDemo() {
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Spinner, YStack } from 'hanzogui'
|
||||
import { Spinner, YStack } from '@hanzo/gui'
|
||||
|
||||
export function SpinnerDemo() {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { XStack, YStack, ZStack } from 'hanzogui'
|
||||
import { XStack, YStack, ZStack } from '@hanzo/gui'
|
||||
|
||||
export function StacksDemo() {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { SizeTokens } from 'hanzogui'
|
||||
import { Label, Separator, Switch, Theme, XStack, YStack } from 'hanzogui'
|
||||
import type { SizeTokens } from '@hanzo/gui'
|
||||
import { Label, Separator, Switch, Theme, XStack, YStack } from '@hanzo/gui'
|
||||
|
||||
export function SwitchDemo() {
|
||||
return (
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useSwitch } from '@hanzogui/switch-headless'
|
||||
|
||||
import type { View } from 'react-native'
|
||||
import { Animated, Pressable } from 'react-native'
|
||||
import { Label, XStack, YStack } from 'hanzogui'
|
||||
import { Label, XStack, YStack } from '@hanzo/gui'
|
||||
|
||||
export function SwitchHeadlessDemo() {
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { View, styled } from '@hanzogui/core'
|
||||
import { createSwitch } from '@hanzogui/switch'
|
||||
import { Label, XStack, YStack } from 'hanzogui'
|
||||
import { Label, XStack, YStack } from '@hanzo/gui'
|
||||
|
||||
const Frame = styled(View, {
|
||||
width: 40,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import type { ViewProps, TabLayout, TabsTabProps } from 'hanzogui'
|
||||
import type { ViewProps, TabLayout, TabsTabProps } from '@hanzo/gui'
|
||||
import {
|
||||
AnimatePresence,
|
||||
Button,
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
XStack,
|
||||
YStack,
|
||||
styled,
|
||||
} from 'hanzogui'
|
||||
} from '@hanzo/gui'
|
||||
|
||||
const demos = ['background', 'underline'] as const
|
||||
const demosTitle: Record<(typeof demos)[number], string> = {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import type { TabsContentProps } from 'hanzogui'
|
||||
import type { TabsContentProps } from '@hanzo/gui'
|
||||
import {
|
||||
Button,
|
||||
H5,
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
YStack,
|
||||
isWeb,
|
||||
useWindowDimensions,
|
||||
} from 'hanzogui'
|
||||
} from '@hanzo/gui'
|
||||
|
||||
const demos = ['horizontal', 'vertical'] as const
|
||||
const demosTitle: Record<(typeof demos)[number], string> = {
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
YStack,
|
||||
isWeb,
|
||||
styled,
|
||||
} from 'hanzogui'
|
||||
} from '@hanzo/gui'
|
||||
import { createTabs } from '@hanzogui/tabs'
|
||||
import { getButtonSized } from '@hanzogui/get-button-sized'
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Em, Paragraph, SizableText, Span, Strong, YStack } from 'hanzogui'
|
||||
import { Em, Paragraph, SizableText, Span, Strong, YStack } from '@hanzo/gui'
|
||||
|
||||
export function TextDemo() {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { SquareProps, ThemeName } from 'hanzogui'
|
||||
import { Square, XStack, YStack } from 'hanzogui'
|
||||
import type { SquareProps, ThemeName } from '@hanzo/gui'
|
||||
import { Square, XStack, YStack } from '@hanzo/gui'
|
||||
|
||||
export function ThemeBuilderDemo() {
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, H5, Theme, XStack, YStack } from 'hanzogui'
|
||||
import { Button, H5, Theme, XStack, YStack } from '@hanzo/gui'
|
||||
|
||||
export function ThemeInverseDemo() {
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isWeb } from 'hanzogui'
|
||||
import { isWeb } from '@hanzo/gui'
|
||||
import {
|
||||
Toast,
|
||||
toast,
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
ArrowUpRight,
|
||||
} from '@hanzogui/lucide-icons-2'
|
||||
import { useRef, useState } from 'react'
|
||||
import { Button, Label, SizableText, Slider, XStack, YStack } from 'hanzogui'
|
||||
import { Button, Label, SizableText, Slider, XStack, YStack } from '@hanzo/gui'
|
||||
|
||||
export const ToastDemo = () => {
|
||||
const [position, setPosition] = useState<ToastPosition>('bottom-right')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CheckCircle2 } from '@hanzogui/lucide-icons-2'
|
||||
import { Toast, toast, useToasts } from '@hanzogui/toast/v2'
|
||||
import { Button, XStack, YStack } from 'hanzogui'
|
||||
import { Button, XStack, YStack } from '@hanzo/gui'
|
||||
|
||||
/**
|
||||
* IMPORTANT NOTE: if you're copy-pasting this demo into your code, make sure to add the Toast wrapper as well.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Toast, useToastController, useToastState } from '@hanzogui/toast'
|
||||
import React from 'react'
|
||||
import { Button, Label, Switch, XStack, YStack } from 'hanzogui'
|
||||
import { Button, Label, Switch, XStack, YStack } from '@hanzo/gui'
|
||||
|
||||
/**
|
||||
* IMPORTANT NOTE: if you're copy-pasting this demo into your code, make sure to add:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AlignCenter, AlignLeft, AlignRight } from '@hanzogui/lucide-icons-2'
|
||||
import { Label, ToggleGroup, XGroup, YGroup, XStack, YStack } from 'hanzogui'
|
||||
import { Label, ToggleGroup, XGroup, YGroup, XStack, YStack } from '@hanzo/gui'
|
||||
|
||||
export function ToggleGroupDemo() {
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Tokens } from '@hanzogui/core'
|
||||
import { getConfig } from '@hanzogui/core'
|
||||
import React from 'react'
|
||||
import { Button, H2, H3, H4, Square, XGroup, XStack, YStack } from 'hanzogui'
|
||||
import { Button, H2, H3, H4, Square, XGroup, XStack, YStack } from '@hanzo/gui'
|
||||
|
||||
type Section = 'spaceNegative' | keyof Tokens
|
||||
|
||||
|
||||
@@ -5,8 +5,16 @@ import {
|
||||
ChevronUp,
|
||||
Circle,
|
||||
} from '@hanzogui/lucide-icons-2'
|
||||
import type { TooltipProps } from 'hanzogui'
|
||||
import { Button, Paragraph, Theme, Tooltip, TooltipGroup, XStack, YStack } from 'hanzogui'
|
||||
import type { TooltipProps } from '@hanzo/gui'
|
||||
import {
|
||||
Button,
|
||||
Paragraph,
|
||||
Theme,
|
||||
Tooltip,
|
||||
TooltipGroup,
|
||||
XStack,
|
||||
YStack,
|
||||
} from '@hanzo/gui'
|
||||
|
||||
export function TooltipDemo() {
|
||||
return (
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
YStack,
|
||||
getVariableValue,
|
||||
useIsomorphicLayoutEffect,
|
||||
} from 'hanzogui'
|
||||
} from '@hanzo/gui'
|
||||
|
||||
const colors = config.tokens.color
|
||||
const colorKeys = Object.keys(colors)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user