@hanzo/build 0.2.0 → 0.2.1
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 +26 -7
- package/lib/builder.d.ts +8 -2
- package/lib/builder.js +31 -13
- package/lib/claims.d.ts +13 -0
- package/lib/claims.js +30 -0
- package/lib/host.d.ts +5 -5
- package/lib/host.js +5 -5
- package/lib/index.d.ts +7 -2
- package/lib/index.js +5 -2
- package/lib/section.d.ts +20 -0
- package/lib/section.js +47 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
The builder is one component, `<Builder host={…} />` from `@hanzo/build`, with
|
|
7
|
+
three hosts: this page (hanzo.build), and the Dev section of the Hanzo app at
|
|
8
|
+
hanzo.ai/dev and hanzo.app/dev. None of them forks it.
|
|
9
9
|
|
|
10
10
|
```
|
|
11
11
|
pnpm install
|
|
@@ -42,6 +42,25 @@ under the mount (`path`) and how to move (`go`), and where to link out
|
|
|
42
42
|
(`links`, `open`). Peers: `@hanzo/ui`, `@hanzo/gui`, `@hanzogui/lucide-icons-2`,
|
|
43
43
|
`react`.
|
|
44
44
|
|
|
45
|
+
### In a host that has its own rail
|
|
46
|
+
|
|
47
|
+
The Hanzo app already has a left column, so it mounts `<Builder host={host}
|
|
48
|
+
rail={false} />` in its pane and lists the builder in its own rail:
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
import { Builder, DevSection } from '@hanzo/build'
|
|
52
|
+
|
|
53
|
+
<Sidebar> {/* the host's rail, @hanzo/ui/chat */}
|
|
54
|
+
<DevSection host={host} /> {/* New run, Artifacts, Templates, the runs */}
|
|
55
|
+
</Sidebar>
|
|
56
|
+
<Builder host={host} rail={false} />
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`DevSection` is rows of `@hanzo/ui/chat`'s Sidebar; `useSessions(host)` is its
|
|
60
|
+
data — the org's coding runs, kept live by the org's feed — for a host that
|
|
61
|
+
draws the rows itself. `administers(token, org)` reads the org-admin bit off a
|
|
62
|
+
hanzo.id token for `Host.admin`. One left column, never two.
|
|
63
|
+
|
|
45
64
|
## Addresses
|
|
46
65
|
|
|
47
66
|
| path | screen |
|
|
@@ -57,10 +76,10 @@ collide.
|
|
|
57
76
|
|
|
58
77
|
## What is on the screen
|
|
59
78
|
|
|
60
|
-
**The rail
|
|
61
|
-
(Templates, Machines, Docs), then the org's coding runs newest
|
|
62
|
-
live status dot, and the account. Collapse is an explicit toggle
|
|
63
|
-
browser.
|
|
79
|
+
**The rail** (hanzo.build). New, Artifacts, Customize (the platform's
|
|
80
|
+
plugins), More (Templates, Machines, Docs), then the org's coding runs newest
|
|
81
|
+
first with a live status dot, and the account. Collapse is an explicit toggle
|
|
82
|
+
kept in this browser. A host with its own rail draws `DevSection` there instead.
|
|
64
83
|
|
|
65
84
|
**New.** "What's up next?", and at the foot the composer: where the run runs
|
|
66
85
|
(Default is the platform's sandbox; the org's machines follow), the repository
|
package/lib/builder.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { type Host } from './host.tsx';
|
|
2
|
-
/**
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* The builder. Mount it under a gui root (`<Hanzo>`), in a box with a height.
|
|
4
|
+
*
|
|
5
|
+
* `rail={false}` leaves the left column to the host, which lists the builder's
|
|
6
|
+
* places and runs in its own rail with `DevSection`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function Builder({ host, rail }: {
|
|
4
9
|
host: Host;
|
|
10
|
+
rail?: boolean;
|
|
5
11
|
}): import("react").JSX.Element;
|
package/lib/builder.js
CHANGED
|
@@ -14,6 +14,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
14
14
|
*
|
|
15
15
|
* A project's workspace takes the whole window — it has its own chat column,
|
|
16
16
|
* bar and panes — and its mark leads back here.
|
|
17
|
+
*
|
|
18
|
+
* A host with a rail of its own mounts `<Builder rail={false}>` and draws the
|
|
19
|
+
* builder's places and runs in that rail with `DevSection` (section.tsx): one
|
|
20
|
+
* left column, never two.
|
|
17
21
|
*/
|
|
18
22
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
19
23
|
import { Blocks, BookOpen, Cpu, LayoutTemplate, Menu, Puzzle } from '@hanzogui/lucide-icons-2';
|
|
@@ -27,15 +31,9 @@ import { Landing } from './landing.js';
|
|
|
27
31
|
import { Project } from './project.js';
|
|
28
32
|
import { path, route } from './route.js';
|
|
29
33
|
import { Run } from './run.js';
|
|
34
|
+
import { DOTS } from './section.js';
|
|
30
35
|
import { Artifacts, Templates } from './shelf.js';
|
|
31
36
|
import { Account, Find } from './account.js';
|
|
32
|
-
const DOTS = {
|
|
33
|
-
running: 'running',
|
|
34
|
-
paused: 'paused',
|
|
35
|
-
done: 'done',
|
|
36
|
-
stopped: 'stopped',
|
|
37
|
-
error: 'error',
|
|
38
|
-
};
|
|
39
37
|
function Shell() {
|
|
40
38
|
const host = useHost();
|
|
41
39
|
const t = useTarget();
|
|
@@ -68,14 +66,34 @@ function Shell() {
|
|
|
68
66
|
];
|
|
69
67
|
return (_jsxs(XStack, { flex: 1, minH: 0, minW: 0, bg: "$background", children: [_jsx(SessionRail, { onNew: () => go(''), fresh: r.kind === 'new', links: links, more: more, recents: rows, active: r.kind === 'run' ? r.id : null, onOpen: (id) => go(id), onSort: () => setOrder(order === 'newest' ? 'running' : 'newest'), sortLabel: order === 'newest' ? 'Show running first' : 'Show newest first', empty: _jsx(SizableText, { size: "$1", color: "$soft", px: "$2", children: !host.person ? 'Sign in to see your runs.' : recents.error ? recents.error.message : recents.loading ? 'Reading…' : 'No runs yet.' }), account: host.person
|
|
70
68
|
? { name: host.person.email || host.person.name, onPress: () => setAccount(true) }
|
|
71
|
-
: { name: 'Sign in', onPress: () => host.signIn?.() }, onSettings: () => host.open(host.links.settings), onSearch: () => setFinding(true), collapsed: collapsed, onCollapse: setCollapsed, mark: _jsx(HanzoMark, { size: 18 }), open: drawer, onOpenChange: setDrawer, label: "Runs" }), _jsxs(YStack, { flex: 1, minW: 0, minH: 0, position: "relative", children: [_jsx(XStack, { height: 44, px: "$2", items: "center", shrink: 0, $md: { display: 'none' }, children: _jsx(Button, { variant: "ghost", size: "icon-sm", onPress: () => setDrawer(true), "aria-label": "Open runs", children: _jsx(Menu, { size: 18 }) }) }),
|
|
69
|
+
: { name: 'Sign in', onPress: () => host.signIn?.() }, onSettings: () => host.open(host.links.settings), onSearch: () => setFinding(true), collapsed: collapsed, onCollapse: setCollapsed, mark: _jsx(HanzoMark, { size: 18 }), open: drawer, onOpenChange: setDrawer, label: "Runs" }), _jsxs(YStack, { flex: 1, minW: 0, minH: 0, position: "relative", children: [_jsx(XStack, { height: 44, px: "$2", items: "center", shrink: 0, $md: { display: 'none' }, children: _jsx(Button, { variant: "ghost", size: "icon-sm", onPress: () => setDrawer(true), "aria-label": "Open runs", children: _jsx(Menu, { size: 18 }) }) }), _jsx(Pane, { onStarted: () => recents.reload() })] }), _jsx(Account, { open: account, onOpenChange: setAccount }), _jsx(Find, { open: finding, onOpenChange: setFinding, recents: rows, onOpen: (id) => { setFinding(false); go(id); } })] }));
|
|
70
|
+
}
|
|
71
|
+
/** Whichever pane the address names, beside a rail — the builder's or the host's. */
|
|
72
|
+
function Pane({ onStarted }) {
|
|
73
|
+
const host = useHost();
|
|
74
|
+
const r = route(host.path);
|
|
75
|
+
if (r.kind === 'run')
|
|
76
|
+
return _jsx(Run, { id: r.id }, r.id);
|
|
77
|
+
if (r.kind === 'screen')
|
|
78
|
+
return r.screen === 'artifacts' ? _jsx(Artifacts, {}) : _jsx(Templates, {});
|
|
79
|
+
return (_jsx(Landing, { onStarted: (id) => {
|
|
80
|
+
onStarted?.(id);
|
|
81
|
+
host.go(id);
|
|
82
|
+
} }));
|
|
72
83
|
}
|
|
73
|
-
function Screens() {
|
|
84
|
+
function Screens({ rail }) {
|
|
74
85
|
const host = useHost();
|
|
75
86
|
const r = route(host.path);
|
|
76
|
-
|
|
87
|
+
if (r.kind === 'project')
|
|
88
|
+
return _jsx(Project, { slug: r.slug }, r.slug);
|
|
89
|
+
return rail ? _jsx(Shell, {}) : _jsx(Pane, {});
|
|
77
90
|
}
|
|
78
|
-
/**
|
|
79
|
-
|
|
80
|
-
|
|
91
|
+
/**
|
|
92
|
+
* The builder. Mount it under a gui root (`<Hanzo>`), in a box with a height.
|
|
93
|
+
*
|
|
94
|
+
* `rail={false}` leaves the left column to the host, which lists the builder's
|
|
95
|
+
* places and runs in its own rail with `DevSection`.
|
|
96
|
+
*/
|
|
97
|
+
export function Builder({ host, rail = true }) {
|
|
98
|
+
return (_jsx(HostProvider, { host: host, children: _jsx(YStack, { flex: 1, minH: 0, minW: 0, height: "100%", bg: "$background", children: _jsx(Screens, { rail: rail }) }) }));
|
|
81
99
|
}
|
package/lib/claims.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a hanzo.id access token says, read without trusting it for anything the
|
|
3
|
+
* platform does not check again.
|
|
4
|
+
*/
|
|
5
|
+
/** The token's claims, or null when it is not a JWT. */
|
|
6
|
+
export declare function claims(token: string | null): Record<string, unknown> | null;
|
|
7
|
+
/**
|
|
8
|
+
* Whether the token says its person administers `org`. Org admin, read off the
|
|
9
|
+
* token's own `orgs` claim — never the reserved `admin` org, which is platform
|
|
10
|
+
* authority and not what a builder decides anything by. The platform enforces
|
|
11
|
+
* it again; this only decides which publish path is offered.
|
|
12
|
+
*/
|
|
13
|
+
export declare function administers(token: string | null, org: string | null): boolean;
|
package/lib/claims.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a hanzo.id access token says, read without trusting it for anything the
|
|
3
|
+
* platform does not check again.
|
|
4
|
+
*/
|
|
5
|
+
/** The token's claims, or null when it is not a JWT. */
|
|
6
|
+
export function claims(token) {
|
|
7
|
+
if (!token)
|
|
8
|
+
return null;
|
|
9
|
+
try {
|
|
10
|
+
const part = token.split('.')[1];
|
|
11
|
+
if (!part)
|
|
12
|
+
return null;
|
|
13
|
+
return JSON.parse(atob(part.replace(/-/g, '+').replace(/_/g, '/')));
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Whether the token says its person administers `org`. Org admin, read off the
|
|
21
|
+
* token's own `orgs` claim — never the reserved `admin` org, which is platform
|
|
22
|
+
* authority and not what a builder decides anything by. The platform enforces
|
|
23
|
+
* it again; this only decides which publish path is offered.
|
|
24
|
+
*/
|
|
25
|
+
export function administers(token, org) {
|
|
26
|
+
if (!org)
|
|
27
|
+
return false;
|
|
28
|
+
const set = claims(token)?.orgs;
|
|
29
|
+
return (Array.isArray(set) ? set : []).some((r) => r?.org === org && r.role === 'admin');
|
|
30
|
+
}
|
package/lib/host.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* What the builder needs from the page that mounts it.
|
|
3
3
|
*
|
|
4
|
-
* The builder is ONE component with
|
|
5
|
-
* signs in through Hanzo IAM and routes with react-router) and the
|
|
6
|
-
*
|
|
7
|
-
* Everything that differs between them is here,
|
|
8
|
-
* forks the builder and the builder imports
|
|
4
|
+
* The builder is ONE component with several hosts: its own page (hanzo.build,
|
|
5
|
+
* which signs in through Hanzo IAM and routes with react-router) and the Hanzo
|
|
6
|
+
* app's Dev section (hanzo.ai/dev and hanzo.app/dev, which already have a
|
|
7
|
+
* session, a router and a rail). Everything that differs between them is here,
|
|
8
|
+
* as values, so no host forks the builder and the builder imports no host.
|
|
9
9
|
*/
|
|
10
10
|
import { type ReactNode } from 'react';
|
|
11
11
|
import type { Target } from './api/call.ts';
|
package/lib/host.js
CHANGED
|
@@ -2,11 +2,11 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
/**
|
|
3
3
|
* What the builder needs from the page that mounts it.
|
|
4
4
|
*
|
|
5
|
-
* The builder is ONE component with
|
|
6
|
-
* signs in through Hanzo IAM and routes with react-router) and the
|
|
7
|
-
*
|
|
8
|
-
* Everything that differs between them is here,
|
|
9
|
-
* forks the builder and the builder imports
|
|
5
|
+
* The builder is ONE component with several hosts: its own page (hanzo.build,
|
|
6
|
+
* which signs in through Hanzo IAM and routes with react-router) and the Hanzo
|
|
7
|
+
* app's Dev section (hanzo.ai/dev and hanzo.app/dev, which already have a
|
|
8
|
+
* session, a router and a rail). Everything that differs between them is here,
|
|
9
|
+
* as values, so no host forks the builder and the builder imports no host.
|
|
10
10
|
*/
|
|
11
11
|
import { createContext, useContext, useMemo } from 'react';
|
|
12
12
|
const Context = createContext(null);
|
package/lib/index.d.ts
CHANGED
|
@@ -3,9 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Mount `<Builder host={…} />` under a gui root in a box with a height. The
|
|
5
5
|
* host says where the platform is, who is signed in, and how to move; the
|
|
6
|
-
* builder does everything else. hanzo.build
|
|
7
|
-
*
|
|
6
|
+
* builder does everything else. hanzo.build hosts it whole; hanzo.ai and the
|
|
7
|
+
* Hanzo App mount it at /dev with `rail={false}` and list its places and runs in
|
|
8
|
+
* their own rail with `DevSection`.
|
|
8
9
|
*/
|
|
9
10
|
export { Builder } from './builder.tsx';
|
|
11
|
+
export { DevSection, DOTS, useSessions } from './section.tsx';
|
|
12
|
+
export { administers } from './claims.ts';
|
|
10
13
|
export type { Host, Person } from './host.tsx';
|
|
14
|
+
export type { Session, Status } from './api/sessions.ts';
|
|
15
|
+
export type { Read } from './data.ts';
|
|
11
16
|
export { path, route, SESSION, SLUG, type Route, type Screen } from './route.ts';
|
package/lib/index.js
CHANGED
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Mount `<Builder host={…} />` under a gui root in a box with a height. The
|
|
5
5
|
* host says where the platform is, who is signed in, and how to move; the
|
|
6
|
-
* builder does everything else. hanzo.build
|
|
7
|
-
*
|
|
6
|
+
* builder does everything else. hanzo.build hosts it whole; hanzo.ai and the
|
|
7
|
+
* Hanzo App mount it at /dev with `rail={false}` and list its places and runs in
|
|
8
|
+
* their own rail with `DevSection`.
|
|
8
9
|
*/
|
|
9
10
|
export { Builder } from './builder.js';
|
|
11
|
+
export { DevSection, DOTS, useSessions } from './section.js';
|
|
12
|
+
export { administers } from './claims.js';
|
|
10
13
|
export { path, route, SESSION, SLUG } from './route.js';
|
package/lib/section.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type SessionStatus } from '@hanzo/ui/chat';
|
|
2
|
+
import { type ReactNode } from 'react';
|
|
3
|
+
import type { Session } from './api/sessions.ts';
|
|
4
|
+
import { type Read } from './data.ts';
|
|
5
|
+
import type { Host } from './host.tsx';
|
|
6
|
+
/** A run's status, as the rail's dot draws it. Anything else is idle. */
|
|
7
|
+
export declare const DOTS: Record<string, SessionStatus>;
|
|
8
|
+
/** The org's coding runs, newest first, kept live by the org's feed. */
|
|
9
|
+
export declare function useSessions(host: Host): Read<Session[]>;
|
|
10
|
+
/**
|
|
11
|
+
* New, Artifacts, Templates, then the runs. `children` sit between the places
|
|
12
|
+
* and the runs — where a host lists its projects. `onPick` fires after a move,
|
|
13
|
+
* so a host that draws its rail as a drawer on a phone can close it.
|
|
14
|
+
*/
|
|
15
|
+
export declare function DevSection({ host, onPick, label, children, }: {
|
|
16
|
+
host: Host;
|
|
17
|
+
onPick?: () => void;
|
|
18
|
+
label?: string;
|
|
19
|
+
children?: ReactNode;
|
|
20
|
+
}): import("react").JSX.Element;
|
package/lib/section.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The builder's places and runs, for a host that draws its own rail.
|
|
4
|
+
*
|
|
5
|
+
* A host that mounts `<Builder rail={false}>` keeps ONE left column — its own —
|
|
6
|
+
* and lists the builder in it with these. `useSessions` is the same read and
|
|
7
|
+
* the same org-wide feed the built-in rail uses, so a run started in the pane
|
|
8
|
+
* appears here with its live status. `DevSection` draws the builder's places
|
|
9
|
+
* and runs as rows of the host's sidebar (the `@hanzo/ui/chat` Sidebar the
|
|
10
|
+
* built-in rail is made of), and every row moves the builder through the
|
|
11
|
+
* host's own `go`.
|
|
12
|
+
*/
|
|
13
|
+
import { SizableText } from '@hanzo/gui';
|
|
14
|
+
import { Blocks, LayoutTemplate, SquarePen } from '@hanzogui/lucide-icons-2';
|
|
15
|
+
import { SidebarItem, SidebarSection, StatusDot } from '@hanzo/ui/chat';
|
|
16
|
+
import { useMemo } from 'react';
|
|
17
|
+
import { useRecents } from './data.js';
|
|
18
|
+
import { path, route } from './route.js';
|
|
19
|
+
/** A run's status, as the rail's dot draws it. Anything else is idle. */
|
|
20
|
+
export const DOTS = {
|
|
21
|
+
running: 'running',
|
|
22
|
+
paused: 'paused',
|
|
23
|
+
done: 'done',
|
|
24
|
+
stopped: 'stopped',
|
|
25
|
+
error: 'error',
|
|
26
|
+
};
|
|
27
|
+
/** The org's coding runs, newest first, kept live by the org's feed. */
|
|
28
|
+
export function useSessions(host) {
|
|
29
|
+
const t = useMemo(() => ({ api: host.api, token: host.token, org: host.org }), [host.api, host.token, host.org]);
|
|
30
|
+
return useRecents(t, Boolean(host.person));
|
|
31
|
+
}
|
|
32
|
+
const Note = ({ children }) => (_jsx(SizableText, { size: "$1", color: "$soft", px: "$2", py: "$1", children: children }));
|
|
33
|
+
/**
|
|
34
|
+
* New, Artifacts, Templates, then the runs. `children` sit between the places
|
|
35
|
+
* and the runs — where a host lists its projects. `onPick` fires after a move,
|
|
36
|
+
* so a host that draws its rail as a drawer on a phone can close it.
|
|
37
|
+
*/
|
|
38
|
+
export function DevSection({ host, onPick, label = 'Runs', children, }) {
|
|
39
|
+
const sessions = useSessions(host);
|
|
40
|
+
const r = route(host.path);
|
|
41
|
+
const go = (p) => {
|
|
42
|
+
host.go(p);
|
|
43
|
+
onPick?.();
|
|
44
|
+
};
|
|
45
|
+
const screen = r.kind === 'screen' ? r.screen : '';
|
|
46
|
+
return (_jsxs(_Fragment, { children: [_jsx(SidebarItem, { icon: _jsx(SquarePen, { size: 16, "aria-hidden": true }), active: r.kind === 'new', onPress: () => go(''), children: "New run" }), _jsx(SidebarItem, { icon: _jsx(Blocks, { size: 16, "aria-hidden": true }), active: screen === 'artifacts', onPress: () => go(path({ kind: 'screen', screen: 'artifacts' })), children: "Artifacts" }), _jsx(SidebarItem, { icon: _jsx(LayoutTemplate, { size: 16, "aria-hidden": true }), active: screen === 'templates', onPress: () => go(path({ kind: 'screen', screen: 'templates' })), children: "Templates" }), children, _jsx(SidebarSection, { label: label, children: !host.person ? (_jsx(Note, { children: "Sign in to see your runs." })) : sessions.value.length ? (sessions.value.map((s) => (_jsx(SidebarItem, { icon: _jsx(StatusDot, { status: DOTS[s.status] ?? 'idle' }), active: r.kind === 'run' && r.id === s.id, onPress: () => go(s.id), children: s.title || 'Untitled run' }, s.id)))) : (_jsx(Note, { children: sessions.error ? sessions.error.message : sessions.loading ? 'Reading…' : 'No runs yet. Say what to build and it lands here.' })) })] }));
|
|
47
|
+
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hanzo/build",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
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 page (hanzo.build) and as a component (
|
|
6
|
+
"description": "Hanzo Build — say what you want, watch it built, deployed and running. The builder as a page (hanzo.build) and as a component (hanzo.ai/dev).",
|
|
7
7
|
"engines": {
|
|
8
8
|
"node": ">=22.18"
|
|
9
9
|
},
|