Format with oxfmt

This commit is contained in:
2026-09-04 09:04:51 -07:00
parent 65f3a66157
commit 834a904e48
505 changed files with 1932 additions and 3687 deletions
-1
View File
@@ -184,7 +184,6 @@ jobs:
fi
npm publish "${TGZ}" --access public --tag stable
- name: Pack + Publish @hanzo/gui (alias of gui)
# Republish the umbrella tarball under the @hanzo/gui scoped name so
# downstream consumers can import either alias. Same dist tree as
+1 -1
View File
@@ -1 +1 @@
{}
{}
+1
View File
@@ -80,6 +80,7 @@ brew tap wix/brew && brew install applesimutils
```
The `run-detox.sh` script will automatically:
- Run `expo prebuild` if the `ios/` directory doesn't exist
- Build the Detox framework cache if missing (needed after Xcode updates)
- Start Metro if not already running
+3 -3
View File
@@ -46,6 +46,7 @@
"clean:build": "hanzogui-build clean:build"
},
"dependencies": {
"@hanzo/gui": "workspace:*",
"@hanzogui/animate-presence": "workspace:*",
"@hanzogui/avatar": "workspace:*",
"@hanzogui/button": "workspace:*",
@@ -55,6 +56,7 @@
"@hanzogui/config": "workspace:*",
"@hanzogui/context-menu": "workspace:*",
"@hanzogui/core": "workspace:*",
"@hanzogui/dev-config": "workspace:*",
"@hanzogui/get-button-sized": "workspace:*",
"@hanzogui/image": "workspace:*",
"@hanzogui/input": "workspace:*",
@@ -75,10 +77,8 @@
"@hanzogui/switch": "workspace:*",
"@hanzogui/switch-headless": "workspace:*",
"@hanzogui/tabs": "workspace:*",
"@hanzogui/dev-config": "workspace:*",
"@hanzogui/theme": "workspace:*",
"@hanzogui/toast": "workspace:*",
"@hanzo/gui": "workspace:*"
"@hanzogui/toast": "workspace:*"
},
"devDependencies": {
"@hanzogui/build": "workspace:*",
+11 -1
View File
@@ -1,4 +1,14 @@
import { type ThemeName, Button, Card, H2, Paragraph, Text, Theme, XStack, YStack } from '@hanzo/gui'
import {
type ThemeName,
Button,
Card,
H2,
Paragraph,
Text,
Theme,
XStack,
YStack,
} from '@hanzo/gui'
// 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
+5 -6
View File
@@ -83,12 +83,11 @@ function SizeSection({ section }: { section: Section }) {
backgroundColor="$color5"
{...(section === 'spaceNegative' && {
backgroundColor: '$red5',
size:
-tokens[
spaceTokensNegative.find(
(t) => Number.parseFloat(t) === -Number.parseFloat(token)
) ?? token
]?.val,
size: -tokens[
spaceTokensNegative.find(
(t) => Number.parseFloat(t) === -Number.parseFloat(token)
) ?? token
]?.val,
})}
{...(section === 'radius' && {
size: allTokens.size[token]?.val as any,
+2 -8
View File
@@ -16,12 +16,6 @@
"declarationMap": false,
"declarationDir": "dist/cjs/.types"
},
"include": [
"src"
],
"exclude": [
"src/**/*.test.*",
"src/**/__tests__/**",
"src/**/*.stories.*"
]
"include": ["src"],
"exclude": ["src/**/*.test.*", "src/**/__tests__/**", "src/**/*.stories.*"]
}
+2 -8
View File
@@ -16,12 +16,6 @@
"declarationMap": true,
"declarationDir": "types"
},
"include": [
"src"
],
"exclude": [
"src/**/*.test.*",
"src/**/__tests__/**",
"src/**/*.stories.*"
]
"include": ["src"],
"exclude": ["src/**/*.test.*", "src/**/__tests__/**", "src/**/*.stories.*"]
}
+2 -2
View File
@@ -2,8 +2,8 @@
"name": "@hanzogui/desktop",
"version": "8.0.0",
"private": true,
"description": "Tauri desktop proof shell — renders hanzogui on the true-black theme in a native webview.",
"type": "module",
"description": "Tauri desktop proof shell \u2014 renders hanzogui on the true-black theme in a native webview.",
"scripts": {
"dev": "vite",
"build": "vite build",
@@ -11,11 +11,11 @@
"tauri": "tauri"
},
"dependencies": {
"@hanzo/gui": "workspace:*",
"@hanzogui/config": "workspace:*",
"@hanzogui/react-native-web-lite": "workspace:*",
"@hanzogui/tauri": "workspace:*",
"@hanzogui/themes": "workspace:*",
"@hanzo/gui": "workspace:*",
"react": "19.2.8",
"react-dom": "19.2.8"
},
+8 -1
View File
@@ -2,7 +2,14 @@ html,
body {
max-width: 100vw;
font-family:
Zen, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
Zen,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
'Helvetica Neue',
Arial,
sans-serif;
scrollbar-color: #b1b1b1 transparent;
}
+2 -7
View File
@@ -6,10 +6,7 @@ import { apiRoute } from '~/features/api/apiRoute'
export default apiRoute(async (req) => {
const { ImageResponse } = await import('@vercel/og')
const [sansRegular, sansBlack] = await Promise.all([
sansRegularP,
sansBlackP,
])
const [sansRegular, sansBlack] = await Promise.all([sansRegularP, sansBlackP])
const { searchParams } = new URL(req.url)
@@ -52,9 +49,7 @@ const sansRegularP = fetchAsset(
new URL(`${getURL()}/fonts/Zen-Regular.ttf`, import.meta.url)
)
const sansBlackP = fetchAsset(
new URL(`${getURL()}/fonts/Zen-Black.ttf`, import.meta.url)
)
const sansBlackP = fetchAsset(new URL(`${getURL()}/fonts/Zen-Black.ttf`, import.meta.url))
const logo = fetchAsset(new URL(`${getURL()}/wordmark-white.png`, import.meta.url))
@@ -68,4 +68,3 @@ export const ContainerXL = styled(YStack, {
variants,
})
@@ -15,7 +15,9 @@ by: nate
<IntroParagraph>
<UL>
<LI>A style library (`@hanzogui/core`) for creating React Native + Web components.</LI>
<LI>
A style library (`@hanzogui/core`) for creating React Native + Web components.
</LI>
<LI>An optimizing compiler (`@hanzogui/static`) for incredible performance.</LI>
<LI>A component kit (`hanzogui`) built using both of the above.</LI>
</UL>
@@ -7,9 +7,7 @@ by: nate
# - 'hanzogui-alpha'
---
<Notice>
Gui is in alpha - we'd love it if you tried it out and gave us feedback
</Notice>
<Notice>Gui is in alpha - we'd love it if you tried it out and gave us feedback</Notice>
Gui takes the next steps towards truly universal styling for React apps.
@@ -7,8 +7,8 @@ poster: banner-rc0.jpg
---
<IntroParagraph>
Gui is an optimizing compiler meets universal style system for React that enables
highly performant styles for React Native and web.
Gui is an optimizing compiler meets universal style system for React that enables highly
performant styles for React Native and web.
</IntroParagraph>
It supports the full APIs of React Native alongside helpful inline style props,
+2 -1
View File
@@ -852,7 +852,8 @@ stay fully OSS.
<Spacer />
<XStack space ai="stretch" maw="100%" $sm={{ fd: 'column' }}>
<Card flex={1} title="📚 Docs">
Your design system and UI auto-generates a beautiful docs site, just like hanzogui.dev.
Your design system and UI auto-generates a beautiful docs site, just like
hanzogui.dev.
</Card>
<Card flex={1} title="🖼️ Studio">
A frontend realtime editor for colors, themes, tokens, default styles and more.
+4 -5
View File
@@ -7,9 +7,8 @@ image: /blog/version-two.jpg
---
<IntroParagraph large>
Gui brings high performing, consistent, typed inline styles to React Native and
React Web, plus composable UI components, a novel theme system and an optimizing
compiler.
Gui brings high performing, consistent, typed inline styles to React Native and React
Web, plus composable UI components, a novel theme system and an optimizing compiler.
</IntroParagraph>
We're excited to announce **the first release candidate for Gui v2 is
@@ -335,9 +334,9 @@ state uses that state's transition, and exiting uses the base transition:
```tsx
<Square
transition="1000ms" // slow exit (back to base)
transition="1000ms" // slow exit (back to base)
hoverStyle={{
transition: '200ms', // fast enter
transition: '200ms', // fast enter
backgroundColor: 'red',
}}
/>
@@ -55,13 +55,16 @@ import '@hanzogui/native/setup-zeego'
```
<Notice theme="blue">
On native, `ContextMenu.Trigger` coordinates with parent Gui pressables
automatically. You should not need extra wrapper views or empty `onPress`
handlers just to prevent a parent `pressStyle` from activating.
On native, `ContextMenu.Trigger` coordinates with parent Gui pressables automatically.
You should not need extra wrapper views or empty `onPress` handlers just to prevent a
parent `pressStyle` from activating.
</Notice>
<Notice theme="yellow">
**Expo Router users:** This import must run _before_ `expo-router/entry`. Create an `index.js` at your project root that imports the setup first, then expo-router, and update your `package.json` main field to `"index.js"`. See the [upgrade guide](/docs/guides/how-to-upgrade#expo-router-entry-point) for details.
**Expo Router users:** This import must run _before_ `expo-router/entry`. Create an
`index.js` at your project root that imports the setup first, then expo-router, and
update your `package.json` main field to `"index.js"`. See the [upgrade
guide](/docs/guides/how-to-upgrade#expo-router-entry-point) for details.
</Notice>
## Anatomy
@@ -158,7 +161,8 @@ Contains every component for the ContextMenu.
name: 'onOpenChange',
type: '(open: boolean, event?: { preventDefault: () => void }) => void',
required: false,
description: 'Called when the menu opens or closes. Call event.preventDefault() to cancel and allow the native context menu to show instead.',
description:
'Called when the menu opens or closes. Call event.preventDefault() to cancel and allow the native context menu to show instead.',
},
{
name: 'onOpenWillChange',
@@ -180,7 +184,8 @@ Contains every component for the ContextMenu.
type: 'ShiftProps | boolean',
required: false,
default: '{ padding: 10 }',
description: 'Shifts the menu horizontally to stay within viewport bounds. Pass an object to customize shift behavior (mainAxis, crossAxis, padding).',
description:
'Shifts the menu horizontally to stay within viewport bounds. Pass an object to customize shift behavior (mainAxis, crossAxis, padding).',
},
{
name: 'allowFlip',
@@ -46,7 +46,7 @@ mode like anything else:
That works because the axis is resolved against the mode. It is worth knowing
why, because the raw CSS does not do this for you: `flex-direction` is **inert**
under grid, and the two engines invert the same word — flex `row` runs across,
while grid's `grid-auto-flow: row` fills rows and therefore runs *down*. Written
while grid's `grid-auto-flow: row` fills rows and therefore runs _down_. Written
by hand, `display: grid` on a flex row silently lays out vertically. `XStack`
says the axis in both vocabularies, so it stays horizontal.
@@ -98,7 +98,7 @@ tracks at 390px, four at 768px, eight at 1440px.
**This is the one case where the two engines disagree, so it is worth knowing
exactly where.** They are pixel-identical while every row is full. They part on
a *partial last row*: flex's `flexGrow` stretches a lone trailing item across
a _partial last row_: flex's `flexGrow` stretches a lone trailing item across
the whole width, while grid holds it to its track. Measured with seven items
over three tracks, only the orphan differs, and only in width — by 328px on a
480px container.
@@ -213,12 +213,12 @@ and never sees the tracks. One source, both engines, no `$platform` branch.
### What cannot cross at all
| grid | why flex has no answer |
| --- | --- |
| named areas | flex has no second dimension |
| `gridColumn` / `gridArea` placement | flex cannot place across rows *and* columns |
| overlapping cells | flex cannot put two children in one cell |
| `gridAutoFlow="dense"` | nothing to backfill in one dimension |
| grid | why flex has no answer |
| ----------------------------------- | ------------------------------------------- |
| named areas | flex has no second dimension |
| `gridColumn` / `gridArea` placement | flex cannot place across rows _and_ columns |
| overlapping cells | flex cannot put two children in one cell |
| `gridAutoFlow="dense"` | nothing to backfill in one dimension |
These are web-only compositions. Reach for them deliberately and give native its
own arrangement.
@@ -3,8 +3,8 @@ title: Gui UI
---
<IntroParagraph>
Gui UI is a complete suite of components that render nicely on both React web and
React Native and come in both styled and unstyled forms.
Gui UI is a complete suite of components that render nicely on both React web and React
Native and come in both styled and unstyled forms.
</IntroParagraph>
You can install each component separately, or all of them at once with:
@@ -18,8 +18,8 @@ yarn add react-native-svg @hanzogui/lucide-icons-2
```
<Notice>
If you're using icons in a monorepo, install `react-native-svg` in the parent
app or workspace root rather than a leaf package.
If you're using icons in a monorepo, install `react-native-svg` in the parent app or
workspace root rather than a leaf package.
</Notice>
## Usage
@@ -50,13 +50,16 @@ import '@hanzogui/native/setup-zeego'
```
<Notice theme="blue">
On native, `Menu.Trigger` coordinates with parent Gui pressables
automatically. You should not need extra wrapper views or empty `onPress`
handlers just to prevent a parent `pressStyle` from activating.
On native, `Menu.Trigger` coordinates with parent Gui pressables automatically. You
should not need extra wrapper views or empty `onPress` handlers just to prevent a parent
`pressStyle` from activating.
</Notice>
<Notice theme="yellow">
**Expo Router users:** This import must run _before_ `expo-router/entry`. Create an `index.js` at your project root that imports the setup first, then expo-router, and update your `package.json` main field to `"index.js"`. See the [upgrade guide](/docs/guides/how-to-upgrade#expo-router-entry-point) for details.
**Expo Router users:** This import must run _before_ `expo-router/entry`. Create an
`index.js` at your project root that imports the setup first, then expo-router, and
update your `package.json` main field to `"index.js"`. See the [upgrade
guide](/docs/guides/how-to-upgrade#expo-router-entry-point) for details.
</Notice>
## Anatomy
@@ -173,7 +176,8 @@ Contains every component for the Menu.
type: 'ShiftProps | boolean',
required: false,
default: '{ padding: 10 }',
description: 'Shifts the menu horizontally to stay within viewport bounds. Pass an object to customize shift behavior (mainAxis, crossAxis, padding).',
description:
'Shifts the menu horizontally to stay within viewport bounds. Pass an object to customize shift behavior (mainAxis, crossAxis, padding).',
},
{
name: 'allowFlip',
@@ -193,7 +197,8 @@ Contains every component for the Menu.
type: 'boolean',
default: 'true',
required: false,
description: 'Constrains the menu to fit within available viewport space. When enabled, use Menu.ScrollView inside Content for scrollable overflow.',
description:
'Constrains the menu to fit within available viewport space. When enabled, use Menu.ScrollView inside Content for scrollable overflow.',
},
{
name: 'unstyled',
@@ -782,9 +787,7 @@ A scrollable container for menu items. Use this inside `Menu.Content` when you h
```jsx
<Menu.Content>
<Menu.ScrollView>
{/* Many menu items */}
</Menu.ScrollView>
<Menu.ScrollView>{/* Many menu items */}</Menu.ScrollView>
</Menu.Content>
```
@@ -22,10 +22,10 @@ npm install @hanzogui/native
<Notice>
`@hanzogui/native` is a light setup package. When you use one of its optional
integrations, make sure the matching native dependency is installed in your
app as well, such as `react-native-gesture-handler`, `react-native-teleport`,
`zeego`, or `burnt`. In a monorepo, install those native packages in the
parent app or workspace root rather than a leaf package.
integrations, make sure the matching native dependency is installed in your app as well,
such as `react-native-gesture-handler`, `react-native-teleport`, `zeego`, or `burnt`. In
a monorepo, install those native packages in the parent app or workspace root rather
than a leaf package.
</Notice>
## Portal
@@ -117,9 +117,7 @@ import { Pressable } from 'react-native'
export function AvatarLink() {
return (
<PressBoundary debugName="AvatarLink">
<Pressable onPress={() => {}}>
{/* your content */}
</Pressable>
<Pressable onPress={() => {}}>{/* your content */}</Pressable>
</PressBoundary>
)
}
@@ -171,9 +171,9 @@ export default () => (
</Paragraph>
<Paragraph numberOfLines={3} maxWidth={200}>
This text will be clamped to three lines. Any overflow beyond the third
line will be hidden. This is useful for card descriptions or preview text
where you want to show a limited amount of content.
This text will be clamped to three lines. Any overflow beyond the third line will be
hidden. This is useful for card descriptions or preview text where you want to show
a limited amount of content.
</Paragraph>
</>
)
@@ -62,9 +62,15 @@ function App() {
## Anatomy
```tsx
<Toast> {/* Root - context and position */}
<Toast.Viewport> {/* Container with portal */}
<Toast.Item> {/* Individual toast with stacking/drag */}
<Toast>
{' '}
{/* Root - context and position */}
<Toast.Viewport>
{' '}
{/* Container with portal */}
<Toast.Item>
{' '}
{/* Individual toast with stacking/drag */}
<Toast.Title />
<Toast.Description />
<Toast.Close />
@@ -100,7 +106,7 @@ toast('Message', {
// Dismiss
const id = toast('Hello')
toast.dismiss(id) // dismiss specific
toast.dismiss() // dismiss all
toast.dismiss() // dismiss all
```
### toast.promise()
@@ -252,6 +258,7 @@ Multiple toasts stack visually. Hover over the stack to expand and see all toast
### Swipe to Dismiss
Toasts can be swiped away. The direction is auto-detected based on position:
- **Web:** Left/right positioned toasts swipe toward their edge, center toasts swipe horizontally
- **Native:** All positions swipe vertically (up/down) to avoid conflicting with iOS/Android navigation back gesture
@@ -295,9 +302,7 @@ Each part can be styled independently:
<Toast.Title color="$blue12" fontWeight="bold">
{t.title}
</Toast.Title>
<Toast.Description color="$blue11">
{t.description}
</Toast.Description>
<Toast.Description color="$blue11">{t.description}</Toast.Description>
</Toast.Item>
```
@@ -214,7 +214,11 @@ list items or buttons, while the heavier `Tooltip` and `Tooltip.Content` live
at the root. It also lets you style all your tooltips consistently in one place.
<Notice>
When using a global scoped tooltip, you should set `zIndex` to ensure tooltips appear above dialogs and other portaled content. This is because the global tooltip's portal is mounted outside the stacking context of other portals. Inline tooltips (rendered alongside their trigger) automatically participate in z-index stacking and don't need this.
When using a global scoped tooltip, you should set `zIndex` to ensure tooltips appear
above dialogs and other portaled content. This is because the global tooltip's portal is
mounted outside the stacking context of other portals. Inline tooltips (rendered
alongside their trigger) automatically participate in z-index stacking and don't need
this.
</Notice>
```tsx fileName=_layout.tsx
@@ -26,26 +26,24 @@ Only set `zIndex` when you need to override the automatic stacking. Common cases
```tsx
// Only if you need to override automatic stacking
<Dialog zIndex={200000}>
...
</Dialog>
<Dialog zIndex={200000}>...</Dialog>
```
## Component Defaults
All overlay components use the automatic stacking system. Here's where to set `zIndex` if needed:
| Component | Where to set zIndex |
|-----------|---------------------|
| [Dialog](/ui/dialog) | `<Dialog zIndex={...}>` |
| [AlertDialog](/ui/alert-dialog) | `<AlertDialog zIndex={...}>` |
| [Sheet](/ui/sheet) | `<Sheet zIndex={...}>` |
| [Select](/ui/select) | `<Select zIndex={...}>` |
| [Popover](/ui/popover) | `<Popover zIndex={...}>` |
| [Tooltip](/ui/tooltip) | `<Tooltip zIndex={...}>` |
| [Menu](/ui/menu) | `<Menu.Portal zIndex={...}>` |
| Component | Where to set zIndex |
| ------------------------------- | ----------------------------------- |
| [Dialog](/ui/dialog) | `<Dialog zIndex={...}>` |
| [AlertDialog](/ui/alert-dialog) | `<AlertDialog zIndex={...}>` |
| [Sheet](/ui/sheet) | `<Sheet zIndex={...}>` |
| [Select](/ui/select) | `<Select zIndex={...}>` |
| [Popover](/ui/popover) | `<Popover zIndex={...}>` |
| [Tooltip](/ui/tooltip) | `<Tooltip zIndex={...}>` |
| [Menu](/ui/menu) | `<Menu.Portal zIndex={...}>` |
| [ContextMenu](/ui/context-menu) | `<ContextMenu.Portal zIndex={...}>` |
| [Toast](/ui/toast) | Via `ToastViewport` |
| [Toast](/ui/toast) | Via `ToastViewport` |
## Stacking Hierarchy
@@ -68,9 +66,7 @@ Nested floating elements automatically stack above their parent:
<Tooltip.Trigger>
<Button>Hover me</Button>
</Tooltip.Trigger>
<Tooltip.Content>
I appear above the dialog!
</Tooltip.Content>
<Tooltip.Content>I appear above the dialog!</Tooltip.Content>
</Tooltip>
</Dialog.Content>
</Dialog>
@@ -85,6 +81,7 @@ The stacking system uses two mechanisms:
### Horizontal Stacking (Siblings)
When multiple overlays open at the same level, each gets an incrementing z-index:
- First dialog: 105001
- Second dialog: 105002
- Third dialog: 105003
@@ -92,6 +89,7 @@ When multiple overlays open at the same level, each gets an incrementing z-index
### Vertical Stacking (Nested)
When content opens inside another portal, it reads the parent's z-index from context and adds 1:
- Dialog opens at 105001
- Popover inside dialog: 105002
- Tooltip inside that popover: 105003
@@ -233,9 +233,9 @@ export default () => (
<Square
size={100}
bg="$color5"
transition="1000ms" // slow exit (returning to base)
transition="1000ms" // slow exit (returning to base)
hoverStyle={{
transition: '200ms', // fast enter (going to hover)
transition: '200ms', // fast enter (going to hover)
bg: '$color10',
}}
/>
@@ -243,6 +243,7 @@ export default () => (
```
In this example:
- **Hover enter**: Uses the 200ms transition (snaps quickly to hover state)
- **Hover exit**: Uses the 1000ms base transition (fades slowly back)
@@ -807,11 +807,7 @@ You can pass a `settings` object to `createGui`:
```tsx
type AllowedValueSetting =
| boolean
| 'strict'
| 'somewhat-strict'
| 'strict-web'
| 'somewhat-strict-web'
boolean | 'strict' | 'somewhat-strict' | 'strict-web' | 'somewhat-strict-web'
type AllowedStyleValuesSetting =
| AllowedValueSetting
+2 -2
View File
@@ -4,8 +4,8 @@ description: Extend and build custom and optimizable components
---
<Notice theme="green">
If you're looking for a full list of style properties accepted by Gui, see the
[Styles page](/docs/intro/styles).
If you're looking for a full list of style properties accepted by Gui, see the [Styles
page](/docs/intro/styles).
</Notice>
Create a new component by extending an existing one:
@@ -36,8 +36,8 @@ export default () => (
```
<Notice theme="blue">
One really important and useful thing to note about Gui style properties: the order
is important! [Read more here](/docs/intro/styles#order-is-important)
One really important and useful thing to note about Gui style properties: the order is
important! [Read more here](/docs/intro/styles#order-is-important)
</Notice>
### With styled()
+2 -3
View File
@@ -4,8 +4,8 @@ description: Command-line tools for building, checking, and managing Gui project
---
<IntroParagraph>
The Gui CLI provides a suite of command-line tools for optimizing components,
managing dependencies, adding fonts and icons, and more.
The Gui CLI provides a suite of command-line tools for optimizing components, managing
dependencies, adding fonts and icons, and more.
</IntroParagraph>
<Notice theme="gray">
@@ -351,4 +351,3 @@ export default {
The check command is strict about version consistency. If you have a specific
reason for version mismatches (like testing), you can document them in your
README.
+1 -3
View File
@@ -57,9 +57,7 @@ module.exports = function (api) {
api.cache(true)
return {
presets: ['babel-preset-expo'],
plugins: [
'@hanzogui/babel-plugin',
],
plugins: ['@hanzogui/babel-plugin'],
}
}
```
@@ -191,8 +191,8 @@ themes: process.env.VITE_ENVIRONMENT === 'client'
<Notice theme="blue">
Once your v5 config is set up, run `npx hanzogui generate-prompt` and commit the output
to your repo. This helps AI assistants understand your Gui config and can speed up
the rest of the migration.
to your repo. This helps AI assistants understand your Gui config and can speed up the
rest of the migration.
</Notice>
## 3. Prop Renames
+11 -9
View File
@@ -136,8 +136,8 @@ export default function RootLayout({ children }: { children: React.ReactNode })
```
<Notice theme="blue">
With React 19, Gui automatically injects runtime styles via style tags. The
`outputCSS` file handles themes and tokens that are generated at build time.
With React 19, Gui automatically injects runtime styles via style tags. The `outputCSS`
file handles themes and tokens that are generated at build time.
</Notice>
Run `npx hanzogui build` once to generate the initial CSS file, then commit it.
@@ -422,8 +422,8 @@ export default class Document extends NextDocument {
```
<Notice theme="blue">
Gui automatically injects styles at runtime. You can optionally generate a static
CSS file - see the [Static CSS Output](#static-css-output) section.
Gui automatically injects styles at runtime. You can optionally generate a static CSS
file - see the [Static CSS Output](#static-css-output) section.
</Notice>
### pages/\_app.tsx
@@ -570,7 +570,8 @@ You can also have the plugin generate CSS during your Next.js build:
const hanzoguiPlugin = withGui({
config: './hanzogui.config.ts',
components: ['@hanzo/gui'],
outputCSS: process.env.NODE_ENV === 'production' ? './public/hanzogui.generated.css' : null,
outputCSS:
process.env.NODE_ENV === 'production' ? './public/hanzogui.generated.css' : null,
// faster dev mode, keeps debugging helpers:
disableExtraction: process.env.NODE_ENV === 'development',
})
@@ -784,7 +785,8 @@ CLI or the Next.js plugin:
const hanzoguiPlugin = withGui({
config: './hanzogui.config.ts',
components: ['@hanzo/gui'],
outputCSS: process.env.NODE_ENV === 'production' ? './public/hanzogui.generated.css' : null,
outputCSS:
process.env.NODE_ENV === 'production' ? './public/hanzogui.generated.css' : null,
// faster dev mode, keeps debugging helpers:
disableExtraction: process.env.NODE_ENV === 'development',
})
@@ -797,7 +799,7 @@ import '../public/hanzogui.generated.css'
```
<Notice theme="green">
With React 19, Gui automatically injects runtime styles via style tags on the
server. The `outputCSS` file handles themes and tokens generated at build time, so you
don't need any `getCSS()` calls in your provider.
With React 19, Gui automatically injects runtime styles via style tags on the server.
The `outputCSS` file handles themes and tokens generated at build time, so you don't
need any `getCSS()` calls in your provider.
</Notice>
+2 -1
View File
@@ -140,7 +140,8 @@ For better performance, use the `outputCSS` option in the Gui Vite plugin:
hanzoguiPlugin({
config: './hanzogui.config.ts',
components: ['@hanzo/gui'],
outputCSS: process.env.NODE_ENV === 'production' ? './public/hanzogui.generated.css' : null,
outputCSS:
process.env.NODE_ENV === 'production' ? './public/hanzogui.generated.css' : null,
})
```
@@ -9,8 +9,8 @@ demoName: ThemeBuilder
</HeroContainer>
<IntroParagraph>
Gui themes start simple, but can do some pretty powerful things. To make them easier
to generate, we've built a few helpers. You can always just skip themes, or add a single
Gui themes start simple, but can do some pretty powerful things. To make them easier to
generate, we've built a few helpers. You can always just skip themes, or add a single
basic theme if you prefer. This guide is for users wanting to generate a more complex
suite of themes.
</IntroParagraph>
+2 -2
View File
@@ -5,8 +5,8 @@ description: Color palettes and customization options
<Notice theme="blue">
This is the Gui documentation site's [own color
palette](https://github.com/hanzoai/gui/blob/main/pkgs/core/themes/src/tokens.tsx).
You can customize your own using
palette](https://github.com/hanzoai/gui/blob/main/pkgs/core/themes/src/tokens.tsx). You
can customize your own using
[@hanzogui/create-theme](https://github.com/hanzoai/gui/blob/738011d99e1dec6002a916edf424b75bb6cc0336/packages/themes/src/themes.tsx).
See [@hanzogui/colors](https://github.com/hanzoai/gui/tree/main/pkgs/core/colors/src)
for Radix colors, or BYO color palette.
@@ -4,8 +4,8 @@ description: Adding the compiler to your apps
---
<IntroParagraph>
The Gui Compiler significantly improves performance of both web and native
applications through partial analysis and view flattening.
The Gui Compiler significantly improves performance of both web and native applications
through partial analysis and view flattening.
</IntroParagraph>
See the [Benchmarks](/docs/intro/benchmarks) or a more
@@ -181,7 +181,8 @@ Some notes on the options:
See the [Vite guide](/docs/guides/vite) for more complete setup.
<Notice theme="yellow">
`@hanzogui/vite-plugin` is ESM-only. Your project must have `"type": "module"` in its `package.json` (or use `.mjs`/`.mts` config files).
`@hanzogui/vite-plugin` is ESM-only. Your project must have `"type": "module"` in its
`package.json` (or use `.mjs`/`.mts` config files).
</Notice>
Add `@hanzogui/vite-plugin` and update your `vite.config.ts`. If you have a
@@ -4,8 +4,8 @@ description: Get Gui set up, step by step
---
<IntroParagraph>
Gui is easy to set up as we've invested into "0-config" installs for all bundlers.
That said, it's easy to get lost if you start configuring too many things too early.
Gui is easy to set up as we've invested into "0-config" installs for all bundlers. That
said, it's easy to get lost if you start configuring too many things too early.
</IntroParagraph>
Use `npm create hanzogui@latest` to pick one of our starter templates.
@@ -43,18 +43,19 @@ The `hanzogui` package is a superset of core, so anywhere the docs reference
`react-native` types in the parent project for proper prop typing and
autocomplete. You can either install `react-native` directly:
```bash
yarn add react-native
```
```bash
yarn add react-native
```
Or, if you only want the types:
Or, if you only want the types:
```bash
yarn add [email protected] @types/react-native
```
```bash
yarn add [email protected] @types/react-native
```
Gui's web runtime is self-contained. In a monorepo, install either option
in the parent app or workspace root rather than a leaf package.
Gui's web runtime is self-contained. In a monorepo, install either option
in the parent app or workspace root rather than a leaf package.
</Notice>
Next, you'll want to set up your [configuration](/docs/core/configuration) and
@@ -96,7 +97,11 @@ declare module '@hanzogui/core' {
}
export default () => {
return <GuiProvider config={config} defaultTheme="light">{/* your app here */}</GuiProvider>
return (
<GuiProvider config={config} defaultTheme="light">
{/* your app here */}
</GuiProvider>
)
}
```
@@ -295,6 +295,7 @@ The `border` shorthand now works cross-platform:
```
On web, the CSS shorthand is passed through directly. On native, it expands to individual properties:
- `borderWidth` → `borderTopWidth`, `borderRightWidth`, `borderBottomWidth`, `borderLeftWidth`
- `borderStyle` → `borderStyle`
- `borderColor` → `borderTopColor`, `borderRightColor`, `borderBottomColor`, `borderLeftColor`
@@ -37,4 +37,3 @@ export function apiRoute(handler: Endpoint) {
}
}) satisfies Endpoint
}
@@ -173,7 +173,12 @@ describe('the conversion preserves what rendered before', () => {
/>
</>
)
same(find('was'), find('now'), ['display', 'gridTemplateColumns', 'rowGap', 'columnGap'])
same(find('was'), find('now'), [
'display',
'gridTemplateColumns',
'rowGap',
'columnGap',
])
// the old form needed an inline style to say grid at all; the new one does not
expect(find('now').getAttribute('style')).toBeNull()
expect(leaked(find('now'))).toEqual([])
@@ -195,7 +195,6 @@ export const HeaderContents = React.memo((props: HeaderProps) => {
</XStack>
</XStack>
</Link>
</XStack>
<View flex={1} />
@@ -588,11 +587,7 @@ const HeaderLinksPopoverContent = React.memo((props: { active: ID | '' }) => {
const getDocsSectionFromPath = (pathName: string): 'core' | 'ui' | null => {
if (!pathName || pathName === '/' || pathName === '') return null
if (pathName.startsWith('/ui/')) return 'ui'
if (
pathName.startsWith('/docs') ||
pathName.startsWith('/blog')
)
return 'core'
if (pathName.startsWith('/docs') || pathName.startsWith('/blog')) return 'core'
return null
}
@@ -742,7 +737,6 @@ const HeaderMenuMoreContents = () => {
</HeadAnchor>
</Link>
<Link asChild href="/blog">
<HeadAnchor menu>Blog</HeadAnchor>
</Link>
@@ -53,11 +53,7 @@ export const TooltipLabelLarge = ({
}
return (
<Link
asChild
delayNavigate
href={href as Href}
>
<Link asChild delayNavigate href={href as Href}>
{content}
</Link>
)
@@ -96,7 +96,6 @@ const HeroContents = memo(function HeroContents() {
mx: '$-4',
}}
>
<YStack items="center" gap="$4">
<HeroH1>
<Span position="relative" color="$accent1">
@@ -51,13 +51,15 @@ export const SearchProvider = memo(({ children }: any) => {
createPortal(
<DocSearchModal
placeholder="Search docs..."
indices={[{
name: INDEX,
searchParameters: {
facetFilters: [],
distinct: 1,
indices={[
{
name: INDEX,
searchParameters: {
facetFilters: [],
distinct: 1,
},
},
}]}
]}
initialQuery={initialQuery || ''}
initialScrollY={window.scrollY}
onClose={onClose}
@@ -71,8 +71,7 @@ const components = [
<UserDropdown />
</Panel>
</>,
<>
</>,
<></>,
<>
<Panel key="" fileToCopyName="Login">
<LoginScreen />
@@ -32,9 +32,7 @@ type ThemeHistoryData = {
}
accent: {
name: 'accent'
anchors: Array<{
/* 同上 */
}>
anchors: Array<{/* 同上 */}>
}
}
schemes: {
@@ -39,8 +39,7 @@ const Contents = ({ themeNameBase }: { themeNameBase: string }) => {
return (
<>
<XStack position="absolute" t="$4" l={30} z={1000} gap="$4" scale={0.75}>
</XStack>
<XStack position="absolute" t="$4" l={30} z={1000} gap="$4" scale={0.75}></XStack>
<XStack position="absolute" t="$4" r="$4" z={1000}>
<XStack
+2 -2
View File
@@ -100,6 +100,7 @@ Themes are organized hierarchically and can be combined:
### Theme Usage
Themes are combined hierarchically. For example, `light_blue_alt1_Button` combines:
- Base: `light`
- Color: `blue`
- Variant: `alt1`
@@ -318,7 +319,7 @@ Media queries can be used as style props or with the `useMedia` hook:
```tsx
// As style props (prefix with $)
<View width="100%" $gtLarge={{ width: "50%" }} />
;<View width="100%" $gtLarge={{ width: '50%' }} />
// Using the useMedia hook
const media = useMedia()
@@ -450,4 +451,3 @@ The following components are available:
- YGroup
- YStack
- ZStack
@@ -3,11 +3,14 @@ import { startTransition, useCallback, useState } from 'react'
export const useTransitionState = ((val) => {
const [a, b] = useState(val)
// @ts-ignore
const wrapped = useCallback((...args) => {
startTransition(() => {
// @ts-ignore
b(...args)
})
}, [b])
const wrapped = useCallback(
(...args) => {
startTransition(() => {
// @ts-ignore
b(...args)
})
},
[b]
)
return [a, wrapped]
}) as typeof useState
+9 -9
View File
@@ -24,6 +24,14 @@
"@docsearch/css": "^5.0.4",
"@docsearch/react": "^5.0.4",
"@github/mini-throttle": "^2.1.1",
"@hanzo/gui": "workspace:*",
"@hanzogui/assert": "workspace:*",
"@hanzogui/demos": "workspace:*",
"@hanzogui/dev-config": "workspace:*",
"@hanzogui/get-token": "workspace:*",
"@hanzogui/logo": "workspace:*",
"@hanzogui/lucide-icons-2": "workspace:*",
"@hanzogui/use-store": "workspace:*",
"@hookform/resolvers": "^5.9.1",
"@leeoniya/ufuzzy": "^1.0.19",
"@ngneat/falso": "^8.0.2",
@@ -32,13 +40,6 @@
"@react-three/fiber": "^9.7.0",
"@rehookify/datepicker": "^6.6.8",
"@sindresorhus/slugify": "^3.0.0",
"@hanzogui/assert": "workspace:*",
"@hanzogui/demos": "workspace:*",
"@hanzogui/get-token": "workspace:*",
"@hanzogui/logo": "workspace:*",
"@hanzogui/lucide-icons-2": "workspace:*",
"@hanzogui/dev-config": "workspace:*",
"@hanzogui/use-store": "workspace:*",
"@tanstack/react-table": "^9.2.4",
"@ts-react/form": "^1.8.3",
"@vercel/og": "^1.0.2",
@@ -71,11 +72,10 @@
"react-native-worklets": "^0.12.1",
"refractor": "^5.0.0",
"rehype-parse": "^9.0.1",
"unified": "^11.0.5",
"sharp": "^0.35.4",
"swr": "^2.5.1",
"@hanzo/gui": "workspace:*",
"three": "^0.185.1",
"unified": "^11.0.5",
"valibot": "^1.4.2",
"victory": "^37.3.6",
"vite": "^8.2.2",
+2 -16
View File
@@ -11,7 +11,6 @@ if (!import.meta.dirname) {
throw new Error(`Not on Node 22`)
}
// use createRequire instead of import.meta.resolve for bun compatibility in vite config
const require = createRequire(import.meta.url)
const resolve = (path: string) => {
@@ -98,7 +97,6 @@ export default {
replacement: pathResolve(import.meta.dirname, '.'),
},
// Standard string-based aliases
{
find: 'react-native-svg',
@@ -127,13 +125,7 @@ export default {
},
ssr: {
external: [
'@vxrn/mdx-rust',
'satteri',
'satteri-expressive-code',
'ws',
'postmark',
],
external: ['@vxrn/mdx-rust', 'satteri', 'satteri-expressive-code', 'ws', 'postmark'],
noExternal: true,
},
@@ -167,7 +159,6 @@ export default {
},
},
ssr: {
dedupeSymlinkedModules: true,
autoDepsOptimization: {
@@ -192,12 +183,7 @@ export default {
config: {
build: {
rollupOptions: {
external: [
'@discordjs/rest',
'@discordjs/ws',
'@vercel/og',
'zlib-sync',
],
external: ['@discordjs/rest', '@discordjs/ws', '@vercel/og', 'zlib-sync'],
},
},
},
+2 -2
View File
@@ -12,6 +12,7 @@
"test": "./run-native-tests.sh"
},
"dependencies": {
"@hanzo/gui": "workspace:*",
"@hanzogui/config": "workspace:*",
"@hanzogui/core": "workspace:*",
"@hanzogui/demos": "workspace:*",
@@ -23,7 +24,6 @@
"expo": "^57.0.18",
"expo-font": "^57.0.2",
"react": "19.2.8",
"react-native": "^0.87.1",
"@hanzo/gui": "workspace:*"
"react-native": "^0.87.1"
}
}
+2 -2
View File
@@ -4,11 +4,11 @@
"private": true,
"main": "src/index.ts",
"dependencies": {
"@hanzo/gui": "workspace:*",
"@hanzogui/config": "workspace:*",
"@hanzogui/demos": "workspace:*",
"@hanzogui/lucide-icons-2": "workspace:*",
"@hanzogui/toast": "workspace:*",
"@hanzo/gui": "workspace:*"
"@hanzogui/toast": "workspace:*"
},
"peerDependencies": {
"react": ">=19.2.8"
+1 -1
View File
@@ -1 +1 @@
{}
{}
+2 -2
View File
@@ -106,10 +106,10 @@
"devDependencies": {
"@babel/core": "^8.0.1",
"@expo/fingerprint": "^0.20.11",
"@playwright/test": "^1.62.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.3",
"@hanzogui/babel-plugin": "workspace:*",
"@hanzogui/metro-plugin": "workspace:*",
"@playwright/test": "^1.62.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.3",
"@types/jest": "^30.0.0",
"@types/pngjs": "^6.0.5",
"babel-plugin-module-resolver": "^5.0.3",
+21 -3
View File
@@ -1,5 +1,23 @@
/* The three faces the kit uses, self-hosted from @hanzo/font — one variable
file per family rather than a weight per request. */
@font-face{font-family:'Zen';src:url('/fonts/Zen-Variable.woff2') format('woff2-variations');font-weight:100 900;font-style:normal;font-display:swap}
@font-face{font-family:'Zen Mono';src:url('/fonts/ZenMono-Variable.woff2') format('woff2-variations');font-weight:100 900;font-style:normal;font-display:swap}
@font-face{font-family:'Zen Pixel Square';src:url('/fonts/ZenPixel-Square.woff2') format('woff2');font-weight:500;font-style:normal;font-display:swap}
@font-face {
font-family: 'Zen';
src: url('/fonts/Zen-Variable.woff2') format('woff2-variations');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Zen Mono';
src: url('/fonts/ZenMono-Variable.woff2') format('woff2-variations');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Zen Pixel Square';
src: url('/fonts/ZenPixel-Square.woff2') format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
}
+10 -3
View File
@@ -1,9 +1,14 @@
type ObjectType = Record<PropertyKey, unknown>
type PickByValue<OBJ_T, VALUE_T> = // From https://stackoverflow.com/a/55153000
type PickByValue<
OBJ_T,
VALUE_T,
> = // From https://stackoverflow.com/a/55153000
Pick<OBJ_T, { [K in keyof OBJ_T]: OBJ_T[K] extends VALUE_T ? K : never }[keyof OBJ_T]>
type ObjectEntries<OBJ_T> = // From https://stackoverflow.com/a/60142095
type ObjectEntries<
OBJ_T,
> = // From https://stackoverflow.com/a/60142095
{ [K in keyof OBJ_T]: [keyof PickByValue<OBJ_T, OBJ_T[K]>, OBJ_T[K]] }[keyof OBJ_T][]
export const objectKeys = <O extends object>(obj: O) => Object.keys(obj) as Array<keyof O>
@@ -20,7 +25,9 @@ type EntriesType =
// Existing Utils
type DeepWritable<OBJ_T> = { -readonly [P in keyof OBJ_T]: DeepWritable<OBJ_T[P]> }
type UnionToIntersection<UNION_T> = // From https://stackoverflow.com/a/50375286
type UnionToIntersection<
UNION_T,
> = // From https://stackoverflow.com/a/50375286
(UNION_T extends any ? (k: UNION_T) => void : never) extends (k: infer I) => void
? I
: never
+5 -5
View File
@@ -20,7 +20,8 @@
"android": "one run:android"
},
"dependencies": {
"@react-native-masked-view/masked-view": "^0.3.2",
"@hanzo/font": "^1.9.3",
"@hanzo/gui": "workspace:*",
"@hanzogui/animations-css": "workspace:*",
"@hanzogui/animations-motion": "workspace:*",
"@hanzogui/colors": "workspace:*",
@@ -28,6 +29,7 @@
"@hanzogui/lucide-icons-2": "workspace:*",
"@hanzogui/react-native-media-driver": "workspace:*",
"@hanzogui/toast": "workspace:*",
"@react-native-masked-view/masked-view": "^0.3.2",
"@vxrn/color-scheme": "^1.25.8",
"expo": "^57.0.18",
"one": "^1.25.8",
@@ -37,14 +39,12 @@
"react-native-safe-area-context": "^5.9.1",
"react-native-screens": "^4.27.0",
"react-native-svg": "^15.15.5",
"react-native-web": "^0.21.2",
"@hanzo/font": "^1.9.3",
"@hanzo/gui": "workspace:*"
"react-native-web": "^0.21.2"
},
"devDependencies": {
"@hanzogui/vite-plugin": "workspace:*",
"@playwright/test": "^1.62.1",
"@react-native-community/cli": "^20.2.0",
"@hanzogui/vite-plugin": "workspace:*",
"@types/react": "19.2.18",
"vite": "^8.2.2",
"wait-port": "^1.1.0",
+3 -3
View File
@@ -14,6 +14,7 @@
"test:web": "true"
},
"dependencies": {
"@hanzo/gui": "workspace:*",
"@hanzogui/config": "workspace:*",
"@hanzogui/lucide-icons-2": "workspace:*",
"@hanzogui/toast": "workspace:*",
@@ -36,14 +37,13 @@
"react-native-screens": "^4.27.0",
"react-native-svg": "^15.15.5",
"react-native-web": "^0.21.2",
"react-native-worklets": "^0.12.1",
"@hanzo/gui": "workspace:*"
"react-native-worklets": "^0.12.1"
},
"devDependencies": {
"@babel/core": "^8.0.1",
"@expo/metro-runtime": "^57.0.14",
"@playwright/test": "^1.62.1",
"@hanzogui/babel-plugin": "workspace:*",
"@playwright/test": "^1.62.1",
"@types/react": "19.2.18",
"serve": "^14.2.6",
"typescript": "7.0.2"
+3 -3
View File
@@ -1,5 +1,6 @@
{
"name": "@hanzogui/remix-starter",
"version": "8.3.1",
"private": true,
"type": "module",
"sideEffects": false,
@@ -22,9 +23,9 @@
"react-router": "^7.15.1"
},
"devDependencies": {
"@hanzogui/vite-plugin": "workspace:*",
"@react-router/dev": "^7.15.1",
"@react-router/fs-routes": "^8.3.1",
"@hanzogui/vite-plugin": "workspace:*",
"@types/react": "19.2.18",
"@types/react-dom": "19.2.5",
"typescript": "7.0.2",
@@ -34,6 +35,5 @@
"engines": {
"node": ">=20.0.0"
},
"packageManager": "[email protected]",
"version": "8.3.1"
"packageManager": "[email protected]"
}
+10 -2
View File
@@ -7,8 +7,16 @@ export default function Home() {
const signedIn = !loading && session != null
const who = session?.user?.email ?? session?.user?.name
const rooms = { href: '/rooms', title: 'Rooms', caption: 'Conversations in your org' } as const
const wallet = { href: '/wallet', title: 'Wallet', caption: 'Balance and AI usage' } as const
const rooms = {
href: '/rooms',
title: 'Rooms',
caption: 'Conversations in your org',
} as const
const wallet = {
href: '/wallet',
title: 'Wallet',
caption: 'Balance and AI usage',
} as const
const views = signedIn
? [rooms, wallet]
: [
+7 -1
View File
@@ -102,7 +102,13 @@ export default function RoomScreen() {
) : null}
</ScrollView>
<XStack p="$3" gap="$2" borderTopWidth={1} borderColor="$borderColor" items="center">
<XStack
p="$3"
gap="$2"
borderTopWidth={1}
borderColor="$borderColor"
items="center"
>
<Input
flex={1}
value={draft}
+2 -1
View File
@@ -18,7 +18,8 @@ export default function RoomsScreen() {
if (token == null) return
let live = true
setState('loading')
void team(token).rooms()
void team(token)
.rooms()
.then((list) => {
if (live) {
setRooms(list)
+2 -1
View File
@@ -17,7 +17,8 @@ export default function WalletScreen() {
if (token == null) return
let live = true
setState('loading')
void billing(token).wallet()
void billing(token)
.wallet()
.then((w) => {
if (live) {
setWallet(w)
+3 -3
View File
@@ -2,6 +2,8 @@
"name": "@hanzogui/team",
"version": "8.3.1",
"private": true,
"license": "BSD-3-Clause",
"author": "Hanzo AI <[email protected]>",
"type": "module",
"scripts": {
"dev": "one dev",
@@ -31,7 +33,5 @@
"@hanzogui/vite-plugin": "workspace:*",
"@types/react": "19.2.18",
"vite": "^8.2.2"
},
"license": "BSD-3-Clause",
"author": "Hanzo AI <[email protected]>"
}
}
+2 -1
View File
@@ -42,7 +42,8 @@ export const team = (token: string) => {
/** Say something. Plain text in; the surface wraps it in the client's markup. */
say: async (id: string, space: string, text: string): Promise<TeamMessage> =>
(await api.postTeamRoomsByIdMessages({ id, teamMessageWrite: { space, text } })).data,
(await api.postTeamRoomsByIdMessages({ id, teamMessageWrite: { space, text } }))
.data,
}
}
+7 -3
View File
@@ -21,7 +21,8 @@ export interface Wallet {
function num(v: unknown): number | undefined {
if (typeof v === 'number' && Number.isFinite(v)) return v
if (typeof v === 'string' && v.trim() !== '' && Number.isFinite(Number(v))) return Number(v)
if (typeof v === 'string' && v.trim() !== '' && Number.isFinite(Number(v)))
return Number(v)
return undefined
}
@@ -29,7 +30,9 @@ function num(v: unknown): number | undefined {
function usd(record: Record<string, unknown>): number | undefined {
const atto = record.balance_atto_usd ?? record.balanceAtto ?? record.atto_usd
if (typeof atto === 'string' && /^-?\d+$/.test(atto)) return Number(BigInt(atto)) / 1e18
const dollars = num(record.balance_usd ?? record.balanceUsd ?? record.balance ?? record.credits)
const dollars = num(
record.balance_usd ?? record.balanceUsd ?? record.balance ?? record.credits
)
if (dollars !== undefined) return dollars
const cents = num(record.balance_cents ?? record.balanceCents)
return cents === undefined ? undefined : cents / 100
@@ -44,7 +47,8 @@ export function read(record: Record<string, unknown>): Wallet {
const period = record.period ?? record.month
if (typeof period === 'string') usage.push({ label: 'Period', value: period })
const spent = num(record.spent_usd ?? record.spentUsd ?? record.usage_usd)
if (spent !== undefined) usage.push({ label: 'Spent this period', value: `$${spent.toFixed(2)}` })
if (spent !== undefined)
usage.push({ label: 'Spent this period', value: `$${spent.toFixed(2)}` })
const requests = num(record.requests ?? record.request_count)
if (requests !== undefined) usage.push({ label: 'Requests', value: String(requests) })
const seats = num(record.seats ?? record.seat_count)
+2 -2
View File
@@ -7,8 +7,8 @@
"generate-prompt": "hanzogui generate-prompt"
},
"dependencies": {
"@hanzo/gui": "workspace:*",
"@hanzogui/cli": "workspace:*",
"@hanzogui/config": "workspace:*",
"@hanzo/gui": "workspace:*"
"@hanzogui/config": "workspace:*"
}
}
+3 -3
View File
@@ -14,18 +14,18 @@
"prod": "NODE_ENV=production webpack --json=dist/compilation-stats.json"
},
"dependencies": {
"@hanzo/gui": "workspace:*",
"@hanzogui/animations-css": "workspace:*",
"@hanzogui/core": "workspace:*",
"@hanzogui/create-theme": "workspace:*",
"@hanzogui/linear-gradient": "workspace:*",
"@hanzogui/react-native-svg": "workspace:*",
"@hanzogui/shorthands": "workspace:*",
"@hanzogui/react-native-web-lite": "workspace:*",
"@hanzo/gui": "workspace:*"
"@hanzogui/shorthands": "workspace:*"
},
"devDependencies": {
"@playwright/test": "^1.62.1",
"@hanzogui/vite-plugin": "workspace:*",
"@playwright/test": "^1.62.1",
"async-retry": "^1.3.3",
"react-dom": "19.2.8",
"vite": "^8.2.2",
+2 -2
View File
@@ -36,8 +36,8 @@
"devDependencies": {
"@babel/preset-react": "^8.0.1",
"@babel/preset-typescript": "^8.0.1",
"@playwright/test": "^1.62.1",
"@hanzogui/build": "workspace:*",
"@hanzogui/loader": "workspace:*"
"@hanzogui/loader": "workspace:*",
"@playwright/test": "^1.62.1"
}
}
+1 -1
View File
@@ -22,8 +22,8 @@
"react-native-web": "^0.21.2"
},
"devDependencies": {
"@playwright/test": "^1.62.1",
"@hanzogui/cli": "workspace:*",
"@playwright/test": "^1.62.1",
"@types/node": "26.4.0",
"@types/react": "19.2.18",
"vitest": "^4.1.11"
+2 -2
View File
@@ -12,8 +12,8 @@
"test:clean": "git checkout packages/app/ && git clean -fd packages/app/"
},
"dependencies": {
"@hanzogui/cli": "workspace:*",
"@hanzo/gui": "workspace:*"
"@hanzo/gui": "workspace:*",
"@hanzogui/cli": "workspace:*"
},
"devDependencies": {
"vitest": "^4.1.11"
@@ -16,11 +16,11 @@
"watch": "hanzogui-build --skip-types --watch"
},
"dependencies": {
"@hanzo/gui": "workspace:*",
"@hanzogui/animations-react-native": "workspace:*",
"@hanzogui/config": "workspace:*",
"@hanzogui/shorthands": "workspace:*",
"@hanzogui/themes": "workspace:*",
"@hanzo/gui": "workspace:*"
"@hanzogui/themes": "workspace:*"
},
"devDependencies": {
"@hanzogui/build": "workspace:*"
@@ -30,8 +30,7 @@ export const headingFont = createZenSansFont({
})
export const bodyFont = createZenSansFont(
{
},
{},
{
sizeSize: (size) => Math.round(size * 1.1),
sizeLineHeight: (size) => Math.round(size * 1.1 + (size > 20 ? 10 : 10)),
@@ -20,11 +20,11 @@
"watch": "hanzogui-build --skip-types --watch"
},
"dependencies": {
"@my/config": "workspace:^",
"@hanzo/gui": "workspace:*",
"@hanzogui/next-theme": "workspace:*",
"@hanzogui/toast": "workspace:*",
"expo-constants": "^57.0.16",
"@hanzo/gui": "workspace:*"
"@my/config": "workspace:^",
"expo-constants": "^57.0.16"
},
"devDependencies": {
"@hanzogui/build": "workspace:*",
+16 -10
View File
@@ -29,6 +29,7 @@ Both leaked credentials are dead; no rotation needed. The anon key currently in
are now enabled and cover ongoing detection.)
### ⚠️ A2. Verify / remove the prod `[email protected]` account
The test-user endpoint previously created this account in the **production**
Supabase project with a static, source-committed password and Pro entitlements.
The static password is now removed (see H5), but if the account was ever created
@@ -39,16 +40,17 @@ in prod it still exists. Confirm in Supabase Auth and delete it if present.
## What was fixed in this repo
### hanzogui.dev web app
| ID | Issue | Fix |
|----|-------|-----|
| C1 ✅ | `GET /api/bento/code?userGithubId=` served paid Bento source with no auth (legacy v1 CLI path; trusted a client-supplied id as the user identity) | Removed the unauthenticated branch; the shipped CLI already uses the token-authenticated `/api/bento/cli/v2/code-download`. `app/api/bento/code+api.ts` |
| C2 ✅ | `POST /api/github/app-hook` was a forgeable webhook stub (no signature verification, logged all headers) | Deleted the dead endpoint. |
| H1 ✅ | Charge endpoints applied a client-supplied `couponId` without re-validating it applied to the product (free/near-free purchases) | Added `features/stripe/assertValidCoupon.ts` and call it in `create-subscription`, `create-v2-subscription`, `add-team-seats` before any coupon is applied. |
| H2 ✅ | `/api/validate-coupon` was unauthenticated → coupon-enumeration oracle | Added `ensureAuth`. |
| H3 ✅ | Team-seat invite minted unlimited active seats and accepted arbitrary `user_id` | Enforced seat capacity vs `total_seats`, dedup of active members, and invitee-exists check. `app/api/team-seat+api.ts` |
| H4 ✅ | `/api/github/users` returned all users incl. **email** (`select('*')`) and had a PostgREST `.or()` filter injection via comma | Returns only `id, full_name, avatar_url`; name-only `ilike` (or exact-email match if `@` present, never returned); strips comma. UI no longer renders others' emails. `app/api/github/users+api.ts`, `NewAccountModal.tsx` |
| H4b ✅ | `/api/discord/search-member` let any logged-in user enumerate the whole guild | Gated behind Pro (`ensureAccess`). |
| H5 ✅ | Hardcoded test-user password (`test-user-password-12345`) usable to sign into prod via Supabase password auth | Generates a fresh random password per run and rotates any existing user's password. `app/api/test-user+api.ts` |
| ID | Issue | Fix |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| C1 ✅ | `GET /api/bento/code?userGithubId=` served paid Bento source with no auth (legacy v1 CLI path; trusted a client-supplied id as the user identity) | Removed the unauthenticated branch; the shipped CLI already uses the token-authenticated `/api/bento/cli/v2/code-download`. `app/api/bento/code+api.ts` |
| C2 ✅ | `POST /api/github/app-hook` was a forgeable webhook stub (no signature verification, logged all headers) | Deleted the dead endpoint. |
| H1 ✅ | Charge endpoints applied a client-supplied `couponId` without re-validating it applied to the product (free/near-free purchases) | Added `features/stripe/assertValidCoupon.ts` and call it in `create-subscription`, `create-v2-subscription`, `add-team-seats` before any coupon is applied. |
| H2 ✅ | `/api/validate-coupon` was unauthenticated → coupon-enumeration oracle | Added `ensureAuth`. |
| H3 ✅ | Team-seat invite minted unlimited active seats and accepted arbitrary `user_id` | Enforced seat capacity vs `total_seats`, dedup of active members, and invitee-exists check. `app/api/team-seat+api.ts` |
| H4 ✅ | `/api/github/users` returned all users incl. **email** (`select('*')`) and had a PostgREST `.or()` filter injection via comma | Returns only `id, full_name, avatar_url`; name-only `ilike` (or exact-email match if `@` present, never returned); strips comma. UI no longer renders others' emails. `app/api/github/users+api.ts`, `NewAccountModal.tsx` |
| H4b ✅ | `/api/discord/search-member` let any logged-in user enumerate the whole guild | Gated behind Pro (`ensureAccess`). |
| H5 ✅ | Hardcoded test-user password (`test-user-password-12345`) usable to sign into prod via Supabase password auth | Generates a fresh random password per run and rotates any existing user's password. `app/api/test-user+api.ts` |
Confirmed already-safe during the audit: Stripe webhook signature verification,
GitHub sponsor webhook constant-time HMAC, subscription cancel/upgrade ownership
@@ -56,6 +58,7 @@ checks, postMessage origin checks, and no server secret inlined into the client
bundle.
### CI/CD pipeline
- ✅ **Fork PRs no longer receive secrets.** `test-ios-native.yml` gated the
`secrets: inherit` build (and its dependent test job) to same-repo refs, so a
fork PR can no longer exfiltrate `KV_STORE_REDIS_REST_*`.
@@ -70,6 +73,7 @@ bundle.
SHAs current.
### Secrets hygiene / scanning
- ✅ `.gitignore`: fixed `.env.test.localp` typo and added key-material globs
(`*.pem`, `*.key`, `*.p12`, `*.p8`, `*.keystore`, …).
- Secret scanning is handled by GitHub native secret scanning + push protection
@@ -77,6 +81,7 @@ bundle.
Action plus full-tree CLI scans weren't worth the cost over native scanning.
### GitHub project configuration (applied live)
- ✅ Secret scanning + **push protection** enabled (would have blocked the A1 leak).
- ✅ Dependabot vulnerability alerts + automated security updates enabled.
- ✅ Private vulnerability reporting enabled; `SECURITY.md` added.
@@ -84,6 +89,7 @@ bundle.
`main` (org admins retain direct-push via bypass).
### npm publish
- ✅ Confirmed no published package ships an install/postinstall hook, the
lockfile is committed, and the interactive release flow supports 2FA/OTP.
+1 -1
View File
@@ -1,7 +1,7 @@
Image "types of property ''aria-hidden'' are incompatible." improve compat with real img
vite 8 monorepo fix:
- added `hanzogui-monorepo-exports-fix` plugin to `@hanzogui/vite-plugin`
- vite 8 (rolldown) resolves workspace subpath imports to filesystem dirs instead of package.json exports
- see https://github.com/vitejs/vite/issues/11676 and https://github.com/vitejs/vite/issues/20390
+9 -9
View File
@@ -195,6 +195,9 @@
"yaml": "2.8.3"
},
"packageManager": "[email protected]",
"agent": {
"memory": true
},
"manypkg": {
"workspaceProtocol": "require",
"ignoredRules": [
@@ -202,6 +205,12 @@
"UNSORTED_DEPENDENCIES"
]
},
"patchedDependencies": {
"[email protected]": "patches/[email protected]",
"[email protected]": "patches/[email protected]",
"[email protected]": "patches/[email protected]",
"[email protected]": "patches/[email protected]"
},
"upgradeSets": {
"takeout": [
"@take-out/*",
@@ -223,14 +232,5 @@
"better-auth",
"@better-auth/*"
]
},
"agent": {
"memory": true
},
"patchedDependencies": {
"[email protected]": "patches/[email protected]",
"[email protected]": "patches/[email protected]",
"[email protected]": "patches/[email protected]",
"[email protected]": "patches/[email protected]"
}
}
+3 -3
View File
@@ -1,9 +1,8 @@
{
"name": "bento-get",
"version": "8.3.1",
"license": "MIT",
"_comment_private": "npm's `bento-get` belongs to upstream (nwienert), not us \u2014 publishing it returns 403. The name survived the rebrand because it carries no `tamagui` string to rewrite, so it is the one publishable package here we do not own. It stays in the tree to keep merges with upstream clean, and is private so the release never reaches for it.",
"private": true,
"license": "MIT",
"bin": {
"bento-get": "dist/esm/cli.js"
},
@@ -66,5 +65,6 @@
},
"engines": {
"node": ">=20"
}
},
"_comment_private": "npm's `bento-get` belongs to upstream (nwienert), not us — publishing it returns 403. The name survived the rebrand because it carries no `tamagui` string to rewrite, so it is the one publishable package here we do not own. It stays in the tree to keep merges with upstream clean, and is private so the release never reaches for it."
}
+2 -8
View File
@@ -16,12 +16,6 @@
"declarationMap": false,
"declarationDir": "dist/cjs/.types"
},
"include": [
"src"
],
"exclude": [
"src/**/*.test.*",
"src/**/__tests__/**",
"src/**/*.stories.*"
]
"include": ["src"],
"exclude": ["src/**/*.test.*", "src/**/__tests__/**", "src/**/*.stories.*"]
}
+2 -8
View File
@@ -16,12 +16,6 @@
"declarationMap": true,
"declarationDir": "types"
},
"include": [
"src"
],
"exclude": [
"src/**/*.test.*",
"src/**/__tests__/**",
"src/**/*.stories.*"
]
"include": ["src"],
"exclude": ["src/**/*.test.*", "src/**/__tests__/**", "src/**/*.stories.*"]
}
+6 -6
View File
@@ -37,12 +37,12 @@ That's the whole integration. `Booker` fetches the public event type, renders a
## Components
| Component | Purpose |
| ---------------- | ----------------------------------------------------------------------- |
| `Booker` | Full scheduling flow: event → month → times → details → confirmation. |
| `CalendarEmbed` | Compact card for a marketing hero — soonest availability inline. |
| `Confirmation` | Booking confirmation card; pass a `booking` or a `uid` to fetch one. |
| `EventHeader`, `MonthCalendar`, `TimeSlots`, `BookingForm` | Composable sub-views for a custom flow. |
| Component | Purpose |
| ---------------------------------------------------------- | --------------------------------------------------------------------- |
| `Booker` | Full scheduling flow: event → month → times → details → confirmation. |
| `CalendarEmbed` | Compact card for a marketing hero — soonest availability inline. |
| `Confirmation` | Booking confirmation card; pass a `booking` or a `uid` to fetch one. |
| `EventHeader`, `MonthCalendar`, `TimeSlots`, `BookingForm` | Composable sub-views for a custom flow. |
### Props
+16 -16
View File
@@ -1,7 +1,7 @@
{
"name": "@hanzo/booker",
"version": "8.3.1",
"description": "Universal (web + native) scheduling UI for the Hanzo Calendar \u2014 a Booker, Confirmation, and compact CalendarEmbed built with hanzogui primitives.",
"description": "Universal (web + native) scheduling UI for the Hanzo Calendar — a Booker, Confirmation, and compact CalendarEmbed built with hanzogui primitives.",
"license": "MIT",
"author": "Hanzo AI <[email protected]>",
"repository": {
@@ -10,6 +10,12 @@
"directory": "pkgs/calendar"
},
"source": "src/index.ts",
"files": [
"src",
"types",
"dist",
"README.md"
],
"type": "module",
"sideEffects": false,
"main": "dist/cjs/index.js",
@@ -27,18 +33,21 @@
"default": "./dist/esm/index.js"
}
},
"files": [
"src",
"types",
"dist",
"README.md"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "hanzogui-build --skip-native",
"watch": "hanzogui-build --watch",
"clean": "hanzogui-build clean",
"clean:build": "hanzogui-build clean:build"
},
"devDependencies": {
"@hanzo/gui": "workspace:*",
"@hanzogui/build": "workspace:*",
"react": "19.2.8",
"react-native": "^0.87.1"
},
"peerDependencies": {
"@hanzo/gui": ">=8.3.1",
"react": ">=19.2.8",
@@ -48,14 +57,5 @@
"react-native": {
"optional": true
}
},
"devDependencies": {
"@hanzogui/build": "workspace:*",
"@hanzo/gui": "workspace:*",
"react": "19.2.8",
"react-native": "^0.87.1"
},
"publishConfig": {
"access": "public"
}
}
+6 -1
View File
@@ -1,7 +1,12 @@
// Orchestration hook — the one booking state machine both shells consume.
import { useEffect, useMemo, useState } from 'react'
import { DEFAULT_API_URL, type Booking, type BookingResponses, type Slot } from './client.ts'
import {
DEFAULT_API_URL,
type Booking,
type BookingResponses,
type Slot,
} from './client.ts'
import {
useAvailableSlots,
useCreateBooking,
+2 -8
View File
@@ -16,12 +16,6 @@
"declarationMap": false,
"declarationDir": "dist/cjs/.types"
},
"include": [
"src"
],
"exclude": [
"src/**/*.test.*",
"src/**/__tests__/**",
"src/**/*.stories.*"
]
"include": ["src"],
"exclude": ["src/**/*.test.*", "src/**/__tests__/**", "src/**/*.stories.*"]
}
+2 -8
View File
@@ -16,12 +16,6 @@
"declarationMap": true,
"declarationDir": "types"
},
"include": [
"src"
],
"exclude": [
"src/**/*.test.*",
"src/**/__tests__/**",
"src/**/*.stories.*"
]
"include": ["src"],
"exclude": ["src/**/*.test.*", "src/**/__tests__/**", "src/**/*.stories.*"]
}
+2 -8
View File
@@ -16,12 +16,6 @@
"declarationMap": false,
"declarationDir": "dist/cjs/.types"
},
"include": [
"src"
],
"exclude": [
"src/**/*.test.*",
"src/**/__tests__/**",
"src/**/*.stories.*"
]
"include": ["src"],
"exclude": ["src/**/*.test.*", "src/**/__tests__/**", "src/**/*.stories.*"]
}
+2 -8
View File
@@ -16,12 +16,6 @@
"declarationMap": true,
"declarationDir": "types"
},
"include": [
"src"
],
"exclude": [
"src/**/*.test.*",
"src/**/__tests__/**",
"src/**/*.stories.*"
]
"include": ["src"],
"exclude": ["src/**/*.test.*", "src/**/__tests__/**", "src/**/*.stories.*"]
}
+8 -8
View File
@@ -10,6 +10,14 @@
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"types": "./types/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"types": "./types/index.d.ts",
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js"
}
},
"publishConfig": {
"access": "public"
},
@@ -32,13 +40,5 @@
},
"devDependencies": {
"@hanzogui/build": "workspace:*"
},
"exports": {
"./package.json": "./package.json",
".": {
"types": "./types/index.d.ts",
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js"
}
}
}
+2 -8
View File
@@ -16,12 +16,6 @@
"declarationMap": false,
"declarationDir": "dist/cjs/.types"
},
"include": [
"src"
],
"exclude": [
"src/**/*.test.*",
"src/**/__tests__/**",
"src/**/*.stories.*"
]
"include": ["src"],
"exclude": ["src/**/*.test.*", "src/**/__tests__/**", "src/**/*.stories.*"]
}
+2 -8
View File
@@ -16,12 +16,6 @@
"declarationMap": true,
"declarationDir": "types"
},
"include": [
"src"
],
"exclude": [
"src/**/*.test.*",
"src/**/__tests__/**",
"src/**/*.stories.*"
]
"include": ["src"],
"exclude": ["src/**/*.test.*", "src/**/__tests__/**", "src/**/*.stories.*"]
}
+2 -8
View File
@@ -16,12 +16,6 @@
"declarationMap": false,
"declarationDir": "dist/cjs/.types"
},
"include": [
"src"
],
"exclude": [
"src/**/*.test.*",
"src/**/__tests__/**",
"src/**/*.stories.*"
]
"include": ["src"],
"exclude": ["src/**/*.test.*", "src/**/__tests__/**", "src/**/*.stories.*"]
}
+2 -8
View File
@@ -16,12 +16,6 @@
"declarationMap": true,
"declarationDir": "types"
},
"include": [
"src"
],
"exclude": [
"src/**/*.test.*",
"src/**/__tests__/**",
"src/**/*.stories.*"
]
"include": ["src"],
"exclude": ["src/**/*.test.*", "src/**/__tests__/**", "src/**/*.stories.*"]
}
+2 -8
View File
@@ -16,12 +16,6 @@
"declarationMap": false,
"declarationDir": "dist/cjs/.types"
},
"include": [
"src"
],
"exclude": [
"src/**/*.test.*",
"src/**/__tests__/**",
"src/**/*.stories.*"
]
"include": ["src"],
"exclude": ["src/**/*.test.*", "src/**/__tests__/**", "src/**/*.stories.*"]
}
+2 -8
View File
@@ -16,12 +16,6 @@
"declarationMap": true,
"declarationDir": "types"
},
"include": [
"src"
],
"exclude": [
"src/**/*.test.*",
"src/**/__tests__/**",
"src/**/*.stories.*"
]
"include": ["src"],
"exclude": ["src/**/*.test.*", "src/**/__tests__/**", "src/**/*.stories.*"]
}
+2 -8
View File
@@ -16,12 +16,6 @@
"declarationMap": false,
"declarationDir": "dist/cjs/.types"
},
"include": [
"src"
],
"exclude": [
"src/**/*.test.*",
"src/**/__tests__/**",
"src/**/*.stories.*"
]
"include": ["src"],
"exclude": ["src/**/*.test.*", "src/**/__tests__/**", "src/**/*.stories.*"]
}
+2 -8
View File
@@ -16,12 +16,6 @@
"declarationMap": true,
"declarationDir": "types"
},
"include": [
"src"
],
"exclude": [
"src/**/*.test.*",
"src/**/__tests__/**",
"src/**/*.stories.*"
]
"include": ["src"],
"exclude": ["src/**/*.test.*", "src/**/__tests__/**", "src/**/*.stories.*"]
}

Some files were not shown because too many files have changed in this diff Show More