Compare commits
62
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2177c0a3ca | ||
|
|
f53dc2513a | ||
|
|
f892023d4a | ||
|
|
38b22e7265 | ||
|
|
aeed548966 | ||
|
|
0483a5b099 | ||
|
|
cf084837a5 | ||
|
|
2a62e59c1c | ||
|
|
b5217aca35 | ||
|
|
350f9f2c6e | ||
|
|
3f7b90359c | ||
|
|
13c4dbf660 | ||
|
|
4be2d12e2e | ||
|
|
45adbc8349 | ||
|
|
bbe5b9ece6 | ||
|
|
689b811db9 | ||
|
|
085d2ca295 | ||
|
|
bd97e87ab3 | ||
|
|
854b1d59d5 | ||
|
|
76247c6366 | ||
|
|
0cb5be28a2 | ||
|
|
c480b014b4 | ||
|
|
9fd1bd7b65 | ||
|
|
782cff59e6 | ||
|
|
11c79d969d | ||
|
|
f6c03b9f63 | ||
|
|
75f734cba3 | ||
|
|
f22c35d920 | ||
|
|
357f9f9690 | ||
|
|
cd9cc63361 | ||
|
|
b888ef8e2a | ||
|
|
ea427bc66f | ||
|
|
926589c982 | ||
|
|
40921f9a3d | ||
|
|
15f130db5f | ||
|
|
07ae7ee84a | ||
|
|
3109fc14c2 | ||
|
|
1ae9e1579b | ||
|
|
1f3d8df185 | ||
|
|
e213be30bc | ||
|
|
7c1765359c | ||
|
|
7087215033 | ||
|
|
416d87c08f | ||
|
|
d164901c6f | ||
|
|
ab3fe3d70c | ||
|
|
2d663eba93 | ||
|
|
6f179ac4f2 | ||
|
|
bce9d6f602 | ||
|
|
8b0e6b0d59 | ||
|
|
36fc4a2fd4 | ||
|
|
41901783a8 | ||
|
|
c9ecfb0df5 | ||
|
|
1485697c4d | ||
|
|
8bb89b0163 | ||
|
|
9b6fb6b759 | ||
|
|
4d18c6d07b | ||
|
|
ae9c3a8e5b | ||
|
|
b512973819 | ||
|
|
8c454dd150 | ||
|
|
b559401d63 | ||
|
|
699e8217a8 | ||
|
|
1d818ebb88 |
@@ -0,0 +1,25 @@
|
||||
name: Build image
|
||||
|
||||
# Builds + pushes ghcr.io/hanzoai/gui (the gui.hanzo.ai site, served on
|
||||
# :3000 by the repo's existing Dockerfile) on a v* semver tag via the
|
||||
# canonical reusable workflow, natively on the self-hosted arcd fleet
|
||||
# (NO GitHub-hosted runners). Semver-only tags. Deployable on DOKS.
|
||||
#
|
||||
# This is the CONTAINER-image path. The repo's publish-gui*.yml workflows
|
||||
# publish the npm library packages and are unaffected.
|
||||
on:
|
||||
push:
|
||||
tags: ['v*']
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
uses: hanzoai/.github/.github/workflows/docker-build.yml@main
|
||||
with:
|
||||
image: ghcr.io/hanzoai/gui
|
||||
secrets: inherit
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
jobs:
|
||||
tagged-release:
|
||||
name: Changelog
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
runs-on: [self-hosted, linux, amd64]
|
||||
# needs: release
|
||||
|
||||
steps:
|
||||
|
||||
@@ -19,7 +19,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
checks:
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
runs-on: [self-hosted, linux, amd64]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
run: bun run lint
|
||||
|
||||
unit-tests:
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
runs-on: [self-hosted, linux, amd64]
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=6144'
|
||||
steps:
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
run: bun turbo run test:web --filter='!@hanzogui/kitchen-sink' --concurrency=1
|
||||
|
||||
changes:
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
runs-on: [self-hosted, linux, amd64]
|
||||
outputs:
|
||||
integration-relevant: ${{ steps.filter.outputs.integration-relevant }}
|
||||
steps:
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
integration-tests:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.integration-relevant == 'true'
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
runs-on: [self-hosted, linux, amd64]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
name: Publish gui (all packages)
|
||||
|
||||
# Publishes the FULL gui surface — every @hanzogui/* primitive AND the
|
||||
# `hanzogui` + `@hanzo/gui` umbrellas — at the current on-disk version
|
||||
# (7.x), with freshly built `dist/`. This is the fix for the broken
|
||||
# 7.0.0 publish, where the primitives shipped without `dist/` and
|
||||
# `@hanzo/gui` therefore failed to resolve for standalone consumers.
|
||||
#
|
||||
# `publish-gui.yml` only ships the umbrella; this ships the primitives it
|
||||
# depends on so a clean `npm i @hanzo/gui` works end-to-end.
|
||||
#
|
||||
# Trigger: manual (workflow_dispatch, type "publish" to confirm) or by
|
||||
# pushing a `release/gui-all-v*` tag.
|
||||
#
|
||||
# NPM_TOKEN resolution mirrors publish-gui.yml:
|
||||
# 1. KMS via Universal Auth (KMS_CLIENT_ID/SECRET → short-lived token)
|
||||
# 2. KMS via long-lived HANZO_API_KEY (legacy bootstrap)
|
||||
# 3. Direct repo secret NPM_TOKEN (fallback)
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
confirm:
|
||||
description: 'Type "publish" to publish ALL gui packages at the current version'
|
||||
required: true
|
||||
default: ''
|
||||
push:
|
||||
tags:
|
||||
- 'release/gui-all-v*'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
publish-all:
|
||||
name: Build + publish all gui packages
|
||||
runs-on: [self-hosted, linux, arm64]
|
||||
if: github.event_name == 'push' || github.event.inputs.confirm == 'publish'
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install
|
||||
uses: ./.github/actions/install
|
||||
|
||||
- name: Build all packages
|
||||
# Fresh checkout has no dist; --republish skips release.ts's own
|
||||
# build, so build here explicitly. `npm pack` (inside release.ts)
|
||||
# will fail loudly if any dist tree is missing.
|
||||
run: bun run build
|
||||
|
||||
- name: Resolve NPM_TOKEN (KMS → fallback to repo secret)
|
||||
id: token
|
||||
env:
|
||||
DIRECT_NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
KMS_CLIENT_ID: ${{ secrets.KMS_CLIENT_ID }}
|
||||
KMS_CLIENT_SECRET: ${{ secrets.KMS_CLIENT_SECRET }}
|
||||
HANZO_API_KEY: ${{ secrets.HANZO_API_KEY }}
|
||||
KMS_ENDPOINT: ${{ vars.KMS_ENDPOINT || 'https://kms.hanzo.ai' }}
|
||||
KMS_WORKSPACE_ID: ${{ vars.KMS_WORKSPACE_ID_GUI || 'e1359bf4-31b4-4dfa-bb90-323e2c298ad8' }}
|
||||
run: |
|
||||
set -eu
|
||||
npm_token=""
|
||||
if [ -n "${KMS_CLIENT_ID:-}" ] && [ -n "${KMS_CLIENT_SECRET:-}" ]; then
|
||||
HANZO_API_KEY=$(curl -sf "${KMS_ENDPOINT}/api/v1/auth/universal-auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"clientId\":\"${KMS_CLIENT_ID}\",\"clientSecret\":\"${KMS_CLIENT_SECRET}\"}" \
|
||||
| jq -r '.accessToken') || true
|
||||
fi
|
||||
if [ -n "${HANZO_API_KEY:-}" ]; then
|
||||
response=$(curl -sf \
|
||||
"${KMS_ENDPOINT}/api/v3/secrets/raw?workspaceId=${KMS_WORKSPACE_ID}&secretPath=/publish&environment=prod" \
|
||||
-H "Authorization: Bearer ${HANZO_API_KEY}" 2>/dev/null) || true
|
||||
if [ -n "${response:-}" ]; then
|
||||
npm_token=$(echo "$response" | jq -r '.secrets[] | select(.secretKey=="NPM_TOKEN") | .secretValue // empty')
|
||||
fi
|
||||
fi
|
||||
if [ -z "$npm_token" ]; then
|
||||
npm_token="${DIRECT_NPM_TOKEN:-}"
|
||||
if [ -n "$npm_token" ]; then
|
||||
echo "Using NPM_TOKEN from repo secret (KMS path unavailable)."
|
||||
fi
|
||||
fi
|
||||
if [ -z "$npm_token" ]; then
|
||||
echo "::error::No NPM_TOKEN available — need KMS_CLIENT_ID/SECRET, HANZO_API_KEY, or NPM_TOKEN repo secret."
|
||||
exit 1
|
||||
fi
|
||||
echo "::add-mask::${npm_token}"
|
||||
echo "npm_token=${npm_token}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Configure npm registry
|
||||
env:
|
||||
NPM_TOKEN: ${{ steps.token.outputs.npm_token }}
|
||||
run: |
|
||||
echo 'registry=https://registry.npmjs.org/' > ~/.npmrc
|
||||
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" >> ~/.npmrc
|
||||
|
||||
- name: Publish gui surface (primitives + umbrella + @hanzo/gui alias)
|
||||
# Targeted publish of just the @hanzogui/* closure + hanzogui +
|
||||
# @hanzo/gui, at the current on-disk version. Avoids release.ts's
|
||||
# whole-workspace fan-out (which fails packing unrelated
|
||||
# @hanzo_network/* packages) and its fragile tarball-rename step.
|
||||
# Idempotent — skips any name@version already on npm.
|
||||
env:
|
||||
NPM_TOKEN: ${{ steps.token.outputs.npm_token }}
|
||||
NODE_AUTH_TOKEN: ${{ steps.token.outputs.npm_token }}
|
||||
run: node ./scripts/publish-gui-surface.mjs
|
||||
@@ -28,7 +28,7 @@ permissions:
|
||||
jobs:
|
||||
publish:
|
||||
name: Build, smoke-test, publish
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
runs-on: [self-hosted, linux, arm64]
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
|
||||
@@ -10,7 +10,7 @@ on:
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
runs-on: [self-hosted, linux, amd64]
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v5
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
|
||||
tagged-release:
|
||||
name: Tagged Release
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
runs-on: [self-hosted, linux, amd64]
|
||||
# needs: release
|
||||
|
||||
steps:
|
||||
|
||||
@@ -179,7 +179,7 @@ jobs:
|
||||
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: hanzo-build-linux-amd64
|
||||
runs-on: [self-hosted, linux, amd64]
|
||||
timeout-minutes: 45
|
||||
outputs:
|
||||
android-cache-key: ${{ steps.final-cache-key.outputs.cache_key }}
|
||||
@@ -446,7 +446,7 @@ jobs:
|
||||
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: hanzo-build-linux-amd64
|
||||
runs-on: [self-hosted, linux, amd64]
|
||||
timeout-minutes: 45
|
||||
# Android emulator in CI is flaky with window focus issues - don't block on failures
|
||||
continue-on-error: true
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
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: `.
|
||||
|
||||
# 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({ ... }),
|
||||
})
|
||||
```
|
||||
|
||||
**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.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Additional notes (merged from LLM.md)
|
||||
|
||||
# gui — AI Assistant Context
|
||||
|
||||
# Hanzo GUI
|
||||
|
||||
<h3 align="center">
|
||||
Style library, design system, composable components, and more.
|
||||
</h3>
|
||||
@@ -3,7 +3,7 @@ import React from 'react'
|
||||
|
||||
import type { FontSizeTokens, SelectProps } from 'hanzogui'
|
||||
import { Adapt, Label, Select, Sheet, Theme, XStack, YStack, getFontSize } from 'hanzogui'
|
||||
import { LinearGradient } from 'gui/linear-gradient'
|
||||
import { LinearGradient } from 'hanzogui/linear-gradient'
|
||||
|
||||
export function SelectDemo() {
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { GuiBuildOptions } from 'hanzogui'
|
||||
|
||||
export default {
|
||||
components: ['gui'],
|
||||
components: ['hanzogui'],
|
||||
logTimings: true,
|
||||
config: '@hanzogui/dev-config',
|
||||
outputCSS: './gui.generated.css',
|
||||
|
||||
@@ -1,10 +1,149 @@
|
||||
import { mkdirSync } from 'node:fs'
|
||||
import { mkdirSync, readdirSync, statSync } from 'node:fs'
|
||||
import { existsSync, writeFileSync, readFileSync } from 'node:fs'
|
||||
import { resolve, dirname } from 'node:path'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
const RECIPES_COMPONENT_PREFIX = '@hanzogui/recipes/component/'
|
||||
|
||||
/**
|
||||
* Recursively collect .ts/.tsx source files under a directory, skipping
|
||||
* node_modules and build output.
|
||||
* @param {string} dir
|
||||
* @param {string[]} acc
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function collectSourceFiles(dir, acc = []) {
|
||||
let entries
|
||||
try {
|
||||
entries = readdirSync(dir)
|
||||
} catch {
|
||||
return acc
|
||||
}
|
||||
for (const name of entries) {
|
||||
if (name === 'node_modules' || name === 'dist' || name === 'recipes-output') continue
|
||||
const full = join(dir, name)
|
||||
let s
|
||||
try {
|
||||
s = statSync(full)
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
if (s.isDirectory()) {
|
||||
collectSourceFiles(full, acc)
|
||||
} else if (name.endsWith('.ts') || name.endsWith('.tsx')) {
|
||||
acc.push(full)
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan the app for deep `@hanzogui/recipes/component/*` imports and the named
|
||||
* bindings each subpath must provide, so concrete stub modules can be generated
|
||||
* when the (optional, pro-only) recipes repo is absent.
|
||||
*
|
||||
* Three import shapes are detected:
|
||||
* 1. namespace -> import * as NS from '<subpath>' (then NS.Member usages)
|
||||
* 2. named -> import { A, B } from '<subpath>'
|
||||
* 3. re-export -> export { A } from '<subpath>'
|
||||
*
|
||||
* @param {string} appDir
|
||||
* @returns {Record<string, string[]>} subpath -> sorted unique export names
|
||||
*/
|
||||
function scanRecipesSubpaths(appDir) {
|
||||
const subpaths = {}
|
||||
for (const file of collectSourceFiles(appDir)) {
|
||||
let src
|
||||
try {
|
||||
src = readFileSync(file, 'utf-8')
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
if (!src.includes(RECIPES_COMPONENT_PREFIX)) continue
|
||||
|
||||
// namespace imports + member accesses
|
||||
for (const m of src.matchAll(/import\s+\*\s+as\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g)) {
|
||||
const [, ns, sp] = m
|
||||
if (!sp.startsWith(RECIPES_COMPONENT_PREFIX)) continue
|
||||
subpaths[sp] ??= new Set()
|
||||
const memberRe = new RegExp('\\b' + ns + '\\.([A-Za-z_][A-Za-z0-9_]*)', 'g')
|
||||
for (const u of src.matchAll(memberRe)) subpaths[sp].add(u[1])
|
||||
}
|
||||
|
||||
// named imports and re-exports
|
||||
for (const m of src.matchAll(/(?:import|export)\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/g)) {
|
||||
const [, names, sp] = m
|
||||
if (!sp.startsWith(RECIPES_COMPONENT_PREFIX)) continue
|
||||
subpaths[sp] ??= new Set()
|
||||
for (const raw of names.split(',')) {
|
||||
const name = raw.trim().split(/\s+as\s+/)[0].trim()
|
||||
if (name) subpaths[sp].add(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return Object.fromEntries(
|
||||
Object.entries(subpaths).map(([k, v]) => [k, [...v].sort()])
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate concrete stub modules for deep `@hanzogui/recipes/component/*`
|
||||
* subpaths used by the showcase tree, returning a map from subpath -> absolute
|
||||
* stub file path. Each stub exports a no-op component (carrying a `.fileName`
|
||||
* field so `Member.fileName` reads don't throw) as the default and as every
|
||||
* named binding the app imports — concrete on-disk named exports are required
|
||||
* because rolldown statically validates `export { X } from` / `import { X }`
|
||||
* and does not transform virtual `\0`-modules through commonjs interop.
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {string} options.appDir - app root (dir containing components/, vite.config.ts)
|
||||
* @param {string} options.outDir - directory to write stub modules into
|
||||
* @returns {Record<string, string>} subpath -> absolute stub path
|
||||
*/
|
||||
export function generateSubpathStubs({ appDir, outDir }) {
|
||||
const map = scanRecipesSubpaths(appDir)
|
||||
mkdirSync(outDir, { recursive: true })
|
||||
const result = {}
|
||||
for (const [subpath, names] of Object.entries(map)) {
|
||||
const flat = subpath.slice(RECIPES_COMPONENT_PREFIX.length).replace(/[^A-Za-z0-9]+/g, '_')
|
||||
const stubPath = resolve(outDir, `${flat}.tsx`)
|
||||
// Hook-shaped exports (useFoo) are called, not rendered, and their result is
|
||||
// often destructured (e.g. `const { sm } = useGroupMedia(...)`), so they must
|
||||
// return a (proxy) object rather than the null-rendering component stub.
|
||||
const namedExports = names
|
||||
.filter((n) => n !== 'default')
|
||||
.map((n) =>
|
||||
/^use[A-Z0-9]/.test(n)
|
||||
? `export const ${n} = recipesSubpathHook`
|
||||
: `export const ${n} = RecipesSubpathStub`
|
||||
)
|
||||
.join('\n')
|
||||
writeFileSync(
|
||||
stubPath,
|
||||
`// AUTO-GENERATED stub for ${subpath}
|
||||
// Recipes is optional (pro users only); the real components live in the
|
||||
// separate recipes repo. Regenerate via scripts/generate-recipes-proxy.mjs.
|
||||
function RecipesSubpathStub() {
|
||||
return null
|
||||
}
|
||||
RecipesSubpathStub.fileName = ''
|
||||
|
||||
// Hook stub: returns an object that yields undefined for any destructured key,
|
||||
// so callers like \`const { sm } = useGroupMedia(...)\` never throw.
|
||||
const recipesSubpathHook = () =>
|
||||
new Proxy({}, { get: () => undefined }) as any
|
||||
|
||||
export default RecipesSubpathStub
|
||||
${namedExports}
|
||||
`
|
||||
)
|
||||
result[subpath] = stubPath
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate recipes proxy files based on whether the recipes repo is available.
|
||||
* Can be called from vite config or run directly as a script.
|
||||
@@ -19,11 +158,23 @@ export function generateRecipesProxy(options = {}) {
|
||||
|
||||
const RECIPES_PATH = resolve(basePath, '../../../../recipes')
|
||||
const HELPERS_DIST_PATH = resolve(basePath, '../helpers/dist')
|
||||
const APP_DIR = resolve(basePath, '..')
|
||||
const hasRecipes = existsSync(RECIPES_PATH)
|
||||
|
||||
// Ensure dist exists
|
||||
mkdirSync(HELPERS_DIST_PATH, { recursive: true })
|
||||
|
||||
// When recipes is absent, generate concrete stub modules for every deep
|
||||
// `@hanzogui/recipes/component/*` subpath the app imports, so the showcase
|
||||
// tree (which is always in the build graph via RecipesComponentSection) can
|
||||
// resolve its named imports/re-exports.
|
||||
const subpathStubs = hasRecipes
|
||||
? {}
|
||||
: generateSubpathStubs({
|
||||
appDir: APP_DIR,
|
||||
outDir: resolve(HELPERS_DIST_PATH, 'recipes-subpath-stubs'),
|
||||
})
|
||||
|
||||
const proxyPath = resolve(HELPERS_DIST_PATH, 'recipes-proxy.ts')
|
||||
const existingContent = existsSync(proxyPath) ? readFileSync(proxyPath, 'utf-8') : ''
|
||||
const isStub = existingContent.includes('Stub file for when recipes is not available')
|
||||
@@ -31,7 +182,7 @@ export function generateRecipesProxy(options = {}) {
|
||||
|
||||
// Skip if already generated with correct state and has all exports
|
||||
if (existingContent && isStub === !hasRecipes && hasCurrentRouteProvider) {
|
||||
return { hasRecipes }
|
||||
return { hasRecipes, subpathStubs }
|
||||
}
|
||||
|
||||
if (!hasRecipes) {
|
||||
@@ -87,7 +238,7 @@ export * as Sections from '../../components/recipes-showcase/sections'
|
||||
}
|
||||
}
|
||||
|
||||
return { hasRecipes }
|
||||
return { hasRecipes, subpathStubs }
|
||||
}
|
||||
|
||||
// Run if called directly (postinstall script)
|
||||
|
||||
@@ -37,8 +37,10 @@ if (!existsSync(vitePluginDist) || !existsSync(staticDist)) {
|
||||
}
|
||||
}
|
||||
|
||||
// Generate recipes proxy files (creates stubs if recipes repo not found)
|
||||
const { hasRecipes } = generateRecipesProxy({
|
||||
// Generate recipes proxy files (creates stubs if recipes repo not found).
|
||||
// When recipes is absent, subpathStubs maps each deep
|
||||
// `@hanzogui/recipes/component/*` import to a concrete generated stub module.
|
||||
const { hasRecipes, subpathStubs } = generateRecipesProxy({
|
||||
basePath: pathResolve(import.meta.dirname, 'scripts'),
|
||||
silent: false,
|
||||
})
|
||||
@@ -61,7 +63,7 @@ const include = [
|
||||
'swr/mutation',
|
||||
'mdx-bundler/client',
|
||||
// core hanzo-gui packages must be pre-bundled together to avoid duplicate instances
|
||||
'gui',
|
||||
'hanzogui',
|
||||
'@hanzogui/core',
|
||||
'@hanzogui/web',
|
||||
// existing
|
||||
@@ -119,17 +121,19 @@ export default {
|
||||
resolve: {
|
||||
preserveSymlinks: false,
|
||||
alias: [
|
||||
// Regex-based alias for recipes components when not available
|
||||
// When recipes is absent, map each deep @hanzogui/recipes/component/*
|
||||
// import to its concrete generated stub. These MUST precede the catch-all
|
||||
// '@hanzogui/recipes' alias below, otherwise that alias swallows the
|
||||
// subpath (resolving to recipes-proxy/component/...) and the build fails
|
||||
// with UNLOADABLE_DEPENDENCY. Stubs carry the exact named exports the
|
||||
// showcase tree imports/re-exports, which rolldown validates statically.
|
||||
...(!hasRecipes
|
||||
? [
|
||||
{
|
||||
find: /^@gui\/recipes\/component\/.+$/,
|
||||
replacement: pathResolve(
|
||||
import.meta.dirname,
|
||||
'./helpers/dist/recipes-component-stub.tsx'
|
||||
),
|
||||
},
|
||||
]
|
||||
? Object.entries(subpathStubs)
|
||||
// longest subpath first: vite matches `id === find || id.startsWith(find + '/')`,
|
||||
// so a parent like `.../user/preferences` must not shadow its child
|
||||
// `.../user/preferences/LocationNotification`.
|
||||
.sort(([a], [b]) => b.length - a.length)
|
||||
.map(([find, replacement]) => ({ find, replacement }))
|
||||
: []),
|
||||
// Standard string-based aliases
|
||||
{
|
||||
@@ -204,43 +208,6 @@ export default {
|
||||
},
|
||||
|
||||
plugins: [
|
||||
// Plugin to stub recipes component imports when recipes repo is not available
|
||||
!hasRecipes && {
|
||||
name: 'stub-recipes-components',
|
||||
enforce: 'pre', // Run before other plugins including alias resolution
|
||||
resolveId(id: string) {
|
||||
// Intercept imports from @hanzogui/recipes/component/*
|
||||
if (id.startsWith('@hanzogui/recipes/component/')) {
|
||||
// Return a virtual module ID
|
||||
return '\0recipes-component-stub:' + id
|
||||
}
|
||||
},
|
||||
load(id: string) {
|
||||
// Handle the virtual module
|
||||
if (id.startsWith('\0recipes-component-stub:')) {
|
||||
// Return stub component code
|
||||
return `
|
||||
import { YStack, Paragraph } from 'hanzogui'
|
||||
|
||||
export default function RecipesComponentStub() {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<YStack p="$4" bc="$borderColor" br="$4">
|
||||
<Paragraph size="$2" color="$color10">
|
||||
Recipes component not available
|
||||
</Paragraph>
|
||||
</YStack>
|
||||
)
|
||||
}
|
||||
|
||||
// Export as default and named for compatibility
|
||||
export const LocationNotification = RecipesComponentStub
|
||||
`
|
||||
}
|
||||
},
|
||||
},
|
||||
guiPlugin({
|
||||
// see gui.build.ts
|
||||
disable: process.env.NODE_ENV !== 'production',
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from 'react'
|
||||
|
||||
import type { FontSizeTokens, SelectProps } from 'hanzogui'
|
||||
import { Adapt, Label, Select, Sheet, XStack, YStack, getFontSize } from 'hanzogui'
|
||||
import { LinearGradient } from 'gui/linear-gradient'
|
||||
import { LinearGradient } from 'hanzogui/linear-gradient'
|
||||
|
||||
export function SelectDemo() {
|
||||
return (
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
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: `.
|
||||
|
||||
# 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({ ... }),
|
||||
})
|
||||
```
|
||||
|
||||
**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.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Additional notes (merged from LLM.md)
|
||||
|
||||
# gui — AI Assistant Context
|
||||
|
||||
# Hanzo GUI
|
||||
|
||||
<h3 align="center">
|
||||
Style library, design system, composable components, and more.
|
||||
</h3>
|
||||
+32
-3
@@ -17,7 +17,14 @@
|
||||
"./apps/kitchen-sink-shared",
|
||||
"./apps/sandbox",
|
||||
"./apps/tests/**/*",
|
||||
"./templates/*"
|
||||
"./templates/*",
|
||||
"./pkgs/ai",
|
||||
"./pkgs/net-ui",
|
||||
"./pkgs/net-state",
|
||||
"./pkgs/net-i18n",
|
||||
"./pkgs/net-message-ts",
|
||||
"./pkgs/net-brand-config",
|
||||
"./pkgs/net-logo"
|
||||
],
|
||||
"scripts": {
|
||||
"postinstall": "bun ./scripts/postinstall.ts",
|
||||
@@ -113,6 +120,8 @@
|
||||
"devDependencies": {
|
||||
"@manypkg/cli": "^0.23.0",
|
||||
"@playwright/test": "^1.49.1",
|
||||
"@tailwindcss/typography": "^0.5.20",
|
||||
"@tailwindcss/vite": "^4",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"dotenv-cli": "^7.4.2",
|
||||
"esbuild": "^0.27.2",
|
||||
@@ -127,6 +136,7 @@
|
||||
"p-map": "^7.0.3",
|
||||
"prompts": "2.1.0",
|
||||
"react-native-screens": "~4.23.0",
|
||||
"tailwindcss": "^4",
|
||||
"turbo": "^2.1.3",
|
||||
"typescript": "~5.9.2",
|
||||
"ultra-runner": "^3.10.5"
|
||||
@@ -134,14 +144,33 @@
|
||||
"overrides": {
|
||||
"@react-navigation/core": "7.9.2",
|
||||
"@react-navigation/elements": "2.8.1",
|
||||
"@react-navigation/native": "7.1.9",
|
||||
"@react-navigation/native": "7.1.34",
|
||||
"@react-navigation/native-stack": "7.6.2",
|
||||
"@react-navigation/routers": "7.4.0",
|
||||
"@swc/core": "1.14.0",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-native": "0.83.2",
|
||||
"whatwg-url": "14.1.1"
|
||||
"whatwg-url": "14.1.1",
|
||||
"shiki": "^3.19.0",
|
||||
"react-resizable-panels": "^3.0.2",
|
||||
"one": "1.12.5",
|
||||
"vxrn": "1.12.5",
|
||||
"@vxrn/compiler": "1.12.5",
|
||||
"@vxrn/resolve": "1.12.5",
|
||||
"@vxrn/color-scheme": "1.12.5",
|
||||
"@vxrn/mdx": "1.12.5",
|
||||
"@vxrn/safe-area": "1.12.5",
|
||||
"@vxrn/utils": "1.12.5",
|
||||
"@vxrn/debug": "1.12.5",
|
||||
"@vxrn/vendor": "1.12.5",
|
||||
"@vxrn/query-string": "1.12.5",
|
||||
"@vxrn/url-parse": "1.12.5",
|
||||
"@vxrn/vite-flow": "1.12.5",
|
||||
"@vxrn/vite-plugin-metro": "1.12.5",
|
||||
"@vxrn/tslib-lite": "1.12.5",
|
||||
"create-vxrn": "1.12.5",
|
||||
"@react-navigation/bottom-tabs": "7.10.1"
|
||||
},
|
||||
"packageManager": "bun@1.3.9",
|
||||
"manypkg": {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# @hanzo/ai — build / migration plan
|
||||
|
||||
Goal: the Hanzo AI app as **one declarative SDK component** (`<HanzoAI {...brand} />`),
|
||||
so `hanzo/zoo/lux` desktop+mobile apps become ~80-line shims — exactly how
|
||||
`zooai/exchange` / `luxfi/exchange` consume `@luxfi/exchange`.
|
||||
|
||||
```
|
||||
@hanzo/ai (this pkg — the app) @luxfi/exchange (the analog)
|
||||
▲ ▲
|
||||
hanzoai/desktop · zooai/app · luxfi/app zooai/exchange · luxfi/exchange
|
||||
<HanzoAI {...brand}/> <Exchange {...brand}/>
|
||||
```
|
||||
|
||||
## Status
|
||||
- [x] Props contract — `BrandConfig` lifted from `hanzoai/desktop/libs/brand-config` → `src/types.ts`.
|
||||
- [x] `<HanzoAI>` component + `BrandProvider`/`useBrand` (prop-driven; replaces `getBrand()` env lookup).
|
||||
- [ ] **Move the app in** (the big rock).
|
||||
- [ ] **Decouple Tauri** (44 files).
|
||||
- [ ] **Bundle + publish** `@hanzo/ai` and its libs.
|
||||
- [ ] **Shims**: rewrite `hanzoai/desktop`, `zooai/app`, `luxfi/app` to the shim.
|
||||
|
||||
## The move (~955 source files)
|
||||
From `hanzoai/desktop`:
|
||||
| Source | Files | Destination |
|
||||
|---|---|---|
|
||||
| `apps/hanzo-desktop/src/**` | 263 | `pkgs/ai/src/app/**` (App.tsx → `./app/App`) |
|
||||
| `libs/hanzo-node-state/src` | 460 | `pkgs/state` → `@hanzo_network/hanzo-node-state` |
|
||||
| `libs/hanzo-ui/src` | 192 | `pkgs/hanzo-ui` → `@hanzo_network/hanzo-ui` |
|
||||
| `libs/hanzo-message-ts/src` | 32 | `pkgs/message-ts` → `@hanzo_network/hanzo-message-ts` |
|
||||
| `libs/hanzo-i18n/src` | 8 | `pkgs/hanzo-i18n` → `@hanzo_network/hanzo-i18n` |
|
||||
|
||||
Mechanical, but two rewrites are required during the move:
|
||||
1. `getBrand()` / `import.meta.env.VITE_BRAND` → `useBrand()` (from `./brand-context`).
|
||||
2. The **44** `@tauri-apps/*` import sites → the injected `host: HostAdapter` prop
|
||||
(`host.invoke` / `host.listen`), so the SDK runs on plain web *and* Tauri *and* Expo.
|
||||
On web with no `host`, those features degrade gracefully.
|
||||
|
||||
## Then the shim (per app) — the whole app:
|
||||
```tsx
|
||||
// luxfi/app — apps/web/src/main.tsx
|
||||
import HanzoAI from '@hanzo/ai'
|
||||
import brand from '@luxfi/brand'
|
||||
import { tauriHost } from './host' // desktop adapter
|
||||
createRoot(root).render(
|
||||
<HanzoAI {...brand} host={tauriHost}
|
||||
features={{ chat:true, wallet:true, mining:true }} />)
|
||||
```
|
||||
Mobile = `apps/mobile` (Expo) with the same `<HanzoAI {...brand} host={expoHost} />`.
|
||||
Build via Dockerfile from the pinned `@hanzo/ai`, brand overlaid — like the exchanges.
|
||||
@@ -0,0 +1,59 @@
|
||||
# Live chat — VERIFIED working end-to-end (web, local) 2026-06-05
|
||||
|
||||
The web app at `:1500` chats with **zen-coder-24b** through a real **hanzo-node**,
|
||||
fully in the browser. Verified: prompt "Reply with exactly: E2E CHAT OK" →
|
||||
zen-coder generated `E2E CHAT OK` (7 tokens), streamed back to the chat UI.
|
||||
|
||||
The full path:
|
||||
|
||||
```
|
||||
browser :1500 ─(vite proxy /v1,/v2,/ws)→ hanzo-node :3700 ─job→ zen_engine provider
|
||||
→ responses-proxy :36906 (rewrites /v1/engine/responses → /v1/responses)
|
||||
→ hanzo-engine :36902 (zen-coder-24b) → "E2E CHAT OK" → streamed back to the UI
|
||||
```
|
||||
|
||||
## The non-obvious fixes that made it work
|
||||
|
||||
1. **The node speaks the OpenAI _Responses_ API, the engine serves it at a
|
||||
different path.** The node's `OpenAI` provider POSTs to
|
||||
`<url>/v1/engine/responses` (`{input:[...], max_output_tokens, ...}`), but
|
||||
hanzo-engine serves the Responses API at **`/v1/responses`** (`/v1/engine/...`
|
||||
→ 404 → the node reports "AI Provider API Error: Unknown error"). A tiny proxy
|
||||
rewrites the path (and injects `enable_thinking:false`). See
|
||||
`local-runtime/responses-proxy.py`.
|
||||
2. **Use `127.0.0.1`, never `localhost`.** `localhost` resolves to IPv6 `::1`
|
||||
here; the engine/embeddings bind IPv4 only → instant connection failure.
|
||||
3. **CORS:** the vite dev server proxies the app's own origin (`/v1`,`/v2`,`/ws`)
|
||||
to the node, so the app's `nodeAddress` is just `http://<host>:1500`.
|
||||
4. **Two migration import bugs** had to be fixed for the chat screens to render:
|
||||
`main-layout.tsx` was missing `Box`/`Boxes`/`Coins` lucide imports, and the
|
||||
merge pulled `react-resizable-panels@4` (renamed exports) — pinned to `^3.0.2`
|
||||
in `pkgs/net-ui` (the app uses `PanelGroup`/`PanelResizeHandle`).
|
||||
|
||||
## Reproduce it
|
||||
|
||||
```bash
|
||||
# 1. engine + embeddings already running: :36902 (zen-coder), :11436 (embed)
|
||||
# 2. responses-proxy (path rewrite):
|
||||
python3 pkgs/ai/local-runtime/responses-proxy.py & # :36906 → :36902
|
||||
# 3. WS proxy (the app derives ws://host:1501 behind the vite proxy):
|
||||
node pkgs/ai/local-runtime/ws-proxy.js & # :1501 → node WS :3701
|
||||
# 4. a /v1 hanzo-node wired to the proxy (fresh storage, no reg code):
|
||||
bash pkgs/ai/local-runtime/run-node.sh & # API :3700, zen_engine → :36906
|
||||
# 5. web app pointed at the node + engine (engine-api proxy avoids CORS):
|
||||
cd pkgs/ai/web && VITE_NODE_API=http://127.0.0.1:3700 VITE_NODE_WS=ws://127.0.0.1:3701 \
|
||||
VITE_ENGINE_BASE_URL=/engine-api VITE_ENGINE_API=http://127.0.0.1:36906 \
|
||||
bun x vite --config vite.config.ts # :1500
|
||||
# 6. open http://spark.local:1500 → agree → Quick Connect (node address = same
|
||||
# origin http://localhost:1500) → /home → chat. Reply streams from zen-coder.
|
||||
```
|
||||
|
||||
## Console — clean (the e2e asserts it)
|
||||
|
||||
The earlier web console errors are fixed and guarded by `e2e/chat.e2e.test.ts`:
|
||||
react-query "data cannot be undefined" (web `invoke` returns null), the
|
||||
`isPermissionGranted`/notification gap, the `:36900/v1/engine/models` CORS
|
||||
(routed via the `/engine-api` proxy), the `ws://…:1501` failure (ws-proxy), the
|
||||
`available_models` 401 (gated `useGetLLMProviders` on auth), and the framer
|
||||
`motion()` deprecation. Remaining: a rare, intermittent static-asset `404` and a
|
||||
node-side `Embedding "Query is not read-only"` warning — neither affects chat.
|
||||
@@ -0,0 +1,80 @@
|
||||
# @hanzo/ai — one app surface, every platform
|
||||
|
||||
The Hanzo / Zoo / Lux AI app is **one** thing: `@hanzo/ai`. Web, desktop and
|
||||
mobile are not three apps — they are the same app with two axes injected.
|
||||
|
||||
```
|
||||
┌────────────────────────── @hanzo/ai ──────────────────────────┐
|
||||
│ src/app/ (the migrated shinkai-fork app — ONE copy, one place) │
|
||||
│ + net-* libs (ui, state, i18n, message-ts, brand-config, logo, │
|
||||
│ artifacts) + src/host/* (the @tauri-apps shim surface) │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
▲ ▲ ▲
|
||||
host = web (default) host = tauriHost host = expoHost
|
||||
brand = getBrand() brand = getBrand() brand = getBrand()
|
||||
│ │ │
|
||||
hanzo.app / .chat hanzoai/desktop @hanzo/gui
|
||||
zoo.cloud / lux.cloud zooai/app · luxfi/app (Expo mobile)
|
||||
```
|
||||
|
||||
## The two orthogonal axes (decomplected)
|
||||
|
||||
| Axis | What it is | How it's injected | Default |
|
||||
|---|---|---|---|
|
||||
| **Brand** | hanzo / zoo / lux identity, cloud endpoints, chain, IAM | `getBrand()` (driven by `VITE_BRAND` or hostname) **or** spread as props `<HanzoAI {...brand}/>` | `getBrand()` → hostname → HANZO |
|
||||
| **Platform** | how native calls (`invoke`, `listen`, fs, window…) resolve | `host` prop — a `HostAdapter` | web no-ops |
|
||||
|
||||
Everything else — the entire app — is shared. A new app is a ~10-line shim:
|
||||
|
||||
```tsx
|
||||
import HanzoAI, { getBrand } from '@hanzo/ai';
|
||||
import { tauriHost } from './tauri-host'; // desktop only; web omits it
|
||||
createRoot(root).render(
|
||||
<HanzoAI {...getBrand()} host={tauriHost} features={{ chat, wallet, agents }} />
|
||||
);
|
||||
```
|
||||
|
||||
## Web ⇄ desktop parity
|
||||
|
||||
The web shim and the desktop shim are **byte-identical except one prop**
|
||||
(`host={tauriHost}`). They import the same `@hanzo/ai`, which is the same
|
||||
`src/app`. So the rendered React tree — every screen, route and component — is
|
||||
identical. The only runtime difference is what `HostAdapter` does:
|
||||
|
||||
| Call | web (default host) | desktop (tauriHost) |
|
||||
|---|---|---|
|
||||
| `invoke(cmd, args)` | no-op, returns `undefined` (logs in dev) | Tauri IPC → Rust |
|
||||
| `listen(event, cb)` | no-op unlisten | Tauri event bus |
|
||||
| `getCurrentWindow()` | no-op Window (emit/listen/geometry) | real Tauri window |
|
||||
| fs / shell / process / updater | no-op / empty | Tauri plugins |
|
||||
|
||||
So on the web the app renders 1:1 with desktop; native-only actions (open a
|
||||
folder, auto-update, tray) simply do nothing instead of crashing. Cloud
|
||||
inference + chat work on both (they go over HTTP to the brand's
|
||||
`inferenceEndpoint`, not through `invoke`).
|
||||
|
||||
## Verification (2026-06-04)
|
||||
|
||||
| Surface | Build | Render |
|
||||
|---|---|---|
|
||||
| web dev server (`pkgs/ai/web`, imports `src`) | ✅ | ✅ Hanzo onboarding |
|
||||
| web prod (unminified, 55M) | ✅ 40s | ✅ |
|
||||
| web prod (minified, 13M / gzip 3.75M) | ✅ 44s | ✅ |
|
||||
| library `dist` (self-contained, react external) | ✅ | — |
|
||||
| **external shim** (`examples/web`, only react provided, app from `dist`) | ✅ | ✅ — proves SDK consumption |
|
||||
|
||||
The render blocker that had to be cleared first: app modules imported
|
||||
`useBrand` from the **`@hanzo/ai` package name**, which resolved to the built
|
||||
`dist` — pulling a second React + a second app copy into the source graph (a
|
||||
cycle → `useContext` of null). Fix: the brand store lives in
|
||||
`@hanzo_network/brand-config` (plain getters, not hooks), aliased to `src`;
|
||||
app code never imports the `@hanzo/ai` package by name.
|
||||
|
||||
## Turning the real repos into shims
|
||||
|
||||
`luxfi/app`, `zooai/app`, `hanzoai/desktop`, `hanzoai/app` each become the
|
||||
shim above. Per repo: depend on `@hanzo/ai` (+ `react`, `react-dom`, and for
|
||||
desktop `@tauri-apps/api`), set `VITE_BRAND`, drop all app source. The
|
||||
Dockerfile/Tauri config builds from the pinned SDK — exactly how
|
||||
`zooai/exchange` builds from `@luxfi/exchange`. (`@hanzo/ai` is not yet
|
||||
published to npm; until then the shims consume it via the workspace.)
|
||||
@@ -0,0 +1,51 @@
|
||||
# Testing @hanzo/ai — and TDD going forward
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
bun run --cwd pkgs/ai test # unit + regression (vitest, jsdom) — fast, no external deps
|
||||
bun run --cwd pkgs/ai test:watch # vitest watch mode (use while writing a fix)
|
||||
bun run --cwd pkgs/ai test:e2e # Playwright e2e — needs the local runtime up (skips if not)
|
||||
```
|
||||
|
||||
`test` runs everything under `src/**/*.{test,spec}.{ts,tsx}` in jsdom. Config:
|
||||
`vitest.config.ts` mirrors the web build's alias graph (so `@/`, `@tauri-apps/*`
|
||||
→ host shims, `@hanzo_network/*` → src resolve), stubs the streamdown markdown
|
||||
stack (esbuild can't resolve its nested micromark), and loads
|
||||
`@testing-library/jest-dom` via `src/__tests__/setup.ts`. Pyodide /
|
||||
python-code-runner tests are excluded (30 MB wasm + network).
|
||||
|
||||
## What's covered
|
||||
|
||||
**Unit + regression** (`src/__tests__/`) — each guards a bug this stack actually hit:
|
||||
- `brand-store.test.ts` — `useBrand`/`getBrand` are plain getters callable
|
||||
outside render (the invalid-hook-call that blocked the whole web app).
|
||||
- `host-shims.test.ts` — `getCurrentWindow().emit` etc. exist; the injected host
|
||||
adapter switches web↔tauri (the "emit is not a function" mount crash).
|
||||
- `resizable.test.tsx` — `react-resizable-panels` v3 API present, net-ui
|
||||
resizable renders (the v4 "Element type is invalid" chat-view crash).
|
||||
- `no-missing-imports.test.ts` — critical screens import every `<Component>`
|
||||
they use (the dropped `Box`/`Boxes`/`Coins` lucide imports → "Box is not defined").
|
||||
|
||||
**App tests** (pre-existing, `src/app/**`) — engine client, machine-state queries,
|
||||
mining page, playground utils. **E2E** (`e2e/chat.e2e.test.ts`) — drives the real
|
||||
browser: onboarding renders, then connect → register → send → assert zen-coder's
|
||||
reply (and assert no "is not defined" / "Element type is invalid" page errors).
|
||||
|
||||
## TDD workflow (going forward)
|
||||
|
||||
Every fix and feature starts with a failing test:
|
||||
|
||||
1. **Red** — write a test that reproduces the bug or specifies the behavior, in
|
||||
`src/__tests__/` (unit) or `e2e/` (flow). Run `test:watch`; confirm it fails.
|
||||
2. **Green** — make the smallest change that passes it.
|
||||
3. **Refactor** — clean up with the test green.
|
||||
4. Run `bun run --cwd pkgs/ai test` before committing; keep it green.
|
||||
|
||||
Most bugs this session were import/dep/runtime issues that *static render checks
|
||||
missed but a test would have caught* — prefer a regression test over a manual
|
||||
re-check. For anything touching the chat path, add/extend the e2e.
|
||||
|
||||
The local runtime for `test:e2e` is in `pkgs/ai/local-runtime/` (see `CHAT.md`):
|
||||
restart `run-node.sh` (fresh storage) before a run so Quick Connect registers as
|
||||
the first device.
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+47
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+31
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,270 @@
|
||||
'use strict';
|
||||
|
||||
const index = require('./index-BSSKeTjn.cjs');
|
||||
|
||||
function arcInnerRadius(d) {
|
||||
return d.innerRadius;
|
||||
}
|
||||
|
||||
function arcOuterRadius(d) {
|
||||
return d.outerRadius;
|
||||
}
|
||||
|
||||
function arcStartAngle(d) {
|
||||
return d.startAngle;
|
||||
}
|
||||
|
||||
function arcEndAngle(d) {
|
||||
return d.endAngle;
|
||||
}
|
||||
|
||||
function arcPadAngle(d) {
|
||||
return d && d.padAngle; // Note: optional!
|
||||
}
|
||||
|
||||
function intersect(x0, y0, x1, y1, x2, y2, x3, y3) {
|
||||
var x10 = x1 - x0, y10 = y1 - y0,
|
||||
x32 = x3 - x2, y32 = y3 - y2,
|
||||
t = y32 * x10 - x32 * y10;
|
||||
if (t * t < index.epsilon) return;
|
||||
t = (x32 * (y0 - y2) - y32 * (x0 - x2)) / t;
|
||||
return [x0 + t * x10, y0 + t * y10];
|
||||
}
|
||||
|
||||
// Compute perpendicular offset line of length rc.
|
||||
// http://mathworld.wolfram.com/Circle-LineIntersection.html
|
||||
function cornerTangents(x0, y0, x1, y1, r1, rc, cw) {
|
||||
var x01 = x0 - x1,
|
||||
y01 = y0 - y1,
|
||||
lo = (cw ? rc : -rc) / index.sqrt(x01 * x01 + y01 * y01),
|
||||
ox = lo * y01,
|
||||
oy = -lo * x01,
|
||||
x11 = x0 + ox,
|
||||
y11 = y0 + oy,
|
||||
x10 = x1 + ox,
|
||||
y10 = y1 + oy,
|
||||
x00 = (x11 + x10) / 2,
|
||||
y00 = (y11 + y10) / 2,
|
||||
dx = x10 - x11,
|
||||
dy = y10 - y11,
|
||||
d2 = dx * dx + dy * dy,
|
||||
r = r1 - rc,
|
||||
D = x11 * y10 - x10 * y11,
|
||||
d = (dy < 0 ? -1 : 1) * index.sqrt(index.max(0, r * r * d2 - D * D)),
|
||||
cx0 = (D * dy - dx * d) / d2,
|
||||
cy0 = (-D * dx - dy * d) / d2,
|
||||
cx1 = (D * dy + dx * d) / d2,
|
||||
cy1 = (-D * dx + dy * d) / d2,
|
||||
dx0 = cx0 - x00,
|
||||
dy0 = cy0 - y00,
|
||||
dx1 = cx1 - x00,
|
||||
dy1 = cy1 - y00;
|
||||
|
||||
// Pick the closer of the two intersection points.
|
||||
// TODO Is there a faster way to determine which intersection to use?
|
||||
if (dx0 * dx0 + dy0 * dy0 > dx1 * dx1 + dy1 * dy1) cx0 = cx1, cy0 = cy1;
|
||||
|
||||
return {
|
||||
cx: cx0,
|
||||
cy: cy0,
|
||||
x01: -ox,
|
||||
y01: -oy,
|
||||
x11: cx0 * (r1 / r - 1),
|
||||
y11: cy0 * (r1 / r - 1)
|
||||
};
|
||||
}
|
||||
|
||||
function d3arc() {
|
||||
var innerRadius = arcInnerRadius,
|
||||
outerRadius = arcOuterRadius,
|
||||
cornerRadius = index.constant(0),
|
||||
padRadius = null,
|
||||
startAngle = arcStartAngle,
|
||||
endAngle = arcEndAngle,
|
||||
padAngle = arcPadAngle,
|
||||
context = null,
|
||||
path = index.withPath(arc);
|
||||
|
||||
function arc() {
|
||||
var buffer,
|
||||
r,
|
||||
r0 = +innerRadius.apply(this, arguments),
|
||||
r1 = +outerRadius.apply(this, arguments),
|
||||
a0 = startAngle.apply(this, arguments) - index.halfPi,
|
||||
a1 = endAngle.apply(this, arguments) - index.halfPi,
|
||||
da = index.abs(a1 - a0),
|
||||
cw = a1 > a0;
|
||||
|
||||
if (!context) context = buffer = path();
|
||||
|
||||
// Ensure that the outer radius is always larger than the inner radius.
|
||||
if (r1 < r0) r = r1, r1 = r0, r0 = r;
|
||||
|
||||
// Is it a point?
|
||||
if (!(r1 > index.epsilon)) context.moveTo(0, 0);
|
||||
|
||||
// Or is it a circle or annulus?
|
||||
else if (da > index.tau - index.epsilon) {
|
||||
context.moveTo(r1 * index.cos(a0), r1 * index.sin(a0));
|
||||
context.arc(0, 0, r1, a0, a1, !cw);
|
||||
if (r0 > index.epsilon) {
|
||||
context.moveTo(r0 * index.cos(a1), r0 * index.sin(a1));
|
||||
context.arc(0, 0, r0, a1, a0, cw);
|
||||
}
|
||||
}
|
||||
|
||||
// Or is it a circular or annular sector?
|
||||
else {
|
||||
var a01 = a0,
|
||||
a11 = a1,
|
||||
a00 = a0,
|
||||
a10 = a1,
|
||||
da0 = da,
|
||||
da1 = da,
|
||||
ap = padAngle.apply(this, arguments) / 2,
|
||||
rp = (ap > index.epsilon) && (padRadius ? +padRadius.apply(this, arguments) : index.sqrt(r0 * r0 + r1 * r1)),
|
||||
rc = index.min(index.abs(r1 - r0) / 2, +cornerRadius.apply(this, arguments)),
|
||||
rc0 = rc,
|
||||
rc1 = rc,
|
||||
t0,
|
||||
t1;
|
||||
|
||||
// Apply padding? Note that since r1 ≥ r0, da1 ≥ da0.
|
||||
if (rp > index.epsilon) {
|
||||
var p0 = index.asin(rp / r0 * index.sin(ap)),
|
||||
p1 = index.asin(rp / r1 * index.sin(ap));
|
||||
if ((da0 -= p0 * 2) > index.epsilon) p0 *= (cw ? 1 : -1), a00 += p0, a10 -= p0;
|
||||
else da0 = 0, a00 = a10 = (a0 + a1) / 2;
|
||||
if ((da1 -= p1 * 2) > index.epsilon) p1 *= (cw ? 1 : -1), a01 += p1, a11 -= p1;
|
||||
else da1 = 0, a01 = a11 = (a0 + a1) / 2;
|
||||
}
|
||||
|
||||
var x01 = r1 * index.cos(a01),
|
||||
y01 = r1 * index.sin(a01),
|
||||
x10 = r0 * index.cos(a10),
|
||||
y10 = r0 * index.sin(a10);
|
||||
|
||||
// Apply rounded corners?
|
||||
if (rc > index.epsilon) {
|
||||
var x11 = r1 * index.cos(a11),
|
||||
y11 = r1 * index.sin(a11),
|
||||
x00 = r0 * index.cos(a00),
|
||||
y00 = r0 * index.sin(a00),
|
||||
oc;
|
||||
|
||||
// Restrict the corner radius according to the sector angle. If this
|
||||
// intersection fails, it’s probably because the arc is too small, so
|
||||
// disable the corner radius entirely.
|
||||
if (da < index.pi) {
|
||||
if (oc = intersect(x01, y01, x00, y00, x11, y11, x10, y10)) {
|
||||
var ax = x01 - oc[0],
|
||||
ay = y01 - oc[1],
|
||||
bx = x11 - oc[0],
|
||||
by = y11 - oc[1],
|
||||
kc = 1 / index.sin(index.acos((ax * bx + ay * by) / (index.sqrt(ax * ax + ay * ay) * index.sqrt(bx * bx + by * by))) / 2),
|
||||
lc = index.sqrt(oc[0] * oc[0] + oc[1] * oc[1]);
|
||||
rc0 = index.min(rc, (r0 - lc) / (kc - 1));
|
||||
rc1 = index.min(rc, (r1 - lc) / (kc + 1));
|
||||
} else {
|
||||
rc0 = rc1 = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Is the sector collapsed to a line?
|
||||
if (!(da1 > index.epsilon)) context.moveTo(x01, y01);
|
||||
|
||||
// Does the sector’s outer ring have rounded corners?
|
||||
else if (rc1 > index.epsilon) {
|
||||
t0 = cornerTangents(x00, y00, x01, y01, r1, rc1, cw);
|
||||
t1 = cornerTangents(x11, y11, x10, y10, r1, rc1, cw);
|
||||
|
||||
context.moveTo(t0.cx + t0.x01, t0.cy + t0.y01);
|
||||
|
||||
// Have the corners merged?
|
||||
if (rc1 < rc) context.arc(t0.cx, t0.cy, rc1, index.atan2(t0.y01, t0.x01), index.atan2(t1.y01, t1.x01), !cw);
|
||||
|
||||
// Otherwise, draw the two corners and the ring.
|
||||
else {
|
||||
context.arc(t0.cx, t0.cy, rc1, index.atan2(t0.y01, t0.x01), index.atan2(t0.y11, t0.x11), !cw);
|
||||
context.arc(0, 0, r1, index.atan2(t0.cy + t0.y11, t0.cx + t0.x11), index.atan2(t1.cy + t1.y11, t1.cx + t1.x11), !cw);
|
||||
context.arc(t1.cx, t1.cy, rc1, index.atan2(t1.y11, t1.x11), index.atan2(t1.y01, t1.x01), !cw);
|
||||
}
|
||||
}
|
||||
|
||||
// Or is the outer ring just a circular arc?
|
||||
else context.moveTo(x01, y01), context.arc(0, 0, r1, a01, a11, !cw);
|
||||
|
||||
// Is there no inner ring, and it’s a circular sector?
|
||||
// Or perhaps it’s an annular sector collapsed due to padding?
|
||||
if (!(r0 > index.epsilon) || !(da0 > index.epsilon)) context.lineTo(x10, y10);
|
||||
|
||||
// Does the sector’s inner ring (or point) have rounded corners?
|
||||
else if (rc0 > index.epsilon) {
|
||||
t0 = cornerTangents(x10, y10, x11, y11, r0, -rc0, cw);
|
||||
t1 = cornerTangents(x01, y01, x00, y00, r0, -rc0, cw);
|
||||
|
||||
context.lineTo(t0.cx + t0.x01, t0.cy + t0.y01);
|
||||
|
||||
// Have the corners merged?
|
||||
if (rc0 < rc) context.arc(t0.cx, t0.cy, rc0, index.atan2(t0.y01, t0.x01), index.atan2(t1.y01, t1.x01), !cw);
|
||||
|
||||
// Otherwise, draw the two corners and the ring.
|
||||
else {
|
||||
context.arc(t0.cx, t0.cy, rc0, index.atan2(t0.y01, t0.x01), index.atan2(t0.y11, t0.x11), !cw);
|
||||
context.arc(0, 0, r0, index.atan2(t0.cy + t0.y11, t0.cx + t0.x11), index.atan2(t1.cy + t1.y11, t1.cx + t1.x11), cw);
|
||||
context.arc(t1.cx, t1.cy, rc0, index.atan2(t1.y11, t1.x11), index.atan2(t1.y01, t1.x01), !cw);
|
||||
}
|
||||
}
|
||||
|
||||
// Or is the inner ring just a circular arc?
|
||||
else context.arc(0, 0, r0, a10, a00, cw);
|
||||
}
|
||||
|
||||
context.closePath();
|
||||
|
||||
if (buffer) return context = null, buffer + "" || null;
|
||||
}
|
||||
|
||||
arc.centroid = function() {
|
||||
var r = (+innerRadius.apply(this, arguments) + +outerRadius.apply(this, arguments)) / 2,
|
||||
a = (+startAngle.apply(this, arguments) + +endAngle.apply(this, arguments)) / 2 - index.pi / 2;
|
||||
return [index.cos(a) * r, index.sin(a) * r];
|
||||
};
|
||||
|
||||
arc.innerRadius = function(_) {
|
||||
return arguments.length ? (innerRadius = typeof _ === "function" ? _ : index.constant(+_), arc) : innerRadius;
|
||||
};
|
||||
|
||||
arc.outerRadius = function(_) {
|
||||
return arguments.length ? (outerRadius = typeof _ === "function" ? _ : index.constant(+_), arc) : outerRadius;
|
||||
};
|
||||
|
||||
arc.cornerRadius = function(_) {
|
||||
return arguments.length ? (cornerRadius = typeof _ === "function" ? _ : index.constant(+_), arc) : cornerRadius;
|
||||
};
|
||||
|
||||
arc.padRadius = function(_) {
|
||||
return arguments.length ? (padRadius = _ == null ? null : typeof _ === "function" ? _ : index.constant(+_), arc) : padRadius;
|
||||
};
|
||||
|
||||
arc.startAngle = function(_) {
|
||||
return arguments.length ? (startAngle = typeof _ === "function" ? _ : index.constant(+_), arc) : startAngle;
|
||||
};
|
||||
|
||||
arc.endAngle = function(_) {
|
||||
return arguments.length ? (endAngle = typeof _ === "function" ? _ : index.constant(+_), arc) : endAngle;
|
||||
};
|
||||
|
||||
arc.padAngle = function(_) {
|
||||
return arguments.length ? (padAngle = typeof _ === "function" ? _ : index.constant(+_), arc) : padAngle;
|
||||
};
|
||||
|
||||
arc.context = function(_) {
|
||||
return arguments.length ? ((context = _ == null ? null : _), arc) : context;
|
||||
};
|
||||
|
||||
return arc;
|
||||
}
|
||||
|
||||
exports.d3arc = d3arc;
|
||||
@@ -0,0 +1,268 @@
|
||||
import { P as withPath, Q as pi, S as cos, T as sin, V as constant, W as halfPi, X as epsilon, Y as tau, Z as sqrt, $ as min, a0 as abs, a1 as atan2, a2 as max, a3 as asin, a4 as acos } from './index-BnaE_tRw.js';
|
||||
|
||||
function arcInnerRadius(d) {
|
||||
return d.innerRadius;
|
||||
}
|
||||
|
||||
function arcOuterRadius(d) {
|
||||
return d.outerRadius;
|
||||
}
|
||||
|
||||
function arcStartAngle(d) {
|
||||
return d.startAngle;
|
||||
}
|
||||
|
||||
function arcEndAngle(d) {
|
||||
return d.endAngle;
|
||||
}
|
||||
|
||||
function arcPadAngle(d) {
|
||||
return d && d.padAngle; // Note: optional!
|
||||
}
|
||||
|
||||
function intersect(x0, y0, x1, y1, x2, y2, x3, y3) {
|
||||
var x10 = x1 - x0, y10 = y1 - y0,
|
||||
x32 = x3 - x2, y32 = y3 - y2,
|
||||
t = y32 * x10 - x32 * y10;
|
||||
if (t * t < epsilon) return;
|
||||
t = (x32 * (y0 - y2) - y32 * (x0 - x2)) / t;
|
||||
return [x0 + t * x10, y0 + t * y10];
|
||||
}
|
||||
|
||||
// Compute perpendicular offset line of length rc.
|
||||
// http://mathworld.wolfram.com/Circle-LineIntersection.html
|
||||
function cornerTangents(x0, y0, x1, y1, r1, rc, cw) {
|
||||
var x01 = x0 - x1,
|
||||
y01 = y0 - y1,
|
||||
lo = (cw ? rc : -rc) / sqrt(x01 * x01 + y01 * y01),
|
||||
ox = lo * y01,
|
||||
oy = -lo * x01,
|
||||
x11 = x0 + ox,
|
||||
y11 = y0 + oy,
|
||||
x10 = x1 + ox,
|
||||
y10 = y1 + oy,
|
||||
x00 = (x11 + x10) / 2,
|
||||
y00 = (y11 + y10) / 2,
|
||||
dx = x10 - x11,
|
||||
dy = y10 - y11,
|
||||
d2 = dx * dx + dy * dy,
|
||||
r = r1 - rc,
|
||||
D = x11 * y10 - x10 * y11,
|
||||
d = (dy < 0 ? -1 : 1) * sqrt(max(0, r * r * d2 - D * D)),
|
||||
cx0 = (D * dy - dx * d) / d2,
|
||||
cy0 = (-D * dx - dy * d) / d2,
|
||||
cx1 = (D * dy + dx * d) / d2,
|
||||
cy1 = (-D * dx + dy * d) / d2,
|
||||
dx0 = cx0 - x00,
|
||||
dy0 = cy0 - y00,
|
||||
dx1 = cx1 - x00,
|
||||
dy1 = cy1 - y00;
|
||||
|
||||
// Pick the closer of the two intersection points.
|
||||
// TODO Is there a faster way to determine which intersection to use?
|
||||
if (dx0 * dx0 + dy0 * dy0 > dx1 * dx1 + dy1 * dy1) cx0 = cx1, cy0 = cy1;
|
||||
|
||||
return {
|
||||
cx: cx0,
|
||||
cy: cy0,
|
||||
x01: -ox,
|
||||
y01: -oy,
|
||||
x11: cx0 * (r1 / r - 1),
|
||||
y11: cy0 * (r1 / r - 1)
|
||||
};
|
||||
}
|
||||
|
||||
function d3arc() {
|
||||
var innerRadius = arcInnerRadius,
|
||||
outerRadius = arcOuterRadius,
|
||||
cornerRadius = constant(0),
|
||||
padRadius = null,
|
||||
startAngle = arcStartAngle,
|
||||
endAngle = arcEndAngle,
|
||||
padAngle = arcPadAngle,
|
||||
context = null,
|
||||
path = withPath(arc);
|
||||
|
||||
function arc() {
|
||||
var buffer,
|
||||
r,
|
||||
r0 = +innerRadius.apply(this, arguments),
|
||||
r1 = +outerRadius.apply(this, arguments),
|
||||
a0 = startAngle.apply(this, arguments) - halfPi,
|
||||
a1 = endAngle.apply(this, arguments) - halfPi,
|
||||
da = abs(a1 - a0),
|
||||
cw = a1 > a0;
|
||||
|
||||
if (!context) context = buffer = path();
|
||||
|
||||
// Ensure that the outer radius is always larger than the inner radius.
|
||||
if (r1 < r0) r = r1, r1 = r0, r0 = r;
|
||||
|
||||
// Is it a point?
|
||||
if (!(r1 > epsilon)) context.moveTo(0, 0);
|
||||
|
||||
// Or is it a circle or annulus?
|
||||
else if (da > tau - epsilon) {
|
||||
context.moveTo(r1 * cos(a0), r1 * sin(a0));
|
||||
context.arc(0, 0, r1, a0, a1, !cw);
|
||||
if (r0 > epsilon) {
|
||||
context.moveTo(r0 * cos(a1), r0 * sin(a1));
|
||||
context.arc(0, 0, r0, a1, a0, cw);
|
||||
}
|
||||
}
|
||||
|
||||
// Or is it a circular or annular sector?
|
||||
else {
|
||||
var a01 = a0,
|
||||
a11 = a1,
|
||||
a00 = a0,
|
||||
a10 = a1,
|
||||
da0 = da,
|
||||
da1 = da,
|
||||
ap = padAngle.apply(this, arguments) / 2,
|
||||
rp = (ap > epsilon) && (padRadius ? +padRadius.apply(this, arguments) : sqrt(r0 * r0 + r1 * r1)),
|
||||
rc = min(abs(r1 - r0) / 2, +cornerRadius.apply(this, arguments)),
|
||||
rc0 = rc,
|
||||
rc1 = rc,
|
||||
t0,
|
||||
t1;
|
||||
|
||||
// Apply padding? Note that since r1 ≥ r0, da1 ≥ da0.
|
||||
if (rp > epsilon) {
|
||||
var p0 = asin(rp / r0 * sin(ap)),
|
||||
p1 = asin(rp / r1 * sin(ap));
|
||||
if ((da0 -= p0 * 2) > epsilon) p0 *= (cw ? 1 : -1), a00 += p0, a10 -= p0;
|
||||
else da0 = 0, a00 = a10 = (a0 + a1) / 2;
|
||||
if ((da1 -= p1 * 2) > epsilon) p1 *= (cw ? 1 : -1), a01 += p1, a11 -= p1;
|
||||
else da1 = 0, a01 = a11 = (a0 + a1) / 2;
|
||||
}
|
||||
|
||||
var x01 = r1 * cos(a01),
|
||||
y01 = r1 * sin(a01),
|
||||
x10 = r0 * cos(a10),
|
||||
y10 = r0 * sin(a10);
|
||||
|
||||
// Apply rounded corners?
|
||||
if (rc > epsilon) {
|
||||
var x11 = r1 * cos(a11),
|
||||
y11 = r1 * sin(a11),
|
||||
x00 = r0 * cos(a00),
|
||||
y00 = r0 * sin(a00),
|
||||
oc;
|
||||
|
||||
// Restrict the corner radius according to the sector angle. If this
|
||||
// intersection fails, it’s probably because the arc is too small, so
|
||||
// disable the corner radius entirely.
|
||||
if (da < pi) {
|
||||
if (oc = intersect(x01, y01, x00, y00, x11, y11, x10, y10)) {
|
||||
var ax = x01 - oc[0],
|
||||
ay = y01 - oc[1],
|
||||
bx = x11 - oc[0],
|
||||
by = y11 - oc[1],
|
||||
kc = 1 / sin(acos((ax * bx + ay * by) / (sqrt(ax * ax + ay * ay) * sqrt(bx * bx + by * by))) / 2),
|
||||
lc = sqrt(oc[0] * oc[0] + oc[1] * oc[1]);
|
||||
rc0 = min(rc, (r0 - lc) / (kc - 1));
|
||||
rc1 = min(rc, (r1 - lc) / (kc + 1));
|
||||
} else {
|
||||
rc0 = rc1 = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Is the sector collapsed to a line?
|
||||
if (!(da1 > epsilon)) context.moveTo(x01, y01);
|
||||
|
||||
// Does the sector’s outer ring have rounded corners?
|
||||
else if (rc1 > epsilon) {
|
||||
t0 = cornerTangents(x00, y00, x01, y01, r1, rc1, cw);
|
||||
t1 = cornerTangents(x11, y11, x10, y10, r1, rc1, cw);
|
||||
|
||||
context.moveTo(t0.cx + t0.x01, t0.cy + t0.y01);
|
||||
|
||||
// Have the corners merged?
|
||||
if (rc1 < rc) context.arc(t0.cx, t0.cy, rc1, atan2(t0.y01, t0.x01), atan2(t1.y01, t1.x01), !cw);
|
||||
|
||||
// Otherwise, draw the two corners and the ring.
|
||||
else {
|
||||
context.arc(t0.cx, t0.cy, rc1, atan2(t0.y01, t0.x01), atan2(t0.y11, t0.x11), !cw);
|
||||
context.arc(0, 0, r1, atan2(t0.cy + t0.y11, t0.cx + t0.x11), atan2(t1.cy + t1.y11, t1.cx + t1.x11), !cw);
|
||||
context.arc(t1.cx, t1.cy, rc1, atan2(t1.y11, t1.x11), atan2(t1.y01, t1.x01), !cw);
|
||||
}
|
||||
}
|
||||
|
||||
// Or is the outer ring just a circular arc?
|
||||
else context.moveTo(x01, y01), context.arc(0, 0, r1, a01, a11, !cw);
|
||||
|
||||
// Is there no inner ring, and it’s a circular sector?
|
||||
// Or perhaps it’s an annular sector collapsed due to padding?
|
||||
if (!(r0 > epsilon) || !(da0 > epsilon)) context.lineTo(x10, y10);
|
||||
|
||||
// Does the sector’s inner ring (or point) have rounded corners?
|
||||
else if (rc0 > epsilon) {
|
||||
t0 = cornerTangents(x10, y10, x11, y11, r0, -rc0, cw);
|
||||
t1 = cornerTangents(x01, y01, x00, y00, r0, -rc0, cw);
|
||||
|
||||
context.lineTo(t0.cx + t0.x01, t0.cy + t0.y01);
|
||||
|
||||
// Have the corners merged?
|
||||
if (rc0 < rc) context.arc(t0.cx, t0.cy, rc0, atan2(t0.y01, t0.x01), atan2(t1.y01, t1.x01), !cw);
|
||||
|
||||
// Otherwise, draw the two corners and the ring.
|
||||
else {
|
||||
context.arc(t0.cx, t0.cy, rc0, atan2(t0.y01, t0.x01), atan2(t0.y11, t0.x11), !cw);
|
||||
context.arc(0, 0, r0, atan2(t0.cy + t0.y11, t0.cx + t0.x11), atan2(t1.cy + t1.y11, t1.cx + t1.x11), cw);
|
||||
context.arc(t1.cx, t1.cy, rc0, atan2(t1.y11, t1.x11), atan2(t1.y01, t1.x01), !cw);
|
||||
}
|
||||
}
|
||||
|
||||
// Or is the inner ring just a circular arc?
|
||||
else context.arc(0, 0, r0, a10, a00, cw);
|
||||
}
|
||||
|
||||
context.closePath();
|
||||
|
||||
if (buffer) return context = null, buffer + "" || null;
|
||||
}
|
||||
|
||||
arc.centroid = function() {
|
||||
var r = (+innerRadius.apply(this, arguments) + +outerRadius.apply(this, arguments)) / 2,
|
||||
a = (+startAngle.apply(this, arguments) + +endAngle.apply(this, arguments)) / 2 - pi / 2;
|
||||
return [cos(a) * r, sin(a) * r];
|
||||
};
|
||||
|
||||
arc.innerRadius = function(_) {
|
||||
return arguments.length ? (innerRadius = typeof _ === "function" ? _ : constant(+_), arc) : innerRadius;
|
||||
};
|
||||
|
||||
arc.outerRadius = function(_) {
|
||||
return arguments.length ? (outerRadius = typeof _ === "function" ? _ : constant(+_), arc) : outerRadius;
|
||||
};
|
||||
|
||||
arc.cornerRadius = function(_) {
|
||||
return arguments.length ? (cornerRadius = typeof _ === "function" ? _ : constant(+_), arc) : cornerRadius;
|
||||
};
|
||||
|
||||
arc.padRadius = function(_) {
|
||||
return arguments.length ? (padRadius = _ == null ? null : typeof _ === "function" ? _ : constant(+_), arc) : padRadius;
|
||||
};
|
||||
|
||||
arc.startAngle = function(_) {
|
||||
return arguments.length ? (startAngle = typeof _ === "function" ? _ : constant(+_), arc) : startAngle;
|
||||
};
|
||||
|
||||
arc.endAngle = function(_) {
|
||||
return arguments.length ? (endAngle = typeof _ === "function" ? _ : constant(+_), arc) : endAngle;
|
||||
};
|
||||
|
||||
arc.padAngle = function(_) {
|
||||
return arguments.length ? (padAngle = typeof _ === "function" ? _ : constant(+_), arc) : padAngle;
|
||||
};
|
||||
|
||||
arc.context = function(_) {
|
||||
return arguments.length ? ((context = _ == null ? null : _), arc) : context;
|
||||
};
|
||||
|
||||
return arc;
|
||||
}
|
||||
|
||||
export { d3arc as d };
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
|
||||
var __viteBrowserExternal = {};
|
||||
|
||||
export { __viteBrowserExternal as default };
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,7 @@
|
||||
const lang = Object.freeze(JSON.parse("{\"displayName\":\"Berry\",\"name\":\"berry\",\"patterns\":[{\"include\":\"#controls\"},{\"include\":\"#strings\"},{\"include\":\"#comment-block\"},{\"include\":\"#comments\"},{\"include\":\"#keywords\"},{\"include\":\"#function\"},{\"include\":\"#member\"},{\"include\":\"#identifier\"},{\"include\":\"#number\"},{\"include\":\"#operator\"}],\"repository\":{\"comment-block\":{\"begin\":\"#-\",\"end\":\"-#\",\"name\":\"comment.berry\",\"patterns\":[{}]},\"comments\":{\"begin\":\"#\",\"end\":\"\\\\n\",\"name\":\"comment.line.berry\",\"patterns\":[{}]},\"controls\":{\"patterns\":[{\"match\":\"\\\\b(if|elif|else|for|while|do|end|break|continue|return|try|except|raise)\\\\b\",\"name\":\"keyword.control.berry\"}]},\"function\":{\"patterns\":[{\"match\":\"\\\\b([A-Z_a-z][0-9A-Z_a-z]*(?=\\\\s*\\\\())\",\"name\":\"entity.name.function.berry\"}]},\"identifier\":{\"patterns\":[{\"match\":\"\\\\b[A-Z_a-z]\\\\w+\\\\b\",\"name\":\"identifier.berry\"}]},\"keywords\":{\"patterns\":[{\"match\":\"\\\\b(var|static|def|class|true|false|nil|self|super|import|as|_class)\\\\b\",\"name\":\"keyword.berry\"}]},\"member\":{\"patterns\":[{\"captures\":{\"0\":{\"name\":\"entity.other.attribute-name.berry\"}},\"match\":\"\\\\.([A-Z_a-z][0-9A-Z_a-z]*)\"}]},\"number\":{\"patterns\":[{\"match\":\"0x\\\\h+|\\\\d+|(\\\\d+\\\\.?|\\\\.\\\\d)\\\\d*([Ee][-+]?\\\\d+)?\",\"name\":\"constant.numeric.berry\"}]},\"operator\":{\"patterns\":[{\"match\":\"[-\\\\]!%\\\\&(-+./:<=>\\\\[^|~]\",\"name\":\"keyword.operator.berry\"}]},\"strings\":{\"patterns\":[{\"begin\":\"f(?=[\\\"'])\",\"patterns\":[{\"begin\":\"\\\"\",\"end\":\"\\\"\",\"name\":\"string.quoted.other.berry\",\"patterns\":[{\"match\":\"(\\\\\\\\x\\\\h{2})|(\\\\\\\\[0-7]{3})|(\\\\\\\\\\\\\\\\)|(\\\\\\\\\\\")|(\\\\\\\\')|(\\\\\\\\a)|(\\\\\\\\b)|(\\\\\\\\f)|(\\\\\\\\n)|(\\\\\\\\r)|(\\\\\\\\t)|(\\\\\\\\v)\",\"name\":\"constant.character.escape.berry\"},{\"match\":\"\\\\{\\\\{[^}]*}}\",\"name\":\"string.quoted.other.berry\"},{\"begin\":\"\\\\{\",\"end\":\"}\",\"name\":\"keyword.other.unit.berry\",\"patterns\":[{\"include\":\"#keywords\"},{\"include\":\"#numbers\"},{\"include\":\"#identifier\"},{\"include\":\"#operator\"},{\"include\":\"#member\"},{\"include\":\"#function\"}]}]},{\"begin\":\"'\",\"end\":\"'\",\"name\":\"string.quoted.other.berry\",\"patterns\":[{\"match\":\"(\\\\\\\\x\\\\h{2})|(\\\\\\\\[0-7]{3})|(\\\\\\\\\\\\\\\\)|(\\\\\\\\\\\")|(\\\\\\\\')|(\\\\\\\\a)|(\\\\\\\\b)|(\\\\\\\\f)|(\\\\\\\\n)|(\\\\\\\\r)|(\\\\\\\\t)|(\\\\\\\\v)\",\"name\":\"constant.character.escape.berry\"},{\"match\":\"\\\\{\\\\{[^}]*}}\",\"name\":\"string.quoted.other.berry\"},{\"begin\":\"\\\\{\",\"end\":\"}\",\"name\":\"keyword.other.unit.berry\",\"patterns\":[{\"include\":\"#keywords\"},{\"include\":\"#numbers\"},{\"include\":\"#identifier\"},{\"include\":\"#operator\"},{\"include\":\"#member\"},{\"include\":\"#function\"}]}]}],\"while\":\"\\\\G|^[\\\\t ]*(?=[\\\"'])\"},{\"begin\":\"([\\\"'])\",\"end\":\"\\\\1\",\"name\":\"string.quoted.double.berry\",\"patterns\":[{\"match\":\"(\\\\\\\\x\\\\h{2})|(\\\\\\\\[0-7]{3})|(\\\\\\\\\\\\\\\\)|(\\\\\\\\\\\")|(\\\\\\\\')|(\\\\\\\\a)|(\\\\\\\\b)|(\\\\\\\\f)|(\\\\\\\\n)|(\\\\\\\\r)|(\\\\\\\\t)|(\\\\\\\\v)\",\"name\":\"constant.character.escape.berry\"}]}]}},\"scopeName\":\"source.berry\",\"aliases\":[\"be\"]}"));
|
||||
|
||||
const berry = [
|
||||
lang
|
||||
];
|
||||
|
||||
export { berry as default };
|
||||
@@ -0,0 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
||||
|
||||
const lang = Object.freeze(JSON.parse("{\"displayName\":\"Berry\",\"name\":\"berry\",\"patterns\":[{\"include\":\"#controls\"},{\"include\":\"#strings\"},{\"include\":\"#comment-block\"},{\"include\":\"#comments\"},{\"include\":\"#keywords\"},{\"include\":\"#function\"},{\"include\":\"#member\"},{\"include\":\"#identifier\"},{\"include\":\"#number\"},{\"include\":\"#operator\"}],\"repository\":{\"comment-block\":{\"begin\":\"#-\",\"end\":\"-#\",\"name\":\"comment.berry\",\"patterns\":[{}]},\"comments\":{\"begin\":\"#\",\"end\":\"\\\\n\",\"name\":\"comment.line.berry\",\"patterns\":[{}]},\"controls\":{\"patterns\":[{\"match\":\"\\\\b(if|elif|else|for|while|do|end|break|continue|return|try|except|raise)\\\\b\",\"name\":\"keyword.control.berry\"}]},\"function\":{\"patterns\":[{\"match\":\"\\\\b([A-Z_a-z][0-9A-Z_a-z]*(?=\\\\s*\\\\())\",\"name\":\"entity.name.function.berry\"}]},\"identifier\":{\"patterns\":[{\"match\":\"\\\\b[A-Z_a-z]\\\\w+\\\\b\",\"name\":\"identifier.berry\"}]},\"keywords\":{\"patterns\":[{\"match\":\"\\\\b(var|static|def|class|true|false|nil|self|super|import|as|_class)\\\\b\",\"name\":\"keyword.berry\"}]},\"member\":{\"patterns\":[{\"captures\":{\"0\":{\"name\":\"entity.other.attribute-name.berry\"}},\"match\":\"\\\\.([A-Z_a-z][0-9A-Z_a-z]*)\"}]},\"number\":{\"patterns\":[{\"match\":\"0x\\\\h+|\\\\d+|(\\\\d+\\\\.?|\\\\.\\\\d)\\\\d*([Ee][-+]?\\\\d+)?\",\"name\":\"constant.numeric.berry\"}]},\"operator\":{\"patterns\":[{\"match\":\"[-\\\\]!%\\\\&(-+./:<=>\\\\[^|~]\",\"name\":\"keyword.operator.berry\"}]},\"strings\":{\"patterns\":[{\"begin\":\"f(?=[\\\"'])\",\"patterns\":[{\"begin\":\"\\\"\",\"end\":\"\\\"\",\"name\":\"string.quoted.other.berry\",\"patterns\":[{\"match\":\"(\\\\\\\\x\\\\h{2})|(\\\\\\\\[0-7]{3})|(\\\\\\\\\\\\\\\\)|(\\\\\\\\\\\")|(\\\\\\\\')|(\\\\\\\\a)|(\\\\\\\\b)|(\\\\\\\\f)|(\\\\\\\\n)|(\\\\\\\\r)|(\\\\\\\\t)|(\\\\\\\\v)\",\"name\":\"constant.character.escape.berry\"},{\"match\":\"\\\\{\\\\{[^}]*}}\",\"name\":\"string.quoted.other.berry\"},{\"begin\":\"\\\\{\",\"end\":\"}\",\"name\":\"keyword.other.unit.berry\",\"patterns\":[{\"include\":\"#keywords\"},{\"include\":\"#numbers\"},{\"include\":\"#identifier\"},{\"include\":\"#operator\"},{\"include\":\"#member\"},{\"include\":\"#function\"}]}]},{\"begin\":\"'\",\"end\":\"'\",\"name\":\"string.quoted.other.berry\",\"patterns\":[{\"match\":\"(\\\\\\\\x\\\\h{2})|(\\\\\\\\[0-7]{3})|(\\\\\\\\\\\\\\\\)|(\\\\\\\\\\\")|(\\\\\\\\')|(\\\\\\\\a)|(\\\\\\\\b)|(\\\\\\\\f)|(\\\\\\\\n)|(\\\\\\\\r)|(\\\\\\\\t)|(\\\\\\\\v)\",\"name\":\"constant.character.escape.berry\"},{\"match\":\"\\\\{\\\\{[^}]*}}\",\"name\":\"string.quoted.other.berry\"},{\"begin\":\"\\\\{\",\"end\":\"}\",\"name\":\"keyword.other.unit.berry\",\"patterns\":[{\"include\":\"#keywords\"},{\"include\":\"#numbers\"},{\"include\":\"#identifier\"},{\"include\":\"#operator\"},{\"include\":\"#member\"},{\"include\":\"#function\"}]}]}],\"while\":\"\\\\G|^[\\\\t ]*(?=[\\\"'])\"},{\"begin\":\"([\\\"'])\",\"end\":\"\\\\1\",\"name\":\"string.quoted.double.berry\",\"patterns\":[{\"match\":\"(\\\\\\\\x\\\\h{2})|(\\\\\\\\[0-7]{3})|(\\\\\\\\\\\\\\\\)|(\\\\\\\\\\\")|(\\\\\\\\')|(\\\\\\\\a)|(\\\\\\\\b)|(\\\\\\\\f)|(\\\\\\\\n)|(\\\\\\\\r)|(\\\\\\\\t)|(\\\\\\\\v)\",\"name\":\"constant.character.escape.berry\"}]}]}},\"scopeName\":\"source.berry\",\"aliases\":[\"be\"]}"));
|
||||
|
||||
const berry = [
|
||||
lang
|
||||
];
|
||||
|
||||
exports.default = berry;
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,14 @@
|
||||
'use strict';
|
||||
|
||||
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
||||
|
||||
const python = require('./python-R5Doq6Ap.cjs');
|
||||
|
||||
const lang = Object.freeze(JSON.parse("{\"displayName\":\"Cairo\",\"name\":\"cairo\",\"patterns\":[{\"begin\":\"\\\\b(if).*\\\\(\",\"beginCaptures\":{\"1\":{\"name\":\"keyword.control.if\"},\"2\":{\"name\":\"entity.name.condition\"}},\"contentName\":\"source.cairo0\",\"end\":\"}\",\"endCaptures\":{\"0\":{\"name\":\"keyword.control.end\"}},\"name\":\"meta.control.if\",\"patterns\":[{\"include\":\"source.cairo0\"}]},{\"begin\":\"\\\\b(with)\\\\s+(.+)\\\\s*\\\\{\",\"beginCaptures\":{\"1\":{\"name\":\"keyword.control.with\"},\"2\":{\"name\":\"entity.name.identifiers\"}},\"contentName\":\"source.cairo0\",\"end\":\"}\",\"endCaptures\":{\"0\":{\"name\":\"keyword.control.end\"}},\"name\":\"meta.control.with\",\"patterns\":[{\"include\":\"source.cairo0\"}]},{\"begin\":\"\\\\b(with_attr)\\\\s+([A-Z_a-z][0-9A-Z_a-z]*)\\\\s*[({]\",\"beginCaptures\":{\"1\":{\"name\":\"keyword.control.with_attr\"},\"2\":{\"name\":\"entity.name.function\"}},\"contentName\":\"source.cairo0\",\"end\":\"}\",\"endCaptures\":{\"0\":{\"name\":\"keyword.control.end\"}},\"name\":\"meta.control.with_attr\",\"patterns\":[{\"include\":\"source.cairo0\"}]},{\"match\":\"\\\\belse\\\\b\",\"name\":\"keyword.control.else\"},{\"match\":\"\\\\b(call|jmp|ret|abs|rel|if)\\\\b\",\"name\":\"keyword.other.opcode\"},{\"match\":\"\\\\b([af]p)\\\\b\",\"name\":\"keyword.other.register\"},{\"match\":\"\\\\b(const|let|local|tempvar|felt|as|from|import|static_assert|return|assert|cast|alloc_locals|with|with_attr|nondet|dw|codeoffset|new|using|and)\\\\b\",\"name\":\"keyword.other.meta\"},{\"match\":\"\\\\b(SIZE(?:OF_LOCALS|))\\\\b\",\"name\":\"markup.italic\"},{\"match\":\"//[^\\\\n]*\\\\n\",\"name\":\"comment.line.sharp\"},{\"match\":\"\\\\b[A-Z_a-z][0-9A-Z_a-z]*:\\\\s*$\",\"name\":\"entity.name.function\"},{\"begin\":\"\\\\b(func)\\\\s+([A-Z_a-z][0-9A-Z_a-z]*)\\\\s*[({]\",\"beginCaptures\":{\"1\":{\"name\":\"storage.type.function.cairo\"},\"2\":{\"name\":\"entity.name.function\"}},\"contentName\":\"source.cairo0\",\"end\":\"}\",\"endCaptures\":{\"0\":{\"name\":\"storage.type.function.cairo\"}},\"name\":\"meta.function.cairo\",\"patterns\":[{\"include\":\"source.cairo0\"}]},{\"begin\":\"\\\\b(struct|namespace)\\\\s+([A-Z_a-z][0-9A-Z_a-z]*)\\\\s*\\\\{\",\"beginCaptures\":{\"1\":{\"name\":\"storage.type.function.cairo\"},\"2\":{\"name\":\"entity.name.function\"}},\"contentName\":\"source.cairo0\",\"end\":\"}\",\"endCaptures\":{\"0\":{\"name\":\"storage.type.function.cairo\"}},\"name\":\"meta.function.cairo\",\"patterns\":[{\"include\":\"source.cairo0\"}]},{\"match\":\"\\\\b[-+]?[0-9]+\\\\b\",\"name\":\"constant.numeric.decimal\"},{\"match\":\"\\\\b[-+]?0x\\\\h+\\\\b\",\"name\":\"constant.numeric.hexadecimal\"},{\"match\":\"'[^']*'\",\"name\":\"string.quoted.single\"},{\"match\":\"\\\"[^\\\"]*\\\"\",\"name\":\"string.quoted.double\"},{\"begin\":\"%\\\\{\",\"beginCaptures\":{\"0\":{\"name\":\"punctuation.section.embedded.begin.python\"}},\"contentName\":\"source.python\",\"end\":\"%}\",\"endCaptures\":{\"0\":{\"name\":\"punctuation.section.embedded.end.python\"},\"1\":{\"name\":\"source.python\"}},\"name\":\"meta.embedded.block.python\",\"patterns\":[{\"include\":\"source.python\"}]}],\"scopeName\":\"source.cairo0\",\"embeddedLangs\":[\"python\"]}"));
|
||||
|
||||
const cairo = [
|
||||
...python.default,
|
||||
lang
|
||||
];
|
||||
|
||||
exports.default = cairo;
|
||||
@@ -0,0 +1,10 @@
|
||||
import python from './python-CRy0opiU.js';
|
||||
|
||||
const lang = Object.freeze(JSON.parse("{\"displayName\":\"Cairo\",\"name\":\"cairo\",\"patterns\":[{\"begin\":\"\\\\b(if).*\\\\(\",\"beginCaptures\":{\"1\":{\"name\":\"keyword.control.if\"},\"2\":{\"name\":\"entity.name.condition\"}},\"contentName\":\"source.cairo0\",\"end\":\"}\",\"endCaptures\":{\"0\":{\"name\":\"keyword.control.end\"}},\"name\":\"meta.control.if\",\"patterns\":[{\"include\":\"source.cairo0\"}]},{\"begin\":\"\\\\b(with)\\\\s+(.+)\\\\s*\\\\{\",\"beginCaptures\":{\"1\":{\"name\":\"keyword.control.with\"},\"2\":{\"name\":\"entity.name.identifiers\"}},\"contentName\":\"source.cairo0\",\"end\":\"}\",\"endCaptures\":{\"0\":{\"name\":\"keyword.control.end\"}},\"name\":\"meta.control.with\",\"patterns\":[{\"include\":\"source.cairo0\"}]},{\"begin\":\"\\\\b(with_attr)\\\\s+([A-Z_a-z][0-9A-Z_a-z]*)\\\\s*[({]\",\"beginCaptures\":{\"1\":{\"name\":\"keyword.control.with_attr\"},\"2\":{\"name\":\"entity.name.function\"}},\"contentName\":\"source.cairo0\",\"end\":\"}\",\"endCaptures\":{\"0\":{\"name\":\"keyword.control.end\"}},\"name\":\"meta.control.with_attr\",\"patterns\":[{\"include\":\"source.cairo0\"}]},{\"match\":\"\\\\belse\\\\b\",\"name\":\"keyword.control.else\"},{\"match\":\"\\\\b(call|jmp|ret|abs|rel|if)\\\\b\",\"name\":\"keyword.other.opcode\"},{\"match\":\"\\\\b([af]p)\\\\b\",\"name\":\"keyword.other.register\"},{\"match\":\"\\\\b(const|let|local|tempvar|felt|as|from|import|static_assert|return|assert|cast|alloc_locals|with|with_attr|nondet|dw|codeoffset|new|using|and)\\\\b\",\"name\":\"keyword.other.meta\"},{\"match\":\"\\\\b(SIZE(?:OF_LOCALS|))\\\\b\",\"name\":\"markup.italic\"},{\"match\":\"//[^\\\\n]*\\\\n\",\"name\":\"comment.line.sharp\"},{\"match\":\"\\\\b[A-Z_a-z][0-9A-Z_a-z]*:\\\\s*$\",\"name\":\"entity.name.function\"},{\"begin\":\"\\\\b(func)\\\\s+([A-Z_a-z][0-9A-Z_a-z]*)\\\\s*[({]\",\"beginCaptures\":{\"1\":{\"name\":\"storage.type.function.cairo\"},\"2\":{\"name\":\"entity.name.function\"}},\"contentName\":\"source.cairo0\",\"end\":\"}\",\"endCaptures\":{\"0\":{\"name\":\"storage.type.function.cairo\"}},\"name\":\"meta.function.cairo\",\"patterns\":[{\"include\":\"source.cairo0\"}]},{\"begin\":\"\\\\b(struct|namespace)\\\\s+([A-Z_a-z][0-9A-Z_a-z]*)\\\\s*\\\\{\",\"beginCaptures\":{\"1\":{\"name\":\"storage.type.function.cairo\"},\"2\":{\"name\":\"entity.name.function\"}},\"contentName\":\"source.cairo0\",\"end\":\"}\",\"endCaptures\":{\"0\":{\"name\":\"storage.type.function.cairo\"}},\"name\":\"meta.function.cairo\",\"patterns\":[{\"include\":\"source.cairo0\"}]},{\"match\":\"\\\\b[-+]?[0-9]+\\\\b\",\"name\":\"constant.numeric.decimal\"},{\"match\":\"\\\\b[-+]?0x\\\\h+\\\\b\",\"name\":\"constant.numeric.hexadecimal\"},{\"match\":\"'[^']*'\",\"name\":\"string.quoted.single\"},{\"match\":\"\\\"[^\\\"]*\\\"\",\"name\":\"string.quoted.double\"},{\"begin\":\"%\\\\{\",\"beginCaptures\":{\"0\":{\"name\":\"punctuation.section.embedded.begin.python\"}},\"contentName\":\"source.python\",\"end\":\"%}\",\"endCaptures\":{\"0\":{\"name\":\"punctuation.section.embedded.end.python\"},\"1\":{\"name\":\"source.python\"}},\"name\":\"meta.embedded.block.python\",\"patterns\":[{\"include\":\"source.python\"}]}],\"scopeName\":\"source.cairo0\",\"embeddedLangs\":[\"python\"]}"));
|
||||
|
||||
const cairo = [
|
||||
...python,
|
||||
lang
|
||||
];
|
||||
|
||||
export { cairo as default };
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user