@hanzo/build 0.2.19 → 0.2.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -7
- package/lib/api/billing.d.ts +30 -0
- package/lib/api/billing.js +16 -0
- package/lib/builder.js +1 -26
- package/lib/landing.d.ts +21 -0
- package/lib/landing.js +41 -12
- package/lib/settings/usage.js +12 -2
- package/package.json +2 -2
- package/lib/pitch.d.ts +0 -4
- package/lib/pitch.js +0 -39
- package/lib/visit.d.ts +0 -31
- package/lib/visit.js +0 -53
package/README.md
CHANGED
|
@@ -3,17 +3,25 @@
|
|
|
3
3
|
Say what you want. Hanzo writes it, runs it, and puts it on a live URL — with
|
|
4
4
|
the database, the sign-in and the storage already there.
|
|
5
5
|
|
|
6
|
-
The builder is one component, `<Builder host={…} />` from `@hanzo/build
|
|
7
|
-
|
|
8
|
-
hanzo.
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
The builder is one component, `<Builder host={…} />` from `@hanzo/build`. The
|
|
7
|
+
Hanzo app at hanzo.ai mounts it as its Dev mode, and that is where people use
|
|
8
|
+
it: hanzo.build and build.hanzo.ai redirect there, to the same place. This
|
|
9
|
+
repository's own page is the development host — the same component, signed in
|
|
10
|
+
through hanzo.id — and a click stays on its origin, with github.com opened only
|
|
11
|
+
for the repository grant.
|
|
12
|
+
|
|
13
|
+
The page is signed-in only. A visitor is drawn nothing: the tab goes to
|
|
14
|
+
hanzo.id (no popup) and comes back to the address it asked for; hanzo.id
|
|
15
|
+
answers at once, with no screen, for a person already signed in there. Words
|
|
16
|
+
carried on arrival as `?q=` wait in New's composer, and a draft sent while
|
|
17
|
+
signed out is sent once its person is in (`landing.tsx` `hold`). It signs in as
|
|
18
|
+
`hanzo-app`, the Hanzo App's client.
|
|
11
19
|
|
|
12
20
|
```
|
|
13
21
|
pnpm install
|
|
14
22
|
pnpm dev # http://localhost:3200
|
|
15
23
|
pnpm test # the transport contract and the address grammar
|
|
16
|
-
pnpm site # every
|
|
24
|
+
pnpm site # every browser spec against the dev server (SITE= for another origin)
|
|
17
25
|
pnpm e2e # one live agent turn, projected the way a run draws it
|
|
18
26
|
pnpm cover # unit tests and every browser spec against a dev server, one coverage report in coverage/
|
|
19
27
|
pnpm build:lib # lib/ — what npm ships
|
|
@@ -100,7 +108,7 @@ collide.
|
|
|
100
108
|
|
|
101
109
|
## What is on the screen
|
|
102
110
|
|
|
103
|
-
**The rail** (
|
|
111
|
+
**The rail** (this page, or `rail` left on). At the top, the mark. Signed in to an organization,
|
|
104
112
|
that spot is the organization switcher: the org, and the projects under it.
|
|
105
113
|
New, Projects, Artifacts, Customize, Automations, then More (Codebase, Issues,
|
|
106
114
|
Templates, Machines, Docs), then the org's coding runs newest first with a live
|
package/lib/api/billing.d.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* POST /v1/billing/topup charge a saved card into the balance
|
|
19
19
|
* GET /v1/billing/usage/rollup the plan's month: included spend and request windows
|
|
20
20
|
* GET /v1/usage/summary?range=month this month's spend, by category
|
|
21
|
+
* GET /v1/allowance the Free plan's allowance, and the pool it is served from
|
|
21
22
|
* GET /v1/billing/alerts spend caps (bare array)
|
|
22
23
|
* POST /v1/billing/alerts open a cap (org admin)
|
|
23
24
|
* PATCH /v1/billing/alerts/{id} change one (org admin)
|
|
@@ -183,6 +184,35 @@ export interface Spend {
|
|
|
183
184
|
}[];
|
|
184
185
|
}
|
|
185
186
|
export declare function spend(t: Target): Promise<Spend>;
|
|
187
|
+
/** The pool every free user shares, as its vendor accounts last said. */
|
|
188
|
+
export interface Pool {
|
|
189
|
+
/** available, busy or exhausted. */
|
|
190
|
+
state: string;
|
|
191
|
+
/** Accounts in the pool, and how many take a free request now. */
|
|
192
|
+
keys: number;
|
|
193
|
+
ready: number;
|
|
194
|
+
/** RFC 3339: when the first account that is out comes back; '' when none is out. */
|
|
195
|
+
resets: string;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* What the caller has left of the Free plan: limited usage, from a pool shared
|
|
199
|
+
* by all free users. `pooled` is false for a paid plan, whose usage is its spend.
|
|
200
|
+
*/
|
|
201
|
+
export interface Allowance {
|
|
202
|
+
plan: string;
|
|
203
|
+
/** Requests allowed in `window`; 0 is no bound. */
|
|
204
|
+
limit: number;
|
|
205
|
+
used: number;
|
|
206
|
+
spent: boolean;
|
|
207
|
+
/** hour or day. */
|
|
208
|
+
window: string;
|
|
209
|
+
/** RFC 3339. */
|
|
210
|
+
resets: string;
|
|
211
|
+
pooled: boolean;
|
|
212
|
+
/** Absent when the platform could not read it. */
|
|
213
|
+
pool: Pool | null;
|
|
214
|
+
}
|
|
215
|
+
export declare function allowance(t: Target): Promise<Allowance>;
|
|
186
216
|
export interface Cap {
|
|
187
217
|
id: string;
|
|
188
218
|
title: string;
|
package/lib/api/billing.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* POST /v1/billing/topup charge a saved card into the balance
|
|
19
19
|
* GET /v1/billing/usage/rollup the plan's month: included spend and request windows
|
|
20
20
|
* GET /v1/usage/summary?range=month this month's spend, by category
|
|
21
|
+
* GET /v1/allowance the Free plan's allowance, and the pool it is served from
|
|
21
22
|
* GET /v1/billing/alerts spend caps (bare array)
|
|
22
23
|
* POST /v1/billing/alerts open a cap (org admin)
|
|
23
24
|
* PATCH /v1/billing/alerts/{id} change one (org admin)
|
|
@@ -256,6 +257,21 @@ export async function spend(t) {
|
|
|
256
257
|
.filter((c) => c.cents > 0),
|
|
257
258
|
};
|
|
258
259
|
}
|
|
260
|
+
const stamp = (unix) => (num(unix) > 0 ? new Date(num(unix) * 1000).toISOString() : '');
|
|
261
|
+
export async function allowance(t) {
|
|
262
|
+
const r = obj(await call(t, 'GET', '/v1/allowance'));
|
|
263
|
+
const p = r.pool && typeof r.pool === 'object' ? obj(r.pool) : null;
|
|
264
|
+
return {
|
|
265
|
+
plan: str(r.plan),
|
|
266
|
+
limit: num(r.limit),
|
|
267
|
+
used: num(r.used),
|
|
268
|
+
spent: r.spent === true,
|
|
269
|
+
window: str(r.window),
|
|
270
|
+
resets: stamp(r.resets),
|
|
271
|
+
pooled: r.pooled === true,
|
|
272
|
+
pool: p && str(p.state) ? { state: str(p.state), keys: num(p.keys), ready: num(p.ready), resets: stamp(p.resets) } : null,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
259
275
|
export function cap(raw) {
|
|
260
276
|
const a = obj(raw);
|
|
261
277
|
return {
|
package/lib/builder.js
CHANGED
|
@@ -32,7 +32,7 @@ import { Blocks, BookOpen, CircleDot, Cpu, FolderGit2, Kanban, LayoutTemplate, M
|
|
|
32
32
|
import { Button } from '@hanzo/ui';
|
|
33
33
|
import { SessionRail } from '@hanzo/ui/chat';
|
|
34
34
|
import { HanzoMark } from '@hanzo/ui/product';
|
|
35
|
-
import {
|
|
35
|
+
import { useMemo, useState } from 'react';
|
|
36
36
|
import { pinBoard } from './choice.js';
|
|
37
37
|
import { Customize } from './customize/index.js';
|
|
38
38
|
import { useKept, useRecents } from './data.js';
|
|
@@ -49,8 +49,6 @@ import { Artifacts, Templates } from './shelf.js';
|
|
|
49
49
|
import { Find } from './find.js';
|
|
50
50
|
import { DOCS, Slack, Who } from './foot.js';
|
|
51
51
|
import { Settings } from './settings/index.js';
|
|
52
|
-
import { Pitch } from './pitch.js';
|
|
53
|
-
import { Bar } from './visit.js';
|
|
54
52
|
function Shell() {
|
|
55
53
|
const host = useHost();
|
|
56
54
|
const t = useTarget();
|
|
@@ -132,26 +130,6 @@ function Name({ onPress }) {
|
|
|
132
130
|
const { name } = useHost();
|
|
133
131
|
return (_jsx(XStack, { render: "button", "aria-label": name, items: "center", onPress: onPress, children: _jsx(SizableText, { size: "$5", fontWeight: "600", color: "$ink", numberOfLines: 1, children: name }) }));
|
|
134
132
|
}
|
|
135
|
-
/**
|
|
136
|
-
* A visitor: no runs to list, so the product's header stands where the rail
|
|
137
|
-
* would. On New the composer fills the first screen and the pitch follows it.
|
|
138
|
-
*/
|
|
139
|
-
function Visit() {
|
|
140
|
-
const host = useHost();
|
|
141
|
-
const r = route(host.path);
|
|
142
|
-
// An address that names a part of the pitch (/#features) opens scrolled to it.
|
|
143
|
-
useEffect(() => {
|
|
144
|
-
const id = window.location.hash.slice(1);
|
|
145
|
-
if (id)
|
|
146
|
-
document.getElementById(id)?.scrollIntoView();
|
|
147
|
-
}, []);
|
|
148
|
-
const start = () => {
|
|
149
|
-
const page = document.getElementById('visit');
|
|
150
|
-
page?.scrollTo({ top: 0, behavior: 'smooth' });
|
|
151
|
-
page?.querySelector('textarea, [contenteditable="true"], input[type="text"]')?.focus();
|
|
152
|
-
};
|
|
153
|
-
return (_jsxs(YStack, { flex: 1, minH: 0, minW: 0, bg: "$background", children: [_jsx(Bar, { name: _jsx(Name, { onPress: () => host.go('') }) }), r.kind === 'new' ? (_jsxs(YStack, { id: "visit", flex: 1, minH: 0, overflow: "scroll", children: [_jsx(YStack, { shrink: 0, style: { height: 'calc(100dvh - 60px)' }, children: _jsx(Pane, {}) }), _jsx(Pitch, { onStart: start })] })) : (_jsx(Pane, {}))] }));
|
|
154
|
-
}
|
|
155
133
|
/** Whichever pane the address names, beside a rail — the builder's or the host's. */
|
|
156
134
|
function Pane({ onStarted }) {
|
|
157
135
|
const host = useHost();
|
|
@@ -190,9 +168,6 @@ function Screens({ rail }) {
|
|
|
190
168
|
return _jsx(Project, { slug: r.slug }, r.slug);
|
|
191
169
|
if (!rail)
|
|
192
170
|
return _jsx(Pane, {});
|
|
193
|
-
// Signed out with no token to wait on: a token still resolving is a person, not a visitor.
|
|
194
|
-
if (!host.person && !host.token() && host.signIn)
|
|
195
|
-
return _jsx(Visit, {});
|
|
196
171
|
return _jsx(Shell, {});
|
|
197
172
|
}
|
|
198
173
|
/**
|
package/lib/landing.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Attached } from './tools.tsx';
|
|
1
2
|
/** One vocabulary for the mode, on New and in a workspace. */
|
|
2
3
|
export declare const MODES: readonly [{
|
|
3
4
|
readonly id: 'build';
|
|
@@ -18,6 +19,26 @@ export declare const EFFORTS: readonly [{
|
|
|
18
19
|
readonly id: 'high';
|
|
19
20
|
readonly label: 'High';
|
|
20
21
|
}];
|
|
22
|
+
/**
|
|
23
|
+
* What was typed here and not sent, kept in this tab.
|
|
24
|
+
*
|
|
25
|
+
* Signing in leaves the page for hanzo.id and comes back (app/enter.ts), and the
|
|
26
|
+
* sentence is still in the composer after. A draft its person SENT while signed
|
|
27
|
+
* out is sent on the return: pressing Send was the ask, and the sign-in stood
|
|
28
|
+
* between it and the run. Only this page writes `sent`, so no link can start a
|
|
29
|
+
* run on somebody's behalf — an address carrying words only fills the composer.
|
|
30
|
+
*
|
|
31
|
+
* sessionStorage, because a draft belongs to the tab it was typed in and not to
|
|
32
|
+
* the next person at this browser; a browser that refuses storage keeps it for
|
|
33
|
+
* the life of the page, as before.
|
|
34
|
+
*/
|
|
35
|
+
export interface Unsent {
|
|
36
|
+
draft: string;
|
|
37
|
+
files: Attached[];
|
|
38
|
+
sent?: boolean;
|
|
39
|
+
}
|
|
40
|
+
/** Hold what New has unsent, or forget it once there is nothing. */
|
|
41
|
+
export declare function hold(u: Unsent): void;
|
|
21
42
|
export declare function Landing({ onStarted }: {
|
|
22
43
|
onStarted: (session: string) => void;
|
|
23
44
|
}): import("react").JSX.Element;
|
package/lib/landing.js
CHANGED
|
@@ -44,13 +44,32 @@ export const EFFORTS = [
|
|
|
44
44
|
];
|
|
45
45
|
const FIRST = { repo: null, branch: '', place: '', mode: 'build', model: ENSO, effort: 'medium', ask: '' };
|
|
46
46
|
const COLUMN = 768;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
const UNSENT = 'hanzo.build.unsent';
|
|
48
|
+
let held = { draft: '', files: [] };
|
|
49
|
+
function unsent() {
|
|
50
|
+
try {
|
|
51
|
+
const v = JSON.parse(window.sessionStorage.getItem(UNSENT) ?? 'null');
|
|
52
|
+
if (v && typeof v.draft === 'string' && Array.isArray(v.files))
|
|
53
|
+
return v;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
/* a browser that refuses storage, or a value that is not ours */
|
|
57
|
+
}
|
|
58
|
+
return held;
|
|
59
|
+
}
|
|
60
|
+
/** Hold what New has unsent, or forget it once there is nothing. */
|
|
61
|
+
export function hold(u) {
|
|
62
|
+
held = u;
|
|
63
|
+
try {
|
|
64
|
+
if (!u.draft && !u.files.length)
|
|
65
|
+
window.sessionStorage.removeItem(UNSENT);
|
|
66
|
+
else
|
|
67
|
+
window.sessionStorage.setItem(UNSENT, JSON.stringify(u));
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
/* kept for the life of the page */
|
|
71
|
+
}
|
|
72
|
+
}
|
|
54
73
|
export function Landing({ onStarted }) {
|
|
55
74
|
const host = useHost();
|
|
56
75
|
const t = useTarget();
|
|
@@ -60,11 +79,10 @@ export function Landing({ onStarted }) {
|
|
|
60
79
|
// Nothing chosen here yet: start from the person's coding defaults (Settings → Code).
|
|
61
80
|
const kept = stored ?? { ...FIRST, ...prefs.code };
|
|
62
81
|
const set = (patch) => keep({ ...kept, ...patch });
|
|
63
|
-
const [
|
|
64
|
-
const [
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}, [draft, files]);
|
|
82
|
+
const [was] = useState(unsent);
|
|
83
|
+
const [draft, setDraft] = useState(kept.ask || was.draft);
|
|
84
|
+
const [files, setFiles] = useState(was.files);
|
|
85
|
+
useEffect(() => hold({ draft, files }), [draft, files]);
|
|
68
86
|
const [busy, setBusy] = useState(false);
|
|
69
87
|
const [note, setNote] = useState('');
|
|
70
88
|
const [adding, setAdding] = useState(null);
|
|
@@ -105,6 +123,7 @@ export function Landing({ onStarted }) {
|
|
|
105
123
|
// The composer sends only a draft with words in it, and not while a send is out.
|
|
106
124
|
const send = async () => {
|
|
107
125
|
if (!signed) {
|
|
126
|
+
hold({ draft, files, sent: true });
|
|
108
127
|
host.signIn?.();
|
|
109
128
|
return;
|
|
110
129
|
}
|
|
@@ -139,6 +158,16 @@ export function Landing({ onStarted }) {
|
|
|
139
158
|
setBusy(false);
|
|
140
159
|
}
|
|
141
160
|
};
|
|
161
|
+
// A draft sent while signed out goes once its person is in, and only once.
|
|
162
|
+
const owed = useRef(was.sent === true);
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
if (!signed || !owed.current || !draft.trim())
|
|
165
|
+
return;
|
|
166
|
+
owed.current = false;
|
|
167
|
+
void send();
|
|
168
|
+
// Sent once, on the sign-in that it waited for.
|
|
169
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
170
|
+
}, [signed]);
|
|
142
171
|
/** Start the agent that finds this codebase's environment, and open it. A refusal is the dialog's to say. */
|
|
143
172
|
const setup = async () => {
|
|
144
173
|
const run = await start(t, { prompt: SETUP, repo: codebase, mode: 'setup' });
|
package/lib/settings/usage.js
CHANGED
|
@@ -8,7 +8,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
8
8
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
9
9
|
import { Button, Input } from '@hanzo/ui';
|
|
10
10
|
import { useState } from 'react';
|
|
11
|
-
import { balance, caps, cents, chosen, credit, label, lift, limit, methods, money, month, monthly, spend, topup, } from '../api/billing.js';
|
|
11
|
+
import { allowance, balance, caps, cents, chosen, credit, label, lift, limit, methods, money, month, monthly, spend, topup, } from '../api/billing.js';
|
|
12
12
|
import { useRead } from '../data.js';
|
|
13
13
|
import { useHost, useTarget } from '../host.js';
|
|
14
14
|
import { path } from '../route.js';
|
|
@@ -40,11 +40,21 @@ function resets(iso) {
|
|
|
40
40
|
return `Resets in ${Math.floor(mins / 60)} hr ${mins % 60} min`;
|
|
41
41
|
return `Resets ${day(iso)}`;
|
|
42
42
|
}
|
|
43
|
+
const POOL = { available: 'Available', busy: 'Busy', exhausted: 'Exhausted' };
|
|
44
|
+
/** `Refills at 16:00 UTC`, for a window or the pool: the UTC time it opens again, with the date when not today. */
|
|
45
|
+
function refills(iso) {
|
|
46
|
+
const at = iso ? new Date(iso) : null;
|
|
47
|
+
if (!at || Number.isNaN(at.getTime()))
|
|
48
|
+
return '';
|
|
49
|
+
const hhmm = `${String(at.getUTCHours()).padStart(2, '0')}:${String(at.getUTCMinutes()).padStart(2, '0')} UTC`;
|
|
50
|
+
return at.toISOString().slice(0, 10) === new Date().toISOString().slice(0, 10) ? `Refills at ${hhmm}` : `Refills ${day(iso)}, ${hhmm}`;
|
|
51
|
+
}
|
|
43
52
|
export function Usage() {
|
|
44
53
|
const host = useHost();
|
|
45
54
|
const t = useTarget();
|
|
46
55
|
const signed = Boolean(host.person);
|
|
47
56
|
const plan = useRead(signed ? () => month(t) : null, null, [t, signed]);
|
|
57
|
+
const free = useRead(signed ? () => allowance(t) : null, null, [t, signed]);
|
|
48
58
|
const left = useRead(signed ? () => balance(t) : null, 0, [t, signed]);
|
|
49
59
|
const granted = useRead(signed ? () => credit(t) : null, 0, [t, signed]);
|
|
50
60
|
const spent = useRead(signed ? () => spend(t) : null, null, [t, signed]);
|
|
@@ -103,7 +113,7 @@ export function Usage() {
|
|
|
103
113
|
ceilings.reload();
|
|
104
114
|
});
|
|
105
115
|
};
|
|
106
|
-
return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: "Usage", detail: `What ${host.org ?? 'this organization'} has used, has left, and may spend.` }), _jsx(Group, { title: "Plan usage limits", detail: m?.plan ? `Included with ${m.plan}.` : 'What the plan includes.', children: plan.error ? (_jsx(Soft, { children: plan.error.message })) : plan.loading && !m ? (_jsx(Soft, { children: "Reading the plan\u2019s limits\u2026" })) : !m || (windows.length === 0 && m.included === 0) ? (_jsx(Card, { children: _jsx(Soft, { children: "This plan sets no usage limits. Usage is paid from the balance." }) })) : (_jsxs(Card, { children: [m.included > 0 ? (_jsx(Meter, { first: true, title: "Included spend", used: m.used, of: m.included, says: `${money(m.used)} of ${money(m.included)}${m.overage ? ` · ${money(m.overage)} over` : ''}` })) : null, windows.map((w, i) => (_jsx(Meter, { first: i === 0 && m.included === 0, title: `${SPAN[w.span] ?? w.span} · ${w.used.toLocaleString()} of ${w.limit.toLocaleString()} requests`, used: w.used, of: w.limit, says: `${Math.round((Math.min(w.used, w.limit) / w.limit) * 100)}% used${resets(w.resets) ? ` · ${resets(w.resets)}` : ''}` }, w.span)))] })) }), _jsx(Group, { title: "Balance", detail: "Prepaid money pays for usage past the plan. Credits are spent first.", action: buying || !host.admin ? undefined : (_jsx(Button, { size: "sm", variant: "outline", onPress: () => setBuying(true), children: "Buy more" })), children: _jsxs(Card, { children: [_jsx(Row, { first: true, title: "Balance", detail: "What is left to spend", trailing: _jsx(Figure, { read: left }) }), _jsx(Row, { title: "Credits", detail: "Granted, and spent before the balance", trailing: _jsx(Figure, { read: granted }) }), _jsx(Row, { title: "Spent this month", detail: s && !s.known ? 'The ledger did not answer, so this is not a measurement' : undefined, trailing: _jsx(Figure, { read: { ...spent, value: s?.total ?? 0 } }) }), buying ? (_jsx(YStack, { gap: "$2", px: "$3", py: "$3", borderTopWidth: 1, borderColor: "$borderColor", children: card ? (_jsxs(_Fragment, { children: [_jsxs(XStack, { gap: "$2", items: "center", flexWrap: "wrap", children: [_jsx(YStack, { width: 140, children: _jsx(Input, { value: amount, onChangeText: setAmount, "aria-label": "Amount in dollars", placeholder: "25", inputMode: "decimal" }) }), _jsxs(SizableText, { flex: 1, minW: 160, size: "$1", color: "$soft", children: ["Charged to ", label(card)] })] }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setBuying(false), children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working, onPress: buy, children: adding ? `Add ${money(adding)}` : 'Add funds' })] })] })) : (_jsxs(XStack, { gap: "$2", items: "center", flexWrap: "wrap", children: [_jsx(SizableText, { flex: 1, minW: 160, size: "$2", color: "$soft", children: cards.loading ? 'Reading cards…' : 'Add a card in Billing to buy more.' }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'settings', section: 'billing' })), children: "Billing" })] })) })) : null] }) }), _jsx(Group, { title: "Spend this month", detail: "By what it paid for.", children: spent.error ? (_jsx(Soft, { children: spent.error.message })) : !s ? (_jsx(Soft, { children: "Reading spend\u2026" })) : s.categories.length === 0 ? (_jsx(Card, { children: _jsx(Soft, { children: "Nothing spent this month." }) })) : (_jsx(Card, { children: s.categories.map((c, i) => (_jsx(Meter, { first: i === 0, title: c.name, used: c.cents, of: top, says: money(c.cents), limit: false }, c.name))) })) }), _jsxs(Group, { title: "Monthly limit", detail: "Spend past it is refused until the month starts over.", children: [ceilings.error ? (_jsx(Soft, { children: ceilings.error.message })) : (_jsxs(Card, { children: [cap ? (_jsx(Meter, { first: true, title: `${money(cap.threshold)} a month`, used: cap.spent ?? 0, of: cap.threshold, says: [cap.spent === null ? 'Spend unknown' : `${money(cap.spent)} spent`, resets(cap.resets)].filter(Boolean).join(' · ') })) : (_jsx(Row, { first: true, title: "No monthly limit", detail: "Spend is bounded by the balance alone." })), others.map((c) => (_jsx(Row, { title: c.title || 'Limit', detail: [c.project && `project ${c.project}`, c.service && `service ${c.service}`, c.enforce ? 'refuses spend past it' : 'warns only']
|
|
116
|
+
return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: "Usage", detail: `What ${host.org ?? 'this organization'} has used, has left, and may spend.` }), free.value?.pooled && free.value.limit > 0 ? (_jsx(Group, { title: "Free plan", detail: "Free \u2014 limited usage, from a pool shared by all free users.", action: _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'screen', screen: 'plans' })), children: "Upgrade" }), children: _jsxs(Card, { children: [_jsx(Meter, { first: true, title: `${Math.max(0, free.value.limit - free.value.used).toLocaleString()} of ${free.value.limit.toLocaleString()} left ${free.value.window === 'hour' ? 'this hour' : free.value.window === 'day' ? 'today' : 'this period'}`, used: free.value.used, of: free.value.limit, says: [`${free.value.used.toLocaleString()} used`, refills(free.value.resets)].filter(Boolean).join(' · ') }), free.value.pool ? (_jsx(Row, { title: "Shared pool", detail: `${free.value.pool.ready} of ${free.value.pool.keys} accounts serving${free.value.pool.state === 'exhausted' && refills(free.value.pool.resets) ? ` · ${refills(free.value.pool.resets)}` : ''}`, trailing: _jsx(SizableText, { size: "$2", color: free.value.pool.state === 'exhausted' ? '$red10' : '$ink', children: POOL[free.value.pool.state] ?? free.value.pool.state }) })) : null] }) })) : null, _jsx(Group, { title: "Plan usage limits", detail: m?.plan ? `Included with ${m.plan}.` : 'What the plan includes.', children: plan.error ? (_jsx(Soft, { children: plan.error.message })) : plan.loading && !m ? (_jsx(Soft, { children: "Reading the plan\u2019s limits\u2026" })) : !m || (windows.length === 0 && m.included === 0) ? (_jsx(Card, { children: _jsx(Soft, { children: "This plan sets no usage limits. Usage is paid from the balance." }) })) : (_jsxs(Card, { children: [m.included > 0 ? (_jsx(Meter, { first: true, title: "Included spend", used: m.used, of: m.included, says: `${money(m.used)} of ${money(m.included)}${m.overage ? ` · ${money(m.overage)} over` : ''}` })) : null, windows.map((w, i) => (_jsx(Meter, { first: i === 0 && m.included === 0, title: `${SPAN[w.span] ?? w.span} · ${w.used.toLocaleString()} of ${w.limit.toLocaleString()} requests`, used: w.used, of: w.limit, says: `${Math.round((Math.min(w.used, w.limit) / w.limit) * 100)}% used${resets(w.resets) ? ` · ${resets(w.resets)}` : ''}` }, w.span)))] })) }), _jsx(Group, { title: "Balance", detail: "Prepaid money pays for usage past the plan. Credits are spent first.", action: buying || !host.admin ? undefined : (_jsx(Button, { size: "sm", variant: "outline", onPress: () => setBuying(true), children: "Buy more" })), children: _jsxs(Card, { children: [_jsx(Row, { first: true, title: "Balance", detail: "What is left to spend", trailing: _jsx(Figure, { read: left }) }), _jsx(Row, { title: "Credits", detail: "Granted, and spent before the balance", trailing: _jsx(Figure, { read: granted }) }), _jsx(Row, { title: "Spent this month", detail: s && !s.known ? 'The ledger did not answer, so this is not a measurement' : undefined, trailing: _jsx(Figure, { read: { ...spent, value: s?.total ?? 0 } }) }), buying ? (_jsx(YStack, { gap: "$2", px: "$3", py: "$3", borderTopWidth: 1, borderColor: "$borderColor", children: card ? (_jsxs(_Fragment, { children: [_jsxs(XStack, { gap: "$2", items: "center", flexWrap: "wrap", children: [_jsx(YStack, { width: 140, children: _jsx(Input, { value: amount, onChangeText: setAmount, "aria-label": "Amount in dollars", placeholder: "25", inputMode: "decimal" }) }), _jsxs(SizableText, { flex: 1, minW: 160, size: "$1", color: "$soft", children: ["Charged to ", label(card)] })] }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setBuying(false), children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working, onPress: buy, children: adding ? `Add ${money(adding)}` : 'Add funds' })] })] })) : (_jsxs(XStack, { gap: "$2", items: "center", flexWrap: "wrap", children: [_jsx(SizableText, { flex: 1, minW: 160, size: "$2", color: "$soft", children: cards.loading ? 'Reading cards…' : 'Add a card in Billing to buy more.' }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => host.go(path({ kind: 'settings', section: 'billing' })), children: "Billing" })] })) })) : null] }) }), _jsx(Group, { title: "Spend this month", detail: "By what it paid for.", children: spent.error ? (_jsx(Soft, { children: spent.error.message })) : !s ? (_jsx(Soft, { children: "Reading spend\u2026" })) : s.categories.length === 0 ? (_jsx(Card, { children: _jsx(Soft, { children: "Nothing spent this month." }) })) : (_jsx(Card, { children: s.categories.map((c, i) => (_jsx(Meter, { first: i === 0, title: c.name, used: c.cents, of: top, says: money(c.cents), limit: false }, c.name))) })) }), _jsxs(Group, { title: "Monthly limit", detail: "Spend past it is refused until the month starts over.", children: [ceilings.error ? (_jsx(Soft, { children: ceilings.error.message })) : (_jsxs(Card, { children: [cap ? (_jsx(Meter, { first: true, title: `${money(cap.threshold)} a month`, used: cap.spent ?? 0, of: cap.threshold, says: [cap.spent === null ? 'Spend unknown' : `${money(cap.spent)} spent`, resets(cap.resets)].filter(Boolean).join(' · ') })) : (_jsx(Row, { first: true, title: "No monthly limit", detail: "Spend is bounded by the balance alone." })), others.map((c) => (_jsx(Row, { title: c.title || 'Limit', detail: [c.project && `project ${c.project}`, c.service && `service ${c.service}`, c.enforce ? 'refuses spend past it' : 'warns only']
|
|
107
117
|
.filter(Boolean)
|
|
108
118
|
.join(' · '), trailing: _jsx(SizableText, { size: "$2", color: "$ink", children: c.threshold ? money(c.threshold) : '—' }) }, c.id))), host.admin ? (_jsxs(XStack, { gap: "$2", items: "center", px: "$3", py: "$3", borderTopWidth: 1, borderColor: "$borderColor", flexWrap: "wrap", children: [_jsx(YStack, { width: 160, children: _jsx(Input, { value: ceiling, onChangeText: setCeiling, "aria-label": "Monthly limit in dollars", placeholder: cap ? String(cap.threshold / 100) : '500', inputMode: "decimal" }) }), _jsxs(XStack, { flex: 1, gap: "$2", justify: "flex-end", children: [cap ? (_jsx(Button, { size: "sm", variant: "ghost", disabled: working, onPress: () => void act(() => lift(t, cap.id), 'The monthly limit is lifted', ceilings.reload), children: "Remove" })) : null, _jsx(Button, { size: "sm", variant: "outline", disabled: working || !ceiling.trim(), onPress: setLimit, children: cap ? 'Change limit' : 'Set limit' })] })] })) : null] })), !host.admin ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "An org admin sets the monthly limit." })) : null] }), _jsx(Note, { children: note })] }));
|
|
109
119
|
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/build",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.21",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0 OR MIT",
|
|
6
|
-
"description": "Hanzo Build — say what you want, watch it built, deployed and running. The builder as a
|
|
6
|
+
"description": "Hanzo Build — say what you want, watch it built, deployed and running. The builder as a component, mounted by the Hanzo app at hanzo.ai.",
|
|
7
7
|
"engines": {
|
|
8
8
|
"node": ">=22.18"
|
|
9
9
|
},
|
package/lib/pitch.d.ts
DELETED
package/lib/pitch.js
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
/**
|
|
3
|
-
* What a visitor reads under New on hanzo.build: the same agent on their own
|
|
4
|
-
* computer, one line to install, and what it brings to a run. Each card names
|
|
5
|
-
* something this page or the CLI does today, and nothing it does not.
|
|
6
|
-
*/
|
|
7
|
-
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
8
|
-
import { Check, Copy, GitPullRequest, LayoutTemplate, ListChecks, MonitorPlay, Plug, Server, Sparkles, Workflow } from '@hanzogui/lucide-icons-2';
|
|
9
|
-
import { Button } from '@hanzo/ui';
|
|
10
|
-
import { useState } from 'react';
|
|
11
|
-
import { DOCS } from './foot.js';
|
|
12
|
-
export const INSTALL = 'curl -fsSL hanzo.sh | sh';
|
|
13
|
-
const FEATURES = [
|
|
14
|
-
{ icon: _jsx(Sparkles, { size: 18 }), title: 'Skills', body: 'Teach it a workflow once. A skill loads when a task matches, or when you call it by name.' },
|
|
15
|
-
{ icon: _jsx(ListChecks, { size: 18 }), title: 'Plan first', body: 'Start a run in plan mode. Nothing is built until you approve the plan.' },
|
|
16
|
-
{ icon: _jsx(Plug, { size: 18 }), title: 'Connectors and plugins', body: 'MCP servers, skills and agents behind one install. Your org’s credentials reach a run through a relay, never the sandbox.' },
|
|
17
|
-
{ icon: _jsx(Server, { size: 18 }), title: 'Environments', body: 'Point it at a repository. An agent works out install and start, and every later run begins from that setup.' },
|
|
18
|
-
{ icon: _jsx(MonitorPlay, { size: 18 }), title: 'Desktop and terminal', body: 'Watch each run’s browser and shell live, beside the conversation.' },
|
|
19
|
-
{ icon: _jsx(GitPullRequest, { size: 18 }), title: 'Review', body: 'Every run ends as a diff on its own branch, with commits and a pull request to review.' },
|
|
20
|
-
{ icon: _jsx(LayoutTemplate, { size: 18 }), title: 'Templates to live', body: 'Start from a working app and publish it to a live address.' },
|
|
21
|
-
{ icon: _jsx(Workflow, { size: 18 }), title: 'Automations', body: 'Hand it repeating work, and it runs on your schedule.' },
|
|
22
|
-
];
|
|
23
|
-
/** One line to copy, with a button that copies it. */
|
|
24
|
-
function Line({ text }) {
|
|
25
|
-
const [copied, setCopied] = useState(false);
|
|
26
|
-
const copy = () => {
|
|
27
|
-
void navigator.clipboard.writeText(text).then(() => {
|
|
28
|
-
setCopied(true);
|
|
29
|
-
setTimeout(() => setCopied(false), 1500);
|
|
30
|
-
});
|
|
31
|
-
};
|
|
32
|
-
return (_jsxs(XStack, { items: "center", gap: "$3", px: "$4", height: 52, rounded: "$4", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", width: "100%", maxW: 560, children: [_jsx(SizableText, { size: "$3", color: "$soft", fontFamily: "$mono", "aria-hidden": true, children: "$" }), _jsx(SizableText, { flex: 1, size: "$3", color: "$ink", fontFamily: "$mono", numberOfLines: 1, children: text }), _jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": copied ? 'Copied' : 'Copy the install command', onPress: copy, children: copied ? _jsx(Check, { size: 16 }) : _jsx(Copy, { size: 16 }) })] }));
|
|
33
|
-
}
|
|
34
|
-
function Card({ icon, title, body }) {
|
|
35
|
-
return (_jsxs(YStack, { gap: "$2", p: "$5", rounded: "$4", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", flex: 1, minW: 240, children: [_jsxs(XStack, { items: "center", gap: "$2.5", children: [icon, _jsx(SizableText, { size: "$4", fontWeight: "600", color: "$ink", children: title })] }), _jsx(SizableText, { size: "$3", color: "$soft", children: body })] }));
|
|
36
|
-
}
|
|
37
|
-
export function Pitch({ onStart }) {
|
|
38
|
-
return (_jsxs(YStack, { items: "center", px: "$4", pb: "$12", gap: "$12", children: [_jsxs(YStack, { id: "features", width: "100%", maxW: 1080, items: "center", gap: "$5", pt: "$6", children: [_jsx(SizableText, { size: "$2", color: "$soft", fontFamily: "$mono", children: "Powered by Zen 6 \u00B7 routed by Enso" }), _jsx(SizableText, { role: "heading", "aria-level": 2, size: "$9", color: "$ink", style: { textAlign: 'center' }, children: "Hanzo Dev" }), _jsx(SizableText, { size: "$4", color: "$soft", maxW: 620, style: { textAlign: 'center' }, children: "The agent. It runs in your terminal, on any codebase, in any language. This page is that same agent in the browser." }), _jsx(Line, { text: INSTALL }), _jsx(SizableText, { size: "$2", color: "$soft", fontFamily: "$mono", children: "dev opens it. dev \"fix the failing test\" hands it a task." }), _jsx("a", { href: DOCS, style: { color: 'inherit', textDecoration: 'none' }, children: _jsx(XStack, { items: "center", height: 34, px: "$3", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", hoverStyle: { bg: '$hover' }, children: _jsx(SizableText, { size: "$3", color: "$ink", children: "Read the docs" }) }) })] }), _jsxs(YStack, { width: "100%", maxW: 1080, gap: "$5", children: [_jsx(SizableText, { role: "heading", "aria-level": 2, size: "$7", color: "$ink", children: "Everything a run needs" }), _jsx(XStack, { flexWrap: "wrap", gap: "$3", children: FEATURES.map((f) => (_jsx(Card, { ...f }, f.title))) })] }), _jsxs(YStack, { width: "100%", maxW: 1080, items: "center", gap: "$4", py: "$8", rounded: "$6", borderWidth: 1, borderColor: "$borderColor", bg: "$panel", children: [_jsx(SizableText, { role: "heading", "aria-level": 2, size: "$7", color: "$ink", style: { textAlign: 'center' }, children: "Say what you want built." }), _jsx(SizableText, { size: "$3", color: "$soft", style: { textAlign: 'center' }, px: "$4", children: "In the browser here, or in your terminal." }), _jsx(Button, { variant: "primary", size: "sm", onPress: onStart, children: "Start building" })] })] }));
|
|
39
|
-
}
|
package/lib/visit.d.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { type ReactNode } from 'react';
|
|
2
|
-
export declare const PLACES: readonly [{
|
|
3
|
-
readonly label: 'Download';
|
|
4
|
-
readonly href: '/#features';
|
|
5
|
-
}, {
|
|
6
|
-
readonly label: 'Enterprise';
|
|
7
|
-
readonly href: "https://hanzo.ai/enterprise";
|
|
8
|
-
}, {
|
|
9
|
-
readonly label: 'Pricing';
|
|
10
|
-
readonly href: "https://hanzo.ai/pricing";
|
|
11
|
-
}];
|
|
12
|
-
export declare const RESOURCES: readonly [{
|
|
13
|
-
readonly label: 'Docs';
|
|
14
|
-
readonly href: 'https://docs.hanzo.ai';
|
|
15
|
-
}, {
|
|
16
|
-
readonly label: 'Blog';
|
|
17
|
-
readonly href: "https://hanzo.ai/blog";
|
|
18
|
-
}, {
|
|
19
|
-
readonly label: 'Customers';
|
|
20
|
-
readonly href: "https://hanzo.ai/customers";
|
|
21
|
-
}, {
|
|
22
|
-
readonly label: 'Learn';
|
|
23
|
-
readonly href: "https://hanzo.ai/learn";
|
|
24
|
-
}, {
|
|
25
|
-
readonly label: 'Support';
|
|
26
|
-
readonly href: "https://hanzo.ai/support";
|
|
27
|
-
}];
|
|
28
|
-
/** The header a visitor sees. `name` is the wordmark the rail shows once signed in. */
|
|
29
|
-
export declare function Bar({ name }: {
|
|
30
|
-
name: ReactNode;
|
|
31
|
-
}): import("react").JSX.Element;
|
package/lib/visit.js
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
/**
|
|
3
|
-
* The page to a visitor: the product's header over New.
|
|
4
|
-
*
|
|
5
|
-
* Signed out there are no runs to list, so the rail gives way to a header —
|
|
6
|
-
* Download (Hanzo Dev, under New, pitch.tsx), Resources, Enterprise, Pricing — and the two
|
|
7
|
-
* doors in, which are one door: IAM's page offers both. New stays the page, and
|
|
8
|
-
* sending from it asks the visitor to sign in with the sentence kept
|
|
9
|
-
* (landing.tsx). Below md the four places fold into one menu beside Log in.
|
|
10
|
-
*/
|
|
11
|
-
import { Popover, SizableText, XStack, YStack } from '@hanzo/gui';
|
|
12
|
-
import { ChevronDown, Menu } from '@hanzogui/lucide-icons-2';
|
|
13
|
-
import { Button } from '@hanzo/ui';
|
|
14
|
-
import { MenuLabel, MenuRow, MenuRule } from '@hanzo/ui/product';
|
|
15
|
-
import { useState } from 'react';
|
|
16
|
-
import { useHost } from './host.js';
|
|
17
|
-
const SITE = 'https://hanzo.ai';
|
|
18
|
-
export const PLACES = [
|
|
19
|
-
{ label: 'Download', href: '/#features' },
|
|
20
|
-
{ label: 'Enterprise', href: `${SITE}/enterprise` },
|
|
21
|
-
{ label: 'Pricing', href: `${SITE}/pricing` },
|
|
22
|
-
];
|
|
23
|
-
export const RESOURCES = [
|
|
24
|
-
{ label: 'Docs', href: 'https://docs.hanzo.ai' },
|
|
25
|
-
{ label: 'Blog', href: `${SITE}/blog` },
|
|
26
|
-
{ label: 'Customers', href: `${SITE}/customers` },
|
|
27
|
-
{ label: 'Learn', href: `${SITE}/learn` },
|
|
28
|
-
{ label: 'Support', href: `${SITE}/support` },
|
|
29
|
-
];
|
|
30
|
-
const leave = (href) => window.location.assign(href);
|
|
31
|
-
/** One place in the header: an address, drawn as text. */
|
|
32
|
-
function Place({ href, children }) {
|
|
33
|
-
return (_jsx("a", { href: href, style: { color: 'inherit', textDecoration: 'none' }, children: _jsx(XStack, { items: "center", height: 34, px: "$2.5", rounded: "$3", hoverStyle: { bg: '$hover' }, children: _jsx(SizableText, { size: "$3", color: "$ink", children: children }) }) }));
|
|
34
|
-
}
|
|
35
|
-
function Resources() {
|
|
36
|
-
const [open, setOpen] = useState(false);
|
|
37
|
-
return (_jsxs(Popover, { open: open, onOpenChange: setOpen, placement: "bottom-start", offset: 6, children: [_jsx(Popover.Trigger, { asChild: true, children: _jsxs(XStack, { render: "button", "aria-label": "Resources", "aria-expanded": open, items: "center", gap: "$1", height: 34, px: "$2.5", rounded: "$3", hoverStyle: { bg: '$hover' }, children: [_jsx(SizableText, { size: "$3", color: "$ink", children: "Resources" }), _jsx(ChevronDown, { size: 14, color: "$soft" })] }) }), _jsx(Popover.Content, { role: "menu", "aria-label": "Resources", items: "stretch", borderWidth: 1, elevation: "$2", p: "$1", width: 220, bg: "$panel", borderColor: "$borderColor", children: _jsx(YStack, { gap: "$1", children: RESOURCES.map((r) => (_jsx(MenuRow, { label: r.label, onPress: () => leave(r.href) }, r.label))) }) })] }));
|
|
38
|
-
}
|
|
39
|
-
/** Below md: every place, the resources and the door, in one menu. */
|
|
40
|
-
function Fold({ onEnter }) {
|
|
41
|
-
const [open, setOpen] = useState(false);
|
|
42
|
-
return (_jsxs(Popover, { open: open, onOpenChange: setOpen, placement: "bottom-end", offset: 6, stayInFrame: true, children: [_jsx(Popover.Trigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Menu", children: _jsx(Menu, { size: 18 }) }) }), _jsx(Popover.Content, { role: "menu", "aria-label": "Menu", items: "stretch", borderWidth: 1, elevation: "$2", p: "$1", width: 260, bg: "$panel", borderColor: "$borderColor", children: _jsxs(YStack, { gap: "$1", children: [PLACES.map((p) => (_jsx(MenuRow, { label: p.label, onPress: () => leave(p.href) }, p.label))), _jsx(MenuRule, {}), _jsx(MenuLabel, { children: "Resources" }), RESOURCES.map((r) => (_jsx(MenuRow, { label: r.label, onPress: () => leave(r.href) }, r.label))), _jsx(MenuRule, {}), _jsx(MenuRow, { label: "Sign up", onPress: () => {
|
|
43
|
-
setOpen(false);
|
|
44
|
-
onEnter();
|
|
45
|
-
} })] }) })] }));
|
|
46
|
-
}
|
|
47
|
-
/** The header a visitor sees. `name` is the wordmark the rail shows once signed in. */
|
|
48
|
-
export function Bar({ name }) {
|
|
49
|
-
const host = useHost();
|
|
50
|
-
const enter = () => host.signIn?.();
|
|
51
|
-
const [features, enterprise, pricing] = PLACES;
|
|
52
|
-
return (_jsxs(XStack, { role: "banner", height: 60, px: "$4", gap: "$2", items: "center", shrink: 0, $md: { px: '$6' }, children: [name, _jsxs(XStack, { role: "navigation", "aria-label": "Site", items: "center", gap: "$1", ml: "$4", "$max-md": { display: 'none' }, children: [_jsx(Place, { href: features.href, children: features.label }), _jsx(Resources, {}), _jsx(Place, { href: enterprise.href, children: enterprise.label }), _jsx(Place, { href: pricing.href, children: pricing.label })] }), _jsx(XStack, { flex: 1 }), _jsx(Button, { variant: "ghost", size: "sm", onPress: enter, children: "Log in" }), _jsx(XStack, { "$max-md": { display: 'none' }, children: _jsx(Button, { size: "sm", variant: "primary", onPress: enter, children: "Sign up" }) }), _jsx(XStack, { $md: { display: 'none' }, children: _jsx(Fold, { onEnter: enter }) })] }));
|
|
53
|
-
}
|