Merge pull request #115 from webzard-io/runtime

add playground to editor
This commit is contained in:
yz-yu 2021-11-06 15:32:51 +08:00 committed by GitHub
commit 25b234c233
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 358 additions and 121 deletions

View File

@ -4,6 +4,12 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>meta-ui runtime example: basic grid layout</title>
<style>
#root {
width: 100vw;
height: 100vh;
}
</style>
</head>
<body>
<div id="root"></div>

View File

@ -38,6 +38,7 @@
},
"devDependencies": {
"@babel/preset-react": "^7.14.5",
"@meta-ui/vite-plugins": "^1.0.0",
"@types/codemirror": "^5.60.5",
"@types/lodash-es": "^4.17.5",
"@vitejs/plugin-react": "^1.0.1",

View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en" style="overflow: hidden">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>meta-ui playground</title>
</head>
<body>
<div id="root"></div>
<script type="module">
import renderPlayground from './src/playground.tsx';
import examples from '@example.json';
renderPlayground(examples);
</script>
</body>
</html>

View File

@ -6,7 +6,6 @@ import { TSchema } from '@sinclair/typebox';
import { Application } from '@meta-ui/core';
import { parseType, parseTypeBox } from '@meta-ui/runtime';
import { eventBus } from '../../eventBus';
import { registry } from '../../metaUI';
import {
ModifyComponentIdOperation,
ModifyComponentPropertyOperation,
@ -15,8 +14,13 @@ import {
import { EventTraitForm } from './EventTraitForm';
import { GeneralTraitFormList } from './GeneralTraitFormList';
import { FetchTraitForm } from './FetchTraitForm';
import { Registry } from '@meta-ui/runtime/lib/services/registry';
type Props = { selectedId: string; app: Application };
type Props = {
registry: Registry;
selectedId: string;
app: Application;
};
export const renderField = (properties: {
key: string;
@ -58,7 +62,7 @@ export const renderField = (properties: {
};
export const ComponentForm: React.FC<Props> = props => {
const { selectedId, app } = props;
const { selectedId, app, registry } = props;
const selectedComponent = app.spec.components.find(c => c.id === selectedId);
if (!selectedComponent) {
@ -110,9 +114,9 @@ export const ComponentForm: React.FC<Props> = props => {
/>
</FormControl>
{propertyFields.length > 0 ? propertyForm : null}
<EventTraitForm component={selectedComponent} />
<EventTraitForm component={selectedComponent} registry={registry} />
<FetchTraitForm component={selectedComponent} />
<GeneralTraitFormList component={selectedComponent} />
<GeneralTraitFormList component={selectedComponent} registry={registry} />
</VStack>
);
};

View File

@ -13,12 +13,13 @@ import { Static } from '@sinclair/typebox';
import { CloseIcon } from '@chakra-ui/icons';
import { useFormik } from 'formik';
import { EventHandlerSchema } from '@meta-ui/runtime';
import { registry } from '../../../metaUI';
import { useAppModel } from '../../../operations/useAppModel';
import { formWrapperCSS } from '../style';
import { KeyValueEditor } from '../../KeyValueEditor';
import { Registry } from '@meta-ui/runtime/lib/services/registry';
type Props = {
registry: Registry;
eventTypes: string[];
handler: Static<typeof EventHandlerSchema>;
onChange: (hanlder: Static<typeof EventHandlerSchema>) => void;
@ -27,7 +28,7 @@ type Props = {
};
export const EventHandlerForm: React.FC<Props> = props => {
const { handler, eventTypes, onChange, onRemove, hideEventType } = props;
const { handler, eventTypes, onChange, onRemove, hideEventType, registry } = props;
const { app } = useAppModel();
const [methods, setMethods] = useState<string[]>([]);

View File

@ -6,21 +6,22 @@ import produce from 'immer';
import { ApplicationComponent } from '@meta-ui/core';
import { EventHandlerSchema } from '@meta-ui/runtime';
import { eventBus } from '../../../eventBus';
import { registry } from '../../../metaUI';
import {
AddTraitOperation,
ModifyTraitPropertyOperation,
} from '../../../operations/Operations';
import { EventHandlerForm } from './EventHandlerForm';
import { Registry } from '@meta-ui/runtime/lib/services/registry';
type EventHandler = Static<typeof EventHandlerSchema>;
type Props = {
registry: Registry;
component: ApplicationComponent;
};
export const EventTraitForm: React.FC<Props> = props => {
const { component } = props;
const { component, registry } = props;
const handlers: EventHandler[] = useMemo(() => {
return component.traits.find(t => t.type === 'core/v1/event')?.properties
@ -63,46 +64,48 @@ export const EventTraitForm: React.FC<Props> = props => {
}
};
const handlerForms = (handlers || []).map((h, i) => {
const onChange = (handler: EventHandler) => {
const newHanlders = produce(handlers!, draft => {
draft[i] = handler;
});
eventBus.send(
'operation',
new ModifyTraitPropertyOperation(
component.id,
'core/v1/event',
'handlers',
newHanlders
)
);
};
const handlerForms = () =>
(handlers || []).map((h, i) => {
const onChange = (handler: EventHandler) => {
const newHanlders = produce(handlers!, draft => {
draft[i] = handler;
});
eventBus.send(
'operation',
new ModifyTraitPropertyOperation(
component.id,
'core/v1/event',
'handlers',
newHanlders
)
);
};
const onRemove = () => {
const newHanlders = produce(handlers!, draft => {
draft.splice(i, 1);
});
eventBus.send(
'operation',
new ModifyTraitPropertyOperation(
component.id,
'core/v1/event',
'handlers',
newHanlders
)
const onRemove = () => {
const newHanlders = produce(handlers!, draft => {
draft.splice(i, 1);
});
eventBus.send(
'operation',
new ModifyTraitPropertyOperation(
component.id,
'core/v1/event',
'handlers',
newHanlders
)
);
};
return (
<EventHandlerForm
key={i}
handler={h}
eventTypes={eventTypes}
onChange={onChange}
onRemove={onRemove}
registry={registry}
/>
);
};
return (
<EventHandlerForm
key={i}
handler={h}
eventTypes={eventTypes}
onChange={onChange}
onRemove={onRemove}
/>
);
});
});
return (
<VStack width="full">

View File

@ -1,14 +1,15 @@
import { AddIcon, ChevronDownIcon } from '@chakra-ui/icons';
import { IconButton, Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/react';
import { Registry } from '@meta-ui/runtime/lib/services/registry';
import { useMemo } from 'react';
import { registry } from '../../../metaUI';
type Props = {
registry: Registry;
onAddTrait: (traitType: string) => void;
};
export const AddTraitButton: React.FC<Props> = props => {
const { onAddTrait } = props;
const { onAddTrait, registry } = props;
const traitTypes = useMemo(() => {
return registry.getAllTraitTypes();

View File

@ -5,16 +5,17 @@ import { CloseIcon } from '@chakra-ui/icons';
import { TSchema } from '@sinclair/typebox';
import { renderField } from '../ComponentForm';
import { formWrapperCSS } from '../style';
import { registry } from '../../../metaUI';
import { Registry } from '@meta-ui/runtime/lib/services/registry';
type Props = {
registry: Registry;
component: ApplicationComponent;
trait: ComponentTrait;
onRemove: () => void;
};
export const GeneralTraitForm: React.FC<Props> = props => {
const { trait, component, onRemove } = props;
const { trait, component, onRemove, registry } = props;
const tImpl = registry.getTraitByType(trait.type);
const properties = Object.assign(

View File

@ -5,16 +5,17 @@ import { TSchema } from '@sinclair/typebox';
import { AddTraitButton } from './AddTraitButton';
import { GeneralTraitForm } from './GeneralTraitForm';
import { eventBus } from '../../../eventBus';
import { registry } from '../../../metaUI';
import { AddTraitOperation, RemoveTraitOperation } from '../../../operations/Operations';
import { ignoreTraitsList } from '../../../constants';
import { Registry } from '@meta-ui/runtime/lib/services/registry';
type Props = {
registry: Registry;
component: ApplicationComponent;
};
export const GeneralTraitFormList: React.FC<Props> = props => {
const { component } = props;
const { component, registry } = props;
const onAddTrait = (type: string) => {
const traitSpec = registry.getTraitByType(type).spec;
@ -36,6 +37,7 @@ export const GeneralTraitFormList: React.FC<Props> = props => {
component={component}
trait={t}
onRemove={onRemoveTrait}
registry={registry}
/>
);
});
@ -44,7 +46,7 @@ export const GeneralTraitFormList: React.FC<Props> = props => {
<VStack width="full" alignItems="start">
<HStack width="full" justify="space-between">
<strong>Traits</strong>
<AddTraitButton onAddTrait={onAddTrait} />
<AddTraitButton onAddTrait={onAddTrait} registry={registry} />
</HStack>
{traitFields}
</VStack>

View File

@ -10,9 +10,13 @@ import {
Box,
} from '@chakra-ui/react';
import { encodeDragDataTransfer, DROP_EXAMPLE_SIZE_PREFIX } from '@meta-ui/runtime';
import { registry } from '../../metaUI';
import { Registry } from '@meta-ui/runtime/lib/services/registry';
export const ComponentList: React.FC = () => {
type Props = {
registry: Registry;
};
export const ComponentList: React.FC<Props> = ({ registry }) => {
return (
<Tabs>
<TabList>

View File

@ -1,8 +1,7 @@
import { useEffect, useMemo, useState } from 'react';
import { GridCallbacks, DIALOG_CONTAINER_ID } from '@meta-ui/runtime';
import { GridCallbacks, DIALOG_CONTAINER_ID, initMetaUI } from '@meta-ui/runtime';
import produce from 'immer';
import { Box, Tabs, TabList, Tab, TabPanels, TabPanel } from '@chakra-ui/react';
import { App, stateStore } from '../metaUI';
import { StructureTree } from './StructureTree';
import {
CreateComponentOperation,
@ -11,14 +10,29 @@ import {
import { eventBus, SelectComponentEvent } from '../eventBus';
import { ComponentForm } from './ComponentForm';
import { ComponentList } from './ComponentsList';
import { appModelManager, useAppModel } from '../operations/useAppModel';
import { useAppModel } from '../operations/useAppModel';
import { EditorHeader } from './EditorHeader';
import { PreviewModal } from './PreviewModal';
import { KeyboardEventWrapper } from './KeyboardEventWrapper';
import { ComponentWrapper } from './ComponentWrapper';
import { StateEditor } from './CodeEditor';
import { AppModelManager } from '../operations/AppModelManager';
export const Editor = () => {
type ReturnOfInit = ReturnType<typeof initMetaUI>;
type Props = {
App: ReturnOfInit['App'];
registry: ReturnOfInit['registry'];
stateStore: ReturnOfInit['stateManager']['store'];
appModelManager: AppModelManager;
};
export const Editor: React.FC<Props> = ({
App,
registry,
stateStore,
appModelManager,
}) => {
const [selectedComponentId, setSelectedComponentId] = useState('');
const [scale, setScale] = useState(100);
const [preview, setPreview] = useState(false);
@ -76,7 +90,7 @@ export const Editor = () => {
return (
<KeyboardEventWrapper selectedComponentId={selectedComponentId}>
<Box display="flex" height="100vh" width="100vw" flexDirection="column">
<Box display="flex" height="100%" width="100%" flexDirection="column">
<EditorHeader
scale={scale}
setScale={setScale}
@ -102,6 +116,7 @@ export const Editor = () => {
app={app}
selectedComponentId={selectedComponentId}
onSelectComponent={id => setSelectedComponentId(id)}
registry={registry}
/>
</TabPanel>
<TabPanel p={0} height="100%">
@ -140,10 +155,14 @@ export const Editor = () => {
</TabList>
<TabPanels flex="1" overflow="auto">
<TabPanel p={0}>
<ComponentForm app={app} selectedId={selectedComponentId} />
<ComponentForm
app={app}
selectedId={selectedComponentId}
registry={registry}
/>
</TabPanel>
<TabPanel p={0}>
<ComponentList />
<ComponentList registry={registry} />
</TabPanel>
</TabPanels>
</Tabs>

View File

@ -12,6 +12,9 @@ export const KeyboardEventWrapper: React.FC<Props> = props => {
&:focus {
outline: none;
}
width: 100%;
height: 100%;
`;
const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {

View File

@ -1,8 +1,8 @@
import { Box, Text, VStack } from '@chakra-ui/react';
import { ApplicationComponent } from '@meta-ui/core';
import { Registry } from '@meta-ui/runtime/lib/services/registry';
import React, { useMemo, useState } from 'react';
import { eventBus } from '../../eventBus';
import { registry } from '../../metaUI';
import {
CreateComponentOperation,
RemoveComponentOperation,
@ -13,6 +13,7 @@ import { DropComponentWrapper } from './DropComponentWrapper';
import { ChildrenMap } from './StructureTree';
type Props = {
registry: Registry;
component: ApplicationComponent;
childrenMap: ChildrenMap;
selectedComponentId: string;
@ -20,7 +21,8 @@ type Props = {
};
export const ComponentTree: React.FC<Props> = props => {
const { component, childrenMap, selectedComponentId, onSelectComponent } = props;
const { component, childrenMap, selectedComponentId, onSelectComponent, registry } =
props;
const slots = registry.getComponentByType(component.type).spec.slots;
const [isExpanded, setIsExpanded] = useState(true);
@ -41,6 +43,7 @@ export const ComponentTree: React.FC<Props> = props => {
childrenMap={childrenMap}
selectedComponentId={selectedComponentId}
onSelectComponent={onSelectComponent}
registry={registry}
/>
);
});

View File

@ -9,18 +9,20 @@ import {
import { ComponentItemView } from './ComponentItemView';
import { ComponentTree } from './ComponentTree';
import { DropComponentWrapper } from './DropComponentWrapper';
import { Registry } from '@meta-ui/runtime/lib/services/registry';
export type ChildrenMap = Map<string, SlotsMap>;
type SlotsMap = Map<string, ApplicationComponent[]>;
type Props = {
registry: Registry;
app: Application;
selectedComponentId: string;
onSelectComponent: (id: string) => void;
};
export const StructureTree: React.FC<Props> = props => {
const { app, selectedComponentId, onSelectComponent } = props;
const { app, selectedComponentId, onSelectComponent, registry } = props;
const topLevelComponents: ApplicationComponent[] = [];
const childrenMap: ChildrenMap = new Map();
@ -52,6 +54,7 @@ export const StructureTree: React.FC<Props> = props => {
childrenMap={childrenMap}
selectedComponentId={selectedComponentId}
onSelectComponent={onSelectComponent}
registry={registry}
/>
));
const dataSourcesEles = dataSources.map(dummy => {

View File

@ -1,16 +1,32 @@
import { ChakraProvider } from '@chakra-ui/react';
import { Application } from '@meta-ui/core';
import { initMetaUI } from '@meta-ui/runtime';
import { StrictMode } from 'react';
import ReactDOM from 'react-dom';
import 'react-grid-layout/css/styles.css';
import 'react-resizable/css/styles.css';
import { Editor } from './components/Editor';
import { DefaultAppSchema } from './constants';
import { AppModelManager } from './operations/AppModelManager';
export default function renderApp(app: Application = DefaultAppSchema) {
const metaUI = initMetaUI();
const App = metaUI.App;
const registry = metaUI.registry;
const stateStore = metaUI.stateManager.store;
const appModelManager = new AppModelManager(app, registry);
export default function renderApp() {
ReactDOM.render(
<StrictMode>
<ChakraProvider>
<Editor />
<Editor
App={App}
registry={registry}
stateStore={stateStore}
appModelManager={appModelManager}
/>
</ChakraProvider>
</StrictMode>,
document.getElementById('root')

View File

@ -1,7 +0,0 @@
import { initMetaUI } from '@meta-ui/runtime';
const metaUI = initMetaUI();
export const App = metaUI.App;
export const registry = metaUI.registry;
export const stateStore = metaUI.stateManager.store;

View File

@ -13,9 +13,9 @@ import {
ModifyTraitPropertiesOperation,
} from './Operations';
import { produce } from 'immer';
import { registry } from '../metaUI';
import { eventBus } from '../eventBus';
import { set, isEqual } from 'lodash-es';
import { Registry } from '@meta-ui/runtime/lib/services/registry';
function genSlotTrait(parentId: string, slot: string): ComponentTrait {
return {
@ -30,6 +30,7 @@ function genSlotTrait(parentId: string, slot: string): ComponentTrait {
}
function genComponent(
registry: Registry,
type: string,
id: string,
parentId?: string,
@ -50,17 +51,21 @@ function genComponent(
export class AppModelManager {
private undoStack: Operations[] = [];
private app: Application;
private registry: Registry;
constructor(app: Application) {
constructor(app: Application, registry: Registry) {
const appFromLS = localStorage.getItem('schema');
if (appFromLS) {
this.app = JSON.parse(appFromLS);
} else {
this.app = app;
}
this.registry = registry;
eventBus.on('undo', () => this.undo());
eventBus.on('operation', o => this.apply(o));
this.updateApp(this.app);
}
getApp() {
@ -95,6 +100,7 @@ export class AppModelManager {
case 'createComponent':
const createO = o as CreateComponentOperation;
const newComponent = genComponent(
this.registry,
createO.componentType,
createO.componentId || this.genId(createO.componentType),
createO.parentId,

View File

@ -2,12 +2,22 @@ import { Application } from '@meta-ui/core';
import { useEffect, useState } from 'react';
import { DefaultAppSchema } from '../constants';
import { eventBus } from '../eventBus';
import { AppModelManager } from './AppModelManager';
export const appModelManager = new AppModelManager(DefaultAppSchema);
function getDefaultAppFromLS() {
try {
const appFromLS = localStorage.getItem('schema');
if (appFromLS) {
return JSON.parse(appFromLS);
}
return DefaultAppSchema;
} catch (error) {
console.warn(error);
return DefaultAppSchema;
}
}
export function useAppModel() {
const [app, setApp] = useState<Application>(appModelManager.getApp());
const [app, setApp] = useState<Application>(getDefaultAppFromLS());
useEffect(() => {
const onAppChange = (app: Application) => {

View File

@ -0,0 +1,105 @@
import { Flex, Box, ChakraProvider, Button } from '@chakra-ui/react';
import { Application } from '@meta-ui/core';
import { initMetaUI } from '@meta-ui/runtime';
import { Registry } from '@meta-ui/runtime/lib/services/registry';
import { StrictMode, useMemo, useState } from 'react';
import ReactDOM from 'react-dom';
import 'react-grid-layout/css/styles.css';
import 'react-resizable/css/styles.css';
import { Editor } from './components/Editor';
import { AppModelManager } from './operations/AppModelManager';
type Example = {
name: string;
value: {
app: Application;
modules?: Parameters<Registry['registerModule']>[0][];
};
};
const Playground: React.FC<{ examples: Example[] }> = ({ examples }) => {
const [example, setExample] = useState<Example | null>(examples[0]);
const { App, registry, stateStore, appModelManager } = useMemo(() => {
if (!example) {
return {};
}
const metaUI = initMetaUI();
const App = metaUI.App;
const registry = metaUI.registry;
const stateStore = metaUI.stateManager.store;
const { app, modules = [] } = example.value;
modules.forEach(m => {
registry.registerModule(m);
});
localStorage.removeItem('schema');
const appModelManager = new AppModelManager(app, registry);
return {
App,
registry,
stateStore,
appModelManager,
};
}, [example]);
return (
<Flex width="100vw" height="100vh">
<Box shadow="md">
<Box width="200px" height="100%" overflow="auto" pl={2}>
{examples.map(e => (
<Button
variant={example === e ? 'solid' : 'ghost'}
key={e.name}
onClick={() => {
/**
* Currently, the data flow between the useAppModel and
* the AppModelManager is a little wierd.
* When initialize the AppModelManager, it will notify
* the useAppModel hook, which will cause the Editor
* component update.
* React does not like this and throw Error to complain
* the Editor and the Playground components are updating
* together.
* So we set example to null, which unmount the editor
* first. Then we can re-create the editor with new example
* spec.
*/
setExample(null);
setTimeout(() => {
setExample(e);
}, 0);
}}
>
{e.name}
</Button>
))}
</Box>
</Box>
<Box flex="1">
{appModelManager && (
<Editor
key={example!.name}
App={App!}
registry={registry!}
stateStore={stateStore!}
appModelManager={appModelManager}
/>
)}
</Box>
</Flex>
);
};
export default function renderPlayground(examples: Example[]) {
ReactDOM.render(
<StrictMode>
<ChakraProvider>
<Playground examples={examples} />
</ChakraProvider>
</StrictMode>,
document.getElementById('root')
);
}

View File

@ -1,5 +1,6 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { virtualExamplePlugin } from '@meta-ui/vite-plugins';
// https://vitejs.dev/config/
export default defineConfig({
@ -20,6 +21,7 @@ export default defineConfig({
],
},
}),
virtualExamplePlugin(),
],
define: {
// https://github.com/satya164/react-simple-code-editor/issues/86

View File

@ -58,6 +58,7 @@
"@babel/preset-env": "^7.15.6",
"@babel/preset-react": "^7.14.5",
"@babel/preset-typescript": "^7.15.0",
"@meta-ui/vite-plugins": "^1.0.0",
"@testing-library/react": "^12.1.0",
"@types/lodash": "^4.14.170",
"@types/lodash-es": "^4.17.5",

View File

@ -1,45 +1,6 @@
import { defineConfig, Plugin } from 'vite';
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import fs from 'fs';
import path from 'path';
function virtualExamplePlugin(): Plugin {
const virtualFileId = '@example.json';
const exampleDir = path.join(__dirname, '../../examples');
const examples = [];
function walk(dirOrFile: string, frags: string[]) {
if (fs.statSync(dirOrFile).isDirectory()) {
for (const subDir of fs.readdirSync(dirOrFile)) {
walk(path.join(dirOrFile, subDir), frags.concat(subDir));
}
} else {
if (path.extname(dirOrFile) !== '.json') {
return;
}
const value = JSON.parse(fs.readFileSync(dirOrFile, 'utf-8'));
const name = frags.join('/');
examples.push({ name, value });
}
}
walk(exampleDir, []);
return {
name: 'virtual-example-plugin',
resolveId(id) {
if (id === virtualFileId) {
return virtualFileId;
}
},
load(id) {
if (id === virtualFileId) {
return JSON.stringify(examples);
}
},
};
}
import { virtualExamplePlugin } from '@meta-ui/vite-plugins';
// https://vitejs.dev/config/
export default defineConfig({

View File

@ -0,0 +1,11 @@
# `@meta-ui/vite-plugins`
> TODO: description
## Usage
```
const vitePlugins = require('@meta-ui/vite-plugins');
// TODO: DEMONSTRATE API
```

View File

@ -0,0 +1,44 @@
const fs = require('fs');
const path = require('path');
function virtualExamplePlugin() {
const virtualFileId = '@example.json';
const exampleDir = path.join(__dirname, '../../examples');
const examples = [];
function walk(dirOrFile, frags) {
if (fs.statSync(dirOrFile).isDirectory()) {
for (const subDir of fs.readdirSync(dirOrFile)) {
walk(path.join(dirOrFile, subDir), frags.concat(subDir));
}
} else {
if (path.extname(dirOrFile) !== '.json') {
return;
}
const value = JSON.parse(fs.readFileSync(dirOrFile, 'utf-8'));
const name = frags.join('/');
examples.push({ name, value });
}
}
walk(exampleDir, []);
return {
name: 'virtual-example-plugin',
resolveId(id) {
if (id === virtualFileId) {
return virtualFileId;
}
},
load(id) {
if (id === virtualFileId) {
return JSON.stringify(examples);
}
},
};
}
module.exports = {
virtualExamplePlugin,
};

View File

@ -0,0 +1,21 @@
{
"name": "@meta-ui/vite-plugins",
"version": "1.0.0",
"description": "vite plugins for meta-ui",
"author": "meta-ui developers",
"homepage": "https://github.com/webzard-io/meta-ui#readme",
"license": "MIT",
"publishConfig": {
"access": "public"
},
"main": "index.js",
"files": [
"lib"
],
"repository": {
"type": "git",
"url": "git+https://github.com/webzard-io/meta-ui.git"
},
"scripts": {
}
}