Compare commits

...

23 Commits

Author SHA1 Message Date
wwsun 564d50b168 chore(release): publish
- @music163/tango-context@0.2.3
 - @music163/tango-core@0.2.2
 - @music163/tango-designer@0.5.1
 - @music163/tango-helpers@0.1.4
 - @music163/tango-sandbox@0.1.7
 - @music163/tango-setting-form@0.2.6
 - @music163/tango-ui@0.1.7
2023-10-23 12:56:20 +08:00
wwsun 0b3e632745 fix: update interface 2023-10-23 12:11:30 +08:00
wwsun 3f073eeb43 fix: custom store template 2023-10-23 11:35:31 +08:00
wwsun 6bbbe634ad fix: add listImportSources to viewModule 2023-10-23 11:30:29 +08:00
wwsun 8fefa6344f fix: add url validator 2023-10-23 11:12:20 +08:00
Wells 5ebbb428fb fix: enhance expSetter (#43)
* docs: Update

* fix: register history tool

* fix: add eslint to InputCode

* fix: enhance expSetter

* fix: transform value to expression code

* fix: value change of expSetter

* fix: input code without wrapper

* fix: disable eslint for InputCode by default

---------

Co-authored-by: wwsun <ww.sww@outlook.com>
2023-10-20 16:48:49 +08:00
wwsun c240c1824b chore(release): publish
- @music163/tango-context@0.2.2
 - @music163/tango-designer@0.5.0
 - @music163/tango-setting-form@0.2.5
 - @music163/tango-ui@0.1.6
2023-10-12 15:24:21 +08:00
Wells d63517ecb9 fix: refactor expSetter (#41)
* fix: refactor expSetter

* feat: support pass custom variable list

---------

Co-authored-by: wwsun <ww.sww@outlook.com>
2023-10-11 23:51:27 -05:00
Samson Yang 1a17ee1e94 docs: fix typo (#39)
修改错别字
2023-10-09 21:24:01 -05:00
864907600cc e1bab66819 feat: add umd resources config when modifying dependency (#36)
* feat: mark dependency when it's in both package.json and tango.config.json

* fix: remove mark for internal resources

* feat: add resource config when adding dependency

* feat: reuse add dependency form modal when editing dependency

* fix: tidy up

---------

Co-authored-by: ccloli <8115912+ccloli@users.noreply.github.com>
2023-10-09 05:58:05 -05:00
wwsun 89bcd45728 chore(release): publish
- @music163/tango-designer@0.4.1
 - @music163/tango-setting-form@0.2.4
 - @music163/tango-ui@0.1.5
2023-10-09 18:44:15 +08:00
wwsun e86a6d7cd9 docs: update 2023-10-09 18:41:21 +08:00
wwsun 46b54459ce fix: update tabs style 2023-09-25 19:38:04 +08:00
wwsun 85898dee19 fix: always switch to design mode after preview is on 2023-09-25 16:31:47 +08:00
wwsun e429610606 fix: update export entry 2023-09-25 16:01:04 +08:00
wwsun b6814d6484 chore: update script 2023-09-22 17:55:30 +08:00
wwsun 3d7b58f19a chore(release): publish
- @music163/tango-context@0.2.1
 - @music163/tango-core@0.2.1
 - @music163/tango-designer@0.4.0
 - @music163/tango-sandbox@0.1.6
 - @music163/tango-setting-form@0.2.3
2023-09-22 17:52:12 +08:00
wwsun b6a47acdde chore: add husky 2023-09-22 17:48:54 +08:00
wwsun 226d71c704 fix: remove useless api 2023-09-22 17:48:15 +08:00
wwsun 1c72ee6ac2 feat: add global widget map 2023-09-22 17:37:26 +08:00
wwsun 09a1e2135a fix: add listServiceFunctions 2023-09-22 16:42:38 +08:00
wwsun 7999f604fd chore(release): publish
- @music163/tango-context@0.2.0
 - @music163/tango-core@0.2.0
 - @music163/tango-designer@0.3.0
 - @music163/tango-sandbox@0.1.5
 - @music163/tango-setting-form@0.2.2
2023-09-21 13:59:44 +08:00
Wells a7df29c3de feat: support multiple service modules & refactor workspace
* feat: support multiple service modules

* style: add memo

* fix: refactor module

* fix: refactor workspace

---------

Co-authored-by: wwsun <ww.sww@outlook.com>
2023-09-21 13:57:04 +08:00
79 changed files with 2124 additions and 1984 deletions
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
yarn commitlint --edit "$1"
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
yarn lint-staged --allow-empty
+1 -1
View File
@@ -14,7 +14,7 @@ export default {
],
devServer: {
host: 'local.netease.com',
port: 7007,
port: 8008,
https: {
key: path.resolve(__dirname, 'local.netease.com-key.pem'),
cert: path.resolve(__dirname, 'local.netease.com.pem'),
+1 -1
View File
@@ -10,7 +10,7 @@
],
"scripts": {
"build": "COMPRESS=none umi build",
"start": "HTTPS=1 umi dev"
"start": "umi dev"
},
"dependencies": {
"@ant-design/icons": "^4.8.0",
@@ -1,6 +1,6 @@
import React from 'react';
import { Row, Switch } from 'antd';
import { observer } from '@music163/tango-designer';
import { observer } from '@music163/tango-context';
const OtherPanel = observer(({ autoRemove, setAutoRemove }: any) => {
const onChange = (checked: boolean) => {
+16 -2
View File
@@ -44,7 +44,7 @@ const tangoConfigJson = {
resources: ['https://unpkg.com/moment@{{version}}/moment.js'],
},
'@music163/tango-boot': {
version: '0.1.3',
version: '0.2.1',
library: 'TangoBoot',
type: 'baseDependency',
resources: ['https://unpkg.com/@music163/tango-boot@{{version}}/dist/boot.js'],
@@ -149,7 +149,7 @@ class App extends React.Component {
render() {
return (
<Page title={tango.stores.app.title}>
<Section>
<Section title="Section Title">
</Section>
<Section>
<Button>button</Button>
@@ -175,6 +175,7 @@ export default defineStore({
const serviceCode = `
import { defineServices } from '@music163/tango-boot';
import './sub';
export default defineServices({
get: {
@@ -201,6 +202,18 @@ export default defineServices({
});
`;
const subServiceCode = `
import { defineServices } from '@music163/tango-boot';
export default defineServices({
list: {
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/anchor-list-normal',
},
}, {
namespace: 'sub',
});
`;
const lessCode = `
body {
font-size: 12px;
@@ -232,6 +245,7 @@ export const sampleFiles = [
{ filename: '/src/stores/app.js', code: storeApp },
{ filename: '/src/stores/counter.js', code: storeCounter },
{ filename: '/src/services/index.js', code: serviceCode },
{ filename: '/src/services/sub.js', code: subServiceCode },
{ filename: '/src/utils/index.js', code: `export function foo() {}` },
];
+1 -1
View File
@@ -2,4 +2,4 @@
@import '~antd/dist/antd.less'; // 引入官方提供的 less 样式入口文件
@primary-color: #2f54eb;
@border-radius-base: 2px;
@border-radius-base: 2px;
+43 -11
View File
@@ -9,15 +9,21 @@ import {
Toolbar,
WorkspacePanel,
WorkspaceView,
ComponentsPanel,
CodeEditor,
Sandbox,
DndQuery,
} from '@music163/tango-designer';
import { createEngine, Workspace } from '@music163/tango-core';
import { Logo, ProjectDetail } from './share';
import { Logo, ProjectDetail, bootHelperVariables } from './share';
import { sampleFiles } from '../mock/project';
import './index.less';
import {
ApiOutlined,
AppstoreAddOutlined,
BuildOutlined,
ClusterOutlined,
FunctionOutlined,
} from '@ant-design/icons';
// 1. 实例化工作区
const workspace = new Workspace({
@@ -44,7 +50,14 @@ export default function App() {
const [menuLoading, setMenuLoading] = useState(true);
const [menuData, setMenuData] = useState(false);
return (
<Designer engine={engine} sandboxQuery={sandboxQuery}>
<Designer
engine={engine}
config={{
customActionVariables: bootHelperVariables,
customExpressionVariables: bootHelperVariables,
}}
sandboxQuery={sandboxQuery}
>
<DesignerPanel
logo={<Logo />}
description={<ProjectDetail />}
@@ -52,10 +65,11 @@ export default function App() {
<Box px="l">
<Toolbar>
<Toolbar.Item key="routeSwitch" placement="left" />
<Toolbar.Item key="preview" placement="left" />
<Toolbar.Item key="modeSwitch" placement="right" />
<Toolbar.Item key="togglePanel" placement="right" />
<Toolbar.Separator />
<Toolbar.Item key="extra" placement="right">
<Toolbar.Item placement="right">
<Space>
<Button type="primary"></Button>
</Space>
@@ -65,13 +79,31 @@ export default function App() {
}
>
<Sidebar>
<Sidebar.Item key="outline" />
<Sidebar.Item key="components">
<ComponentsPanel menuData={menuData as any} loading={menuLoading} />
</Sidebar.Item>
<Sidebar.Item key="variables" isFloat width={800} />
<Sidebar.Item key="dataSource" isFloat width={800} />
<Sidebar.Item key="dependency" isFloat width={800} />
<Sidebar.Item key="outline" label="结构" icon={<BuildOutlined />} />
<Sidebar.Item
key="components"
label="组件"
icon={<AppstoreAddOutlined />}
widgetProps={{
menuData: menuData as any,
loading: menuLoading,
}}
/>
<Sidebar.Item
key="variables"
label="变量"
icon={<FunctionOutlined />}
isFloat
width={800}
/>
<Sidebar.Item key="dataSource" label="接口" icon={<ApiOutlined />} isFloat width={800} />
<Sidebar.Item
key="dependency"
label="依赖"
icon={<ClusterOutlined />}
isFloat
width={800}
/>
</Sidebar>
<WorkspacePanel>
<WorkspaceView mode="design">
+31 -1
View File
@@ -6,6 +6,36 @@ import { registerSetter } from '@music163/tango-designer';
import type { ComponentPrototypeType } from '@music163/tango-helpers';
import { FooSetter } from '../components';
export const bootHelperVariables = [
{
key: '$helpers',
title: '工具函数',
children: [
{
title: 'setStoreValue',
key: '() => tango.setStoreValue("variableName", "variableValue")',
type: 'function',
},
{
title: 'getStoreValue',
key: '() => tango.getStoreValue("variableName")',
type: 'function',
},
{ title: 'openModal', key: '() => tango.openModal("")', type: 'function' },
{ title: 'closeModal', key: '() => tango.closeModal("")', type: 'function' },
{ title: 'navigateTo', key: '() => tango.navigateTo("/")', type: 'function' },
{ title: 'showToast', key: '() => tango.showToast("hello")', type: 'function' },
{ title: 'formatDate', key: '() => tango.formatDate("2022-12-12")', type: 'function' },
{ title: 'formatNumber', key: '() => tango.formatDate(9999)', type: 'function' },
{
title: 'copyToClipboard',
key: '() => tango.copyToClipboard("hello")',
type: 'function',
},
],
},
];
// folder-name
// 物料列表定义
const bizToggleButtonPrototype: ComponentPrototypeType = {
@@ -133,7 +163,7 @@ export function ProjectDetail() {
return (
<Box display="flex" alignItems="center" columnGap="l">
<Box className="ProjectName" fontSize="18px" fontWeight="bold">
lc-online-test
community-test
</Box>
<Box className="BranchName" as="code" fontSize="14px">
<BranchesOutlined /> feature/list
+10 -1
View File
@@ -10,11 +10,20 @@
项目使用的 npm 包依赖信息,用于定义依赖的类型以及外置依赖资源。该配置项为一个 map,key 为对应的 npm 包名,value 为该依赖的配置信息。
- version 包的版本号,必选
- library UMD 包中的全局变量名,必选
- type 依赖的类型
- baseDependency 基础依赖包
- bizDependency 业务组件依赖包
- dependency 其他依赖包
- resources 外置依赖资源列表
- designerResources 在设计器视图载入的外置资源列表,用来需要区别加载时使用
- description 描述信息
依赖的类型 `type` 可以是 `baseDependency` 即项目的基础依赖,用于获取项目基础物料列表的信息;也可以是 `bizDependency` 即业务组件依赖,用于获取合并到物料面板业务组件菜单中的业务组件列表;也可以是 `depenency` 即项目的其他依赖,只用于提供 external 信息与版本信息等,不会和物料面板产生关系。
当配置了外置依赖资源 `resources` 与其对应的全局变量 `library` 时,当这些依赖被 import 时,相关资源不会被打包到 bundle 中,而是在运行时再去外部获取这些扩展依赖,其等效于之前的 `sandbox.externalResources` 与 `sandbox.externals` 配置项。此外,可以将 `resource` 中的版本号替换为 `{{version}}`,其会在平台上构建时替换为版本号。特殊的,可以定义 `designerResources` 用于适配平台的设计器视图。
```json
{
"react": {
+6 -2
View File
@@ -2,10 +2,10 @@ import Link from '@docusaurus/Link';
# 快速开始
Tango 是一个用于快速构建低代码工具的前端框架借助 Tango 只需要少量代码就可以快速的搭建一个基本的低代码设计器。本文将会帮助你借助 Tango 实现一个基本的低代码可视化设计器
Tango 是一个用于快速构建低代码平台的低代码设计器框架借助 Tango 只需要数行代码就可以完成一个基本的低代码平台前端系统的搭建
:::warning 测试版免责声明
基于 Tango 低代码引擎的低代码平台已经在网易云音乐内部服务了内部众多业务开发场景。由于开源工作涉及到大量的代码分离工作,当前开源版本为低代码引擎的 alpha 演示版本,API 可能不稳定,不建议在生产环境中使用。我们预计在 2023 年 Q4 发布可以运行在生产环境的正式版。
当前开源版本为低代码引擎的 beta 演示版本,API 可能不稳定,不建议在生产环境中使用。为了保障社区版本能够有效与内部版本解藕,并且项目开源涉及到大量的额外的代码分离工作,我们预计在 2023 年 Q4 发布可以运行在生产环境的正式版。
:::
## 环境要求
@@ -34,6 +34,10 @@ Tango 是一个用于快速构建低代码工具的前端框架。借助 Tango
本地开发除了需要启动 Tango 的 playground 外,还需要一个可用的沙箱环境来承载设计器。
:::tip
沙箱部分来源于 codesandbox,不属于 tango 的核心代码库,我们在原有的 codesandbox 基础上进行了一系列的改造
:::
如果没有可用的在线沙箱,可以通过本地启动一个沙箱来完成开发;如果已有一个在线沙箱,且沙箱与本地的 playground 共用一级域名,请直接跳至第 7 步修改 `bundlerURL` 为在线沙箱的地址。
1. 克隆仓库至本地
+17 -23
View File
@@ -2,36 +2,30 @@ import Link from '@docusaurus/Link';
# 简介
Tango 是一个用于快速构建低代码平台的低代码设计器框架,借助 Tango 只需要数行代码就可以完成一个基本的低代码平台前端系统的搭建。Tango 低代码设计器**直接读取前端项目的源代码,并以源代码为中心,执行和渲染前端视图,并为用户提供低代码可视化搭建能力,用户的搭建操作会转为对代码的修改**。借助 Tango 可以实现 **源码进,源码出**的效果。
Tango 是一个用于快速构建低代码平台的低代码设计器框架,借助 Tango 只需要数行代码就可以完成一个基本的低代码平台前端系统的搭建。Tango 低代码设计器直接读取前端项目的源代码,并以源代码为中心,执行和渲染前端视图,并为用户提供低代码可视化搭建能力,用户的搭建操作会转为对代码的修改。借助 Tango 构建的低代码工具或平台,可以实现 源码进,源码出的效果,无缝与企业内部现有的研发体系进行集成
## 引擎架构概览
<img src="https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/30108735057/7ba9/dced/9ac3/420f6e04b371dd47de06e7d71142560d.gif" alt="preview" />
Tango 低代码引擎在实现上进行了分层解藕,使得上层的低代码平台与底层的低代码引擎可以独立开发和维护,快速集成部署。另一方面,Tango 低代码引擎定义了一套开放的物料生态体系,开发者可以自由的贡献扩展组件配置能力的属性设置器,以及扩展低代码物料的二方三方业务组件。
## 核心特性
如下图所示:
- 经历网易云音乐内网生产环境的实际检验,可灵活集成应用于低代码平台,本地开发工具等
- 基于源码 AST 驱动,无私有 DSL 和协议
- 提供实时出码能力,支持源码进,源码出
- 开箱即用的前端低代码设计器,提供灵活易用的设计器 React 组件
- 使用 TypeScript 开发,提供完整的类型定义文件
<img src="https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/18236990116/aabf/8a16/28ee/f0fd29a84c7aa40bb26383cdef12b88c.png" />
## 实现原理
## 引擎设计器构成概览
Tango 低代码引擎不依赖私有搭建协议和 DSL,而是直接使用源代码驱动,引擎内部将源码转为 AST,用户的所有的搭建操作转为对 AST 的遍历和修改,进而将 AST 重新生成为代码,将代码同步给在线沙箱执行。具体的过程如下图所示,首先将源代码解析为 AST。用户的拖拉拽等操作则映射为对 AST 的遍历和修改。最后将新的 AST 重新生成代码,交给设计器沙箱去渲染执行。而对 AST 的解析、遍历、修改、生成,则可以借助大量的社区工具,这里我们选择的是 babel!
Tango 低代码引擎设计器用于低成本初始化一个基本的低代码平台,其前端部分主要包括如下几个部分:
![Tango 低代码引擎实现分析](https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/30578073085/61cd/b2db/e103/9ed9dd334a6679c6ec18a02270efe446.png)
- 设计器主框架:进行外框架初始化,状态管理,拖拽引擎绑定等核心逻辑。
- 侧边栏面板:提供可扩展的侧边面板,除了内置通用的面板,用户还可以任意添加自定义的面板。
- 属性设置面板:属性设置器面板,支持用户进行属性配置。
- 运行时沙箱:用于设计器运行时视图的渲染。
- Web IDE:用于进行在线源代码编辑。
## 技术架构
<img src="https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/30108642346/b8cf/e86d/ef5a/514d90b722b5d8dc0e18516ed594a07b.png" />
Tango 低代码引擎主要包括 3 个核心组成部分,分别是:
## 基于源代码的引擎内核
- 引擎内核:扶额建立文件,节点模型,提供输入输出能力。
- 拖拽引擎和可视化面板:提供可视化开发能力
- 渲染沙箱:提供源码在浏览器上的编译执行能力。
Tango 低代码引擎不依赖私有搭建协议和 DSL,而是直接使用源代码驱动,引擎内部将源码转为 AST,用户的所有的搭建操作转为对 AST 的遍历和修改,进而将 AST 重新生成为代码,将代码同步给在线沙箱执行。与传统的借助 [Schema 驱动的低代码方案](https://mp.weixin.qq.com/s/yqYey76qLGYPfDtpGkVFfA) 相比,不受到私有 DSL 和协议的限制,能够完美的实现低代码搭建与源码开发的无缝集成。
<img src="https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13140534982/ee2e/f42c/cc9a/184e2918a011b57d46e6c64a2722fa44.png" />
## 源码进,源码出
由于引擎内核完全基于源代码驱动实现,Tango 低代码引擎能够实现源代码进,源代码出的可视化搭建能力,不提供任何私有的中间产物。从而使得,Tango 构建的线上研发能力可以与团队现有的研发服务(代码托管、构建、部署、CDN)无缝衔接与集成。
<img src="https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13208022400/b809/b82e/77b0/5e4fe78a8f11c8ed89c9ec9ced43e845.png" />
![引擎构成](https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/30579167082/1404/27e2/b8e5/0c719ca82494a282080d73adeff7196e.png)
+1 -1
View File
@@ -80,7 +80,7 @@ const config = {
type: 'docSidebar',
sidebarId: 'designer',
position: 'left',
label: '引擎文档',
label: '文档',
},
{
type: 'docSidebar',
@@ -3,37 +3,37 @@ import clsx from 'clsx';
import styles from './styles.module.css';
import Translate, { translate } from '@docusaurus/Translate';
type FeatureItem = {
interface FeatureItem {
title: string;
Svg: React.ComponentType<React.ComponentProps<'svg'>>;
description: JSX.Element;
};
}
const FeatureList: FeatureItem[] = [
{
title: translate({ id: 'homepage.features.low-code', message: '可视化搭建' }),
Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default,
title: translate({ id: 'homepage.features.code-driven', message: '源码驱动' }),
Svg: require('@site/static/img/html-d.svg').default,
description: (
<Translate id="homepage.features.low-code-content">
<Translate id="homepage.features.code-driven-content">
</Translate>
),
},
{
title: translate({ id: 'homepage.features.code-driven', message: '源码驱动' }),
Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default,
title: translate({ id: 'homepage.features.low-code', message: '实时出码' }),
Svg: require('@site/static/img/digital-content-4.svg').default,
description: (
<Translate id="homepage.features.code-driven-content">
使DSL线
<Translate id="homepage.features.low-code-content">
</Translate>
),
},
{
title: translate({ id: 'homepage.features.outbox', message: '开箱即用' }),
Svg: require('@site/static/img/undraw_docusaurus_react.svg').default,
Svg: require('@site/static/img/cpu-f.svg').default,
description: (
<Translate id="homepage.features.outbox-content">
</Translate>
),
},
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 300" data-imageid="digital-content-4" imageName="Coding" class="illustrations_image" style="width: 261px;"><g id="_87_coding_outline" data-name="#87_coding_outline"><path d="M289.32,90.26V223.54a3.65,3.65,0,0,1-3.65,3.65H130.85a3.65,3.65,0,0,1-3.65-3.65V90.26a3.65,3.65,0,0,1,3.65-3.66H285.67A3.65,3.65,0,0,1,289.32,90.26Z" fill="#2e8555" class="target-color"/><path d="M285.67,228.19H130.85a4.66,4.66,0,0,1-4.65-4.65V90.26a4.66,4.66,0,0,1,4.65-4.66H285.67a4.66,4.66,0,0,1,4.65,4.66V223.54A4.65,4.65,0,0,1,285.67,228.19ZM130.85,87.6a2.66,2.66,0,0,0-2.65,2.66V223.54a2.65,2.65,0,0,0,2.65,2.65H285.67a2.65,2.65,0,0,0,2.65-2.65V90.26a2.66,2.66,0,0,0-2.65-2.66Z" fill="#093f68"/><path d="M272.8,76.47V209.75a3.65,3.65,0,0,1-3.65,3.65H114.33a3.65,3.65,0,0,1-3.65-3.65V76.47a3.65,3.65,0,0,1,3.65-3.66H269.15A3.65,3.65,0,0,1,272.8,76.47Z" fill="#fff"/><path d="M269.15,214.4H114.33a4.65,4.65,0,0,1-4.65-4.65V76.47a4.66,4.66,0,0,1,4.65-4.66H269.15a4.66,4.66,0,0,1,4.65,4.66V209.75A4.65,4.65,0,0,1,269.15,214.4ZM114.33,73.81a2.66,2.66,0,0,0-2.65,2.66V209.75a2.65,2.65,0,0,0,2.65,2.65H269.15a2.65,2.65,0,0,0,2.65-2.65V76.47a2.66,2.66,0,0,0-2.65-2.66Z" fill="#093f68"/><path d="M272.8,76.47V209.75a3.65,3.65,0,0,1-3.65,3.65h-103V72.81h103A3.65,3.65,0,0,1,272.8,76.47Z" fill="#093f68"/><path d="M269.15,214.4h-103a1,1,0,0,1-1-1V72.81a1,1,0,0,1,1-1h103a4.66,4.66,0,0,1,4.65,4.66V209.75A4.65,4.65,0,0,1,269.15,214.4Zm-102-2h102a2.65,2.65,0,0,0,2.65-2.65V76.47a2.66,2.66,0,0,0-2.65-2.66h-102Z" fill="#093f68"/><path d="M272.8,76.47v15H110.68v-15a3.65,3.65,0,0,1,3.65-3.66H269.15A3.65,3.65,0,0,1,272.8,76.47Z" fill="#fff"/><path d="M272.8,92.48H110.68a1,1,0,0,1-1-1v-15a4.66,4.66,0,0,1,4.65-4.66H269.15a4.66,4.66,0,0,1,4.65,4.66v15A1,1,0,0,1,272.8,92.48Zm-161.12-2H271.8v-14a2.66,2.66,0,0,0-2.65-2.66H114.33a2.66,2.66,0,0,0-2.65,2.66Z" fill="#093f68"/><circle cx="123.19" cy="81.63" r="3.38" fill="#f56132"/><path d="M123.19,86a4.38,4.38,0,1,1,4.38-4.38A4.39,4.39,0,0,1,123.19,86Zm0-6.76a2.38,2.38,0,1,0,2.38,2.38A2.39,2.39,0,0,0,123.19,79.25Z" fill="#093f68"/><circle cx="136.84" cy="81.63" r="3.38" fill="#ffbc0e"/><path d="M136.84,86a4.38,4.38,0,1,1,4.37-4.38A4.39,4.39,0,0,1,136.84,86Zm0-6.76a2.38,2.38,0,1,0,2.37,2.38A2.38,2.38,0,0,0,136.84,79.25Z" fill="#093f68"/><circle cx="150.48" cy="81.63" r="3.38" fill="#70cc40"/><path d="M150.48,86a4.38,4.38,0,1,1,4.38-4.38A4.39,4.39,0,0,1,150.48,86Zm0-6.76a2.38,2.38,0,1,0,2.38,2.38A2.39,2.39,0,0,0,150.48,79.25Z" fill="#093f68"/><path d="M148.3,104.19H120.73a1,1,0,0,1,0-2H148.3a1,1,0,0,1,0,2Z" fill="#dfeaef"/><path d="M153,116.59H129.79a1,1,0,0,1,0-2H153a1,1,0,0,1,0,2Z" fill="#dfeaef"/><path d="M156.18,129h-19.3a1,1,0,0,1,0-2h19.3a1,1,0,0,1,0,2Z" fill="#dfeaef"/><path d="M149.88,141.41H125.46a1,1,0,0,1,0-2h24.42a1,1,0,0,1,0,2Z" fill="#dfeaef"/><path d="M143.31,153.81H120.73a1,1,0,0,1,0-2h22.58a1,1,0,0,1,0,2Z" fill="#dfeaef"/><path d="M148.3,166.22h-20a1,1,0,0,1,0-2h20a1,1,0,0,1,0,2Z" fill="#dfeaef"/><path d="M141.41,178.62H120.73a1,1,0,0,1,0-2h20.68a1,1,0,0,1,0,2Z" fill="#dfeaef"/><path d="M148.3,191h-20a1,1,0,0,1,0-2h20a1,1,0,0,1,0,2Z" fill="#dfeaef"/><path d="M193.62,104.19h-16.8a1,1,0,0,1,0-2h16.8a1,1,0,0,1,0,2Z" fill="#fff"/><path d="M216.82,116.59H182.34a1,1,0,0,1,0-2h34.48a1,1,0,0,1,0,2Z" fill="#ffbc0e"/><path d="M208.26,129H182.34a1,1,0,0,1,0-2h25.92a1,1,0,0,1,0,2Z" fill="#ffbc0e"/><path d="M194.58,141.41H179.7a1,1,0,0,1,0-2h14.88a1,1,0,0,1,0,2Z" fill="#fff"/><path d="M200.5,153.81H183.7a1,1,0,0,1,0-2h16.8a1,1,0,0,1,0,2Z" fill="#f56132"/><path d="M209.06,166.22H183.12a1,1,0,0,1,0-2h25.94a1,1,0,0,1,0,2Z" fill="#fff"/><path d="M189.42,178.62h-12.6a1,1,0,0,1,0-2h12.6a1,1,0,0,1,0,2Z" fill="#fff"/><path d="M209.06,191h-27.6a1,1,0,0,1,0-2h27.6a1,1,0,0,1,0,2Z" fill="#70cc40"/><path d="M235.5,191H217.38a1,1,0,0,1,0-2H235.5a1,1,0,0,1,0,2Z" fill="#ffbc0e"/><path d="M229.62,129h-12.8a1,1,0,0,1,0-2h12.8a1,1,0,0,1,0,2Z" fill="#fff"/><path d="M247.87,116.59H223.22a1,1,0,0,1,0-2h24.65a1,1,0,0,1,0,2Z" fill="#2e8555" class="target-color"/><path d="M243.79,129h-7.21a1,1,0,0,1,0-2h7.21a1,1,0,0,1,0,2Z" fill="#f56132"/><path d="M212.66,153.81h-7.2a1,1,0,1,1,0-2h7.2a1,1,0,1,1,0,2Z" fill="#f56132"/><path d="M247.87,153.81H218.58a1,1,0,1,1,0-2h29.29a1,1,0,0,1,0,2Z" fill="#ffbc0e"/><path d="M223.22,166.22h-6.4a1,1,0,0,1,0-2h6.4a1,1,0,0,1,0,2Z" fill="#2e8555" class="target-color"/><path d="M247.87,191h-4.08a1,1,0,0,1,0-2h4.08a1,1,0,0,1,0,2Z" fill="#fff"/></g></svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 300" data-imageid="html-d" imageName="Javascript" class="illustrations_image" style="width: 261px;"><g id="_181_javascript_outline" data-name="#181_javascript_outline"><rect x="144.72" y="69.25" width="130.1" height="153.64" rx="10.96" fill="#2e8555" class="target-color"/><path d="M263.86,223.89H155.67a12,12,0,0,1-11.95-11.95V80.21a12,12,0,0,1,11.95-12H263.86a12,12,0,0,1,12,12V211.94A12,12,0,0,1,263.86,223.89ZM155.67,70.25a10,10,0,0,0-9.95,10V211.94a10,10,0,0,0,9.95,9.95H263.86a10,10,0,0,0,10-9.95V80.21a10,10,0,0,0-10-10Z" fill="#093f68"/><path d="M257.86,230.75H141.93a7.08,7.08,0,0,1-7.08-7.08V84.75a7.08,7.08,0,0,1,7.08-7.08h93.81l29.2,27.19V223.67A7.08,7.08,0,0,1,257.86,230.75Z" fill="#fff"/><path d="M257.86,231.75H141.93a8.09,8.09,0,0,1-8.08-8.08V84.75a8.09,8.09,0,0,1,8.08-8.08h93.81a1,1,0,0,1,.68.27l29.21,27.19a1,1,0,0,1,.31.73V223.67A8.09,8.09,0,0,1,257.86,231.75ZM141.93,78.67a6.09,6.09,0,0,0-6.08,6.08V223.67a6.09,6.09,0,0,0,6.08,6.08H257.86a6.08,6.08,0,0,0,6.08-6.08V105.3l-28.6-26.63Z" fill="#093f68"/><path d="M235.74,77.67V99.06a5.46,5.46,0,0,0,5.39,5.47l23.81.33Z" fill="#2e8555" class="target-color"/><path d="M264.94,105.86h0l-23.81-.33a6.49,6.49,0,0,1-6.38-6.47V77.67a1,1,0,0,1,1.68-.73l29.21,27.19a1,1,0,0,1,.24,1.1A1,1,0,0,1,264.94,105.86ZM236.74,80V99.06a4.47,4.47,0,0,0,4.4,4.47l21.22.29Z" fill="#093f68"/><path d="M182.76,164.08,164,172.64v.14l18.78,8.56v9.39l-30-13.67v-8.7l30-13.68Z" fill="#093f68"/><path d="M182.76,191.73a.85.85,0,0,1-.41-.09l-30-13.67a1,1,0,0,1-.58-.91v-8.7a1,1,0,0,1,.58-.91l30-13.68a1,1,0,0,1,1.41.91v9.4a1,1,0,0,1-.58.91l-17,7.72,17,7.72a1,1,0,0,1,.58.91v9.39a1,1,0,0,1-.46.85A1,1,0,0,1,182.76,191.73Zm-29-15.31,28,12.76V182l-18.2-8.29a1,1,0,0,1-.58-.91v-.14a1,1,0,0,1,.58-.91l18.2-8.3v-7.19l-28,12.76Zm29-12.34h0Z" fill="#093f68"/><path d="M205,141.08l6.76,2.41L194,197.3l-6.7-2.42Z" fill="#093f68"/><path d="M194,198.3a1,1,0,0,1-.34-.06l-6.7-2.42a1,1,0,0,1-.61-1.25l17.72-53.81a1,1,0,0,1,.51-.58,1,1,0,0,1,.78,0l6.75,2.41a1,1,0,0,1,.62,1.26L195,197.61a1,1,0,0,1-.51.59A1.13,1.13,0,0,1,194,198.3Zm-5.44-4,4.81,1.74,17.15-51.9-4.87-1.75Z" fill="#093f68"/><path d="M216.36,181.34l18.78-8.56v-.14l-18.78-8.56v-9.4l30,13.68v8.7l-30,13.67Z" fill="#093f68"/><path d="M216.36,191.73a1,1,0,0,1-.54-.15,1,1,0,0,1-.46-.85v-9.39a1,1,0,0,1,.58-.91l16.94-7.72L215.94,165a1,1,0,0,1-.58-.91v-9.4a1,1,0,0,1,1.41-.91l30,13.68a1,1,0,0,1,.58.91v8.7a1,1,0,0,1-.58.91l-30,13.67A.85.85,0,0,1,216.36,191.73Zm1-9.75v7.2l28-12.76V169l-28-12.76v7.19l18.2,8.3a1,1,0,0,1,.58.91v.14a1,1,0,0,1-.58.91Z" fill="#093f68"/><rect x="120.98" y="92.53" width="71.35" height="44.25" rx="2.13" fill="#2e8555" class="target-color"/><path d="M190.2,137.78H123.11a3.13,3.13,0,0,1-3.13-3.12v-40a3.14,3.14,0,0,1,3.13-3.13H190.2a3.13,3.13,0,0,1,3.13,3.13v40A3.13,3.13,0,0,1,190.2,137.78ZM123.11,93.53A1.14,1.14,0,0,0,122,94.66v40a1.13,1.13,0,0,0,1.13,1.12H190.2a1.12,1.12,0,0,0,1.13-1.12v-40a1.13,1.13,0,0,0-1.13-1.13Z" fill="#093f68"/><path d="M151.53,122.88a7.29,7.29,0,0,1-1.07,2.59,6,6,0,0,1-2.18,2,7.46,7.46,0,0,1-3.61.77,7.3,7.3,0,0,1-4.6-1.43,6.78,6.78,0,0,1-2.46-3.67l4.64-1.56a2.7,2.7,0,0,0,.78,1.38,2,2,0,0,0,1.4.52,1.69,1.69,0,0,0,1.7-.89,5.49,5.49,0,0,0,.42-2.33V105.87h5.28v14.3A13.07,13.07,0,0,1,151.53,122.88Z" fill="#093f68"/><path d="M167.67,111.52a4.69,4.69,0,0,0-1.77-1.37,5,5,0,0,0-2.1-.51,6.44,6.44,0,0,0-1.06.1,2.81,2.81,0,0,0-1,.35,2.31,2.31,0,0,0-.72.67,1.84,1.84,0,0,0-.29,1.06,1.78,1.78,0,0,0,.23.93,2.06,2.06,0,0,0,.67.64,7,7,0,0,0,1.06.51c.41.15.87.31,1.39.47.73.25,1.5.52,2.3.82a8.49,8.49,0,0,1,2.18,1.18,6.3,6.3,0,0,1,1.62,1.82,5.29,5.29,0,0,1,.65,2.72,7.1,7.1,0,0,1-.69,3.24,6.51,6.51,0,0,1-1.86,2.26,8.14,8.14,0,0,1-2.67,1.32,11.49,11.49,0,0,1-3.1.43,12.59,12.59,0,0,1-4.51-.82A9.59,9.59,0,0,1,154.4,125l3.44-3.5a6.32,6.32,0,0,0,2.11,1.64,5.66,5.66,0,0,0,2.59.66,5.38,5.38,0,0,0,1.14-.12,3.08,3.08,0,0,0,1-.4,2,2,0,0,0,.66-.74,2.28,2.28,0,0,0,.25-1.11,1.71,1.71,0,0,0-.31-1,2.9,2.9,0,0,0-.87-.78A7,7,0,0,0,163,119l-1.92-.63a19.51,19.51,0,0,1-2-.79,7,7,0,0,1-1.78-1.19,5.49,5.49,0,0,1-1.26-1.75,6,6,0,0,1-.47-2.5,6.16,6.16,0,0,1,.73-3.1,6.57,6.57,0,0,1,1.94-2.12,8.49,8.49,0,0,1,2.7-1.21,12.06,12.06,0,0,1,3-.38,11.27,11.27,0,0,1,3.76.67,9.66,9.66,0,0,1,3.36,2Z" fill="#093f68"/><path d="M191.53,101.63H121a1,1,0,0,1,0-2h70.55a1,1,0,0,1,0,2Z" fill="#093f68"/><path d="M131.06,98.11h-5.12a1,1,0,0,1,0-2h5.12a1,1,0,0,1,0,2Z" fill="#093f68"/><path d="M257.68,212.94h-113a1,1,0,1,1,0-2h113a1,1,0,0,1,0,2Z" fill="#dfeaef"/></g></svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

@@ -1,171 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1088" height="687.962" viewBox="0 0 1088 687.962">
<title>Easy to Use</title>
<g id="Group_12" data-name="Group 12" transform="translate(-57 -56)">
<g id="Group_11" data-name="Group 11" transform="translate(57 56)">
<path id="Path_83" data-name="Path 83" d="M1017.81,560.461c-5.27,45.15-16.22,81.4-31.25,110.31-20,38.52-54.21,54.04-84.77,70.28a193.275,193.275,0,0,1-27.46,11.94c-55.61,19.3-117.85,14.18-166.74,3.99a657.282,657.282,0,0,0-104.09-13.16q-14.97-.675-29.97-.67c-15.42.02-293.07,5.29-360.67-131.57-16.69-33.76-28.13-75-32.24-125.27-11.63-142.12,52.29-235.46,134.74-296.47,155.97-115.41,369.76-110.57,523.43,7.88C941.15,276.621,1036.99,396.031,1017.81,560.461Z" transform="translate(-56 -106.019)" fill="#3f3d56"/>
<path id="Path_84" data-name="Path 84" d="M986.56,670.771c-20,38.52-47.21,64.04-77.77,80.28a193.272,193.272,0,0,1-27.46,11.94c-55.61,19.3-117.85,14.18-166.74,3.99a657.3,657.3,0,0,0-104.09-13.16q-14.97-.675-29.97-.67-23.13.03-46.25,1.72c-100.17,7.36-253.82-6.43-321.42-143.29L382,283.981,444.95,445.6l20.09,51.59,55.37-75.98L549,381.981l130.2,149.27,36.8-81.27L970.78,657.9l14.21,11.59Z" transform="translate(-56 -106.019)" fill="#f2f2f2"/>
<path id="Path_85" data-name="Path 85" d="M302,282.962l26-57,36,83-31-60Z" opacity="0.1"/>
<path id="Path_86" data-name="Path 86" d="M610.5,753.821q-14.97-.675-29.97-.67L465.04,497.191Z" transform="translate(-56 -106.019)" opacity="0.1"/>
<path id="Path_87" data-name="Path 87" d="M464.411,315.191,493,292.962l130,150-132-128Z" opacity="0.1"/>
<path id="Path_88" data-name="Path 88" d="M908.79,751.051a193.265,193.265,0,0,1-27.46,11.94L679.2,531.251Z" transform="translate(-56 -106.019)" opacity="0.1"/>
<circle id="Ellipse_11" data-name="Ellipse 11" cx="3" cy="3" r="3" transform="translate(479 98.962)" fill="#f2f2f2"/>
<circle id="Ellipse_12" data-name="Ellipse 12" cx="3" cy="3" r="3" transform="translate(396 201.962)" fill="#f2f2f2"/>
<circle id="Ellipse_13" data-name="Ellipse 13" cx="2" cy="2" r="2" transform="translate(600 220.962)" fill="#f2f2f2"/>
<circle id="Ellipse_14" data-name="Ellipse 14" cx="2" cy="2" r="2" transform="translate(180 265.962)" fill="#f2f2f2"/>
<circle id="Ellipse_15" data-name="Ellipse 15" cx="2" cy="2" r="2" transform="translate(612 96.962)" fill="#f2f2f2"/>
<circle id="Ellipse_16" data-name="Ellipse 16" cx="2" cy="2" r="2" transform="translate(736 192.962)" fill="#f2f2f2"/>
<circle id="Ellipse_17" data-name="Ellipse 17" cx="2" cy="2" r="2" transform="translate(858 344.962)" fill="#f2f2f2"/>
<path id="Path_89" data-name="Path 89" d="M306,121.222h-2.76v-2.76h-1.48v2.76H299V122.7h2.76v2.759h1.48V122.7H306Z" fill="#f2f2f2"/>
<path id="Path_90" data-name="Path 90" d="M848,424.222h-2.76v-2.76h-1.48v2.76H841V425.7h2.76v2.759h1.48V425.7H848Z" fill="#f2f2f2"/>
<path id="Path_91" data-name="Path 91" d="M1144,719.981c0,16.569-243.557,74-544,74s-544-57.431-544-74,243.557,14,544,14S1144,703.413,1144,719.981Z" transform="translate(-56 -106.019)" fill="#3f3d56"/>
<path id="Path_92" data-name="Path 92" d="M1144,719.981c0,16.569-243.557,74-544,74s-544-57.431-544-74,243.557,14,544,14S1144,703.413,1144,719.981Z" transform="translate(-56 -106.019)" opacity="0.1"/>
<ellipse id="Ellipse_18" data-name="Ellipse 18" cx="544" cy="30" rx="544" ry="30" transform="translate(0 583.962)" fill="#3f3d56"/>
<path id="Path_93" data-name="Path 93" d="M624,677.981c0,33.137-14.775,24-33,24s-33,9.137-33-24,33-96,33-96S624,644.844,624,677.981Z" transform="translate(-56 -106.019)" fill="#ff6584"/>
<path id="Path_94" data-name="Path 94" d="M606,690.66c0,15.062-6.716,10.909-15,10.909s-15,4.153-15-10.909,15-43.636,15-43.636S606,675.6,606,690.66Z" transform="translate(-56 -106.019)" opacity="0.1"/>
<rect id="Rectangle_97" data-name="Rectangle 97" width="92" height="18" rx="9" transform="translate(489 604.962)" fill="#2f2e41"/>
<rect id="Rectangle_98" data-name="Rectangle 98" width="92" height="18" rx="9" transform="translate(489 586.962)" fill="#2f2e41"/>
<path id="Path_95" data-name="Path 95" d="M193,596.547c0,55.343,34.719,100.126,77.626,100.126" transform="translate(-56 -106.019)" fill="#3f3d56"/>
<path id="Path_96" data-name="Path 96" d="M270.626,696.673c0-55.965,38.745-101.251,86.626-101.251" transform="translate(-56 -106.019)" fill="#6c63ff"/>
<path id="Path_97" data-name="Path 97" d="M221.125,601.564c0,52.57,22.14,95.109,49.5,95.109" transform="translate(-56 -106.019)" fill="#6c63ff"/>
<path id="Path_98" data-name="Path 98" d="M270.626,696.673c0-71.511,44.783-129.377,100.126-129.377" transform="translate(-56 -106.019)" fill="#3f3d56"/>
<path id="Path_99" data-name="Path 99" d="M254.3,697.379s11.009-.339,14.326-2.7,16.934-5.183,17.757-1.395,16.544,18.844,4.115,18.945-28.879-1.936-32.19-3.953S254.3,697.379,254.3,697.379Z" transform="translate(-56 -106.019)" fill="#a8a8a8"/>
<path id="Path_100" data-name="Path 100" d="M290.716,710.909c-12.429.1-28.879-1.936-32.19-3.953-2.522-1.536-3.527-7.048-3.863-9.591l-.368.014s.7,8.879,4.009,10.9,19.761,4.053,32.19,3.953c3.588-.029,4.827-1.305,4.759-3.2C294.755,710.174,293.386,710.887,290.716,710.909Z" transform="translate(-56 -106.019)" opacity="0.2"/>
<path id="Path_101" data-name="Path 101" d="M777.429,633.081c0,38.029,23.857,68.8,53.341,68.8" transform="translate(-56 -106.019)" fill="#3f3d56"/>
<path id="Path_102" data-name="Path 102" d="M830.769,701.882c0-38.456,26.623-69.575,59.525-69.575" transform="translate(-56 -106.019)" fill="#6c63ff"/>
<path id="Path_103" data-name="Path 103" d="M796.755,636.528c0,36.124,15.213,65.354,34.014,65.354" transform="translate(-56 -106.019)" fill="#6c63ff"/>
<path id="Path_104" data-name="Path 104" d="M830.769,701.882c0-49.139,30.773-88.9,68.8-88.9" transform="translate(-56 -106.019)" fill="#3f3d56"/>
<path id="Path_105" data-name="Path 105" d="M819.548,702.367s7.565-.233,9.844-1.856,11.636-3.562,12.2-.958,11.368,12.949,2.828,13.018-19.844-1.33-22.119-2.716S819.548,702.367,819.548,702.367Z" transform="translate(-56 -106.019)" fill="#a8a8a8"/>
<path id="Path_106" data-name="Path 106" d="M844.574,711.664c-8.54.069-19.844-1.33-22.119-2.716-1.733-1.056-2.423-4.843-2.654-6.59l-.253.01s.479,6.1,2.755,7.487,13.579,2.785,22.119,2.716c2.465-.02,3.317-.9,3.27-2.2C847.349,711.159,846.409,711.649,844.574,711.664Z" transform="translate(-56 -106.019)" opacity="0.2"/>
<path id="Path_107" data-name="Path 107" d="M949.813,724.718s11.36-1.729,14.5-4.591,16.89-7.488,18.217-3.667,19.494,17.447,6.633,19.107-30.153,1.609-33.835-.065S949.813,724.718,949.813,724.718Z" transform="translate(-56 -106.019)" fill="#a8a8a8"/>
<path id="Path_108" data-name="Path 108" d="M989.228,734.173c-12.86,1.659-30.153,1.609-33.835-.065-2.8-1.275-4.535-6.858-5.2-9.45l-.379.061s1.833,9.109,5.516,10.783,20.975,1.725,33.835.065c3.712-.479,4.836-1.956,4.529-3.906C993.319,732.907,991.991,733.817,989.228,734.173Z" transform="translate(-56 -106.019)" opacity="0.2"/>
<path id="Path_109" data-name="Path 109" d="M670.26,723.9s9.587-1.459,12.237-3.875,14.255-6.32,15.374-3.095,16.452,14.725,5.6,16.125-25.448,1.358-28.555-.055S670.26,723.9,670.26,723.9Z" transform="translate(-56 -106.019)" fill="#a8a8a8"/>
<path id="Path_110" data-name="Path 110" d="M703.524,731.875c-10.853,1.4-25.448,1.358-28.555-.055-2.367-1.076-3.827-5.788-4.39-7.976l-.32.051s1.547,7.687,4.655,9.1,17.7,1.456,28.555.055c3.133-.4,4.081-1.651,3.822-3.3C706.977,730.807,705.856,731.575,703.524,731.875Z" transform="translate(-56 -106.019)" opacity="0.2"/>
<path id="Path_111" data-name="Path 111" d="M178.389,719.109s7.463-1.136,9.527-3.016,11.1-4.92,11.969-2.409,12.808,11.463,4.358,12.553-19.811,1.057-22.23-.043S178.389,719.109,178.389,719.109Z" transform="translate(-56 -106.019)" fill="#a8a8a8"/>
<path id="Path_112" data-name="Path 112" d="M204.285,725.321c-8.449,1.09-19.811,1.057-22.23-.043-1.842-.838-2.979-4.506-3.417-6.209l-.249.04s1.2,5.984,3.624,7.085,13.781,1.133,22.23.043c2.439-.315,3.177-1.285,2.976-2.566C206.973,724.489,206.1,725.087,204.285,725.321Z" transform="translate(-56 -106.019)" opacity="0.2"/>
<path id="Path_113" data-name="Path 113" d="M439.7,707.337c0,30.22-42.124,20.873-93.7,20.873s-93.074,9.347-93.074-20.873,42.118-36.793,93.694-36.793S439.7,677.117,439.7,707.337Z" transform="translate(-56 -106.019)" opacity="0.1"/>
<path id="Path_114" data-name="Path 114" d="M439.7,699.9c0,30.22-42.124,20.873-93.7,20.873s-93.074,9.347-93.074-20.873S295.04,663.1,346.616,663.1,439.7,669.676,439.7,699.9Z" transform="translate(-56 -106.019)" fill="#3f3d56"/>
</g>
<g id="docusaurus_keytar" transform="translate(312.271 493.733)">
<path id="Path_40" data-name="Path 40" d="M99,52h91.791V89.153H99Z" transform="translate(5.904 -14.001)" fill="#fff" fill-rule="evenodd"/>
<path id="Path_41" data-name="Path 41" d="M24.855,163.927A21.828,21.828,0,0,1,5.947,153a21.829,21.829,0,0,0,18.908,32.782H46.71V163.927Z" transform="translate(-3 -4.634)" fill="#3ecc5f" fill-rule="evenodd"/>
<path id="Path_42" data-name="Path 42" d="M121.861,61.1l76.514-4.782V45.39A21.854,21.854,0,0,0,176.52,23.535H78.173L75.441,18.8a3.154,3.154,0,0,0-5.464,0l-2.732,4.732L64.513,18.8a3.154,3.154,0,0,0-5.464,0l-2.732,4.732L53.586,18.8a3.154,3.154,0,0,0-5.464,0L45.39,23.535c-.024,0-.046,0-.071,0l-4.526-4.525a3.153,3.153,0,0,0-5.276,1.414l-1.5,5.577-5.674-1.521a3.154,3.154,0,0,0-3.863,3.864L26,34.023l-5.575,1.494a3.155,3.155,0,0,0-1.416,5.278l4.526,4.526c0,.023,0,.046,0,.07L18.8,48.122a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,59.05a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,69.977a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,80.9a3.154,3.154,0,0,0,0,5.464L23.535,89.1,18.8,91.832a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,102.76a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,113.687a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,124.615a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,135.542a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,146.469a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,157.4a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,168.324a3.154,3.154,0,0,0,0,5.464l4.732,2.732A21.854,21.854,0,0,0,45.39,198.375H176.52a21.854,21.854,0,0,0,21.855-21.855V89.1l-76.514-4.782a11.632,11.632,0,0,1,0-23.219" transform="translate(-1.681 -17.226)" fill="#3ecc5f" fill-rule="evenodd"/>
<path id="Path_43" data-name="Path 43" d="M143,186.71h32.782V143H143Z" transform="translate(9.984 -5.561)" fill="#3ecc5f" fill-rule="evenodd"/>
<path id="Path_44" data-name="Path 44" d="M196.71,159.855a5.438,5.438,0,0,0-.7.07c-.042-.164-.081-.329-.127-.493a5.457,5.457,0,1,0-5.4-9.372q-.181-.185-.366-.367a5.454,5.454,0,1,0-9.384-5.4c-.162-.046-.325-.084-.486-.126a5.467,5.467,0,1,0-10.788,0c-.162.042-.325.08-.486.126a5.457,5.457,0,1,0-9.384,5.4,21.843,21.843,0,1,0,36.421,21.02,5.452,5.452,0,1,0,.7-10.858" transform="translate(10.912 -6.025)" fill="#44d860" fill-rule="evenodd"/>
<path id="Path_45" data-name="Path 45" d="M153,124.855h32.782V103H153Z" transform="translate(10.912 -9.271)" fill="#3ecc5f" fill-rule="evenodd"/>
<path id="Path_46" data-name="Path 46" d="M194.855,116.765a2.732,2.732,0,1,0,0-5.464,2.811,2.811,0,0,0-.349.035c-.022-.082-.04-.164-.063-.246a2.733,2.733,0,0,0-1.052-5.253,2.7,2.7,0,0,0-1.648.566q-.09-.093-.184-.184a2.7,2.7,0,0,0,.553-1.633,2.732,2.732,0,0,0-5.245-1.07,10.928,10.928,0,1,0,0,21.031,2.732,2.732,0,0,0,5.245-1.07,2.7,2.7,0,0,0-.553-1.633q.093-.09.184-.184a2.7,2.7,0,0,0,1.648.566,2.732,2.732,0,0,0,1.052-5.253c.023-.081.042-.164.063-.246a2.814,2.814,0,0,0,.349.035" transform="translate(12.767 -9.377)" fill="#44d860" fill-rule="evenodd"/>
<path id="Path_47" data-name="Path 47" d="M65.087,56.891a2.732,2.732,0,0,1-2.732-2.732,8.2,8.2,0,0,0-16.391,0,2.732,2.732,0,0,1-5.464,0,13.659,13.659,0,0,1,27.319,0,2.732,2.732,0,0,1-2.732,2.732" transform="translate(0.478 -15.068)" fill-rule="evenodd"/>
<path id="Path_48" data-name="Path 48" d="M103,191.347h65.565a21.854,21.854,0,0,0,21.855-21.855V93H124.855A21.854,21.854,0,0,0,103,114.855Z" transform="translate(6.275 -10.199)" fill="#ffff50" fill-rule="evenodd"/>
<path id="Path_49" data-name="Path 49" d="M173.216,129.787H118.535a1.093,1.093,0,1,1,0-2.185h54.681a1.093,1.093,0,0,1,0,2.185m0,21.855H118.535a1.093,1.093,0,1,1,0-2.186h54.681a1.093,1.093,0,0,1,0,2.186m0,21.855H118.535a1.093,1.093,0,1,1,0-2.185h54.681a1.093,1.093,0,0,1,0,2.185m0-54.434H118.535a1.093,1.093,0,1,1,0-2.185h54.681a1.093,1.093,0,0,1,0,2.185m0,21.652H118.535a1.093,1.093,0,1,1,0-2.186h54.681a1.093,1.093,0,0,1,0,2.186m0,21.855H118.535a1.093,1.093,0,1,1,0-2.186h54.681a1.093,1.093,0,0,1,0,2.186M189.585,61.611c-.013,0-.024-.007-.037-.005-3.377.115-4.974,3.492-6.384,6.472-1.471,3.114-2.608,5.139-4.473,5.078-2.064-.074-3.244-2.406-4.494-4.874-1.436-2.835-3.075-6.049-6.516-5.929-3.329.114-4.932,3.053-6.346,5.646-1.5,2.762-2.529,4.442-4.5,4.364-2.106-.076-3.225-1.972-4.52-4.167-1.444-2.443-3.112-5.191-6.487-5.1-3.272.113-4.879,2.606-6.3,4.808-1.5,2.328-2.552,3.746-4.551,3.662-2.156-.076-3.27-1.65-4.558-3.472-1.447-2.047-3.077-4.363-6.442-4.251-3.2.109-4.807,2.153-6.224,3.954-1.346,1.709-2.4,3.062-4.621,2.977a1.093,1.093,0,0,0-.079,2.186c3.3.11,4.967-1.967,6.417-3.81,1.286-1.635,2.4-3.045,4.582-3.12,2.1-.09,3.091,1.218,4.584,3.327,1.417,2,3.026,4.277,6.263,4.394,3.391.114,5.022-2.42,6.467-4.663,1.292-2,2.406-3.734,4.535-3.807,1.959-.073,3.026,1.475,4.529,4.022,1.417,2.4,3.023,5.121,6.324,5.241,3.415.118,5.064-2.863,6.5-5.5,1.245-2.282,2.419-4.437,4.5-4.509,1.959-.046,2.981,1.743,4.492,4.732,1.412,2.79,3.013,5.95,6.365,6.071l.185,0c3.348,0,4.937-3.36,6.343-6.331,1.245-2.634,2.423-5.114,4.444-5.216Z" transform="translate(7.109 -13.11)" fill-rule="evenodd"/>
<path id="Path_50" data-name="Path 50" d="M83,186.71h43.71V143H83Z" transform="translate(4.42 -5.561)" fill="#3ecc5f" fill-rule="evenodd"/>
<g id="Group_8" data-name="Group 8" transform="matrix(0.966, -0.259, 0.259, 0.966, 109.327, 91.085)">
<rect id="Rectangle_3" data-name="Rectangle 3" width="92.361" height="36.462" rx="2" transform="translate(0 0)" fill="#d8d8d8"/>
<g id="Group_2" data-name="Group 2" transform="translate(1.531 23.03)">
<rect id="Rectangle_4" data-name="Rectangle 4" width="5.336" height="5.336" rx="1" transform="translate(16.797 0)" fill="#4a4a4a"/>
<rect id="Rectangle_5" data-name="Rectangle 5" width="5.336" height="5.336" rx="1" transform="translate(23.12 0)" fill="#4a4a4a"/>
<rect id="Rectangle_6" data-name="Rectangle 6" width="5.336" height="5.336" rx="1" transform="translate(29.444 0)" fill="#4a4a4a"/>
<rect id="Rectangle_7" data-name="Rectangle 7" width="5.336" height="5.336" rx="1" transform="translate(35.768 0)" fill="#4a4a4a"/>
<rect id="Rectangle_8" data-name="Rectangle 8" width="5.336" height="5.336" rx="1" transform="translate(42.091 0)" fill="#4a4a4a"/>
<rect id="Rectangle_9" data-name="Rectangle 9" width="5.336" height="5.336" rx="1" transform="translate(48.415 0)" fill="#4a4a4a"/>
<rect id="Rectangle_10" data-name="Rectangle 10" width="5.336" height="5.336" rx="1" transform="translate(54.739 0)" fill="#4a4a4a"/>
<rect id="Rectangle_11" data-name="Rectangle 11" width="5.336" height="5.336" rx="1" transform="translate(61.063 0)" fill="#4a4a4a"/>
<rect id="Rectangle_12" data-name="Rectangle 12" width="5.336" height="5.336" rx="1" transform="translate(67.386 0)" fill="#4a4a4a"/>
<path id="Path_51" data-name="Path 51" d="M1.093,0H14.518a1.093,1.093,0,0,1,1.093,1.093V4.243a1.093,1.093,0,0,1-1.093,1.093H1.093A1.093,1.093,0,0,1,0,4.243V1.093A1.093,1.093,0,0,1,1.093,0ZM75,0H88.426a1.093,1.093,0,0,1,1.093,1.093V4.243a1.093,1.093,0,0,1-1.093,1.093H75a1.093,1.093,0,0,1-1.093-1.093V1.093A1.093,1.093,0,0,1,75,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
</g>
<g id="Group_3" data-name="Group 3" transform="translate(1.531 10.261)">
<path id="Path_52" data-name="Path 52" d="M1.093,0H6.218A1.093,1.093,0,0,1,7.31,1.093V4.242A1.093,1.093,0,0,1,6.218,5.335H1.093A1.093,1.093,0,0,1,0,4.242V1.093A1.093,1.093,0,0,1,1.093,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
<rect id="Rectangle_13" data-name="Rectangle 13" width="5.336" height="5.336" rx="1" transform="translate(8.299 0)" fill="#4a4a4a"/>
<rect id="Rectangle_14" data-name="Rectangle 14" width="5.336" height="5.336" rx="1" transform="translate(14.623 0)" fill="#4a4a4a"/>
<rect id="Rectangle_15" data-name="Rectangle 15" width="5.336" height="5.336" rx="1" transform="translate(20.947 0)" fill="#4a4a4a"/>
<rect id="Rectangle_16" data-name="Rectangle 16" width="5.336" height="5.336" rx="1" transform="translate(27.271 0)" fill="#4a4a4a"/>
<rect id="Rectangle_17" data-name="Rectangle 17" width="5.336" height="5.336" rx="1" transform="translate(33.594 0)" fill="#4a4a4a"/>
<rect id="Rectangle_18" data-name="Rectangle 18" width="5.336" height="5.336" rx="1" transform="translate(39.918 0)" fill="#4a4a4a"/>
<rect id="Rectangle_19" data-name="Rectangle 19" width="5.336" height="5.336" rx="1" transform="translate(46.242 0)" fill="#4a4a4a"/>
<rect id="Rectangle_20" data-name="Rectangle 20" width="5.336" height="5.336" rx="1" transform="translate(52.565 0)" fill="#4a4a4a"/>
<rect id="Rectangle_21" data-name="Rectangle 21" width="5.336" height="5.336" rx="1" transform="translate(58.888 0)" fill="#4a4a4a"/>
<rect id="Rectangle_22" data-name="Rectangle 22" width="5.336" height="5.336" rx="1" transform="translate(65.212 0)" fill="#4a4a4a"/>
<rect id="Rectangle_23" data-name="Rectangle 23" width="5.336" height="5.336" rx="1" transform="translate(71.536 0)" fill="#4a4a4a"/>
<rect id="Rectangle_24" data-name="Rectangle 24" width="5.336" height="5.336" rx="1" transform="translate(77.859 0)" fill="#4a4a4a"/>
<rect id="Rectangle_25" data-name="Rectangle 25" width="5.336" height="5.336" rx="1" transform="translate(84.183 0)" fill="#4a4a4a"/>
</g>
<g id="Group_4" data-name="Group 4" transform="translate(91.05 9.546) rotate(180)">
<path id="Path_53" data-name="Path 53" d="M1.093,0H6.219A1.093,1.093,0,0,1,7.312,1.093v3.15A1.093,1.093,0,0,1,6.219,5.336H1.093A1.093,1.093,0,0,1,0,4.243V1.093A1.093,1.093,0,0,1,1.093,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
<rect id="Rectangle_26" data-name="Rectangle 26" width="5.336" height="5.336" rx="1" transform="translate(8.299 0)" fill="#4a4a4a"/>
<rect id="Rectangle_27" data-name="Rectangle 27" width="5.336" height="5.336" rx="1" transform="translate(14.623 0)" fill="#4a4a4a"/>
<rect id="Rectangle_28" data-name="Rectangle 28" width="5.336" height="5.336" rx="1" transform="translate(20.947 0)" fill="#4a4a4a"/>
<rect id="Rectangle_29" data-name="Rectangle 29" width="5.336" height="5.336" rx="1" transform="translate(27.271 0)" fill="#4a4a4a"/>
<rect id="Rectangle_30" data-name="Rectangle 30" width="5.336" height="5.336" rx="1" transform="translate(33.594 0)" fill="#4a4a4a"/>
<rect id="Rectangle_31" data-name="Rectangle 31" width="5.336" height="5.336" rx="1" transform="translate(39.918 0)" fill="#4a4a4a"/>
<rect id="Rectangle_32" data-name="Rectangle 32" width="5.336" height="5.336" rx="1" transform="translate(46.242 0)" fill="#4a4a4a"/>
<rect id="Rectangle_33" data-name="Rectangle 33" width="5.336" height="5.336" rx="1" transform="translate(52.565 0)" fill="#4a4a4a"/>
<rect id="Rectangle_34" data-name="Rectangle 34" width="5.336" height="5.336" rx="1" transform="translate(58.889 0)" fill="#4a4a4a"/>
<rect id="Rectangle_35" data-name="Rectangle 35" width="5.336" height="5.336" rx="1" transform="translate(65.213 0)" fill="#4a4a4a"/>
<rect id="Rectangle_36" data-name="Rectangle 36" width="5.336" height="5.336" rx="1" transform="translate(71.537 0)" fill="#4a4a4a"/>
<rect id="Rectangle_37" data-name="Rectangle 37" width="5.336" height="5.336" rx="1" transform="translate(77.86 0)" fill="#4a4a4a"/>
<rect id="Rectangle_38" data-name="Rectangle 38" width="5.336" height="5.336" rx="1" transform="translate(84.183 0)" fill="#4a4a4a"/>
<rect id="Rectangle_39" data-name="Rectangle 39" width="5.336" height="5.336" rx="1" transform="translate(8.299 0)" fill="#4a4a4a"/>
<rect id="Rectangle_40" data-name="Rectangle 40" width="5.336" height="5.336" rx="1" transform="translate(14.623 0)" fill="#4a4a4a"/>
<rect id="Rectangle_41" data-name="Rectangle 41" width="5.336" height="5.336" rx="1" transform="translate(20.947 0)" fill="#4a4a4a"/>
<rect id="Rectangle_42" data-name="Rectangle 42" width="5.336" height="5.336" rx="1" transform="translate(27.271 0)" fill="#4a4a4a"/>
<rect id="Rectangle_43" data-name="Rectangle 43" width="5.336" height="5.336" rx="1" transform="translate(33.594 0)" fill="#4a4a4a"/>
<rect id="Rectangle_44" data-name="Rectangle 44" width="5.336" height="5.336" rx="1" transform="translate(39.918 0)" fill="#4a4a4a"/>
<rect id="Rectangle_45" data-name="Rectangle 45" width="5.336" height="5.336" rx="1" transform="translate(46.242 0)" fill="#4a4a4a"/>
<rect id="Rectangle_46" data-name="Rectangle 46" width="5.336" height="5.336" rx="1" transform="translate(52.565 0)" fill="#4a4a4a"/>
<rect id="Rectangle_47" data-name="Rectangle 47" width="5.336" height="5.336" rx="1" transform="translate(58.889 0)" fill="#4a4a4a"/>
<rect id="Rectangle_48" data-name="Rectangle 48" width="5.336" height="5.336" rx="1" transform="translate(65.213 0)" fill="#4a4a4a"/>
<rect id="Rectangle_49" data-name="Rectangle 49" width="5.336" height="5.336" rx="1" transform="translate(71.537 0)" fill="#4a4a4a"/>
<rect id="Rectangle_50" data-name="Rectangle 50" width="5.336" height="5.336" rx="1" transform="translate(77.86 0)" fill="#4a4a4a"/>
<rect id="Rectangle_51" data-name="Rectangle 51" width="5.336" height="5.336" rx="1" transform="translate(84.183 0)" fill="#4a4a4a"/>
</g>
<g id="Group_6" data-name="Group 6" transform="translate(1.531 16.584)">
<path id="Path_54" data-name="Path 54" d="M1.093,0h7.3A1.093,1.093,0,0,1,9.485,1.093v3.15A1.093,1.093,0,0,1,8.392,5.336h-7.3A1.093,1.093,0,0,1,0,4.243V1.094A1.093,1.093,0,0,1,1.093,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
<g id="Group_5" data-name="Group 5" transform="translate(10.671 0)">
<rect id="Rectangle_52" data-name="Rectangle 52" width="5.336" height="5.336" rx="1" fill="#4a4a4a"/>
<rect id="Rectangle_53" data-name="Rectangle 53" width="5.336" height="5.336" rx="1" transform="translate(6.324 0)" fill="#4a4a4a"/>
<rect id="Rectangle_54" data-name="Rectangle 54" width="5.336" height="5.336" rx="1" transform="translate(12.647 0)" fill="#4a4a4a"/>
<rect id="Rectangle_55" data-name="Rectangle 55" width="5.336" height="5.336" rx="1" transform="translate(18.971 0)" fill="#4a4a4a"/>
<rect id="Rectangle_56" data-name="Rectangle 56" width="5.336" height="5.336" rx="1" transform="translate(25.295 0)" fill="#4a4a4a"/>
<rect id="Rectangle_57" data-name="Rectangle 57" width="5.336" height="5.336" rx="1" transform="translate(31.619 0)" fill="#4a4a4a"/>
<rect id="Rectangle_58" data-name="Rectangle 58" width="5.336" height="5.336" rx="1" transform="translate(37.942 0)" fill="#4a4a4a"/>
<rect id="Rectangle_59" data-name="Rectangle 59" width="5.336" height="5.336" rx="1" transform="translate(44.265 0)" fill="#4a4a4a"/>
<rect id="Rectangle_60" data-name="Rectangle 60" width="5.336" height="5.336" rx="1" transform="translate(50.589 0)" fill="#4a4a4a"/>
<rect id="Rectangle_61" data-name="Rectangle 61" width="5.336" height="5.336" rx="1" transform="translate(56.912 0)" fill="#4a4a4a"/>
<rect id="Rectangle_62" data-name="Rectangle 62" width="5.336" height="5.336" rx="1" transform="translate(63.236 0)" fill="#4a4a4a"/>
</g>
<path id="Path_55" data-name="Path 55" d="M1.094,0H8A1.093,1.093,0,0,1,9.091,1.093v3.15A1.093,1.093,0,0,1,8,5.336H1.093A1.093,1.093,0,0,1,0,4.243V1.094A1.093,1.093,0,0,1,1.093,0Z" transform="translate(80.428 0)" fill="#4a4a4a" fill-rule="evenodd"/>
</g>
<g id="Group_7" data-name="Group 7" transform="translate(1.531 29.627)">
<rect id="Rectangle_63" data-name="Rectangle 63" width="5.336" height="5.336" rx="1" transform="translate(0 0)" fill="#4a4a4a"/>
<rect id="Rectangle_64" data-name="Rectangle 64" width="5.336" height="5.336" rx="1" transform="translate(6.324 0)" fill="#4a4a4a"/>
<rect id="Rectangle_65" data-name="Rectangle 65" width="5.336" height="5.336" rx="1" transform="translate(12.647 0)" fill="#4a4a4a"/>
<rect id="Rectangle_66" data-name="Rectangle 66" width="5.336" height="5.336" rx="1" transform="translate(18.971 0)" fill="#4a4a4a"/>
<path id="Path_56" data-name="Path 56" d="M1.093,0H31.515a1.093,1.093,0,0,1,1.093,1.093V4.244a1.093,1.093,0,0,1-1.093,1.093H1.093A1.093,1.093,0,0,1,0,4.244V1.093A1.093,1.093,0,0,1,1.093,0ZM34.687,0h3.942a1.093,1.093,0,0,1,1.093,1.093V4.244a1.093,1.093,0,0,1-1.093,1.093H34.687a1.093,1.093,0,0,1-1.093-1.093V1.093A1.093,1.093,0,0,1,34.687,0Z" transform="translate(25.294 0)" fill="#4a4a4a" fill-rule="evenodd"/>
<rect id="Rectangle_67" data-name="Rectangle 67" width="5.336" height="5.336" rx="1" transform="translate(66.003 0)" fill="#4a4a4a"/>
<rect id="Rectangle_68" data-name="Rectangle 68" width="5.336" height="5.336" rx="1" transform="translate(72.327 0)" fill="#4a4a4a"/>
<rect id="Rectangle_69" data-name="Rectangle 69" width="5.336" height="5.336" rx="1" transform="translate(84.183 0)" fill="#4a4a4a"/>
<path id="Path_57" data-name="Path 57" d="M5.336,0V1.18A1.093,1.093,0,0,1,4.243,2.273H1.093A1.093,1.093,0,0,1,0,1.18V0Z" transform="translate(83.59 2.273) rotate(180)" fill="#4a4a4a"/>
<path id="Path_58" data-name="Path 58" d="M5.336,0V1.18A1.093,1.093,0,0,1,4.243,2.273H1.093A1.093,1.093,0,0,1,0,1.18V0Z" transform="translate(78.255 3.063)" fill="#4a4a4a"/>
</g>
<rect id="Rectangle_70" data-name="Rectangle 70" width="88.927" height="2.371" rx="1.085" transform="translate(1.925 1.17)" fill="#4a4a4a"/>
<rect id="Rectangle_71" data-name="Rectangle 71" width="4.986" height="1.581" rx="0.723" transform="translate(4.1 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_72" data-name="Rectangle 72" width="4.986" height="1.581" rx="0.723" transform="translate(10.923 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_73" data-name="Rectangle 73" width="4.986" height="1.581" rx="0.723" transform="translate(16.173 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_74" data-name="Rectangle 74" width="4.986" height="1.581" rx="0.723" transform="translate(21.421 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_75" data-name="Rectangle 75" width="4.986" height="1.581" rx="0.723" transform="translate(26.671 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_76" data-name="Rectangle 76" width="4.986" height="1.581" rx="0.723" transform="translate(33.232 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_77" data-name="Rectangle 77" width="4.986" height="1.581" rx="0.723" transform="translate(38.48 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_78" data-name="Rectangle 78" width="4.986" height="1.581" rx="0.723" transform="translate(43.73 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_79" data-name="Rectangle 79" width="4.986" height="1.581" rx="0.723" transform="translate(48.978 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_80" data-name="Rectangle 80" width="4.986" height="1.581" rx="0.723" transform="translate(55.54 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_81" data-name="Rectangle 81" width="4.986" height="1.581" rx="0.723" transform="translate(60.788 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_82" data-name="Rectangle 82" width="4.986" height="1.581" rx="0.723" transform="translate(66.038 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_83" data-name="Rectangle 83" width="4.986" height="1.581" rx="0.723" transform="translate(72.599 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_84" data-name="Rectangle 84" width="4.986" height="1.581" rx="0.723" transform="translate(77.847 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_85" data-name="Rectangle 85" width="4.986" height="1.581" rx="0.723" transform="translate(83.097 1.566)" fill="#d8d8d8" opacity="0.136"/>
</g>
<path id="Path_59" data-name="Path 59" d="M146.71,159.855a5.439,5.439,0,0,0-.7.07c-.042-.164-.081-.329-.127-.493a5.457,5.457,0,1,0-5.4-9.372q-.181-.185-.366-.367a5.454,5.454,0,1,0-9.384-5.4c-.162-.046-.325-.084-.486-.126a5.467,5.467,0,1,0-10.788,0c-.162.042-.325.08-.486.126a5.457,5.457,0,1,0-9.384,5.4,21.843,21.843,0,1,0,36.421,21.02,5.452,5.452,0,1,0,.7-10.858" transform="translate(6.275 -6.025)" fill="#44d860" fill-rule="evenodd"/>
<path id="Path_60" data-name="Path 60" d="M83,124.855h43.71V103H83Z" transform="translate(4.42 -9.271)" fill="#3ecc5f" fill-rule="evenodd"/>
<path id="Path_61" data-name="Path 61" d="M134.855,116.765a2.732,2.732,0,1,0,0-5.464,2.811,2.811,0,0,0-.349.035c-.022-.082-.04-.164-.063-.246a2.733,2.733,0,0,0-1.052-5.253,2.7,2.7,0,0,0-1.648.566q-.09-.093-.184-.184a2.7,2.7,0,0,0,.553-1.633,2.732,2.732,0,0,0-5.245-1.07,10.928,10.928,0,1,0,0,21.031,2.732,2.732,0,0,0,5.245-1.07,2.7,2.7,0,0,0-.553-1.633q.093-.09.184-.184a2.7,2.7,0,0,0,1.648.566,2.732,2.732,0,0,0,1.052-5.253c.023-.081.042-.164.063-.246a2.811,2.811,0,0,0,.349.035" transform="translate(7.202 -9.377)" fill="#44d860" fill-rule="evenodd"/>
<path id="Path_62" data-name="Path 62" d="M143.232,42.33a2.967,2.967,0,0,1-.535-.055,2.754,2.754,0,0,1-.514-.153,2.838,2.838,0,0,1-.471-.251,4.139,4.139,0,0,1-.415-.339,3.2,3.2,0,0,1-.338-.415A2.7,2.7,0,0,1,140.5,39.6a2.968,2.968,0,0,1,.055-.535,3.152,3.152,0,0,1,.152-.514,2.874,2.874,0,0,1,.252-.47,2.633,2.633,0,0,1,.753-.754,2.837,2.837,0,0,1,.471-.251,2.753,2.753,0,0,1,.514-.153,2.527,2.527,0,0,1,1.071,0,2.654,2.654,0,0,1,.983.4,4.139,4.139,0,0,1,.415.339,4.019,4.019,0,0,1,.339.415,2.786,2.786,0,0,1,.251.47,2.864,2.864,0,0,1,.208,1.049,2.77,2.77,0,0,1-.8,1.934,4.139,4.139,0,0,1-.415.339,2.722,2.722,0,0,1-1.519.459m21.855-1.366a2.789,2.789,0,0,1-1.935-.8,4.162,4.162,0,0,1-.338-.415,2.7,2.7,0,0,1-.459-1.519,2.789,2.789,0,0,1,.8-1.934,4.139,4.139,0,0,1,.415-.339,2.838,2.838,0,0,1,.471-.251,2.752,2.752,0,0,1,.514-.153,2.527,2.527,0,0,1,1.071,0,2.654,2.654,0,0,1,.983.4,4.139,4.139,0,0,1,.415.339,2.79,2.79,0,0,1,.8,1.934,3.069,3.069,0,0,1-.055.535,2.779,2.779,0,0,1-.153.514,3.885,3.885,0,0,1-.251.47,4.02,4.02,0,0,1-.339.415,4.138,4.138,0,0,1-.415.339,2.722,2.722,0,0,1-1.519.459" transform="translate(9.753 -15.532)" fill-rule="evenodd"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 31 KiB

@@ -1,170 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1041.277" height="554.141" viewBox="0 0 1041.277 554.141">
<title>Powered by React</title>
<g id="Group_24" data-name="Group 24" transform="translate(-440 -263)">
<g id="Group_23" data-name="Group 23" transform="translate(439.989 262.965)">
<path id="Path_299" data-name="Path 299" d="M1040.82,611.12q-1.74,3.75-3.47,7.4-2.7,5.67-5.33,11.12c-.78,1.61-1.56,3.19-2.32,4.77-8.6,17.57-16.63,33.11-23.45,45.89A73.21,73.21,0,0,1,942.44,719l-151.65,1.65h-1.6l-13,.14-11.12.12-34.1.37h-1.38l-17.36.19h-.53l-107,1.16-95.51,1-11.11.12-69,.75H429l-44.75.48h-.48l-141.5,1.53-42.33.46a87.991,87.991,0,0,1-10.79-.54h0c-1.22-.14-2.44-.3-3.65-.49a87.38,87.38,0,0,1-51.29-27.54C116,678.37,102.75,655,93.85,629.64q-1.93-5.49-3.6-11.12C59.44,514.37,97,380,164.6,290.08q4.25-5.64,8.64-11l.07-.08c20.79-25.52,44.1-46.84,68.93-62,44-26.91,92.75-34.49,140.7-11.9,40.57,19.12,78.45,28.11,115.17,30.55,3.71.24,7.42.42,11.11.53,84.23,2.65,163.17-27.7,255.87-47.29,3.69-.78,7.39-1.55,11.12-2.28,66.13-13.16,139.49-20.1,226.73-5.51a189.089,189.089,0,0,1,26.76,6.4q5.77,1.86,11.12,4c41.64,16.94,64.35,48.24,74,87.46q1.37,5.46,2.37,11.11C1134.3,384.41,1084.19,518.23,1040.82,611.12Z" transform="translate(-79.34 -172.91)" fill="#f2f2f2"/>
<path id="Path_300" data-name="Path 300" d="M576.36,618.52a95.21,95.21,0,0,1-1.87,11.12h93.7V618.52Zm-78.25,62.81,11.11-.09V653.77c-3.81-.17-7.52-.34-11.11-.52ZM265.19,618.52v11.12h198.5V618.52ZM1114.87,279h-74V191.51q-5.35-2.17-11.12-4V279H776.21V186.58c-3.73.73-7.43,1.5-11.12,2.28V279H509.22V236.15c-3.69-.11-7.4-.29-11.11-.53V279H242.24V217c-24.83,15.16-48.14,36.48-68.93,62h-.07v.08q-4.4,5.4-8.64,11h8.64V618.52h-83q1.66,5.63,3.6,11.12h79.39v93.62a87,87,0,0,0,12.2,2.79c1.21.19,2.43.35,3.65.49h0a87.991,87.991,0,0,0,10.79.54l42.33-.46v-97H498.11v94.21l11.11-.12V629.64H765.09V721l11.12-.12V629.64H1029.7v4.77c.76-1.58,1.54-3.16,2.32-4.77q2.63-5.45,5.33-11.12,1.73-3.64,3.47-7.4v-321h76.42Q1116.23,284.43,1114.87,279ZM242.24,618.52V290.08H498.11V618.52Zm267,0V290.08H765.09V618.52Zm520.48,0H776.21V290.08H1029.7Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
<path id="Path_301" data-name="Path 301" d="M863.09,533.65v13l-151.92,1.4-1.62.03-57.74.53-1.38.02-17.55.15h-.52l-106.98.99L349.77,551.4h-.15l-44.65.42-.48.01-198.4,1.82v-15l46.65-28,93.6-.78,2-.01.66-.01,2-.03,44.94-.37,2.01-.01.64-.01,2-.01L315,509.3l.38-.01,35.55-.3h.29l277.4-2.34,6.79-.05h.68l5.18-.05,37.65-.31,2-.03,1.85-.02h.96l11.71-.09,2.32-.03,3.11-.02,9.75-.09,15.47-.13,2-.02,3.48-.02h.65l74.71-.64Z" fill="#65617d"/>
<path id="Path_302" data-name="Path 302" d="M863.09,533.65v13l-151.92,1.4-1.62.03-57.74.53-1.38.02-17.55.15h-.52l-106.98.99L349.77,551.4h-.15l-44.65.42-.48.01-198.4,1.82v-15l46.65-28,93.6-.78,2-.01.66-.01,2-.03,44.94-.37,2.01-.01.64-.01,2-.01L315,509.3l.38-.01,35.55-.3h.29l277.4-2.34,6.79-.05h.68l5.18-.05,37.65-.31,2-.03,1.85-.02h.96l11.71-.09,2.32-.03,3.11-.02,9.75-.09,15.47-.13,2-.02,3.48-.02h.65l74.71-.64Z" opacity="0.2"/>
<path id="Path_303" data-name="Path 303" d="M375.44,656.57v24.49a6.13,6.13,0,0,1-3.5,5.54,6,6,0,0,1-2.5.6l-34.9.74a6,6,0,0,1-2.7-.57,6.12,6.12,0,0,1-3.57-5.57V656.57Z" transform="translate(-79.34 -172.91)" fill="#3f3d56"/>
<path id="Path_304" data-name="Path 304" d="M375.44,656.57v24.49a6.13,6.13,0,0,1-3.5,5.54,6,6,0,0,1-2.5.6l-34.9.74a6,6,0,0,1-2.7-.57,6.12,6.12,0,0,1-3.57-5.57V656.57Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
<path id="Path_305" data-name="Path 305" d="M377.44,656.57v24.49a6.13,6.13,0,0,1-3.5,5.54,6,6,0,0,1-2.5.6l-34.9.74a6,6,0,0,1-2.7-.57,6.12,6.12,0,0,1-3.57-5.57V656.57Z" transform="translate(-79.34 -172.91)" fill="#3f3d56"/>
<rect id="Rectangle_137" data-name="Rectangle 137" width="47.17" height="31.5" transform="translate(680.92 483.65)" fill="#3f3d56"/>
<rect id="Rectangle_138" data-name="Rectangle 138" width="47.17" height="31.5" transform="translate(680.92 483.65)" opacity="0.1"/>
<rect id="Rectangle_139" data-name="Rectangle 139" width="47.17" height="31.5" transform="translate(678.92 483.65)" fill="#3f3d56"/>
<path id="Path_306" data-name="Path 306" d="M298.09,483.65v4.97l-47.17,1.26v-6.23Z" opacity="0.1"/>
<path id="Path_307" data-name="Path 307" d="M460.69,485.27v168.2a4,4,0,0,1-3.85,3.95l-191.65,5.1h-.05a4,4,0,0,1-3.95-3.95V485.27a4,4,0,0,1,3.95-3.95h191.6a4,4,0,0,1,3.95,3.95Z" transform="translate(-79.34 -172.91)" fill="#65617d"/>
<path id="Path_308" data-name="Path 308" d="M265.19,481.32v181.2h-.05a4,4,0,0,1-3.95-3.95V485.27a4,4,0,0,1,3.95-3.95Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
<path id="Path_309" data-name="Path 309" d="M194.59,319.15h177.5V467.4l-177.5,4Z" fill="#39374d"/>
<path id="Path_310" data-name="Path 310" d="M726.09,483.65v6.41l-47.17-1.26v-5.15Z" opacity="0.1"/>
<path id="Path_311" data-name="Path 311" d="M867.69,485.27v173.3a4,4,0,0,1-4,3.95h0L672,657.42a4,4,0,0,1-3.85-3.95V485.27a4,4,0,0,1,3.95-3.95H863.7a4,4,0,0,1,3.99,3.95Z" transform="translate(-79.34 -172.91)" fill="#65617d"/>
<path id="Path_312" data-name="Path 312" d="M867.69,485.27v173.3a4,4,0,0,1-4,3.95h0V481.32h0a4,4,0,0,1,4,3.95Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
<path id="Path_313" data-name="Path 313" d="M775.59,319.15H598.09V467.4l177.5,4Z" fill="#39374d"/>
<path id="Path_314" data-name="Path 314" d="M663.19,485.27v168.2a4,4,0,0,1-3.85,3.95l-191.65,5.1h0a4,4,0,0,1-4-3.95V485.27a4,4,0,0,1,3.95-3.95h191.6A4,4,0,0,1,663.19,485.27Z" transform="translate(-79.34 -172.91)" fill="#65617d"/>
<path id="Path_315" data-name="Path 315" d="M397.09,319.15h177.5V467.4l-177.5,4Z" fill="#4267b2"/>
<path id="Path_316" data-name="Path 316" d="M863.09,533.65v13l-151.92,1.4-1.62.03-57.74.53-1.38.02-17.55.15h-.52l-106.98.99L349.77,551.4h-.15l-44.65.42-.48.01-198.4,1.82v-15l202.51-1.33h.48l40.99-.28h.19l283.08-1.87h.29l.17-.01h.47l4.79-.03h1.46l74.49-.5,4.4-.02.98-.01Z" opacity="0.1"/>
<circle id="Ellipse_111" data-name="Ellipse 111" cx="51.33" cy="51.33" r="51.33" transform="translate(435.93 246.82)" fill="#fbbebe"/>
<path id="Path_317" data-name="Path 317" d="M617.94,550.07s-99.5,12-90,0c3.44-4.34,4.39-17.2,4.2-31.85-.06-4.45-.22-9.06-.45-13.65-1.1-22-3.75-43.5-3.75-43.5s87-41,77-8.5c-4,13.13-2.69,31.57.35,48.88.89,5.05,1.92,10,3,14.7a344.66,344.66,0,0,0,9.65,33.92Z" transform="translate(-79.34 -172.91)" fill="#fbbebe"/>
<path id="Path_318" data-name="Path 318" d="M585.47,546c11.51-2.13,23.7-6,34.53-1.54,2.85,1.17,5.47,2.88,8.39,3.86s6.12,1.22,9.16,1.91c10.68,2.42,19.34,10.55,24.9,20s8.44,20.14,11.26,30.72l6.9,25.83c6,22.45,12,45.09,13.39,68.3a2437.506,2437.506,0,0,1-250.84,1.43c5.44-10.34,11-21.31,10.54-33s-7.19-23.22-4.76-34.74c1.55-7.34,6.57-13.39,9.64-20.22,8.75-19.52,1.94-45.79,17.32-60.65,6.92-6.68,17-9.21,26.63-8.89,12.28.41,24.85,4.24,37,6.11C555.09,547.48,569.79,548.88,585.47,546Z" transform="translate(-79.34 -172.91)" fill="#ff6584"/>
<path id="Path_319" data-name="Path 319" d="M716.37,657.17l-.1,1.43v.1l-.17,2.3-1.33,18.51-1.61,22.3-.46,6.28-1,13.44v.17l-107,1-175.59,1.9v.84h-.14v-1.12l.45-14.36.86-28.06.74-23.79.07-2.37a10.53,10.53,0,0,1,11.42-10.17c4.72.4,10.85.89,18.18,1.41l3,.22c42.33,2.94,120.56,6.74,199.5,2,1.66-.09,3.33-.19,5-.31,12.24-.77,24.47-1.76,36.58-3a10.53,10.53,0,0,1,11.6,11.23Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
<path id="Path_320" data-name="Path 320" d="M429.08,725.44v-.84l175.62-1.91,107-1h.3v-.17l1-13.44.43-6,1.64-22.61,1.29-17.9v-.44a10.617,10.617,0,0,0-.11-2.47.3.3,0,0,0,0-.1,10.391,10.391,0,0,0-2-4.64,10.54,10.54,0,0,0-9.42-4c-12.11,1.24-24.34,2.23-36.58,3-1.67.12-3.34.22-5,.31-78.94,4.69-157.17.89-199.5-2l-3-.22c-7.33-.52-13.46-1-18.18-1.41a10.54,10.54,0,0,0-11.24,8.53,11,11,0,0,0-.18,1.64l-.68,22.16L429.54,710l-.44,14.36v1.12Z" transform="translate(-79.34 -172.91)" fill="#3f3d56"/>
<path id="Path_321" data-name="Path 321" d="M716.67,664.18l-1.23,15.33-1.83,22.85-.46,5.72-1,12.81-.06.64v.17h0l-.15,1.48.11-1.48h-.29l-107,1-175.65,1.9v-.28l.49-14.36,1-28.06.64-18.65A6.36,6.36,0,0,1,434.3,658a6.25,6.25,0,0,1,3.78-.9c2.1.17,4.68.37,7.69.59,4.89.36,10.92.78,17.94,1.22,13,.82,29.31,1.7,48,2.42,52,2,122.2,2.67,188.88-3.17,3-.26,6.1-.55,9.13-.84a6.26,6.26,0,0,1,3.48.66,5.159,5.159,0,0,1,.86.54,6.14,6.14,0,0,1,2,2.46,3.564,3.564,0,0,1,.25.61A6.279,6.279,0,0,1,716.67,664.18Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
<path id="Path_322" data-name="Path 322" d="M377.44,677.87v3.19a6.13,6.13,0,0,1-3.5,5.54l-40.1.77a6.12,6.12,0,0,1-3.57-5.57v-3Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
<path id="Path_323" data-name="Path 323" d="M298.59,515.57l-52.25,1V507.9l52.25-1Z" fill="#3f3d56"/>
<path id="Path_324" data-name="Path 324" d="M298.59,515.57l-52.25,1V507.9l52.25-1Z" opacity="0.1"/>
<path id="Path_325" data-name="Path 325" d="M300.59,515.57l-52.25,1V507.9l52.25-1Z" fill="#3f3d56"/>
<path id="Path_326" data-name="Path 326" d="M758.56,679.87v3.19a6.13,6.13,0,0,0,3.5,5.54l40.1.77a6.12,6.12,0,0,0,3.57-5.57v-3Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
<path id="Path_327" data-name="Path 327" d="M678.72,517.57l52.25,1V509.9l-52.25-1Z" opacity="0.1"/>
<path id="Path_328" data-name="Path 328" d="M676.72,517.57l52.25,1V509.9l-52.25-1Z" fill="#3f3d56"/>
<path id="Path_329" data-name="Path 329" d="M534.13,486.79c.08,7-3.16,13.6-5.91,20.07a163.491,163.491,0,0,0-12.66,74.71c.73,11,2.58,22,.73,32.9s-8.43,21.77-19,24.9c17.53,10.45,41.26,9.35,57.76-2.66,8.79-6.4,15.34-15.33,21.75-24.11a97.86,97.86,0,0,1-13.31,44.75A103.43,103.43,0,0,0,637,616.53c4.31-5.81,8.06-12.19,9.72-19.23,3.09-13-1.22-26.51-4.51-39.5a266.055,266.055,0,0,1-6.17-33c-.43-3.56-.78-7.22.1-10.7,1-4.07,3.67-7.51,5.64-11.22,5.6-10.54,5.73-23.3,2.86-34.88s-8.49-22.26-14.06-32.81c-4.46-8.46-9.3-17.31-17.46-22.28-5.1-3.1-11-4.39-16.88-5.64l-25.37-5.43c-5.55-1.19-11.26-2.38-16.87-1.51-9.47,1.48-16.14,8.32-22,15.34-4.59,5.46-15.81,15.71-16.6,22.86-.72,6.59,5.1,17.63,6.09,24.58,1.3,9,2.22,6,7.3,11.52C532,478.05,534.07,482,534.13,486.79Z" transform="translate(-79.34 -172.91)" fill="#3f3d56"/>
</g>
<g id="docusaurus_keytar" transform="translate(670.271 615.768)">
<path id="Path_40" data-name="Path 40" d="M99,52h43.635V69.662H99Z" transform="translate(-49.132 -33.936)" fill="#fff" fill-rule="evenodd"/>
<path id="Path_41" data-name="Path 41" d="M13.389,158.195A10.377,10.377,0,0,1,4.4,153a10.377,10.377,0,0,0,8.988,15.584H23.779V158.195Z" transform="translate(-3 -82.47)" fill="#3ecc5f" fill-rule="evenodd"/>
<path id="Path_42" data-name="Path 42" d="M66.967,38.083l36.373-2.273V30.615A10.389,10.389,0,0,0,92.95,20.226H46.2l-1.3-2.249a1.5,1.5,0,0,0-2.6,0L41,20.226l-1.3-2.249a1.5,1.5,0,0,0-2.6,0l-1.3,2.249-1.3-2.249a1.5,1.5,0,0,0-2.6,0l-1.3,2.249-.034,0-2.152-2.151a1.5,1.5,0,0,0-2.508.672L25.21,21.4l-2.7-.723a1.5,1.5,0,0,0-1.836,1.837l.722,2.7-2.65.71a1.5,1.5,0,0,0-.673,2.509l2.152,2.152c0,.011,0,.022,0,.033l-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6L20.226,41l-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3A10.389,10.389,0,0,0,30.615,103.34H92.95A10.389,10.389,0,0,0,103.34,92.95V51.393L66.967,49.12a5.53,5.53,0,0,1,0-11.038" transform="translate(-9.836 -17.226)" fill="#3ecc5f" fill-rule="evenodd"/>
<path id="Path_43" data-name="Path 43" d="M143,163.779h15.584V143H143Z" transform="translate(-70.275 -77.665)" fill="#3ecc5f" fill-rule="evenodd"/>
<path id="Path_44" data-name="Path 44" d="M173.779,148.389a2.582,2.582,0,0,0-.332.033c-.02-.078-.038-.156-.06-.234a2.594,2.594,0,1,0-2.567-4.455q-.086-.088-.174-.175a2.593,2.593,0,1,0-4.461-2.569c-.077-.022-.154-.04-.231-.06a2.6,2.6,0,1,0-5.128,0c-.077.02-.154.038-.231.06a2.594,2.594,0,1,0-4.461,2.569,10.384,10.384,0,1,0,17.314,9.992,2.592,2.592,0,1,0,.332-5.161" transform="translate(-75.08 -75.262)" fill="#44d860" fill-rule="evenodd"/>
<path id="Path_45" data-name="Path 45" d="M153,113.389h15.584V103H153Z" transform="translate(-75.08 -58.444)" fill="#3ecc5f" fill-rule="evenodd"/>
<path id="Path_46" data-name="Path 46" d="M183.389,108.944a1.3,1.3,0,1,0,0-2.6,1.336,1.336,0,0,0-.166.017c-.01-.039-.019-.078-.03-.117a1.3,1.3,0,0,0-.5-2.5,1.285,1.285,0,0,0-.783.269q-.043-.044-.087-.087a1.285,1.285,0,0,0,.263-.776,1.3,1.3,0,0,0-2.493-.509,5.195,5.195,0,1,0,0,10,1.3,1.3,0,0,0,2.493-.509,1.285,1.285,0,0,0-.263-.776q.044-.043.087-.087a1.285,1.285,0,0,0,.783.269,1.3,1.3,0,0,0,.5-2.5c.011-.038.02-.078.03-.117a1.337,1.337,0,0,0,.166.017" transform="translate(-84.691 -57.894)" fill="#44d860" fill-rule="evenodd"/>
<path id="Path_47" data-name="Path 47" d="M52.188,48.292a1.3,1.3,0,0,1-1.3-1.3,3.9,3.9,0,0,0-7.792,0,1.3,1.3,0,1,1-2.6,0,6.493,6.493,0,0,1,12.987,0,1.3,1.3,0,0,1-1.3,1.3" transform="translate(-21.02 -28.41)" fill-rule="evenodd"/>
<path id="Path_48" data-name="Path 48" d="M103,139.752h31.168a10.389,10.389,0,0,0,10.389-10.389V93H113.389A10.389,10.389,0,0,0,103,103.389Z" transform="translate(-51.054 -53.638)" fill="#ffff50" fill-rule="evenodd"/>
<path id="Path_49" data-name="Path 49" d="M141.1,94.017H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m0,10.389H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m0,10.389H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m0-25.877H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m0,10.293H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m0,10.389H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m7.782-47.993c-.006,0-.011,0-.018,0-1.605.055-2.365,1.66-3.035,3.077-.7,1.48-1.24,2.443-2.126,2.414-.981-.035-1.542-1.144-2.137-2.317-.683-1.347-1.462-2.876-3.1-2.819-1.582.054-2.344,1.451-3.017,2.684-.715,1.313-1.2,2.112-2.141,2.075-1-.036-1.533-.938-2.149-1.981-.686-1.162-1.479-2.467-3.084-2.423-1.555.053-2.319,1.239-2.994,2.286-.713,1.106-1.213,1.781-2.164,1.741-1.025-.036-1.554-.784-2.167-1.65-.688-.973-1.463-2.074-3.062-2.021a3.815,3.815,0,0,0-2.959,1.879c-.64.812-1.14,1.456-2.2,1.415a.52.52,0,0,0-.037,1.039,3.588,3.588,0,0,0,3.05-1.811c.611-.777,1.139-1.448,2.178-1.483,1-.043,1.47.579,2.179,1.582.674.953,1.438,2.033,2.977,2.089,1.612.054,2.387-1.151,3.074-2.217.614-.953,1.144-1.775,2.156-1.81.931-.035,1.438.7,2.153,1.912.674,1.141,1.437,2.434,3.006,2.491,1.623.056,2.407-1.361,3.09-2.616.592-1.085,1.15-2.109,2.14-2.143.931-.022,1.417.829,2.135,2.249.671,1.326,1.432,2.828,3.026,2.886l.088,0c1.592,0,2.347-1.6,3.015-3.01.592-1.252,1.152-2.431,2.113-2.479Z" transform="translate(-55.378 -38.552)" fill-rule="evenodd"/>
<path id="Path_50" data-name="Path 50" d="M83,163.779h20.779V143H83Z" transform="translate(-41.443 -77.665)" fill="#3ecc5f" fill-rule="evenodd"/>
<g id="Group_8" data-name="Group 8" transform="matrix(0.966, -0.259, 0.259, 0.966, 51.971, 43.3)">
<rect id="Rectangle_3" data-name="Rectangle 3" width="43.906" height="17.333" rx="2" transform="translate(0 0)" fill="#d8d8d8"/>
<g id="Group_2" data-name="Group 2" transform="translate(0.728 10.948)">
<rect id="Rectangle_4" data-name="Rectangle 4" width="2.537" height="2.537" rx="1" transform="translate(7.985 0)" fill="#4a4a4a"/>
<rect id="Rectangle_5" data-name="Rectangle 5" width="2.537" height="2.537" rx="1" transform="translate(10.991 0)" fill="#4a4a4a"/>
<rect id="Rectangle_6" data-name="Rectangle 6" width="2.537" height="2.537" rx="1" transform="translate(13.997 0)" fill="#4a4a4a"/>
<rect id="Rectangle_7" data-name="Rectangle 7" width="2.537" height="2.537" rx="1" transform="translate(17.003 0)" fill="#4a4a4a"/>
<rect id="Rectangle_8" data-name="Rectangle 8" width="2.537" height="2.537" rx="1" transform="translate(20.009 0)" fill="#4a4a4a"/>
<rect id="Rectangle_9" data-name="Rectangle 9" width="2.537" height="2.537" rx="1" transform="translate(23.015 0)" fill="#4a4a4a"/>
<rect id="Rectangle_10" data-name="Rectangle 10" width="2.537" height="2.537" rx="1" transform="translate(26.021 0)" fill="#4a4a4a"/>
<rect id="Rectangle_11" data-name="Rectangle 11" width="2.537" height="2.537" rx="1" transform="translate(29.028 0)" fill="#4a4a4a"/>
<rect id="Rectangle_12" data-name="Rectangle 12" width="2.537" height="2.537" rx="1" transform="translate(32.034 0)" fill="#4a4a4a"/>
<path id="Path_51" data-name="Path 51" d="M.519,0H6.9A.519.519,0,0,1,7.421.52v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.017V.519A.519.519,0,0,1,.519,0ZM35.653,0h6.383a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H35.652a.519.519,0,0,1-.519-.519V.519A.519.519,0,0,1,35.652,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
</g>
<g id="Group_3" data-name="Group 3" transform="translate(0.728 4.878)">
<path id="Path_52" data-name="Path 52" d="M.519,0H2.956a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.017V.519A.519.519,0,0,1,.519,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
<rect id="Rectangle_13" data-name="Rectangle 13" width="2.537" height="2.537" rx="1" transform="translate(3.945 0)" fill="#4a4a4a"/>
<rect id="Rectangle_14" data-name="Rectangle 14" width="2.537" height="2.537" rx="1" transform="translate(6.951 0)" fill="#4a4a4a"/>
<rect id="Rectangle_15" data-name="Rectangle 15" width="2.537" height="2.537" rx="1" transform="translate(9.958 0)" fill="#4a4a4a"/>
<rect id="Rectangle_16" data-name="Rectangle 16" width="2.537" height="2.537" rx="1" transform="translate(12.964 0)" fill="#4a4a4a"/>
<rect id="Rectangle_17" data-name="Rectangle 17" width="2.537" height="2.537" rx="1" transform="translate(15.97 0)" fill="#4a4a4a"/>
<rect id="Rectangle_18" data-name="Rectangle 18" width="2.537" height="2.537" rx="1" transform="translate(18.976 0)" fill="#4a4a4a"/>
<rect id="Rectangle_19" data-name="Rectangle 19" width="2.537" height="2.537" rx="1" transform="translate(21.982 0)" fill="#4a4a4a"/>
<rect id="Rectangle_20" data-name="Rectangle 20" width="2.537" height="2.537" rx="1" transform="translate(24.988 0)" fill="#4a4a4a"/>
<rect id="Rectangle_21" data-name="Rectangle 21" width="2.537" height="2.537" rx="1" transform="translate(27.994 0)" fill="#4a4a4a"/>
<rect id="Rectangle_22" data-name="Rectangle 22" width="2.537" height="2.537" rx="1" transform="translate(31 0)" fill="#4a4a4a"/>
<rect id="Rectangle_23" data-name="Rectangle 23" width="2.537" height="2.537" rx="1" transform="translate(34.006 0)" fill="#4a4a4a"/>
<rect id="Rectangle_24" data-name="Rectangle 24" width="2.537" height="2.537" rx="1" transform="translate(37.012 0)" fill="#4a4a4a"/>
<rect id="Rectangle_25" data-name="Rectangle 25" width="2.537" height="2.537" rx="1" transform="translate(40.018 0)" fill="#4a4a4a"/>
</g>
<g id="Group_4" data-name="Group 4" transform="translate(43.283 4.538) rotate(180)">
<path id="Path_53" data-name="Path 53" d="M.519,0H2.956a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.017V.519A.519.519,0,0,1,.519,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
<rect id="Rectangle_26" data-name="Rectangle 26" width="2.537" height="2.537" rx="1" transform="translate(3.945 0)" fill="#4a4a4a"/>
<rect id="Rectangle_27" data-name="Rectangle 27" width="2.537" height="2.537" rx="1" transform="translate(6.951 0)" fill="#4a4a4a"/>
<rect id="Rectangle_28" data-name="Rectangle 28" width="2.537" height="2.537" rx="1" transform="translate(9.958 0)" fill="#4a4a4a"/>
<rect id="Rectangle_29" data-name="Rectangle 29" width="2.537" height="2.537" rx="1" transform="translate(12.964 0)" fill="#4a4a4a"/>
<rect id="Rectangle_30" data-name="Rectangle 30" width="2.537" height="2.537" rx="1" transform="translate(15.97 0)" fill="#4a4a4a"/>
<rect id="Rectangle_31" data-name="Rectangle 31" width="2.537" height="2.537" rx="1" transform="translate(18.976 0)" fill="#4a4a4a"/>
<rect id="Rectangle_32" data-name="Rectangle 32" width="2.537" height="2.537" rx="1" transform="translate(21.982 0)" fill="#4a4a4a"/>
<rect id="Rectangle_33" data-name="Rectangle 33" width="2.537" height="2.537" rx="1" transform="translate(24.988 0)" fill="#4a4a4a"/>
<rect id="Rectangle_34" data-name="Rectangle 34" width="2.537" height="2.537" rx="1" transform="translate(27.994 0)" fill="#4a4a4a"/>
<rect id="Rectangle_35" data-name="Rectangle 35" width="2.537" height="2.537" rx="1" transform="translate(31.001 0)" fill="#4a4a4a"/>
<rect id="Rectangle_36" data-name="Rectangle 36" width="2.537" height="2.537" rx="1" transform="translate(34.007 0)" fill="#4a4a4a"/>
<rect id="Rectangle_37" data-name="Rectangle 37" width="2.537" height="2.537" rx="1" transform="translate(37.013 0)" fill="#4a4a4a"/>
<rect id="Rectangle_38" data-name="Rectangle 38" width="2.537" height="2.537" rx="1" transform="translate(40.018 0)" fill="#4a4a4a"/>
<rect id="Rectangle_39" data-name="Rectangle 39" width="2.537" height="2.537" rx="1" transform="translate(3.945 0)" fill="#4a4a4a"/>
<rect id="Rectangle_40" data-name="Rectangle 40" width="2.537" height="2.537" rx="1" transform="translate(6.951 0)" fill="#4a4a4a"/>
<rect id="Rectangle_41" data-name="Rectangle 41" width="2.537" height="2.537" rx="1" transform="translate(9.958 0)" fill="#4a4a4a"/>
<rect id="Rectangle_42" data-name="Rectangle 42" width="2.537" height="2.537" rx="1" transform="translate(12.964 0)" fill="#4a4a4a"/>
<rect id="Rectangle_43" data-name="Rectangle 43" width="2.537" height="2.537" rx="1" transform="translate(15.97 0)" fill="#4a4a4a"/>
<rect id="Rectangle_44" data-name="Rectangle 44" width="2.537" height="2.537" rx="1" transform="translate(18.976 0)" fill="#4a4a4a"/>
<rect id="Rectangle_45" data-name="Rectangle 45" width="2.537" height="2.537" rx="1" transform="translate(21.982 0)" fill="#4a4a4a"/>
<rect id="Rectangle_46" data-name="Rectangle 46" width="2.537" height="2.537" rx="1" transform="translate(24.988 0)" fill="#4a4a4a"/>
<rect id="Rectangle_47" data-name="Rectangle 47" width="2.537" height="2.537" rx="1" transform="translate(27.994 0)" fill="#4a4a4a"/>
<rect id="Rectangle_48" data-name="Rectangle 48" width="2.537" height="2.537" rx="1" transform="translate(31.001 0)" fill="#4a4a4a"/>
<rect id="Rectangle_49" data-name="Rectangle 49" width="2.537" height="2.537" rx="1" transform="translate(34.007 0)" fill="#4a4a4a"/>
<rect id="Rectangle_50" data-name="Rectangle 50" width="2.537" height="2.537" rx="1" transform="translate(37.013 0)" fill="#4a4a4a"/>
<rect id="Rectangle_51" data-name="Rectangle 51" width="2.537" height="2.537" rx="1" transform="translate(40.018 0)" fill="#4a4a4a"/>
</g>
<g id="Group_6" data-name="Group 6" transform="translate(0.728 7.883)">
<path id="Path_54" data-name="Path 54" d="M.519,0h3.47a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.017V.52A.519.519,0,0,1,.519,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
<g id="Group_5" data-name="Group 5" transform="translate(5.073 0)">
<rect id="Rectangle_52" data-name="Rectangle 52" width="2.537" height="2.537" rx="1" transform="translate(0 0)" fill="#4a4a4a"/>
<rect id="Rectangle_53" data-name="Rectangle 53" width="2.537" height="2.537" rx="1" transform="translate(3.006 0)" fill="#4a4a4a"/>
<rect id="Rectangle_54" data-name="Rectangle 54" width="2.537" height="2.537" rx="1" transform="translate(6.012 0)" fill="#4a4a4a"/>
<rect id="Rectangle_55" data-name="Rectangle 55" width="2.537" height="2.537" rx="1" transform="translate(9.018 0)" fill="#4a4a4a"/>
<rect id="Rectangle_56" data-name="Rectangle 56" width="2.537" height="2.537" rx="1" transform="translate(12.025 0)" fill="#4a4a4a"/>
<rect id="Rectangle_57" data-name="Rectangle 57" width="2.537" height="2.537" rx="1" transform="translate(15.031 0)" fill="#4a4a4a"/>
<rect id="Rectangle_58" data-name="Rectangle 58" width="2.537" height="2.537" rx="1" transform="translate(18.037 0)" fill="#4a4a4a"/>
<rect id="Rectangle_59" data-name="Rectangle 59" width="2.537" height="2.537" rx="1" transform="translate(21.042 0)" fill="#4a4a4a"/>
<rect id="Rectangle_60" data-name="Rectangle 60" width="2.537" height="2.537" rx="1" transform="translate(24.049 0)" fill="#4a4a4a"/>
<rect id="Rectangle_61" data-name="Rectangle 61" width="2.537" height="2.537" rx="1" transform="translate(27.055 0)" fill="#4a4a4a"/>
<rect id="Rectangle_62" data-name="Rectangle 62" width="2.537" height="2.537" rx="1" transform="translate(30.061 0)" fill="#4a4a4a"/>
</g>
<path id="Path_55" data-name="Path 55" d="M.52,0H3.8a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.017V.52A.519.519,0,0,1,.519,0Z" transform="translate(38.234 0)" fill="#4a4a4a" fill-rule="evenodd"/>
</g>
<g id="Group_7" data-name="Group 7" transform="translate(0.728 14.084)">
<rect id="Rectangle_63" data-name="Rectangle 63" width="2.537" height="2.537" rx="1" transform="translate(0 0)" fill="#4a4a4a"/>
<rect id="Rectangle_64" data-name="Rectangle 64" width="2.537" height="2.537" rx="1" transform="translate(3.006 0)" fill="#4a4a4a"/>
<rect id="Rectangle_65" data-name="Rectangle 65" width="2.537" height="2.537" rx="1" transform="translate(6.012 0)" fill="#4a4a4a"/>
<rect id="Rectangle_66" data-name="Rectangle 66" width="2.537" height="2.537" rx="1" transform="translate(9.018 0)" fill="#4a4a4a"/>
<path id="Path_56" data-name="Path 56" d="M.519,0H14.981A.519.519,0,0,1,15.5.519v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.018V.519A.519.519,0,0,1,.519,0Zm15.97,0h1.874a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H16.489a.519.519,0,0,1-.519-.519V.519A.519.519,0,0,1,16.489,0Z" transform="translate(12.024 0)" fill="#4a4a4a" fill-rule="evenodd"/>
<rect id="Rectangle_67" data-name="Rectangle 67" width="2.537" height="2.537" rx="1" transform="translate(31.376 0)" fill="#4a4a4a"/>
<rect id="Rectangle_68" data-name="Rectangle 68" width="2.537" height="2.537" rx="1" transform="translate(34.382 0)" fill="#4a4a4a"/>
<rect id="Rectangle_69" data-name="Rectangle 69" width="2.537" height="2.537" rx="1" transform="translate(40.018 0)" fill="#4a4a4a"/>
<path id="Path_57" data-name="Path 57" d="M2.537,0V.561a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,.561V0Z" transform="translate(39.736 1.08) rotate(180)" fill="#4a4a4a"/>
<path id="Path_58" data-name="Path 58" d="M2.537,0V.561a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,.561V0Z" transform="translate(37.2 1.456)" fill="#4a4a4a"/>
</g>
<rect id="Rectangle_70" data-name="Rectangle 70" width="42.273" height="1.127" rx="0.564" transform="translate(0.915 0.556)" fill="#4a4a4a"/>
<rect id="Rectangle_71" data-name="Rectangle 71" width="2.37" height="0.752" rx="0.376" transform="translate(1.949 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_72" data-name="Rectangle 72" width="2.37" height="0.752" rx="0.376" transform="translate(5.193 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_73" data-name="Rectangle 73" width="2.37" height="0.752" rx="0.376" transform="translate(7.688 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_74" data-name="Rectangle 74" width="2.37" height="0.752" rx="0.376" transform="translate(10.183 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_75" data-name="Rectangle 75" width="2.37" height="0.752" rx="0.376" transform="translate(12.679 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_76" data-name="Rectangle 76" width="2.37" height="0.752" rx="0.376" transform="translate(15.797 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_77" data-name="Rectangle 77" width="2.37" height="0.752" rx="0.376" transform="translate(18.292 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_78" data-name="Rectangle 78" width="2.37" height="0.752" rx="0.376" transform="translate(20.788 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_79" data-name="Rectangle 79" width="2.37" height="0.752" rx="0.376" transform="translate(23.283 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_80" data-name="Rectangle 80" width="2.37" height="0.752" rx="0.376" transform="translate(26.402 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_81" data-name="Rectangle 81" width="2.37" height="0.752" rx="0.376" transform="translate(28.897 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_82" data-name="Rectangle 82" width="2.37" height="0.752" rx="0.376" transform="translate(31.393 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_83" data-name="Rectangle 83" width="2.37" height="0.752" rx="0.376" transform="translate(34.512 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_84" data-name="Rectangle 84" width="2.37" height="0.752" rx="0.376" transform="translate(37.007 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_85" data-name="Rectangle 85" width="2.37" height="0.752" rx="0.376" transform="translate(39.502 0.744)" fill="#d8d8d8" opacity="0.136"/>
</g>
<path id="Path_59" data-name="Path 59" d="M123.779,148.389a2.583,2.583,0,0,0-.332.033c-.02-.078-.038-.156-.06-.234a2.594,2.594,0,1,0-2.567-4.455q-.086-.088-.174-.175a2.593,2.593,0,1,0-4.461-2.569c-.077-.022-.154-.04-.231-.06a2.6,2.6,0,1,0-5.128,0c-.077.02-.154.038-.231.06a2.594,2.594,0,1,0-4.461,2.569,10.384,10.384,0,1,0,17.314,9.992,2.592,2.592,0,1,0,.332-5.161" transform="translate(-51.054 -75.262)" fill="#44d860" fill-rule="evenodd"/>
<path id="Path_60" data-name="Path 60" d="M83,113.389h20.779V103H83Z" transform="translate(-41.443 -58.444)" fill="#3ecc5f" fill-rule="evenodd"/>
<path id="Path_61" data-name="Path 61" d="M123.389,108.944a1.3,1.3,0,1,0,0-2.6,1.338,1.338,0,0,0-.166.017c-.01-.039-.019-.078-.03-.117a1.3,1.3,0,0,0-.5-2.5,1.285,1.285,0,0,0-.783.269q-.043-.044-.087-.087a1.285,1.285,0,0,0,.263-.776,1.3,1.3,0,0,0-2.493-.509,5.195,5.195,0,1,0,0,10,1.3,1.3,0,0,0,2.493-.509,1.285,1.285,0,0,0-.263-.776q.044-.043.087-.087a1.285,1.285,0,0,0,.783.269,1.3,1.3,0,0,0,.5-2.5c.011-.038.02-.078.03-.117a1.335,1.335,0,0,0,.166.017" transform="translate(-55.859 -57.894)" fill="#44d860" fill-rule="evenodd"/>
<path id="Path_62" data-name="Path 62" d="M141.8,38.745a1.41,1.41,0,0,1-.255-.026,1.309,1.309,0,0,1-.244-.073,1.349,1.349,0,0,1-.224-.119,1.967,1.967,0,0,1-.2-.161,1.52,1.52,0,0,1-.161-.2,1.282,1.282,0,0,1-.218-.722,1.41,1.41,0,0,1,.026-.255,1.5,1.5,0,0,1,.072-.244,1.364,1.364,0,0,1,.12-.223,1.252,1.252,0,0,1,.358-.358,1.349,1.349,0,0,1,.224-.119,1.309,1.309,0,0,1,.244-.073,1.2,1.2,0,0,1,.509,0,1.262,1.262,0,0,1,.468.192,1.968,1.968,0,0,1,.2.161,1.908,1.908,0,0,1,.161.2,1.322,1.322,0,0,1,.12.223,1.361,1.361,0,0,1,.1.5,1.317,1.317,0,0,1-.379.919,1.968,1.968,0,0,1-.2.161,1.346,1.346,0,0,1-.223.119,1.332,1.332,0,0,1-.5.1m10.389-.649a1.326,1.326,0,0,1-.92-.379,1.979,1.979,0,0,1-.161-.2,1.282,1.282,0,0,1-.218-.722,1.326,1.326,0,0,1,.379-.919,1.967,1.967,0,0,1,.2-.161,1.351,1.351,0,0,1,.224-.119,1.308,1.308,0,0,1,.244-.073,1.2,1.2,0,0,1,.509,0,1.262,1.262,0,0,1,.468.192,1.967,1.967,0,0,1,.2.161,1.326,1.326,0,0,1,.379.919,1.461,1.461,0,0,1-.026.255,1.323,1.323,0,0,1-.073.244,1.847,1.847,0,0,1-.119.223,1.911,1.911,0,0,1-.161.2,1.967,1.967,0,0,1-.2.161,1.294,1.294,0,0,1-.722.218" transform="translate(-69.074 -26.006)" fill-rule="evenodd"/>
</g>
<g id="React-icon" transform="translate(906.3 541.56)">
<path id="Path_330" data-name="Path 330" d="M263.668,117.179c0-5.827-7.3-11.35-18.487-14.775,2.582-11.4,1.434-20.477-3.622-23.382a7.861,7.861,0,0,0-4.016-1v4a4.152,4.152,0,0,1,2.044.466c2.439,1.4,3.5,6.724,2.672,13.574-.2,1.685-.52,3.461-.914,5.272a86.9,86.9,0,0,0-11.386-1.954,87.469,87.469,0,0,0-7.459-8.965c5.845-5.433,11.332-8.41,15.062-8.41V78h0c-4.931,0-11.386,3.514-17.913,9.611-6.527-6.061-12.982-9.539-17.913-9.539v4c3.712,0,9.216,2.959,15.062,8.356a84.687,84.687,0,0,0-7.405,8.947,83.732,83.732,0,0,0-11.4,1.972c-.412-1.793-.717-3.532-.932-5.2-.843-6.85.2-12.175,2.618-13.592a3.991,3.991,0,0,1,2.062-.466v-4h0a8,8,0,0,0-4.052,1c-5.039,2.9-6.168,11.96-3.568,23.328-11.153,3.443-18.415,8.947-18.415,14.757,0,5.828,7.3,11.35,18.487,14.775-2.582,11.4-1.434,20.477,3.622,23.382a7.882,7.882,0,0,0,4.034,1c4.931,0,11.386-3.514,17.913-9.611,6.527,6.061,12.982,9.539,17.913,9.539a8,8,0,0,0,4.052-1c5.039-2.9,6.168-11.96,3.568-23.328C256.406,128.511,263.668,122.988,263.668,117.179Zm-23.346-11.96c-.663,2.313-1.488,4.7-2.421,7.083-.735-1.434-1.506-2.869-2.349-4.3-.825-1.434-1.7-2.833-2.582-4.2C235.517,104.179,237.974,104.645,240.323,105.219Zm-8.212,19.1c-1.4,2.421-2.833,4.716-4.321,6.85-2.672.233-5.379.359-8.1.359-2.708,0-5.415-.126-8.069-.341q-2.232-3.2-4.339-6.814-2.044-3.523-3.73-7.136c1.112-2.4,2.367-4.805,3.712-7.154,1.4-2.421,2.833-4.716,4.321-6.85,2.672-.233,5.379-.359,8.1-.359,2.708,0,5.415.126,8.069.341q2.232,3.2,4.339,6.814,2.044,3.523,3.73,7.136C234.692,119.564,233.455,121.966,232.11,124.315Zm5.792-2.331c.968,2.4,1.793,4.805,2.474,7.136-2.349.574-4.823,1.058-7.387,1.434.879-1.381,1.757-2.8,2.582-4.25C236.4,124.871,237.167,123.419,237.9,121.984ZM219.72,141.116a73.921,73.921,0,0,1-4.985-5.738c1.614.072,3.263.126,4.931.126,1.685,0,3.353-.036,4.985-.126A69.993,69.993,0,0,1,219.72,141.116ZM206.38,130.555c-2.546-.377-5-.843-7.352-1.417.663-2.313,1.488-4.7,2.421-7.083.735,1.434,1.506,2.869,2.349,4.3S205.5,129.192,206.38,130.555ZM219.63,93.241a73.924,73.924,0,0,1,4.985,5.738c-1.614-.072-3.263-.126-4.931-.126-1.686,0-3.353.036-4.985.126A69.993,69.993,0,0,1,219.63,93.241ZM206.362,103.8c-.879,1.381-1.757,2.8-2.582,4.25-.825,1.434-1.6,2.869-2.331,4.3-.968-2.4-1.793-4.805-2.474-7.136C201.323,104.663,203.8,104.179,206.362,103.8Zm-16.227,22.449c-6.348-2.708-10.454-6.258-10.454-9.073s4.106-6.383,10.454-9.073c1.542-.663,3.228-1.255,4.967-1.811a86.122,86.122,0,0,0,4.034,10.92,84.9,84.9,0,0,0-3.981,10.866C193.38,127.525,191.694,126.915,190.134,126.252Zm9.647,25.623c-2.439-1.4-3.5-6.724-2.672-13.574.2-1.686.52-3.461.914-5.272a86.9,86.9,0,0,0,11.386,1.954,87.465,87.465,0,0,0,7.459,8.965c-5.845,5.433-11.332,8.41-15.062,8.41A4.279,4.279,0,0,1,199.781,151.875Zm42.532-13.663c.843,6.85-.2,12.175-2.618,13.592a3.99,3.99,0,0,1-2.062.466c-3.712,0-9.216-2.959-15.062-8.356a84.689,84.689,0,0,0,7.405-8.947,83.731,83.731,0,0,0,11.4-1.972A50.194,50.194,0,0,1,242.313,138.212Zm6.9-11.96c-1.542.663-3.228,1.255-4.967,1.811a86.12,86.12,0,0,0-4.034-10.92,84.9,84.9,0,0,0,3.981-10.866c1.775.556,3.461,1.165,5.039,1.829,6.348,2.708,10.454,6.258,10.454,9.073C259.67,119.994,255.564,123.562,249.216,126.252Z" fill="#61dafb"/>
<path id="Path_331" data-name="Path 331" d="M320.8,78.4Z" transform="translate(-119.082 -0.328)" fill="#61dafb"/>
<circle id="Ellipse_112" data-name="Ellipse 112" cx="8.194" cy="8.194" r="8.194" transform="translate(211.472 108.984)" fill="#61dafb"/>
<path id="Path_332" data-name="Path 332" d="M520.5,78.1Z" transform="translate(-282.975 -0.082)" fill="#61dafb"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 35 KiB

@@ -1,40 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1129" height="663" viewBox="0 0 1129 663">
<title>Focus on What Matters</title>
<circle cx="321" cy="321" r="321" fill="#f2f2f2" />
<ellipse cx="559" cy="635.49998" rx="514" ry="27.50002" fill="#3f3d56" />
<ellipse cx="558" cy="627" rx="460" ry="22" opacity="0.2" />
<rect x="131" y="152.5" width="840" height="50" fill="#3f3d56" />
<path d="M166.5,727.3299A21.67009,21.67009,0,0,0,188.1701,749H984.8299A21.67009,21.67009,0,0,0,1006.5,727.3299V296h-840Z" transform="translate(-35.5 -118.5)" fill="#3f3d56" />
<path d="M984.8299,236H188.1701A21.67009,21.67009,0,0,0,166.5,257.6701V296h840V257.6701A21.67009,21.67009,0,0,0,984.8299,236Z" transform="translate(-35.5 -118.5)" fill="#3f3d56" />
<path d="M984.8299,236H188.1701A21.67009,21.67009,0,0,0,166.5,257.6701V296h840V257.6701A21.67009,21.67009,0,0,0,984.8299,236Z" transform="translate(-35.5 -118.5)" opacity="0.2" />
<circle cx="181" cy="147.5" r="13" fill="#3f3d56" />
<circle cx="217" cy="147.5" r="13" fill="#3f3d56" />
<circle cx="253" cy="147.5" r="13" fill="#3f3d56" />
<rect x="168" y="213.5" width="337" height="386" rx="5.33505" fill="#606060" />
<rect x="603" y="272.5" width="284" height="22" rx="5.47638" fill="#2e8555" />
<rect x="537" y="352.5" width="416" height="15" rx="5.47638" fill="#2e8555" />
<rect x="537" y="396.5" width="416" height="15" rx="5.47638" fill="#2e8555" />
<rect x="537" y="440.5" width="416" height="15" rx="5.47638" fill="#2e8555" />
<rect x="537" y="484.5" width="416" height="15" rx="5.47638" fill="#2e8555" />
<rect x="865" y="552.5" width="88" height="26" rx="7.02756" fill="#3ecc5f" />
<path d="M1088.60287,624.61594a30.11371,30.11371,0,0,0,3.98291-15.266c0-13.79652-8.54358-24.98081-19.08256-24.98081s-19.08256,11.18429-19.08256,24.98081a30.11411,30.11411,0,0,0,3.98291,15.266,31.248,31.248,0,0,0,0,30.53213,31.248,31.248,0,0,0,0,30.53208,31.248,31.248,0,0,0,0,30.53208,30.11408,30.11408,0,0,0-3.98291,15.266c0,13.79652,8.54353,24.98081,19.08256,24.98081s19.08256-11.18429,19.08256-24.98081a30.11368,30.11368,0,0,0-3.98291-15.266,31.248,31.248,0,0,0,0-30.53208,31.248,31.248,0,0,0,0-30.53208,31.248,31.248,0,0,0,0-30.53213Z" transform="translate(-35.5 -118.5)" fill="#3f3d56" />
<ellipse cx="1038.00321" cy="460.31783" rx="19.08256" ry="24.9808" fill="#3f3d56" />
<ellipse cx="1038.00321" cy="429.78574" rx="19.08256" ry="24.9808" fill="#3f3d56" />
<path d="M1144.93871,339.34489a91.61081,91.61081,0,0,0,7.10658-10.46092l-50.141-8.23491,54.22885.4033a91.566,91.566,0,0,0,1.74556-72.42605l-72.75449,37.74139,67.09658-49.32086a91.41255,91.41255,0,1,0-150.971,102.29805,91.45842,91.45842,0,0,0-10.42451,16.66946l65.0866,33.81447-69.40046-23.292a91.46011,91.46011,0,0,0,14.73837,85.83669,91.40575,91.40575,0,1,0,143.68892,0,91.41808,91.41808,0,0,0,0-113.02862Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
<path d="M981.6885,395.8592a91.01343,91.01343,0,0,0,19.56129,56.51431,91.40575,91.40575,0,1,0,143.68892,0C1157.18982,436.82067,981.6885,385.60008,981.6885,395.8592Z" transform="translate(-35.5 -118.5)" opacity="0.1" />
<path d="M365.62,461.43628H477.094v45.12043H365.62Z" transform="translate(-35.5 -118.5)" fill="#fff" fill-rule="evenodd" />
<path d="M264.76252,608.74122a26.50931,26.50931,0,0,1-22.96231-13.27072,26.50976,26.50976,0,0,0,22.96231,39.81215H291.304V608.74122Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
<path d="M384.17242,468.57061l92.92155-5.80726V449.49263a26.54091,26.54091,0,0,0-26.54143-26.54143H331.1161l-3.31768-5.74622a3.83043,3.83043,0,0,0-6.63536,0l-3.31768,5.74622-3.31767-5.74622a3.83043,3.83043,0,0,0-6.63536,0l-3.31768,5.74622L301.257,417.205a3.83043,3.83043,0,0,0-6.63536,0L291.304,422.9512c-.02919,0-.05573.004-.08625.004l-5.49674-5.49541a3.8293,3.8293,0,0,0-6.4071,1.71723l-1.81676,6.77338L270.607,424.1031a3.82993,3.82993,0,0,0-4.6912,4.69253l1.84463,6.89148-6.77072,1.81411a3.8315,3.8315,0,0,0-1.71988,6.40975l5.49673,5.49673c0,.02787-.004.05574-.004.08493l-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74621,3.31768L259.0163,466.081a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31767a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31767a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31768a3.83042,3.83042,0,0,0,0,6.63535l5.74622,3.31768-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768L259.0163,558.976a3.83042,3.83042,0,0,0,0,6.63535l5.74622,3.31768-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31768a3.83042,3.83042,0,0,0,0,6.63535l5.74622,3.31768-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768A26.54091,26.54091,0,0,0,291.304,635.28265H450.55254A26.5409,26.5409,0,0,0,477.094,608.74122V502.5755l-92.92155-5.80727a14.12639,14.12639,0,0,1,0-28.19762" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
<path d="M424.01111,635.28265h39.81214V582.19979H424.01111Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
<path d="M490.36468,602.10586a6.60242,6.60242,0,0,0-.848.08493c-.05042-.19906-.09821-.39945-.15393-.59852A6.62668,6.62668,0,1,0,482.80568,590.21q-.2203-.22491-.44457-.44589a6.62391,6.62391,0,1,0-11.39689-6.56369c-.1964-.05575-.39414-.10218-.59056-.15262a6.63957,6.63957,0,1,0-13.10086,0c-.1964.05042-.39414.09687-.59056.15262a6.62767,6.62767,0,1,0-11.39688,6.56369,26.52754,26.52754,0,1,0,44.23127,25.52756,6.6211,6.6211,0,1,0,.848-13.18579" transform="translate(-35.5 -118.5)" fill="#44d860" fill-rule="evenodd" />
<path d="M437.28182,555.65836H477.094V529.11693H437.28182Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
<path d="M490.36468,545.70532a3.31768,3.31768,0,0,0,0-6.63536,3.41133,3.41133,0,0,0-.42333.04247c-.02655-.09953-.04911-.19907-.077-.29859a3.319,3.319,0,0,0-1.278-6.37923,3.28174,3.28174,0,0,0-2.00122.68742q-.10947-.11346-.22294-.22295a3.282,3.282,0,0,0,.67149-1.98265,3.31768,3.31768,0,0,0-6.37-1.2992,13.27078,13.27078,0,1,0,0,25.54082,3.31768,3.31768,0,0,0,6.37-1.2992,3.282,3.282,0,0,0-.67149-1.98265q.11347-.10947.22294-.22294a3.28174,3.28174,0,0,0,2.00122.68742,3.31768,3.31768,0,0,0,1.278-6.37923c.02786-.0982.05042-.19907.077-.29859a3.41325,3.41325,0,0,0,.42333.04246" transform="translate(-35.5 -118.5)" fill="#44d860" fill-rule="evenodd" />
<path d="M317.84538,466.081a3.31768,3.31768,0,0,1-3.31767-3.31768,9.953,9.953,0,1,0-19.90608,0,3.31768,3.31768,0,1,1-6.63535,0,16.58839,16.58839,0,1,1,33.17678,0,3.31768,3.31768,0,0,1-3.31768,3.31768" transform="translate(-35.5 -118.5)" fill-rule="evenodd" />
<path d="M370.92825,635.28265h79.62429A26.5409,26.5409,0,0,0,477.094,608.74122v-92.895H397.46968a26.54091,26.54091,0,0,0-26.54143,26.54143Z" transform="translate(-35.5 -118.5)" fill="#ffff50" fill-rule="evenodd" />
<path d="M457.21444,556.98543H390.80778a1.32707,1.32707,0,0,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414m0,26.54143H390.80778a1.32707,1.32707,0,1,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414m0,26.54143H390.80778a1.32707,1.32707,0,1,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414m0-66.10674H390.80778a1.32707,1.32707,0,0,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414m0,26.29459H390.80778a1.32707,1.32707,0,0,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414m0,26.54143H390.80778a1.32707,1.32707,0,0,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414M477.094,474.19076c-.01592,0-.0292-.008-.04512-.00663-4.10064.13934-6.04083,4.24132-7.75274,7.86024-1.78623,3.78215-3.16771,6.24122-5.43171,6.16691-2.50685-.09024-3.94007-2.92222-5.45825-5.91874-1.74377-3.44243-3.73438-7.34667-7.91333-7.20069-4.04227.138-5.98907,3.70784-7.70631,6.857-1.82738,3.35484-3.07084,5.39455-5.46887,5.30033-2.55727-.09289-3.91619-2.39536-5.48877-5.06013-1.75306-2.96733-3.77951-6.30359-7.8775-6.18946-3.97326.13669-5.92537,3.16507-7.64791,5.83912-1.82207,2.82666-3.09872,4.5492-5.52725,4.447-2.61832-.09289-3.9706-2.00388-5.53522-4.21611-1.757-2.4856-3.737-5.299-7.82308-5.16231-3.88567.13271-5.83779,2.61434-7.559,4.80135-1.635,2.07555-2.9116,3.71846-5.61218,3.615a1.32793,1.32793,0,1,0-.09555,2.65414c4.00377.134,6.03154-2.38873,7.79257-4.6275,1.562-1.9853,2.91027-3.69855,5.56441-3.78879,2.55594-.10882,3.75429,1.47968,5.56707,4.04093,1.7212,2.43385,3.67465,5.19416,7.60545,5.33616,4.11789.138,6.09921-2.93946,7.8536-5.66261,1.56861-2.43385,2.92221-4.53461,5.50734-4.62352,2.37944-.08892,3.67466,1.79154,5.50072,4.885,1.72121,2.91557,3.67069,6.21865,7.67977,6.36463,4.14709.14332,6.14965-3.47693,7.89475-6.68181,1.51155-2.77092,2.93814-5.38791,5.46621-5.4755,2.37944-.05573,3.62025,2.11668,5.45558,5.74622,1.71459,3.388,3.65875,7.22591,7.73019,7.37321l.22429.004c4.06614,0,5.99571-4.08074,7.70364-7.68905,1.51154-3.19825,2.94211-6.21069,5.3972-6.33411Z" transform="translate(-35.5 -118.5)" fill-rule="evenodd" />
<path d="M344.38682,635.28265h53.08286V582.19979H344.38682Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
<path d="M424.01111,602.10586a6.60242,6.60242,0,0,0-.848.08493c-.05042-.19906-.09821-.39945-.15394-.59852A6.62667,6.62667,0,1,0,416.45211,590.21q-.2203-.22491-.44458-.44589a6.62391,6.62391,0,1,0-11.39689-6.56369c-.1964-.05575-.39413-.10218-.59054-.15262a6.63957,6.63957,0,1,0-13.10084,0c-.19641.05042-.39414.09687-.59055.15262a6.62767,6.62767,0,1,0-11.39689,6.56369,26.52755,26.52755,0,1,0,44.2313,25.52756,6.6211,6.6211,0,1,0,.848-13.18579" transform="translate(-35.5 -118.5)" fill="#44d860" fill-rule="evenodd" />
<path d="M344.38682,555.65836h53.08286V529.11693H344.38682Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
<path d="M410.74039,545.70532a3.31768,3.31768,0,1,0,0-6.63536,3.41133,3.41133,0,0,0-.42333.04247c-.02655-.09953-.04911-.19907-.077-.29859a3.319,3.319,0,0,0-1.278-6.37923,3.28174,3.28174,0,0,0-2.00122.68742q-.10947-.11346-.22294-.22295a3.282,3.282,0,0,0,.67149-1.98265,3.31768,3.31768,0,0,0-6.37-1.2992,13.27078,13.27078,0,1,0,0,25.54082,3.31768,3.31768,0,0,0,6.37-1.2992,3.282,3.282,0,0,0-.67149-1.98265q.11347-.10947.22294-.22294a3.28174,3.28174,0,0,0,2.00122.68742,3.31768,3.31768,0,0,0,1.278-6.37923c.02786-.0982.05042-.19907.077-.29859a3.41325,3.41325,0,0,0,.42333.04246" transform="translate(-35.5 -118.5)" fill="#44d860" fill-rule="evenodd" />
<path d="M424.01111,447.8338a3.60349,3.60349,0,0,1-.65028-.06636,3.34415,3.34415,0,0,1-.62372-.18579,3.44679,3.44679,0,0,1-.572-.30522,5.02708,5.02708,0,0,1-.50429-.4114,3.88726,3.88726,0,0,1-.41007-.50428,3.27532,3.27532,0,0,1-.55737-1.84463,3.60248,3.60248,0,0,1,.06636-.65027,3.82638,3.82638,0,0,1,.18447-.62373,3.48858,3.48858,0,0,1,.30656-.57064,3.197,3.197,0,0,1,.91436-.91568,3.44685,3.44685,0,0,1,.572-.30523,3.344,3.344,0,0,1,.62372-.18578,3.06907,3.06907,0,0,1,1.30053,0,3.22332,3.22332,0,0,1,1.19436.491,5.02835,5.02835,0,0,1,.50429.41139,4.8801,4.8801,0,0,1,.41139.50429,3.38246,3.38246,0,0,1,.30522.57064,3.47806,3.47806,0,0,1,.25215,1.274A3.36394,3.36394,0,0,1,426.36,446.865a5.02708,5.02708,0,0,1-.50429.4114,3.3057,3.3057,0,0,1-1.84463.55737m26.54143-1.65884a3.38754,3.38754,0,0,1-2.35024-.96877,5.04185,5.04185,0,0,1-.41007-.50428,3.27532,3.27532,0,0,1-.55737-1.84463,3.38659,3.38659,0,0,1,.96744-2.34892,5.02559,5.02559,0,0,1,.50429-.41139,3.44685,3.44685,0,0,1,.572-.30523,3.3432,3.3432,0,0,1,.62373-.18579,3.06952,3.06952,0,0,1,1.30052,0,3.22356,3.22356,0,0,1,1.19436.491,5.02559,5.02559,0,0,1,.50429.41139,3.38792,3.38792,0,0,1,.96876,2.34892,3.72635,3.72635,0,0,1-.06636.65026,3.37387,3.37387,0,0,1-.18579.62373,4.71469,4.71469,0,0,1-.30522.57064,4.8801,4.8801,0,0,1-.41139.50429,5.02559,5.02559,0,0,1-.50429.41139,3.30547,3.30547,0,0,1-1.84463.55737" transform="translate(-35.5 -118.5)" fill-rule="evenodd" />
</svg>

Before

Width:  |  Height:  |  Size: 12 KiB

+7
View File
@@ -0,0 +1,7 @@
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'scope-case': [0],
'subject-case': [0],
},
};
+10 -3
View File
@@ -21,7 +21,8 @@
"ver": "lerna version --no-private --conventional-commits",
"release": "yarn eslint && yarn build && yarn run ver && lerna publish from-git",
"release:beta": "yarn eslint && yarn build && yarn run ver && lerna publish from-package --dist-tag beta",
"up": "yarn upgrade-interactive --latest"
"up": "yarn upgrade-interactive --latest",
"prepare": "husky install"
},
"devDependencies": {
"@babel/preset-env": "^7.19.3",
@@ -49,11 +50,11 @@
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"http-server": "^14.1.0",
"husky": "^8.0.1",
"husky": "^8.0.3",
"jest": "^29.1.1",
"jest-environment-jsdom": "^29.1.1",
"lerna": "^6.6.1",
"lint-staged": "^13.1.0",
"lint-staged": "^14.0.1",
"prettier": "^2.8.7",
"react": "^17.0.0",
"react-dom": "^17.0.0",
@@ -63,5 +64,11 @@
},
"resolutions": {
"@yarnpkg/parsers": "3.0.0-rc.48.1"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write"
]
}
}
+20
View File
@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.2.3](https://github.com/netease/tango/compare/@music163/tango-context@0.2.2...@music163/tango-context@0.2.3) (2023-10-23)
**Note:** Version bump only for package @music163/tango-context
## [0.2.2](https://github.com/netease/tango/compare/@music163/tango-context@0.2.1...@music163/tango-context@0.2.2) (2023-10-12)
### Bug Fixes
- refactor expSetter ([#41](https://github.com/netease/tango/issues/41)) ([d63517e](https://github.com/netease/tango/commit/d63517ecb936e4227e70c33e610664316625f4f4))
## [0.2.1](https://github.com/netease/tango/compare/@music163/tango-context@0.2.0...@music163/tango-context@0.2.1) (2023-09-22)
**Note:** Version bump only for package @music163/tango-context
# [0.2.0](https://github.com/netease/tango/compare/@music163/tango-context@0.1.4...@music163/tango-context@0.2.0) (2023-09-21)
### Features
- support multiple service modules & refactor workspace ([a7df29c](https://github.com/netease/tango/commit/a7df29c3debc56b187792d3e203b470e9d368ea5))
## [0.1.4](https://github.com/netease/tango/compare/@music163/tango-context@0.1.3...@music163/tango-context@0.1.4) (2023-09-18)
**Note:** Version bump only for package @music163/tango-context
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-context",
"version": "0.1.4",
"version": "0.2.3",
"description": "react context for tango-apps",
"keywords": [
"react",
@@ -31,8 +31,8 @@
"react": ">= 16.8"
},
"dependencies": {
"@music163/tango-core": "^0.1.4",
"@music163/tango-helpers": "^0.1.3",
"@music163/tango-core": "^0.2.2",
"@music163/tango-helpers": "^0.1.4",
"mobx-react-lite": "4.0.2"
},
"publishConfig": {
+46 -39
View File
@@ -1,11 +1,25 @@
import type { Engine } from '@music163/tango-core';
import { createContext } from '@music163/tango-helpers';
interface CustomVariableData {
key: string;
title: string;
children?: CustomVariableData[];
[key: string]: any;
}
export interface ITangoEngineContext {
/**
* 低代码引擎
*/
engine: Engine;
/**
* 自定义配置数据
*/
config: {
customActionVariables: CustomVariableData[];
customExpressionVariables: CustomVariableData[];
};
}
const [TangoEngineProvider, useTangoEngine] = createContext<ITangoEngineContext>({
@@ -22,38 +36,15 @@ export const useDesigner = () => {
return useTangoEngine()?.engine.designer;
};
const builtinHelpers = [
{
title: 'setStoreValue',
key: '() => tango.setStoreValue("variableName", "variableValue")',
type: 'function',
},
{
title: 'getStoreValue',
key: '() => tango.getStoreValue("variableName")',
type: 'function',
},
{ title: 'openModal', key: '() => tango.openModal("")', type: 'function' },
{ title: 'closeModal', key: '() => tango.closeModal("")', type: 'function' },
{ title: 'navigateTo', key: '() => tango.navigateTo("/")', type: 'function' },
{ title: 'showToast', key: '() => tango.showToast("hello")', type: 'function' },
{ title: 'formatDate', key: '() => tango.formatDate("2022-12-12")', type: 'function' },
{ title: 'formatNumber', key: '() => tango.formatDate(9999)', type: 'function' },
{
title: 'copyToClipboard',
key: '() => tango.copyToClipboard("hello")',
type: 'function',
},
];
export const useWorkspaceData = () => {
const ctx = useTangoEngine();
const workspace = useWorkspace();
const modelVariables: any[] = []; // 绑定变量列表
const storeActionVariables: any[] = []; // 模型中的所有 actions
const storeVariables: any[] = []; // 模型中的所有变量
const serviceVariables: any[] = []; // 服务中的所有变量
workspace.listStoreModules?.().forEach((file) => {
Object.values(workspace.storeModules).forEach((file) => {
const prefix = `stores.${file.name}`;
const states = file.states.map((item) => ({
title: item.name,
@@ -91,11 +82,17 @@ export const useWorkspaceData = () => {
});
});
Object.keys(workspace.serviceModule?.serviceFunctions || {}).forEach((key) => {
Object.values(workspace.serviceModules).forEach((file) => {
const prefix = file.name !== 'index' ? `services.${file.name}` : 'services';
serviceVariables.push({
title: key,
key: `services.${key}`,
type: 'function',
title: file.name,
key: prefix,
selectable: false,
children: Object.keys(file.serviceFunctions || {}).map((key) => ({
title: key,
key: [prefix, key].join('.'),
type: 'function',
})),
});
});
@@ -105,19 +102,29 @@ export const useWorkspaceData = () => {
value: item.path,
}));
let actionVariables: CustomVariableData[] = [
buildVariableOptions('数据模型', '$stores', storeActionVariables),
buildVariableOptions('服务函数', '$services', serviceVariables),
];
if (ctx.config?.customActionVariables) {
actionVariables = actionVariables.concat(ctx.config?.customActionVariables);
}
let expressionVariables: CustomVariableData[] = [
buildVariableOptions('数据模型', '$stores', storeVariables),
buildVariableOptions('服务函数', '$services', serviceVariables),
];
if (ctx.config?.customExpressionVariables) {
expressionVariables = expressionVariables.concat(ctx.config?.customExpressionVariables);
}
return {
modelVariables: [buildVariableOptions('数据模型', 'stores', modelVariables)],
actionVariables: [
buildVariableOptions('数据模型', 'stores', storeActionVariables),
buildVariableOptions('服务函数', 'services', serviceVariables),
buildVariableOptions('工具函数', 'helpers', builtinHelpers),
],
actionVariables,
storeVariables,
expressionVariables: [
buildVariableOptions('数据模型', 'stores', storeVariables),
buildVariableOptions('服务函数', 'services', serviceVariables),
buildVariableOptions('工具函数', 'helpers', builtinHelpers),
],
serviceVariables,
expressionVariables,
routeOptions,
};
};
+20
View File
@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.2.2](https://github.com/netease/tango/compare/@music163/tango-core@0.2.1...@music163/tango-core@0.2.2) (2023-10-23)
### Bug Fixes
- add listImportSources to viewModule ([6bbbe63](https://github.com/netease/tango/commit/6bbbe634ad52acaf06a0d776aeff450993f7b69a))
- enhance expSetter ([#43](https://github.com/netease/tango/issues/43)) ([5ebbb42](https://github.com/netease/tango/commit/5ebbb428fb3fb786d330ab01959028443338d315))
- update interface ([0b3e632](https://github.com/netease/tango/commit/0b3e632745e30d578268458329d5702dbd729010))
## [0.2.1](https://github.com/netease/tango/compare/@music163/tango-core@0.2.0...@music163/tango-core@0.2.1) (2023-09-22)
### Bug Fixes
- add listServiceFunctions ([09a1e21](https://github.com/netease/tango/commit/09a1e2135a1f51b0a5f6c0a507ba42d2e6355c24))
# [0.2.0](https://github.com/netease/tango/compare/@music163/tango-core@0.1.4...@music163/tango-core@0.2.0) (2023-09-21)
### Features
- support multiple service modules & refactor workspace ([a7df29c](https://github.com/netease/tango/commit/a7df29c3debc56b187792d3e203b470e9d368ea5))
## [0.1.4](https://github.com/netease/tango/compare/@music163/tango-core@0.1.3...@music163/tango-core@0.1.4) (2023-09-18)
**Note:** Version bump only for package @music163/tango-core
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-core",
"version": "0.1.4",
"version": "0.2.2",
"description": "tango core",
"author": "wwsun <ww.sun@outlook.com>",
"homepage": "",
@@ -28,7 +28,7 @@
"@babel/parser": "^7.22.15",
"@babel/traverse": "^7.22.15",
"@babel/types": "^7.22.15",
"@music163/tango-helpers": "^0.1.3",
"@music163/tango-helpers": "^0.1.4",
"@types/babel__generator": "^7.6.4",
"@types/babel__traverse": "^7.18.3",
"mobx": "6.9.0",
+9 -33
View File
@@ -11,14 +11,9 @@ import {
StringOrNumber,
Dict,
parseDndId,
upperCamelCase
upperCamelCase,
} from '@music163/tango-helpers';
import {
keyNode2value,
jsxAttributeValueNode2value,
node2value,
node2code,
} from './generate';
import { keyNode2value, jsxAttributeValueNode2value, node2value, node2code } from './generate';
import {
value2jsxAttributeValueNode,
value2jsxChildrenValueNode,
@@ -957,42 +952,22 @@ export function updateServiceConfigToServiceFile(
return ast;
}
function visitObjectProperties(
node: t.ObjectExpression,
visitor: (propName: string, node: t.ObjectProperty) => void,
) {
for (const propNode of node.properties) {
if (t.isObjectProperty(propNode)) {
const propName = keyNode2value(propNode.key);
visitor?.(propName as string, propNode);
}
}
}
export function updateBaseConfigToServiceFile(ast: t.File, configName: string, configValue: any) {
export function updateBaseConfigToServiceFile(ast: t.File, config: object) {
traverse(ast, {
CallExpression(path) {
const calleeName = keyNode2value(path.node.callee) as string;
if (isDefineService(calleeName)) {
switch (path.node.arguments.length) {
case 1:
path.node.arguments.push(value2node({ [configName]: configValue }));
// 没有 baseConfig,直接创建新的参数即可
path.node.arguments.push(value2node(config));
break;
case 2: {
// 已存在 baseConfig,需要进行参数的合并
const baseConfigNode = path.node.arguments[1];
if (t.isObjectExpression(baseConfigNode)) {
let found = false;
visitObjectProperties(baseConfigNode, (propName, propNode) => {
if (propName === configName) {
found = true;
propNode.value = value2node(configValue);
}
});
if (!found) {
baseConfigNode.properties.push(
t.objectProperty(t.identifier(configName), value2node(configValue)),
);
}
const baseConfig = node2value(baseConfigNode);
path.node.arguments[1] = value2node({ ...baseConfig, ...config });
}
break;
}
@@ -1020,6 +995,7 @@ export function traverseServiceFile(ast: t.File) {
if (path.node.arguments[1]) {
const config = node2value(path.node.arguments[1]);
baseConfig.encryptFetch = !!config.encryptFetch;
baseConfig.namespace = config.namespace || 'index';
}
}
path.stop();
+60
View File
@@ -0,0 +1,60 @@
import { getVariableContent, isVariableString } from '@music163/tango-helpers';
import { value2node, expression2code } from './ast';
/**
* 将 js value 转换为代码字符串
*/
export function value2code(value: any) {
const node = value2node(value);
const code = expression2code(node);
return code;
}
/**
* 是否是字符串代码
* @param code
* @returns
*/
function isStringCode(code: string) {
return /^".*"$/.test(code?.trim());
}
/**
* js value 转为表达式代码
* @example 1 => 1
* @example hello => "hello"
* @example { foo: bar } => {{ foo: bar }}
* @example [1,2,3] => {[1,2,3]}
*
* @param val js value
* @returns 表达式代码
*/
export function value2expressionCode(val: any) {
if (!val) return '';
let ret;
switch (typeof val) {
case 'string': {
if (isVariableString(val)) {
ret = getVariableContent(val);
} else if (isStringCode(val)) {
ret = val;
} else {
ret = `"${val}"`;
}
break;
}
case 'number':
ret = String(val);
break;
case 'object':
ret = value2code(val);
break;
default:
ret = '';
break;
}
return ret;
}
+1
View File
@@ -1,5 +1,6 @@
export * from './ast';
export * from './assert';
export * from './code-helpers';
export * from './string';
export * from './object';
export * from './prototype';
+4 -6
View File
@@ -1,4 +1,5 @@
import { IImportDeclarationPayload } from '../types';
import { Dict } from '@music163/tango-helpers';
import { IImportDeclarationPayload, IImportSpecifierData } from '../types';
/**
* 导入列表解析为导入声明对象
@@ -6,15 +7,12 @@ import { IImportDeclarationPayload } from '../types';
* @param nameMap
* @returns
*/
export function namesToImportDeclarations(
names: string[],
nameMap: Record<string, { package: string; isDefault?: boolean }>,
) {
export function namesToImportDeclarations(names: string[], nameMap: Dict<IImportSpecifierData>) {
const map = {};
names.forEach((name) => {
const mod = nameMap[name];
if (mod) {
updateMod(map, mod.package, name, mod.isDefault, !map[mod.package]);
updateMod(map, mod.source, name, mod.isDefault, !map[mod.source]);
}
});
return Object.keys(map).map((sourcePath) => ({
+1 -11
View File
@@ -1,6 +1,5 @@
import path from 'path';
import { camelCase, upperCamelCase } from '@music163/tango-helpers';
import { value2node, expression2code } from './ast';
import { FileType } from './../types';
/**
@@ -35,7 +34,7 @@ export function inferFileType(filename: string): FileType {
}
// FIXME: 暂时写死,后续要支持多 services 文件解析
if (/\/services\/index.js$/.test(filename)) {
if (/\/services\/.+\.js$/.test(filename)) {
return FileType.ServiceModule;
}
@@ -141,15 +140,6 @@ export function formatCode(code: string, parser = 'babel') {
return code;
}
/**
* 将 js value 转换为代码字符串
*/
export function value2code(value: any) {
const node = value2node(value);
const code = expression2code(node);
return code;
}
/**
* 是否匹配路由
* @example isPathnameMatchRoute('/user/123', '/users/:id') -> true
+4
View File
@@ -7,5 +7,9 @@ export * from './drag-source';
export * from './history';
export * from './file';
export * from './module';
export * from './route-module';
export * from './service-module';
export * from './store-module';
export * from './view-module';
export * from './node';
export * from './interfaces';
+24 -46
View File
@@ -9,11 +9,13 @@ import {
InsertChildPositionType,
ITangoConfigPackages,
IPageConfigData,
IProjectData,
IServiceFunctionPayload,
IImportSpecifierData,
} from '../types';
import { TangoFile, TangoJsonFile } from './file';
import { TangoRouteModule, TangoServiceModule, TangoStoreModule } from './module';
import { TangoRouteModule } from './route-module';
import { TangoStoreModule } from './store-module';
import { TangoServiceModule } from './service-module';
export interface IViewFile {
readonly workspace: IWorkspace;
@@ -23,7 +25,11 @@ export interface IViewFile {
/**
* 通过导入组件名查找组件来自的包
*/
importMap?: Dict<{ package: string; isDefault?: boolean }>;
importMap?: Dict<IImportSpecifierData>;
listImportSources?: () => string[];
listModals?: () => Array<{ label: string; value: string }>;
listForms?: () => Record<string, string[]>;
update: (code?: string, isFormatCode?: boolean, refreshWorkspace?: boolean) => void;
@@ -139,7 +145,8 @@ export interface IWorkspace {
tangoConfigJson: TangoJsonFile;
routeModule?: TangoRouteModule;
serviceModule?: TangoServiceModule;
storeModules?: Record<string, TangoStoreModule>;
serviceModules?: Record<string, TangoServiceModule>;
refresh: (names: string[]) => void;
ready: () => void;
@@ -150,11 +157,6 @@ export interface IWorkspace {
setComponentPrototypes: (prototypes: Record<string, ComponentPrototypeType>) => void;
getPrototype: (name: string | ComponentPrototypeType) => ComponentPrototypeType;
/**
* 获取项目数据
*/
getProjectData?: () => IProjectData;
/**
* 查询节点
* @param id 节点 ID
@@ -162,73 +164,55 @@ export interface IWorkspace {
* @returns 返回节点对象
*/
getNode: (id: string, module?: string) => IViewNode;
listModals?: () => Array<{ label: string; value: string }>;
listForms?: () => Record<string, string[]>;
getFile: (filename: string) => TangoFile;
listFiles: () => Record<string, string>;
addFile: (filename: string, code: string, fileType?: FileType) => void;
addFiles: (files: IFileConfig[]) => void;
updateFile: (filename: string, code: string, shouldFormatCode?: boolean) => void;
removeFile: (filename: string) => void;
renameFile: (oldFilename: string, newFilename: string) => void;
renameFolder: (oldFoldername: string, newFoldername: string) => void;
getFile: (filename: string) => TangoFile;
listFiles: () => Record<string, string>;
addViewPage: (name: string, code: string) => void;
removeSelectedNode: () => void;
cloneSelectedNode: () => void;
copySelectedNode: () => void;
pasteSelectedNode: () => void;
insertToSelectedNode: (childNameOrPrototype: string | ComponentPrototypeType) => void;
dropNode: () => void;
insertToNode: (
targetNodeId: string,
sourceNameOrPrototype: string | ComponentPrototypeType,
) => void;
replaceNode: (
targetNodeId: string,
sourceNameOrPrototype: string | ComponentPrototypeType,
) => void;
updateSelectedNodeAttributes: (
attributes: Record<string, any>,
relatedImports?: string[],
) => void;
addBlock?: (files: object, name: string) => void;
generateBlockFilesBySelectedNode?: () => Record<string, string>;
removeServiceFunction?: (serviceName: string) => void;
addServiceFunction?: (payload: IServiceFunctionPayload | IServiceFunctionPayload[]) => void;
updateServiceFunction?: (payload: any) => void;
updateServiceBaseConfig?: (name: string, value: any) => void;
listStoreModules?: () => TangoStoreModule[];
getServiceFunction?: (serviceKey: string) => object;
listServiceFunctions?: () => Record<string, object>;
removeServiceFunction?: (serviceName: string, modName?: string) => void;
addServiceFunction?: (
payload: IServiceFunctionPayload | IServiceFunctionPayload[],
modName?: string,
) => void;
updateServiceFunction?: (payload: IServiceFunctionPayload, modName?: string) => void;
updateServiceBaseConfig?: (IServiceFunctionPayload: object, modName?: string) => void;
addStoreModule?: (storeName: string, code: string) => void;
removeStoreModule?: (storeName: string) => void;
addStoreState?: (storeName: string, stateName: string, initValue: string) => void;
removeStoreState?: (storeName: string, stateName: string) => void;
updateModuleCodeByVariablePath?: (variablePath: string, code: string) => void;
// TODO: 是否需要,和 addFile 有啥区别 ???
addViewPage: (name: string, code: string) => void;
removeViewModule: (routePath: string) => void;
copyViewPage: (sourceRoutePath: string, targetPageData: IPageConfigData) => void;
updateRoute: (sourceRoutePath: string, targetPageData: IPageConfigData) => void;
@@ -258,14 +242,8 @@ export interface IWorkspace {
removeBizComp?: (name: string) => void;
get activeViewModule(): IViewFile;
// TODO: -> getStoreModules
get storeModules(): TangoStoreModule[];
// TODO: -> getPages
get pages(): any[];
// TODO: getBizComps
get bizComps(): string[];
// TODO: getBaseComps
get baseComps(): string[];
// TODO: getBlocks
get blocks(): any[];
}
+7 -645
View File
@@ -1,60 +1,17 @@
import * as t from '@babel/types';
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { clone, ComponentPrototypeType, Dict, isNil, logger } from '@music163/tango-helpers';
import {
code2ast,
ast2code,
traverseRouteFile,
traverseViewFile,
traverseServiceFile,
traverseStoreFile,
traverseStoreEntryFile,
addStoreToEntryFile,
updateServiceConfigToServiceFile,
removeJSXElement,
insertSiblingAfterJSXElement,
getModuleNameByFilename,
addRouteToRouteFile,
removeRouteFromRouteFile,
appendChildToJSXElement,
addImportDeclaration,
updateImportDeclaration,
deleteServiceConfigFromServiceFile,
replaceJSXElement,
formatCode,
getImportDeclarationPayloadByPrototype,
addStoreState,
removeUnusedImportSpecifiers,
insertSiblingBeforeJSXElement,
updateStoreState,
removeStoreState,
removeStoreToEntryFile,
replaceRootJSXElementChildren,
updateRouteToRouteFile,
updateBaseConfigToServiceFile,
IdGenerator,
updateJSXAttributes,
} from '../helpers';
import { TangoNode } from './node';
import { action, computed, makeObservable, observable } from 'mobx';
import { isNil } from '@music163/tango-helpers';
import { code2ast, ast2code, formatCode } from '../helpers';
import { TangoFile } from './file';
import {
IRouteData,
IFileConfig,
IClassPropertyNodeData,
IServiceFunctionPayload,
ITangoViewNodeData,
IStorePropertyData,
IImportDeclarationPayload,
InsertChildPositionType,
} from '../types';
import { IViewFile, IWorkspace } from './interfaces';
import { IFileConfig } from '../types';
import { IWorkspace } from './interfaces';
/**
* 模块实现规范
* JS 模块实现规范
* - ast 操纵类方法,统一返回 this,支持外层链式调用
* - observable state 统一用 _foo 格式,并提供 getter 方法
*/
class TangoModule extends TangoFile {
export class TangoModule extends TangoFile {
ast: t.File;
constructor(workspace: IWorkspace, props: IFileConfig, isSyncCode = true) {
@@ -132,598 +89,3 @@ export class TangoJsModule extends TangoModule {
});
}
}
/**
* 入口配置模块
*/
export class TangoStoreEntryModule extends TangoModule {
_stores: string[] = [];
get stores() {
return toJS(this._stores);
}
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code, true, false);
makeObservable(this, {
_stores: observable,
_code: observable,
_cleanCode: observable,
stores: computed,
code: computed,
cleanCode: computed,
update: action,
});
}
_analysisAst() {
this._stores = traverseStoreEntryFile(this.ast);
}
/**
* 新建模型
* @param name
*/
addStore(name: string) {
this.ast = addStoreToEntryFile(this.ast, name);
return this;
}
/**
* 删除模型
* @param name
*/
removeStore(name: string) {
this.ast = removeStoreToEntryFile(this.ast, name);
return this;
}
}
/**
* 路由配置模块
*/
export class TangoRouteModule extends TangoModule {
_routes: IRouteData[];
get routes() {
return toJS(this._routes);
}
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code, true, false);
makeObservable(this, {
_routes: observable,
_code: observable,
_cleanCode: observable,
routes: computed,
code: computed,
cleanCode: computed,
update: action,
});
}
/**
* 根据路由地址获取 route 对象
*/
getRouteByRoutePath(route: string) {
let record;
for (const item of this.routes) {
if (item.path === route) {
record = item;
break;
}
}
return record;
}
/**
* 添加一条新路由
* @param name
*/
addRoute(routePath: string, importFilePath: string) {
this.ast = addRouteToRouteFile(this.ast, routePath, importFilePath);
return this;
}
/**
* 更新页面路由
* @param oldRoutePath
* @param newRoutePath
* @returns
*/
updateRoute(oldRoutePath: string, newRoutePath: string) {
this.ast = updateRouteToRouteFile(this.ast, oldRoutePath, newRoutePath);
return this;
}
/**
* 删除一条路由
* @param route 路由地址
*/
removeRoute(route: string) {
if (route === '/') {
console.warn('index route should not be removed!');
return;
}
const record = this.getRouteByRoutePath(route);
this.ast = removeRouteFromRouteFile(this.ast, route, record.importPath);
return this;
}
_analysisAst() {
this._routes = traverseRouteFile(this.ast);
}
}
/**
* 视图模块
*/
export class TangoViewModule extends TangoModule implements IViewFile {
// 解析为树结构的 jsxNodes 数组
_nodesTree: ITangoViewNodeData[];
/**
* 通过导入组件名查找组件来自的包
*/
importMap: Dict<{ package: string; isDefault?: boolean }>;
/**
* 视图中依赖的 tango 变量,仅 stores 和 services
*/
variables: string[];
/**
* 节点列表
*/
private _nodes: Map<string, TangoNode>;
/**
* 导入的模块
*/
private _importedModules: Dict<IImportDeclarationPayload | IImportDeclarationPayload[]>;
/**
* ID 生成器
*/
private _idGenerator: IdGenerator;
get nodes() {
return this._nodes;
}
get nodesTree() {
return toJS(this._nodesTree);
}
get tree() {
return this.ast;
}
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this._nodes = new Map();
this._idGenerator = new IdGenerator({ prefix: props.filename });
this.update(props.code, true, false);
makeObservable(this, {
_nodesTree: observable,
_code: observable,
_cleanCode: observable,
code: computed,
cleanCode: computed,
update: action,
});
}
_syncByAst() {
// 空方法,逻辑合并到 this._analysisAst
}
_analysisAst() {
const {
ast: newAst,
cleanAst,
nodes,
importedModules,
variables,
} = traverseViewFile(this.ast, this._idGenerator);
this.ast = newAst;
this._code = ast2code(newAst);
this._cleanCode = ast2code(cleanAst);
this._importedModules = importedModules;
this.importMap = this.buildImportMap(importedModules);
this.variables = variables;
this._nodes.clear();
nodes.forEach((cur) => {
const node = new TangoNode({
...cur,
file: this,
});
this._nodes.set(cur.id, node);
});
this._nodesTree = nodeListToTreeData(nodes);
}
/**
* 基于组件的 prototype 信息更新导入信息
* @param prototype
* @param shouldUpdateCode
*/
updateImportSpecifiersByPrototype(sourcePrototype: string | ComponentPrototypeType) {
const prototype = this.workspace.getPrototype(sourcePrototype);
if (prototype) {
const importDeclaration = getImportDeclarationPayloadByPrototype(prototype, this.filename);
this.updateImportSpecifiers(importDeclaration);
}
return this;
}
/**
* 更新导入的变量(新版)
*/
updateImportSpecifiers(importDeclaration: IImportDeclarationPayload) {
const mods = this._importedModules[importDeclaration.sourcePath];
let ast;
// 如果模块已存在,需要去重
if (mods) {
const targetMod = Array.isArray(mods) ? mods[0] : mods;
const specifiers = Array.isArray(mods)
? mods.reduce((prev, cur) => prev.concat(cur.specifiers || []), [])
: mods.specifiers;
// 去掉已存在的导入声明
const newSpecifiers = importDeclaration.specifiers.filter(
(name) => !specifiers.includes(name),
);
ast = updateImportDeclaration(this.ast, {
...importDeclaration,
specifiers: newSpecifiers.concat(targetMod.specifiers),
});
} else {
ast = addImportDeclaration(this.ast, importDeclaration);
}
this.ast = ast;
return this;
}
/**
* 清除无效的导入声明
*/
removeUnusedImportSpecifiers() {
this.ast = removeUnusedImportSpecifiers(this.ast);
return this;
}
getNode(nodeId: string) {
return this._nodes.get(nodeId);
}
/**
* 删除节点
* @param nodeId
*/
removeNode(nodeId: string) {
this.ast = removeJSXElement(this.ast, nodeId);
return this;
}
/**
* 更新节点的属性
* @deprecated 使用 updateNodeAttributes 代替
*/
updateNodeAttribute(
nodeId: string,
attrName: string,
attrValue?: any,
relatedImports?: string[],
) {
return this.updateNodeAttributes(nodeId, { [attrName]: attrValue }, relatedImports);
}
updateNodeAttributes(nodeId: string, config: Record<string, any>, relatedImports?: string[]) {
if (relatedImports && relatedImports.length) {
// 导入依赖的组件
relatedImports.forEach((name: string) => {
const proto = this.workspace.getPrototype(name);
this.updateImportSpecifiersByPrototype(proto);
});
}
this.ast = updateJSXAttributes(this.ast, nodeId, config);
return this;
}
/**
* 插入子节点的最后面
* @param targetNodeId
* @param newNode
* @param sourceName
* @returns
*/
insertChild(
targetNodeId: string,
newNode: t.JSXElement,
position: InsertChildPositionType = 'last',
sourceName: string | ComponentPrototypeType,
) {
this.ast = appendChildToJSXElement(this.ast, targetNodeId, newNode, position);
if (sourceName) {
this.updateImportSpecifiersByPrototype(sourceName);
}
return this;
}
insertAfter(
targetNodeId: string,
newNode: t.JSXElement,
sourceName?: string | ComponentPrototypeType,
) {
this.ast = insertSiblingAfterJSXElement(this.ast, targetNodeId, newNode);
if (sourceName) {
this.updateImportSpecifiersByPrototype(sourceName);
}
return this;
}
insertBefore(
targetNodeId: string,
newNode: t.JSXElement,
sourceName?: string | ComponentPrototypeType,
) {
this.ast = insertSiblingBeforeJSXElement(this.ast, targetNodeId, newNode);
if (sourceName) {
this.updateImportSpecifiersByPrototype(sourceName);
}
return this;
}
/**
* 替换目标节点为新节点
* @param targetNodeId
* @param newNode
* @param sourcePrototype
*/
replaceNode(
targetNodeId: string,
newNode: t.JSXElement,
sourceName?: string | ComponentPrototypeType,
) {
this.ast = replaceJSXElement(this.ast, targetNodeId, newNode);
if (sourceName) {
this.updateImportSpecifiersByPrototype(sourceName);
}
return this;
}
/**
* 替换 jsx 跟结点的子元素
*/
replaceViewChildren(
childrenNodes: t.JSXElement[],
importDeclarations?: IImportDeclarationPayload[],
) {
if (childrenNodes.length) {
this.ast = replaceRootJSXElementChildren(this.ast, childrenNodes);
}
if (importDeclarations?.length) {
importDeclarations.forEach((item) => {
this.updateImportSpecifiers(item);
});
}
return this;
}
private buildImportMap(
importedModules: Dict<IImportDeclarationPayload | IImportDeclarationPayload[]>,
) {
const map = {};
Object.keys(importedModules).forEach((modName) => {
const mod = importedModules[modName];
(Array.isArray(mod) ? mod : [mod]).forEach((item) => {
if (item.defaultSpecifier) {
map[item.defaultSpecifier] = {
package: modName,
isDefault: true,
};
}
if (item.specifiers.length) {
item.specifiers.forEach((spe) => {
map[spe] = { package: modName };
});
}
});
});
return map;
}
}
/**
* 将节点列表转换为 tree data 嵌套数组
* @param list
*/
export function nodeListToTreeData(list: ITangoViewNodeData[]) {
const map: Record<string, ITangoViewNodeData> = {};
list.forEach((item) => {
// 如果不存在,则初始化
if (!map[item.id]) {
map[item.id] = {
...item,
children: [],
};
}
// 是否找到父节点,找到则塞进去
if (item.parentId && map[item.parentId]) {
map[item.parentId].children.push(map[item.id]);
}
});
// 保留根节点
const ret = Object.values(map).filter((item) => !item.parentId);
return ret;
}
/**
* 数据服务模块
*/
export class TangoServiceModule extends TangoModule {
/**
* 模块名
*/
name: string;
_serviceFunctions: Dict;
_baseConfig: Dict;
get serviceFunctions() {
return toJS(this._serviceFunctions);
}
get baseConfig() {
return toJS(this._baseConfig);
}
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.name = getModuleNameByFilename(props.filename);
this.update(props.code, true, false);
makeObservable(this, {
_serviceFunctions: observable,
_baseConfig: observable,
_code: observable,
_cleanCode: observable,
serviceFunctions: computed,
baseConfig: computed,
cleanCode: computed,
code: computed,
update: action,
});
}
_analysisAst() {
const { services, baseConfig } = traverseServiceFile(this.ast);
this._serviceFunctions = services;
this._baseConfig = baseConfig;
}
addServiceFunction(payload: IServiceFunctionPayload) {
const { name, ...rest } = payload;
this.ast = updateServiceConfigToServiceFile(this.ast, { [name]: clone(rest, false) });
return this;
}
addServiceFunctions(payloads: IServiceFunctionPayload[]) {
const config = payloads.reduce((acc, cur) => {
const { name, ...rest } = cur;
acc[name] = clone(rest, false);
return acc;
}, {});
this.ast = updateServiceConfigToServiceFile(this.ast, config);
return this;
}
updateServiceFunction(payload: IServiceFunctionPayload) {
const { name, ...rest } = payload;
this.ast = updateServiceConfigToServiceFile(this.ast, { [name]: clone(rest, false) });
return this;
}
deleteServiceFunction(serviceFunctionName: string) {
try {
this.ast = deleteServiceConfigFromServiceFile(this.ast, serviceFunctionName);
} catch (e) {
logger.error(e);
}
return this;
}
/**
* 更新服务的基础配置
*/
updateBaseConfig(optionName: string, optionValue: any) {
this.ast = updateBaseConfigToServiceFile(this.ast, optionName, optionValue);
return this;
}
}
/**
* 状态模型模块
*/
export class TangoStoreModule extends TangoModule {
/**
* 模块名
*/
name: string;
namespace: string;
states: IStorePropertyData[];
actions: IStorePropertyData[];
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.name = getModuleNameByFilename(props.filename);
this.update(props.code, true, false);
makeObservable(this, {
states: observable,
actions: observable,
_code: observable,
_cleanCode: observable,
cleanCode: computed,
code: computed,
update: action,
});
}
/**
* 添加状态属性
* @param stateName
* @param initValue
*/
addState(stateName: string, initValue: string) {
this.ast = addStoreState(this.ast, stateName, initValue);
return this;
}
/**
* 移除状态
*/
removeState(stateName: string) {
this.ast = removeStoreState(this.ast, stateName);
return this;
}
/**
* 更新状态代码
* @param stateName 状态名
* @param code 代码
*/
updateState(stateName: string, code: string) {
this.ast = updateStoreState(this.ast, stateName, code);
return this;
}
_analysisAst() {
const { namespace, states, actions } = traverseStoreFile(this.ast);
this.namespace = namespace || this.name;
this.states = states;
this.actions = actions;
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
import { JSXElement, SourceLocation } from '@babel/types';
import { cloneJSXElementWithoutTrackingData, getJSXElementAttributes } from '../helpers';
import { ITangoViewNodeData } from '../types';
import { TangoViewModule } from './module';
import { TangoViewModule } from './view-module';
import { IViewNode } from './interfaces';
type TangoNodeConstructorPropsType = ITangoViewNodeData & {
+88
View File
@@ -0,0 +1,88 @@
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import {
traverseRouteFile,
addRouteToRouteFile,
removeRouteFromRouteFile,
updateRouteToRouteFile,
} from '../helpers';
import { IRouteData, IFileConfig } from '../types';
import { IWorkspace } from './interfaces';
import { TangoModule } from './module';
/**
* 路由配置模块
*/
export class TangoRouteModule extends TangoModule {
_routes: IRouteData[];
get routes() {
return toJS(this._routes);
}
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code, true, false);
makeObservable(this, {
_routes: observable,
_code: observable,
_cleanCode: observable,
routes: computed,
code: computed,
cleanCode: computed,
update: action,
});
}
/**
* 根据路由地址获取 route 对象
*/
getRouteByRoutePath(route: string) {
let record;
for (const item of this.routes) {
if (item.path === route) {
record = item;
break;
}
}
return record;
}
/**
* 添加一条新路由
* @param name
*/
addRoute(routePath: string, importFilePath: string) {
this.ast = addRouteToRouteFile(this.ast, routePath, importFilePath);
return this;
}
/**
* 更新页面路由
* @param oldRoutePath
* @param newRoutePath
* @returns
*/
updateRoute(oldRoutePath: string, newRoutePath: string) {
this.ast = updateRouteToRouteFile(this.ast, oldRoutePath, newRoutePath);
return this;
}
/**
* 删除一条路由
* @param route 路由地址
*/
removeRoute(route: string) {
if (route === '/') {
console.warn('index route should not be removed!');
return;
}
const record = this.getRouteByRoutePath(route);
this.ast = removeRouteFromRouteFile(this.ast, route, record.importPath);
return this;
}
_analysisAst() {
this._routes = traverseRouteFile(this.ast);
}
}
+107
View File
@@ -0,0 +1,107 @@
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { clone, Dict, logger } from '@music163/tango-helpers';
import {
traverseServiceFile,
updateServiceConfigToServiceFile,
getModuleNameByFilename,
deleteServiceConfigFromServiceFile,
updateBaseConfigToServiceFile,
} from '../helpers';
import { IFileConfig, IServiceFunctionPayload } from '../types';
import { IWorkspace } from './interfaces';
import { TangoModule } from './module';
/**
* 数据服务模块
*/
export class TangoServiceModule extends TangoModule {
/**
* 服务函数的模块名,默认为 index
*/
name: string;
/**
* 原始注册的服务函数
* @example { get: {}, list: {} }
*/
_serviceFunctions: Dict;
/**
* 原始的基础配置
*/
_baseConfig: Dict;
get serviceFunctions() {
return toJS(this._serviceFunctions);
}
get baseConfig() {
return toJS(this._baseConfig);
}
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.name = getModuleNameByFilename(props.filename);
this.update(props.code, true, false);
makeObservable(this, {
_serviceFunctions: observable,
_baseConfig: observable,
_code: observable,
_cleanCode: observable,
serviceFunctions: computed,
baseConfig: computed,
cleanCode: computed,
code: computed,
update: action,
});
}
_analysisAst() {
const { services, baseConfig } = traverseServiceFile(this.ast);
this._serviceFunctions = services;
this._baseConfig = baseConfig;
if (baseConfig.namespace) {
this.name = baseConfig.namespace;
}
}
addServiceFunction(payload: IServiceFunctionPayload) {
const { name, ...rest } = payload;
this.ast = updateServiceConfigToServiceFile(this.ast, { [name]: clone(rest, false) });
return this;
}
addServiceFunctions(payloads: IServiceFunctionPayload[]) {
const config = payloads.reduce((acc, cur) => {
const { name, ...rest } = cur;
acc[name] = clone(rest, false);
return acc;
}, {});
this.ast = updateServiceConfigToServiceFile(this.ast, config);
return this;
}
updateServiceFunction(payload: IServiceFunctionPayload) {
const { name, ...rest } = payload;
this.ast = updateServiceConfigToServiceFile(this.ast, { [name]: clone(rest, false) });
return this;
}
deleteServiceFunction(serviceFunctionName: string) {
try {
this.ast = deleteServiceConfigFromServiceFile(this.ast, serviceFunctionName);
} catch (e) {
logger.error(e);
}
return this;
}
/**
* 更新服务的基础配置
*/
updateBaseConfig(data: object) {
this.ast = updateBaseConfigToServiceFile(this.ast, data);
return this;
}
}
+129
View File
@@ -0,0 +1,129 @@
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import {
traverseStoreFile,
traverseStoreEntryFile,
addStoreToEntryFile,
getModuleNameByFilename,
addStoreState,
updateStoreState,
removeStoreState,
removeStoreToEntryFile,
} from '../helpers';
import { IFileConfig, IStorePropertyData } from '../types';
import { IWorkspace } from './interfaces';
import { TangoModule } from './module';
/**
* 入口配置模块
*/
export class TangoStoreEntryModule extends TangoModule {
_stores: string[] = [];
get stores() {
return toJS(this._stores);
}
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code, true, false);
makeObservable(this, {
_stores: observable,
_code: observable,
_cleanCode: observable,
stores: computed,
code: computed,
cleanCode: computed,
update: action,
});
}
_analysisAst() {
this._stores = traverseStoreEntryFile(this.ast);
}
/**
* 新建模型
* @param name
*/
addStore(name: string) {
this.ast = addStoreToEntryFile(this.ast, name);
return this;
}
/**
* 删除模型
* @param name
*/
removeStore(name: string) {
this.ast = removeStoreToEntryFile(this.ast, name);
return this;
}
}
/**
* 状态模型模块
*/
export class TangoStoreModule extends TangoModule {
/**
* 模块名
*/
name: string;
namespace: string;
states: IStorePropertyData[];
actions: IStorePropertyData[];
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.name = getModuleNameByFilename(props.filename);
this.update(props.code, true, false);
makeObservable(this, {
states: observable,
actions: observable,
_code: observable,
_cleanCode: observable,
cleanCode: computed,
code: computed,
update: action,
});
}
/**
* 添加状态属性
* @param stateName
* @param initValue
*/
addState(stateName: string, initValue: string) {
this.ast = addStoreState(this.ast, stateName, initValue);
return this;
}
/**
* 移除状态
*/
removeState(stateName: string) {
this.ast = removeStoreState(this.ast, stateName);
return this;
}
/**
* 更新状态代码
* @param stateName 状态名
* @param code 代码
*/
updateState(stateName: string, code: string) {
this.ast = updateStoreState(this.ast, stateName, code);
return this;
}
_analysisAst() {
const { namespace, states, actions } = traverseStoreFile(this.ast);
this.namespace = namespace || this.name;
this.states = states;
this.actions = actions;
}
}
+390
View File
@@ -0,0 +1,390 @@
import * as t from '@babel/types';
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { ComponentPrototypeType, Dict } from '@music163/tango-helpers';
import {
ast2code,
traverseViewFile,
removeJSXElement,
insertSiblingAfterJSXElement,
appendChildToJSXElement,
addImportDeclaration,
updateImportDeclaration,
replaceJSXElement,
getImportDeclarationPayloadByPrototype,
removeUnusedImportSpecifiers,
insertSiblingBeforeJSXElement,
replaceRootJSXElementChildren,
IdGenerator,
updateJSXAttributes,
queryXFormItemFields,
} from '../helpers';
import { TangoNode } from './node';
import {
IFileConfig,
ITangoViewNodeData,
IImportDeclarationPayload,
InsertChildPositionType,
IImportSpecifierData,
} from '../types';
import { IViewFile, IWorkspace } from './interfaces';
import { TangoModule } from './module';
/**
* 视图模块
*/
export class TangoViewModule extends TangoModule implements IViewFile {
// 解析为树结构的 jsxNodes 数组
_nodesTree: ITangoViewNodeData[];
/**
* 通过导入组件名查找组件来自的包
*/
importMap: Dict<IImportSpecifierData>;
/**
* 视图中依赖的 tango 变量,仅 stores 和 services
*/
variables: string[];
/**
* 节点列表
*/
private _nodes: Map<string, TangoNode>;
/**
* 导入的模块
*/
private _importedModules: Dict<IImportDeclarationPayload | IImportDeclarationPayload[]>;
/**
* ID 生成器
*/
private _idGenerator: IdGenerator;
get nodes() {
return this._nodes;
}
get nodesTree() {
return toJS(this._nodesTree);
}
get tree() {
return this.ast;
}
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this._nodes = new Map();
this._idGenerator = new IdGenerator({ prefix: props.filename });
this.update(props.code, true, false);
makeObservable(this, {
_nodesTree: observable,
_code: observable,
_cleanCode: observable,
code: computed,
cleanCode: computed,
update: action,
});
}
_syncByAst() {
// 空方法,逻辑合并到 this._analysisAst
}
_analysisAst() {
const {
ast: newAst,
cleanAst,
nodes,
importedModules,
variables,
} = traverseViewFile(this.ast, this._idGenerator);
this.ast = newAst;
this._code = ast2code(newAst);
this._cleanCode = ast2code(cleanAst);
this._importedModules = importedModules;
this.importMap = this.buildImportMap(importedModules);
this.variables = variables;
this._nodes.clear();
nodes.forEach((cur) => {
const node = new TangoNode({
...cur,
file: this,
});
this._nodes.set(cur.id, node);
});
this._nodesTree = nodeListToTreeData(nodes);
}
/**
* 依赖列表
*/
listImportSources() {
return Object.keys(this._importedModules);
}
/**
* 弹窗列表
*/
listModals() {
const modals: Array<{ label: string; value: string }> = [];
const activeViewNodes = this.nodes || new Map();
Array.from(activeViewNodes.values()).forEach((node) => {
if (['Modal', 'Drawer'].includes(node.component) && node.props.id) {
modals.push({
label: `${node.component}(${node.props.id})`,
value: node.props.id,
});
}
});
return modals;
}
/**
* 表单列表
*/
listForms() {
const forms: Record<string, string[]> = {};
const activeViewNodes = this.nodes;
Array.from(activeViewNodes.values()).forEach((node) => {
if (
['XAction', 'XColumnAction', 'XForm', 'XStepForm', 'XSearchForm', 'XFormList'].includes(
node.component,
)
) {
forms[node.id] = queryXFormItemFields(node.rawNode);
}
});
return forms;
}
/**
* 基于组件的 prototype 信息更新导入信息
* @param prototype
* @param shouldUpdateCode
*/
updateImportSpecifiersByPrototype(sourcePrototype: string | ComponentPrototypeType) {
const prototype = this.workspace.getPrototype(sourcePrototype);
if (prototype) {
const importDeclaration = getImportDeclarationPayloadByPrototype(prototype, this.filename);
this.updateImportSpecifiers(importDeclaration);
}
return this;
}
/**
* 更新导入的变量(新版)
*/
updateImportSpecifiers(importDeclaration: IImportDeclarationPayload) {
const mods = this._importedModules[importDeclaration.sourcePath];
let ast;
// 如果模块已存在,需要去重
if (mods) {
const targetMod = Array.isArray(mods) ? mods[0] : mods;
const specifiers = Array.isArray(mods)
? mods.reduce((prev, cur) => prev.concat(cur.specifiers || []), [])
: mods.specifiers;
// 去掉已存在的导入声明
const newSpecifiers = importDeclaration.specifiers.filter(
(name) => !specifiers.includes(name),
);
ast = updateImportDeclaration(this.ast, {
...importDeclaration,
specifiers: newSpecifiers.concat(targetMod.specifiers),
});
} else {
ast = addImportDeclaration(this.ast, importDeclaration);
}
this.ast = ast;
return this;
}
/**
* 清除无效的导入声明
*/
removeUnusedImportSpecifiers() {
this.ast = removeUnusedImportSpecifiers(this.ast);
return this;
}
getNode(nodeId: string) {
return this._nodes.get(nodeId);
}
/**
* 删除节点
* @param nodeId
*/
removeNode(nodeId: string) {
this.ast = removeJSXElement(this.ast, nodeId);
return this;
}
/**
* 更新节点的属性
* @deprecated 使用 updateNodeAttributes 代替
*/
updateNodeAttribute(
nodeId: string,
attrName: string,
attrValue?: any,
relatedImports?: string[],
) {
return this.updateNodeAttributes(nodeId, { [attrName]: attrValue }, relatedImports);
}
updateNodeAttributes(nodeId: string, config: Record<string, any>, relatedImports?: string[]) {
if (relatedImports && relatedImports.length) {
// 导入依赖的组件
relatedImports.forEach((name: string) => {
const proto = this.workspace.getPrototype(name);
this.updateImportSpecifiersByPrototype(proto);
});
}
this.ast = updateJSXAttributes(this.ast, nodeId, config);
return this;
}
/**
* 插入子节点的最后面
* @param targetNodeId
* @param newNode
* @param sourceName
* @returns
*/
insertChild(
targetNodeId: string,
newNode: t.JSXElement,
position: InsertChildPositionType = 'last',
sourceName: string | ComponentPrototypeType,
) {
this.ast = appendChildToJSXElement(this.ast, targetNodeId, newNode, position);
if (sourceName) {
this.updateImportSpecifiersByPrototype(sourceName);
}
return this;
}
insertAfter(
targetNodeId: string,
newNode: t.JSXElement,
sourceName?: string | ComponentPrototypeType,
) {
this.ast = insertSiblingAfterJSXElement(this.ast, targetNodeId, newNode);
if (sourceName) {
this.updateImportSpecifiersByPrototype(sourceName);
}
return this;
}
insertBefore(
targetNodeId: string,
newNode: t.JSXElement,
sourceName?: string | ComponentPrototypeType,
) {
this.ast = insertSiblingBeforeJSXElement(this.ast, targetNodeId, newNode);
if (sourceName) {
this.updateImportSpecifiersByPrototype(sourceName);
}
return this;
}
/**
* 替换目标节点为新节点
* @param targetNodeId
* @param newNode
* @param sourcePrototype
*/
replaceNode(
targetNodeId: string,
newNode: t.JSXElement,
sourceName?: string | ComponentPrototypeType,
) {
this.ast = replaceJSXElement(this.ast, targetNodeId, newNode);
if (sourceName) {
this.updateImportSpecifiersByPrototype(sourceName);
}
return this;
}
/**
* 替换 jsx 跟结点的子元素
*/
replaceViewChildren(
childrenNodes: t.JSXElement[],
importDeclarations?: IImportDeclarationPayload[],
) {
if (childrenNodes.length) {
this.ast = replaceRootJSXElementChildren(this.ast, childrenNodes);
}
if (importDeclarations?.length) {
importDeclarations.forEach((item) => {
this.updateImportSpecifiers(item);
});
}
return this;
}
private buildImportMap(
importedModules: Dict<IImportDeclarationPayload | IImportDeclarationPayload[]>,
) {
const map: Dict<IImportSpecifierData> = {};
Object.keys(importedModules).forEach((modName) => {
const mod = importedModules[modName];
(Array.isArray(mod) ? mod : [mod]).forEach((item) => {
if (item.defaultSpecifier) {
map[item.defaultSpecifier] = {
source: modName,
isDefault: true,
};
}
if (item.specifiers.length) {
item.specifiers.forEach((spe) => {
map[spe] = { source: modName };
});
}
});
});
return map;
}
}
/**
* 将节点列表转换为 tree data 嵌套数组
* @param list
*/
function nodeListToTreeData(list: ITangoViewNodeData[]) {
const map: Record<string, ITangoViewNodeData> = {};
list.forEach((item) => {
// 如果不存在,则初始化
if (!map[item.id]) {
map[item.id] = {
...item,
children: [],
};
}
// 是否找到父节点,找到则塞进去
if (item.parentId && map[item.parentId]) {
map[item.parentId].children.push(map[item.id]);
}
});
// 保留根节点
const ret = Object.values(map).filter((item) => !item.parentId);
return ret;
}
+100 -157
View File
@@ -16,20 +16,12 @@ import {
getJSXElementChildrenNames,
namesToImportDeclarations,
getBlockNameByFilename,
queryXFormItemFields,
getPrivilegeCode,
} from '../helpers';
import { DropMethod } from './drop-target';
import { HistoryMessage, TangoHistory } from './history';
import { TangoNode } from './node';
import {
TangoStoreEntryModule,
TangoRouteModule,
TangoServiceModule,
TangoStoreModule,
TangoViewModule,
TangoJsModule,
} from './module';
import { TangoJsModule } from './module';
import { TangoFile, TangoJsonFile, TangoLessFile } from './file';
import { IWorkspace } from './interfaces';
import {
@@ -37,11 +29,14 @@ import {
FileType,
ITangoConfigPackages,
IPageConfigData,
IProjectData,
IServiceFunctionPayload,
} from '../types';
import { SelectSource } from './select-source';
import { DragSource } from './drag-source';
import { TangoRouteModule } from './route-module';
import { TangoStoreEntryModule, TangoStoreModule } from './store-module';
import { TangoServiceModule } from './service-module';
import { TangoViewModule } from './view-module';
export interface IWorkspaceOptions {
/**
@@ -96,14 +91,10 @@ export class Workspace extends EventTarget implements IWorkspace {
/**
* 本地区块 { [blockName]: filePath }
* FIXME: 修正类型 Record<string, TangoBlockModule>
*/
localBlocks: Record<string, string>;
/**
* service 模块
*/
serviceModule: TangoServiceModule;
/**
* 路由配置模块
*/
@@ -114,6 +105,10 @@ export class Workspace extends EventTarget implements IWorkspace {
*/
storeEntryModule: TangoStoreEntryModule;
storeModules: Record<string, TangoStoreModule> = {};
serviceModules: Record<string, TangoServiceModule> = {};
/**
* package.json 文件
*/
@@ -161,19 +156,6 @@ export class Workspace extends EventTarget implements IWorkspace {
return this.files.get(this.activeViewFile) as TangoViewModule;
}
/**
* store 模块列表
*/
get storeModules() {
const mods: TangoStoreModule[] = [];
this.files.forEach((file) => {
if (file instanceof TangoStoreModule) {
mods.push(file);
}
});
return mods;
}
/**
* 获取页面列表
*/
@@ -270,36 +252,6 @@ export class Workspace extends EventTarget implements IWorkspace {
return name as ComponentPrototypeType;
}
getProjectData() {
const pages = this.pages.map((item) => {
const filename = this.getFilenameByRoutePath(item.path);
return {
title: item.name,
path: item.path,
filename,
includes: {
variables: (this.getFile(filename) as TangoViewModule).variables,
},
};
});
const stores = this.storeModules.reduce((acc, item) => {
acc[item.name] = { filename: item.filename };
return acc;
}, {});
const services = {
index: {
filename: this.serviceModule?.filename,
functions: Object.keys(this.serviceModule.serviceFunctions),
},
};
const data: IProjectData = {
pages,
stores,
services,
};
return data;
}
/**
* 设置当前路由
* @param routePath 路由路径
@@ -395,10 +347,11 @@ export class Workspace extends EventTarget implements IWorkspace {
break;
case FileType.ServiceModule:
module = new TangoServiceModule(this, props);
this.serviceModule = module; // 仅支持单个 service 文件
this.serviceModules[module.name] = module;
break;
case FileType.StoreModule:
module = new TangoStoreModule(this, props);
this.storeModules[module.name] = module;
break;
case FileType.BlockEntryModule: {
const blockName = getBlockNameByFilename(props.filename);
@@ -524,24 +477,6 @@ export class Workspace extends EventTarget implements IWorkspace {
return ret;
}
listStoreModules() {
return Array.from(this.files.values()).filter(
(file) => file instanceof TangoStoreModule,
) as TangoStoreModule[];
}
getStoreModuleByName(storeName: string) {
let mod;
const files = this.files.values();
for (const file of files) {
if (file instanceof TangoStoreModule && file.name === storeName) {
mod = file;
break;
}
}
return mod;
}
/**
* 删除视图模块
* @param route 路由名称
@@ -641,45 +576,6 @@ export class Workspace extends EventTarget implements IWorkspace {
}
}
/**
* TODO: 仅获取当前视图即可
* 获取当前视图的弹窗列表
* @returns
*/
listModals() {
const modals: Array<{ label: string; value: string }> = [];
const activeViewNodes = this.activeViewModule?.nodes || new Map();
Array.from(activeViewNodes.values()).forEach((node) => {
if (['Modal', 'Drawer'].includes(node.component) && node.props.id) {
modals.push({
label: `${node.component}(${node.props.id})`,
value: node.props.id,
});
}
});
return modals;
}
/**
* TODO: 获取当前视图的表单列表
*/
listForms() {
const forms: Record<string, string[]> = {};
const activeViewNodes = this.activeViewModule?.nodes;
Array.from(activeViewNodes.values()).forEach((node) => {
if (
['XAction', 'XColumnAction', 'XForm', 'XStepForm', 'XSearchForm', 'XFormList'].includes(
node.component,
)
) {
forms[node.id] = queryXFormItemFields(node.rawNode);
}
});
return forms;
}
/**
* 应用代码初始化完成
*/
@@ -696,34 +592,6 @@ export class Workspace extends EventTarget implements IWorkspace {
}
}
/**
* 添加区块文件
* @param files 文件
* @param name 区块文件名
*/
addBlock(files: Object, name: string) {
const blockFiles = {};
Object.keys(files).forEach((path) => {
if (!path.includes('/src')) return;
blockFiles[path.replace('/src/', '')] = files[path];
});
this.addFiles(
Object.keys(blockFiles).map((filepath) => ({
filename: `/src/blocks/${name}/${filepath}`,
code: blockFiles[filepath],
})),
);
const { dependencies } = JSON.parse(files['/package.json']);
Object.keys(dependencies).forEach((pkg) => {
this.packageJson?.setValue('dependencies', (deps) => {
if (!deps[pkg]) {
deps[pkg] = dependencies[pkg];
}
return deps;
});
});
}
/**
* 添加新的模型文件
* @param name 模型名
@@ -752,7 +620,7 @@ export class Workspace extends EventTarget implements IWorkspace {
* @param initValue
*/
addStoreState(storeName: string, stateName: string, initValue: string) {
this.getStoreModuleByName(storeName).addState(stateName, initValue).update();
this.storeModules[storeName]?.addState(stateName, initValue).update();
}
/**
@@ -761,7 +629,7 @@ export class Workspace extends EventTarget implements IWorkspace {
* @param stateName
*/
removeStoreState(storeName: string, stateName: string) {
this.getStoreModuleByName(storeName).removeState(stateName).update();
this.storeModules[storeName]?.removeState(stateName).update();
}
/**
@@ -772,26 +640,65 @@ export class Workspace extends EventTarget implements IWorkspace {
*/
updateModuleCodeByVariablePath(variablePath: string, code: string) {
if (/^stores\.\w+\.\w+$/.test(variablePath)) {
const [type, storeName, stateName] = variablePath.split('.');
this.getStoreModuleByName(storeName).updateState(stateName, code).update();
const [, storeName, stateName] = variablePath.split('.');
this.storeModules[storeName]?.updateState(stateName, code).update();
}
}
/**
* 获取服务函数的详情
* TODO: 不要 services 前缀
* @param serviceKey `services.list` 或 `services.sub.list`
* @returns
*/
getServiceFunction(serviceKey: string) {
const { name, moduleName } = this.parseServiceKey(serviceKey);
if (!name) {
return;
}
return {
name,
moduleName,
config: this.serviceModules[moduleName]?.serviceFunctions[name],
};
}
/**
* 获取服务函数的列表
*/
listServiceFunctions() {
const ret: Record<string, object> = {};
Object.values(this.serviceModules).forEach((module) => {
Object.keys(module.serviceFunctions).forEach((name) => {
const serviceKey =
module.baseConfig.namespace !== 'index'
? name
: [module.baseConfig.namespace, name].join('.');
ret[serviceKey] = module.serviceFunctions[name];
});
});
return ret;
}
/**
* 更新服务函数
*/
updateServiceFunction(payload: any) {
this.serviceModule.updateServiceFunction(payload).update();
updateServiceFunction(payload: IServiceFunctionPayload, moduleName = 'index') {
this.serviceModules[moduleName].updateServiceFunction(payload).update();
}
/**
* 新增服务函数,支持批量添加
*/
addServiceFunction(payload: IServiceFunctionPayload | IServiceFunctionPayload[]) {
addServiceFunction(
payload: IServiceFunctionPayload | IServiceFunctionPayload[],
moduleName = 'index',
) {
if (Array.isArray(payload)) {
this.serviceModule.addServiceFunctions(payload).update();
this.serviceModules[moduleName]?.addServiceFunctions(payload).update();
} else {
this.serviceModule.addServiceFunction(payload).update();
this.serviceModules[moduleName]?.addServiceFunction(payload).update();
}
}
@@ -799,15 +706,15 @@ export class Workspace extends EventTarget implements IWorkspace {
* 删除服务函数
* @param name
*/
removeServiceFunction(name: string) {
this.serviceModule.deleteServiceFunction(name).update();
removeServiceFunction(name: string, moduleName = 'index') {
this.serviceModules[moduleName]?.deleteServiceFunction(name).update();
}
/**
* 更新服务的基础配置
*/
updateServiceBaseConfig(name: string, value: any) {
this.serviceModule.updateBaseConfig(name, value).update();
updateServiceBaseConfig(config: object, moduleName = 'index') {
this.serviceModules[moduleName]?.updateBaseConfig(config).update();
}
/**
@@ -1295,4 +1202,40 @@ export class Workspace extends EventTarget implements IWorkspace {
logger.error('copyFiles failed, source: %s, target: %s', sourceFilePath, targetFilePath);
}
}
/**
* 解析 serviceKey
* @param serviceKey
* @returns
*
* @example services.list => { moduleName: 'index', name: 'list' }
* @example services.sub.list => { moduleName: 'sub', name: 'list' }
* @example foo => undefined
*/
private parseServiceKey(serviceKey: string) {
const parts = serviceKey.split('.');
if (parts[0] !== 'services') {
return {};
}
let moduleName = 'index';
let name = '';
switch (parts.length) {
case 2: {
name = parts[1];
break;
}
case 3: {
moduleName = parts[1];
name = parts[2];
break;
}
default:
break;
}
return {
moduleName,
name,
};
}
}
+8
View File
@@ -72,6 +72,14 @@ export interface ITangoViewNodeData<T = JSXElement> {
children?: Array<ITangoViewNodeData<T>>;
}
/**
* 导入变量的解析数据
*/
export interface IImportSpecifierData {
source: string;
isDefault?: boolean;
}
/**
* 模块导入的参数类型
*/
-2
View File
@@ -27,8 +27,6 @@ describe('engine', () => {
}),
});
expect(engine.workspace.activeViewModule).toBeUndefined();
expect(engine.workspace.serviceModule).toBeUndefined();
expect(engine.workspace.routeModule).toBeUndefined();
expect(engine.workspace?.storeModules.length).toEqual(0);
});
});
+41
View File
@@ -3,6 +3,47 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.5.1](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.0...@music163/tango-designer@0.5.1) (2023-10-23)
### Bug Fixes
- add url validator ([8fefa63](https://github.com/netease/tango/commit/8fefa6344fc884355c32c4e62a5f1199ef31d65c))
- custom store template ([3f073ee](https://github.com/netease/tango/commit/3f073eeb436b7a929c9cad7af3daa15ba20e6be5))
- enhance expSetter ([#43](https://github.com/netease/tango/issues/43)) ([5ebbb42](https://github.com/netease/tango/commit/5ebbb428fb3fb786d330ab01959028443338d315))
# [0.5.0](https://github.com/netease/tango/compare/@music163/tango-designer@0.4.1...@music163/tango-designer@0.5.0) (2023-10-12)
### Bug Fixes
- refactor expSetter ([#41](https://github.com/netease/tango/issues/41)) ([d63517e](https://github.com/netease/tango/commit/d63517ecb936e4227e70c33e610664316625f4f4))
### Features
- add umd resources config when modifying dependency ([#36](https://github.com/netease/tango/issues/36)) ([e1bab66](https://github.com/netease/tango/commit/e1bab66819cf9a0bd7b87b72b2962498dfd51b2c))
## [0.4.1](https://github.com/netease/tango/compare/@music163/tango-designer@0.4.0...@music163/tango-designer@0.4.1) (2023-10-09)
### Bug Fixes
- always switch to design mode after preview is on ([85898de](https://github.com/netease/tango/commit/85898dee19b6031016679e33a4ce9edbd1089713))
- update export entry ([e429610](https://github.com/netease/tango/commit/e4296106066ad290ac838135fe6a57bc50ad2680))
# [0.4.0](https://github.com/netease/tango/compare/@music163/tango-designer@0.3.0...@music163/tango-designer@0.4.0) (2023-09-22)
### Bug Fixes
- remove useless api ([226d71c](https://github.com/netease/tango/commit/226d71c704f41a756ee777d6de4627a7eb7b4259))
### Features
- add global widget map ([1c72ee6](https://github.com/netease/tango/commit/1c72ee6ac287c1b6fb5eb3806a68b877f3749405))
# [0.3.0](https://github.com/netease/tango/compare/@music163/tango-designer@0.2.1...@music163/tango-designer@0.3.0) (2023-09-21)
### Features
- support multiple service modules & refactor workspace ([a7df29c](https://github.com/netease/tango/commit/a7df29c3debc56b187792d3e203b470e9d368ea5))
## [0.2.1](https://github.com/netease/tango/compare/@music163/tango-designer@0.2.0...@music163/tango-designer@0.2.1) (2023-09-18)
**Note:** Version bump only for package @music163/tango-designer
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-designer",
"version": "0.2.1",
"version": "0.5.1",
"description": "lowcode designer",
"keywords": [
"react"
@@ -33,12 +33,12 @@
"dependencies": {
"@ant-design/icons": "^4.8.0",
"@music163/request": "^0.1.0",
"@music163/tango-context": "^0.1.4",
"@music163/tango-core": "^0.1.4",
"@music163/tango-helpers": "^0.1.3",
"@music163/tango-sandbox": "^0.1.4",
"@music163/tango-setting-form": "^0.2.1",
"@music163/tango-ui": "^0.1.4",
"@music163/tango-context": "^0.2.3",
"@music163/tango-core": "^0.2.2",
"@music163/tango-helpers": "^0.1.4",
"@music163/tango-sandbox": "^0.1.7",
"@music163/tango-setting-form": "^0.2.6",
"@music163/tango-ui": "^0.1.7",
"antd": "^4.24.2",
"cash-dom": "^8.1.2",
"classnames": "^2.3.2",
@@ -11,10 +11,15 @@ import {
Empty,
Popconfirm,
Tooltip,
Alert,
} from 'antd';
import { Box, Text, css, Link } from 'coral-system';
import { PlusOutlined, FunctionOutlined, DeleteOutlined } from '@ant-design/icons';
import { Box, Text, css } from 'coral-system';
import {
PlusOutlined,
FunctionOutlined,
DeleteOutlined,
CopyOutlined,
EyeOutlined,
} from '@ant-design/icons';
import {
filterTreeData,
isFunction,
@@ -96,10 +101,6 @@ export interface EditableVariableTreeProps {
* 选择列表项时的回调
*/
onSelect?: (data: IVariableTreeNode) => void;
/**
* 点击使用此变量按钮时的回调
*/
onUse?: (data: IVariableTreeNode) => void;
/**
* 保存结点定义的回调
*/
@@ -124,10 +125,6 @@ export interface EditableVariableTreeProps {
* 搜索框的后置结点
*/
searchAddonAfter?: React.ReactNode;
/**
* 是否展示预览值的提示信息
*/
showPreviewAlert?: boolean;
}
const previewOptions = [
@@ -145,11 +142,9 @@ export function EditableVariableTree({
onDeleteStore = noop,
getPreviewValue = noop,
onSelect = noop,
onUse = noop,
onSave = noop,
modes = ['add', 'preview', 'define'],
defaultMode = 'preview',
showPreviewAlert = true,
}: EditableVariableTreeProps) {
const [keyword, setKeyword] = useState('');
const [node, setNode] = useState<IVariableTreeNode>();
@@ -184,11 +179,16 @@ export function EditableVariableTree({
</Box>
<VariableTree
dataSource={treeData}
showViewIcon
onSelect={(item) => {
setNode(item);
mode === 'add' && setMode(defaultMode);
onSelect(item);
}}
onView={(item) => {
setNode(item);
mode === 'add' && setMode(defaultMode);
}}
onAdd={(item) => {
setNode(item);
setMode('add');
@@ -225,33 +225,15 @@ export function EditableVariableTree({
borderRadius="m"
position="sticky"
top="0"
bodyProps={{ px: 'l' }}
bodyProps={{ px: 'm' }}
>
{mode === 'preview' && (
<>
<Box mb="m" display={showPreviewAlert ? 'block' : 'none'}>
<Alert
message={
<Space>
<Text>
<Text as="strong"></Text>tango.{node.key}
</Text>
<Link onClick={() => onUse(node)}>使</Link>
<CopyClipboard text={`tango.${node.key.replaceAll('.', '?.')}`}>
{(copied) => <Link>{copied ? '已复制' : '复制变量路径'}</Link>}
</CopyClipboard>
</Space>
}
type="info"
/>
</Box>
<ValuePreview
value={getPreviewValue(node)}
onSelect={(valuePath) => {
return ['tango', node.key.replaceAll('.', '?.'), valuePath].join('.');
}}
/>
</>
<ValuePreview
value={getPreviewValue(node)}
onSelect={(valuePath) => {
return ['tango', node.key.replaceAll('.', '?.'), valuePath].join('.');
}}
/>
)}
{mode === 'define' && (
<NodeDefineForm
@@ -308,7 +290,7 @@ export function EditableVariableTreeModal({
{React.cloneElement(trigger, { onClick: on })}
<Modal
title={title}
visible={visible}
open={visible}
onCancel={off}
okButtonProps={{
disabled: !node,
@@ -345,6 +327,11 @@ const varTreeStyle = css`
.ant-tree-indent-unit {
width: 12px;
}
.anticon-function {
margin-left: 4px;
color: var(--tango-colors-text2);
}
`;
interface VariableTreeProps {
@@ -352,17 +339,26 @@ interface VariableTreeProps {
onSelect?: (data: IVariableTreeNode) => void;
onAdd?: (data: IVariableTreeNode) => void;
onRemove?: (data: IVariableTreeNode) => void;
onCopy?: (data: IVariableTreeNode) => void;
onView?: (data: IVariableTreeNode) => void;
showDeleteIcon?: boolean;
showViewIcon?: boolean;
}
function VariableTree({
export function VariableTree({
dataSource = [],
onSelect = noop,
onAdd = noop,
onRemove = noop,
onCopy = noop,
onView = noop,
showDeleteIcon = false,
showViewIcon = false,
}: VariableTreeProps) {
return (
<Box className="VariableTree" css={varTreeStyle}>
<Tree.DirectoryTree
<Tree
blockNode
showIcon={false}
defaultExpandAll
treeData={dataSource}
@@ -370,25 +366,18 @@ function VariableTree({
onSelect(detail.node);
}}
titleRender={(node) => {
if (node.showAddChildIcon || node.showRemoveIcon) {
const isLeaf = !node.children;
if (isLeaf) {
const isDeletable = node.showDeleteIcon ?? showDeleteIcon;
return (
<Box display="flex" justifyContent="space-between" alignItems="center" pr="l">
<Text>{node.title}</Text>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Text>
{node.title}
{node.type === 'function' && <FunctionOutlined />}
</Text>
<Box>
{node.showAddChildIcon && (
<Tooltip title={`${node.title} 中添加变量`}>
<Button
type="text"
size="small"
icon={<PlusOutlined />}
onClick={(e) => {
e.stopPropagation();
onAdd(node);
}}
/>
</Tooltip>
)}
{node.showRemoveIcon && (
{isDeletable && (
<Popconfirm
title="确认删除吗?该操作会导致引用此模型的代码报错,请谨慎操作!"
onConfirm={() => onRemove(node)}
@@ -403,14 +392,61 @@ function VariableTree({
/>
</Popconfirm>
)}
<CopyClipboard text={`tango.${node.key.replaceAll('.', '?.')}`}>
{({ copied, onClick }) => {
const label = copied ? '已复制' : '复制变量路径';
return (
<Tooltip title={label}>
<Button
type="text"
size="small"
icon={<CopyOutlined />}
onClick={(e) => {
e.stopPropagation();
onClick();
onCopy(node);
}}
/>
</Tooltip>
);
}}
</CopyClipboard>
{showViewIcon && (
<Tooltip title="查看变量详情">
<Button
type="text"
size="small"
icon={<EyeOutlined />}
onClick={(e) => {
e.stopPropagation();
onView(node);
}}
/>
</Tooltip>
)}
</Box>
</Box>
);
}
return (
<Box>
<Box display="flex" alignItems="center" justifyContent="space-between">
<Text mr="m">{node.title}</Text>
{node.type === 'function' && <FunctionOutlined />}
<Box>
{node.showAddChildIcon && (
<Tooltip title={`${node.title} 中添加变量`}>
<Button
type="text"
size="small"
icon={<PlusOutlined />}
onClick={(e) => {
e.stopPropagation();
onAdd(node);
}}
/>
</Tooltip>
)}
</Box>
</Box>
);
}}
+2 -1
View File
@@ -27,6 +27,7 @@ export interface DesignerProps extends IDesignerContext, ITangoEngineContext {
export function Designer(props: DesignerProps) {
const {
engine,
config,
theme: themeProp,
sandboxQuery,
remoteServices = {},
@@ -42,7 +43,7 @@ export function Designer(props: DesignerProps) {
return (
<SystemProvider theme={theme} prefix="--tango">
<ConfigProvider locale={zhCN}>
<TangoEngineProvider value={{ engine }}>
<TangoEngineProvider value={{ engine, config }}>
<DesignerProvider value={{ sandboxQuery, remoteServices }}>{children}</DesignerProvider>
</TangoEngineProvider>
</ConfigProvider>
+1
View File
@@ -11,6 +11,7 @@ export * from './sandbox';
export * from './sidebar';
export * from './toolbar';
export * from './selection-menu';
export * from './widgets';
export { register as registerSetter } from '@music163/tango-setting-form';
+3 -5
View File
@@ -19,7 +19,6 @@ interface ISandboxEventHandlerConfig {
export type SandboxProps = Omit<CodeSandboxProps, 'files' | 'eventHandlers' | 'onMessage'> & {
isPreview?: boolean;
selectionTools?: SelectionToolsProps['actions'];
builtinSelectionMenuMap?: SelectionToolsProps['builtinActionMap'];
/**
* tangoConfigJson 处理器
*/
@@ -32,10 +31,10 @@ export type SandboxProps = Omit<CodeSandboxProps, 'files' | 'eventHandlers' | 'o
onLoad?: (config?: ISandboxEventHandlerConfig) => void;
};
export type CombinedSandboxRef = {
export interface CombinedSandboxRef {
designSandbox: CodeSandbox;
previewSandbox: CodeSandbox;
};
}
const LANDING_PAGE_PATH = '/__background_landing_page__';
@@ -281,7 +280,6 @@ export const Sandbox = observer(
({
isPreview: isPreviewProp,
selectionTools,
builtinSelectionMenuMap,
bundlerURL,
mode = 'combined',
...props
@@ -352,7 +350,7 @@ export const Sandbox = observer(
}}
/>
<Simulator>
<Viewport selectionTools={selectionTools} builtinActionMap={builtinSelectionMenuMap}>
<Viewport selectionTools={selectionTools}>
{mode === 'single' && (
<DesignSandbox
ref={sandboxRef}
@@ -4,7 +4,8 @@ import { AutoComplete } from 'antd';
import { ActionSelect } from '@music163/tango-ui';
import { FormItemComponentProps } from '@music163/tango-setting-form';
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
import { ExpressionModal, getInputValue } from './expression-setter';
import { wrapCode } from '@music163/tango-helpers';
import { ExpressionModal } from './expression-setter';
enum EventAction {
NoAction = 'noAction',
@@ -43,7 +44,7 @@ export function EventSetter(props: EventSetterProps) {
const { actionVariables, routeOptions } = useWorkspaceData();
const workspace = useWorkspace();
const modalOptions = workspace.listModals() || [];
const modalOptions = workspace.activeViewModule.listModals() || [];
const handleChange = useCallback<FormItemComponentProps['onChange']>(
(nextValue: any, ...args) => {
@@ -71,19 +72,19 @@ export function EventSetter(props: EventSetterProps) {
};
const actionText = getActionText(type, temp, value);
const inputValue = getInputValue(value);
const inputValue = value;
return (
<Box css={wrapperStyle}>
<ActionSelect options={options} onSelect={onAction} text={actionText} />
<ExpressionModal
title={modalTitle}
defaultValue={inputValue}
value={inputValue}
visible={expModalVisible}
onCancel={() => setExpModalVisible(false)}
onOk={(nextValue) => {
setExpModalVisible(false);
handleChange(nextValue);
handleChange(wrapCode(nextValue));
}}
dataSource={actionVariables}
/>
@@ -141,7 +142,7 @@ function getActionText(type: EventAction, temp: string, value: any) {
if (handlerMap[type]) {
text = getExpressionValue(type, temp);
} else if (value) {
text = getInputValue(value);
text = value;
}
text = text || '请选择';
return text;
@@ -1,20 +1,21 @@
import React, { useState, useEffect, useCallback } from 'react';
import { Box, css } from 'coral-system';
import { Alert, Modal, Tabs } from 'antd';
import { value2code, isValidExpressionCode } from '@music163/tango-core';
import { Box, Text, css } from 'coral-system';
import { Modal } from 'antd';
import { isValidExpressionCode, value2expressionCode } from '@music163/tango-core';
import {
isVariableString,
getVariableContent,
noop,
useBoolean,
getValue,
wrapCode,
} from '@music163/tango-helpers';
import { CloseCircleFilled, ExpandAltOutlined, QuestionCircleOutlined } from '@ant-design/icons';
import { IconButton, Panel, InputCode, ChatInput } from '@music163/tango-ui';
import { CloseCircleFilled, ExpandAltOutlined } from '@ant-design/icons';
import { IconButton, Panel, InputCode } from '@music163/tango-ui';
import { FormItemComponentProps } from '@music163/tango-setting-form';
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
import { EditableVariableTree, IVariableTreeNode } from '../components';
import { useRemoteServices, useSandboxQuery } from '../context';
import { useSandboxQuery } from '../context';
export const expressionValueValidate = (value: string) => {
if (isVariableString(value)) {
@@ -36,31 +37,6 @@ export const jsonValueValidate = (value: string) => {
}
};
// FIXME: 这里应该用原始的代码 raw value
export const getInputValue = (val: any) => {
if (!val) return '';
let ret;
switch (typeof val) {
case 'string':
ret = val;
break;
case 'number':
ret = String(val);
break;
case 'object':
ret = value2code(val);
ret = `{${ret}}`;
break;
default:
ret = '';
break;
}
return ret;
};
const suffixStyle = css`
.anticon-close-circle {
color: rgba(0, 0, 0, 0.25);
@@ -83,32 +59,37 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
modalTitle,
modalTip,
autoCompleteOptions,
placeholder = '表达式请使用 {} 包裹',
placeholder = '输入JS表达式代码',
value: valueProp,
status,
allowClear = true,
} = props;
const [visible, { on, off }] = useBoolean();
const [inputValue, setInputValue] = useState(() => {
return getInputValue(valueProp);
return value2expressionCode(valueProp);
});
const sandbox = useSandboxQuery();
const evaluateContext = sandbox.window;
// when receive new value, sync state
useEffect(() => {
setInputValue(getInputValue(valueProp));
setInputValue(value2expressionCode(valueProp));
}, [valueProp]);
const change = (code: string) => {
if (!code) {
code = undefined;
}
const change = useCallback(
(code: string) => {
if (!code) {
onChange(undefined);
return;
}
if (getVariableContent(code) === value2expressionCode(valueProp)) {
return;
}
if (code !== valueProp) {
onChange(code);
}
};
onChange(wrapCode(code));
},
[valueProp, onChange],
);
return (
<Box className="ExpressionSetter">
@@ -147,7 +128,7 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
placeholder={placeholder}
autoCompleteOptions={autoCompleteOptions}
visible={visible}
defaultValue={inputValue}
value={inputValue}
onCancel={() => off()}
onOk={(value) => {
onChange(value);
@@ -164,6 +145,7 @@ export interface ExpressionModalProps {
placeholder?: string;
visible?: boolean;
defaultValue?: string;
value?: string;
onCancel?: () => void;
onOk?: (value: string) => void;
dataSource?: IVariableTreeNode[];
@@ -178,10 +160,12 @@ export function ExpressionModal({
onCancel = noop,
onOk = noop,
defaultValue,
value,
dataSource,
autoCompleteOptions,
}: ExpressionModalProps) {
const [exp, setExp] = useState(defaultValue);
const [exp, setExp] = useState(value ?? defaultValue);
const [error, setError] = useState('');
const workspace = useWorkspace();
const onAction = useCallback(
(action: string, args: unknown[]) => {
@@ -190,38 +174,35 @@ export function ExpressionModal({
[workspace],
);
const sandbox = useSandboxQuery();
const remoteServices = useRemoteServices();
const { expressionVariables } = useWorkspaceData();
const evaluateContext = sandbox.window;
const gptService = remoteServices?.GptService;
const handleExpInputChange = (val: string) => {
setExp(val?.trim());
};
useEffect(() => {
setExp(value);
}, [value]);
return (
<Modal
title={title}
closable={false}
destroyOnClose
width="70%"
open={visible}
onCancel={onCancel}
onOk={() => {
onOk(exp);
}}
destroyOnClose
okButtonProps={{
disabled: !!error,
}}
bodyStyle={{
padding: 0,
}}
>
<Panel
title="绑定的表达式"
subTitle={subTitle}
extra={
<IconButton
icon={<QuestionCircleOutlined />}
tooltip="你可以直接编辑下面的表达式,表达式需要使用 { } 进行包裹,点击此图标可以查看详细文档。"
href="https://music-doc.st.netease.com/st/tango-docs/docs/guide/javascript/overview"
/>
}
shape="solid"
borderRadius="m"
>
<Panel title={`${title} 设置为引用变量或自定义表达式`} subTitle={subTitle} shape="solid">
<InputCode
shape="inset"
minHeight="56px"
@@ -229,95 +210,58 @@ export function ExpressionModal({
value={exp}
placeholder={placeholder}
onChange={handleExpInputChange}
onBlur={() => {
setError(expressionValueValidate(exp));
}}
autoCompleteContext={evaluateContext}
autoCompleteOptions={autoCompleteOptions}
/>
{error ? <Text color="red"></Text> : null}
</Panel>
<Box mt="m" maxHeight={400} overflow="auto">
<Tabs type="card" size="small">
<Tabs.TabPane key="list" tab="从变量列表中选择">
<Box my="m">
<Alert
type="info"
message="你可以直接从下方的列表中选择已定义的变量"
banner
closable
/>
</Box>
<EditableVariableTree
// height={400}
dataSource={dataSource || expressionVariables}
onUse={(node) => {
let str;
if (/^(stores|services)\./.test(node.key)) {
str = `{tango.${node.key.replaceAll('.', '?.')}}`;
} else {
str = `{${node.key}}`;
}
setExp(str);
}}
onAddVariable={(storeName, data) => {
onAction('addStoreState', [storeName, data.name, data.initialValue]);
}}
onDeleteVariable={(storeName, stateName) => {
onAction('removeStoreState', [storeName, stateName]);
}}
onDeleteStore={(storeName) => {
onAction('removeStoreModule', [storeName]);
}}
onSave={(code, node) => {
onAction('updateModuleCodeByVariablePath', [node.key, code]);
}}
getPreviewValue={(node) => {
if (!node || !node.key) {
return;
}
<Panel title="从变量列表中选中" shape="solid" borderTop="0">
<EditableVariableTree
height={380}
dataSource={dataSource || expressionVariables}
onSelect={(node) => {
if (!node.key) {
return;
}
if (node.key.split('.').length < 2) {
return;
}
let str;
if (/^(stores|services)\./.test(node.key)) {
str = `{tango.${node.key.replaceAll('.', '?.')}}`;
} else {
str = `{${node.key}}`;
}
setExp(str);
}}
onAddVariable={(storeName, data) => {
onAction('addStoreState', [storeName, data.name, data.initialValue]);
}}
onDeleteVariable={(storeName, stateName) => {
onAction('removeStoreState', [storeName, stateName]);
}}
onDeleteStore={(storeName) => {
onAction('removeStoreModule', [storeName]);
}}
onSave={(code, node) => {
onAction('updateModuleCodeByVariablePath', [node.key, code]);
}}
getPreviewValue={(node) => {
if (!node || !node.key) {
return;
}
if (node.type === 'function') {
return node.raw;
}
if (node.type === 'function') {
return node.raw;
}
return getValue(evaluateContext['tango'], node.key);
}}
/>
</Tabs.TabPane>
<Tabs.TabPane key="ai" tab="使用 AI 助手生成" disabled={!gptService?.textCompletion}>
<ChatInput
placeholder="例如:实现一个函数,从给定的数组中过滤出name为bob的数据"
onGenerate={(text) =>
gptService
.textCompletion({
prompt: getExpressionCode(text),
model: 'gpt-3.5-turbo',
temperature: 0.5,
topP: 1,
presencePenalty: 0,
frequencyPenalty: 0,
mode: 'chat',
})
.then((res: any) => {
return res?.detail?.choices?.[0]?.text;
})
}
onApply={(preview) => {
setExp(`{${preview}}`);
}}
showCloseIcon={false}
/>
</Tabs.TabPane>
</Tabs>
</Box>
return getValue(evaluateContext['tango'], node.key);
}}
/>
</Panel>
</Modal>
);
}
function getExpressionCode(userInput: string) {
return `你是一个代码生成助手。你使用的编程语言是 JavaScript。不要解释代码,只返回代码块本身,代码必须包裹在 {} 内部。
标准的输出格式如下:
1. {this.a}
2. {() => {}}
3. {tango.stores.user.name}
${userInput}
`;
}
@@ -123,7 +123,6 @@ export function ModelSetter({ value, onChange }: FormItemComponentProps) {
onBlur={onInputBlur}
suffix={
<EditableVariableTreeModal
showPreviewAlert={false}
title="同步到的变量"
trigger={
<Tooltip title="从模型列表选择" placement="topRight">
@@ -1,9 +1,9 @@
import React, { useState, useEffect, useMemo } from 'react';
import { observer, useWorkspace } from '@music163/tango-context';
import { observer, useWorkspace, useWorkspaceData } from '@music163/tango-context';
import { Box, css } from 'coral-system';
import { Button, FormProps, Empty, Space, Dropdown } from 'antd';
import { PlayCircleOutlined } from '@ant-design/icons';
import { Form, InputCode, Menu, Panel, JsonView, Search } from '@music163/tango-ui';
import { Form, InputCode, Panel, JsonView, Search } from '@music163/tango-ui';
import {
isNil,
isVariableString,
@@ -11,8 +11,10 @@ import {
isValidFunctionCode,
logger,
code2object,
filterTreeData,
} from '@music163/tango-helpers';
import { useSandboxQuery } from '../../context';
import { VariableTree } from '../../components';
/*
* 服务函数的操作类型
@@ -39,7 +41,7 @@ enum ServiceFunctionMethodType {
* requestType
* headers: Content-Type
*/
enum ESFHTTRequestType {
enum DataServiceRequestType {
'application/json' = 'json',
'application/x-www-form-urlencoded' = 'x-www-form-urlencoded',
}
@@ -65,11 +67,11 @@ export const getApiDefaultName = (url: string) =>
// 方法名以数字开头,添加 api 前缀
.replace(/^\d/, (str) => `api${str}`);
type DataServiceViewProps = {
interface DataServiceViewProps {
onAdd?: (values: Record<string, string> | Array<Record<string, string>>) => void;
onUpdate?: (values: Record<string, string>) => void;
onDelete?: (values: Record<string, string>) => void;
};
}
// http 方法类型
const httpMethods = Object.keys(ServiceFunctionMethodType).map((key) => ({
@@ -78,9 +80,9 @@ const httpMethods = Object.keys(ServiceFunctionMethodType).map((key) => ({
}));
// requestType 类型
const requestTypes = Object.keys(ESFHTTRequestType).map((key) => ({
const requestTypes = Object.keys(DataServiceRequestType).map((key) => ({
label: key,
value: ESFHTTRequestType[key],
value: DataServiceRequestType[key],
}));
const detailFormStyle = css`
@@ -90,31 +92,32 @@ const detailFormStyle = css`
`;
const DataSourceView = observer(({ onAdd, onUpdate, onDelete }: DataServiceViewProps) => {
const [serviceKey, setServiceKey] = useState<string>();
const [serviceData, setServiceData] = useState<any>();
const [keyword, setKeyword] = useState<string>();
const [visible, setVisible] = useState(null);
const workspace = useWorkspace();
const sandbox = useSandboxQuery();
const { serviceVariables } = useWorkspaceData();
const serviceModules = Object.keys(workspace.serviceModules).map((key) => ({
label: key === 'index' ? '默认模块' : key,
value: key,
}));
const serviceFunctions = serviceVariables.reduce((acc, cur) => {
acc = acc.concat(cur.children.map((item: any) => item.key));
return acc;
}, []);
const dataSource = useMemo(() => {
if (!keyword) {
return serviceVariables;
}
return filterTreeData(
serviceVariables,
(leaf) => leaf.title.includes(keyword),
'children',
true,
);
}, [serviceVariables, keyword]);
const serviceMap = workspace.serviceModule.serviceFunctions;
const serviceItems = useMemo(() => {
if (!serviceMap) {
return [];
}
let list = Object.keys(serviceMap).map((key) => ({
key,
label: key,
note: serviceMap[key].app || 'http',
deletable: true,
deletableConfirm: '确定删除此服务函数吗?',
}));
if (keyword) {
list = list.filter((item) => item.label.includes(keyword));
}
return list;
}, [serviceMap, keyword]);
const serviceData = serviceMap?.[serviceKey];
const isAddMode = !serviceKey;
const isAddMode = serviceData && !serviceData.name;
return (
<Box className="ServiceFunctionList" display="flex" borderTopColor="line.normal">
@@ -136,52 +139,48 @@ const DataSourceView = observer(({ onAdd, onUpdate, onDelete }: DataServiceViewP
<Button
block
onClick={() => {
setServiceKey(undefined);
setVisible('detail');
setServiceData({});
}}
>
</Button>
<Search placeholder="搜索服务函数" onChange={setKeyword} />
</Box>
<Menu
activeKey={serviceKey}
items={serviceItems}
onItemClick={(key) => {
setServiceKey(key);
setVisible('detail');
<VariableTree
dataSource={dataSource}
showDeleteIcon
onSelect={(item) => {
setServiceData({ key: item.key, ...workspace.getServiceFunction(item.key) });
}}
onDelete={(key) => {
const values = {
name: key,
...workspace.serviceModule.serviceFunctions[key],
};
workspace.removeServiceFunction(key);
onDelete && onDelete(values);
setVisible(null);
onRemove={(item) => {
workspace.removeServiceFunction(item.key);
onDelete?.(item);
}}
/>
</Panel>
</Box>
<Box width="65%" overflow="auto" css={detailFormStyle}>
{visible === 'detail' && (
{serviceData && (
<>
<Panel title={isAddMode ? '新建服务函数' : '服务函数详情'}>
<ServiceDetailForm
key={serviceKey || 'createSF'}
serviceKeys={Object.keys(serviceMap)}
key={serviceData.key || 'createSF'}
serviceModules={serviceModules}
serviceKeys={serviceFunctions}
initialValues={
isAddMode
? {}
? {
moduleName: 'index',
}
: {
name: serviceKey,
name: serviceData.name,
moduleName: serviceData.moduleName,
method: 'get',
...serviceData,
...serviceData.config,
}
}
onCancel={() => {
setServiceKey(undefined);
setVisible(null);
setServiceData(undefined);
}}
onSubmit={(values, mode: ServiceFunctionOperationModeType) => {
function shapeServiceValues(val: any) {
@@ -194,28 +193,27 @@ const DataSourceView = observer(({ onAdd, onUpdate, onDelete }: DataServiceViewP
return shapeValues;
}
// 移除掉不必要的属性
const data = shapeServiceValues(values);
const { moduleName, ...data } = shapeServiceValues(values);
if (mode === ServiceFunctionOperationModeType.ADD) {
workspace.addServiceFunction(data);
workspace.addServiceFunction(data, moduleName);
onAdd && onAdd(values);
} else if (mode === ServiceFunctionOperationModeType.UPDATE) {
workspace.updateServiceFunction(data);
workspace.updateServiceFunction(data, moduleName);
onUpdate && onUpdate(values);
}
setServiceKey(values.name);
}}
/>
</Panel>
{serviceKey ? (
{serviceData?.key ? (
<ServiceFunctionPreview
key={serviceKey}
key={serviceData.key}
appContext={sandbox?.window['tango']}
functionName={serviceKey}
functionName={serviceData.key}
/>
) : null}
</>
)}
{!visible && (
{!serviceData && (
<Box py="xxl">
<Empty description="请从左侧选择数据服务函数或新建数据服务函数" />
</Box>
@@ -227,12 +225,14 @@ const DataSourceView = observer(({ onAdd, onUpdate, onDelete }: DataServiceViewP
interface ServiceDetailFormProps extends FormProps {
serviceKeys?: string[];
serviceModules?: any[];
onCancel?: () => void;
onSubmit?: (values: any, mode: ServiceFunctionOperationModeType) => void;
}
function ServiceDetailForm({
serviceKeys = [],
serviceModules = [],
onCancel,
onSubmit,
initialValues,
@@ -274,6 +274,20 @@ function ServiceDetailForm({
}}
{...formProps}
>
<Form.Item
label="所属模块"
name="moduleName"
rules={[
{
required: true,
},
]}
component="select"
componentProps={{
options: serviceModules,
disabled: disabled || isModifyMode,
}}
/>
<Form.Item
label="方法名"
name="name"
@@ -282,7 +296,9 @@ function ServiceDetailForm({
{ pattern: /^[a-z]\w+$/, message: '请输入合法的方法名称' },
!isModifyMode && {
validator(_, value) {
const isExist = serviceKeys.includes(value);
const isExist = serviceKeys.includes(
['services', form.getFieldValue('moduleName'), value].join('.'),
);
return isExist
? Promise.reject(new Error('重复的方法名,请换一个!'))
: Promise.resolve();
@@ -316,7 +332,7 @@ function ServiceDetailForm({
<Form.Item
label="路径"
name="url"
rules={[{ required: true }]}
rules={[{ required: true, type: 'url' }]}
component="input"
componentProps={{
placeholder: '请输入数据服务调用名称',
+298 -149
View File
@@ -1,4 +1,4 @@
import React, { useMemo, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { observer, useWorkspace } from '@music163/tango-context';
import {
Button,
@@ -9,15 +9,17 @@ import {
Tooltip,
Popconfirm,
Radio,
Select,
message,
Tag,
Checkbox,
ModalProps,
} from 'antd';
import { FormListProps } from 'antd/lib/form';
import { Box, Text } from 'coral-system';
import semverLt from 'semver/functions/lt';
import semverValid from 'semver/functions/valid';
import { ConfigGroup, ConfigItem } from '@music163/tango-ui';
import { PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons';
import { MinusCircleOutlined, PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons';
import { useBoolean } from '@music163/tango-helpers';
import { isUndefined } from 'lodash-es';
import { Workspace } from '@music163/tango-core';
@@ -177,18 +179,9 @@ function RenderItem({
onUpgrade,
showTags = true,
}: RenderItemProps) {
const [version, setVersion] = useState<string>();
const [versionList, setVersionList] = useState<Array<{ label: string; value: string }>>([]);
const [open, { on, off }] = useBoolean(false);
const workspace = useWorkspace() as Workspace;
// const remoteServices = useRemoteServices();
// useEffect(() => {
// remoteServices?.DependencyService?.listPackageVersions({ packageName: record.name }).then(
// (data: any) => setVersionList((data || []).map((v: string) => ({ label: v, value: v }))),
// );
// }, [remoteServices, record]);
const basePackage = useMemo(() => {
if (type !== DependencyItemType. || !templateBaseDependencies) return undefined;
@@ -198,9 +191,29 @@ function RenderItem({
/**
* 处理依赖升级的逻辑
*/
function onDepUpgrade() {
function onDepUpgrade(values: Record<string, any>) {
// 根据是否有 packages 字段,判断是否已升级为新的规范
const hasUpgraded = !!workspace.tangoConfigJson?.getValue('packages');
const { version, umd, library, resources, designerResources } = values || {};
let packageConfig = record?.package;
if (umd === false && packageConfig) {
packageConfig = {
...packageConfig,
version,
library: undefined,
resources: undefined,
designerResources: undefined,
};
} else {
packageConfig = {
...packageConfig,
version,
library,
resources,
designerResources,
};
}
switch (type) {
case DependencyItemType.业务组件:
@@ -210,7 +223,7 @@ function RenderItem({
version,
hasUpgraded
? {
package: record?.package,
package: packageConfig,
}
: {},
);
@@ -225,7 +238,7 @@ function RenderItem({
basePackage?.version || 'latest',
hasUpgraded
? {
package: record?.package,
package: packageConfig,
}
: {},
);
@@ -295,7 +308,6 @@ function RenderItem({
}
function onCloseModal() {
setVersion(undefined);
off();
}
@@ -317,10 +329,8 @@ function RenderItem({
const { resources } = packageInfo || {};
const dependencies = workspace.packageJson?.getValue('dependencies');
const hasUmd = !!resources?.length;
const unsafeUmd =
hasUmd && resources.some((e: string) => /\/\/unpkg\.f[nt]\.netease\.com\//.test(e));
const packageJsonOnly = dependencies?.[name] && !packageInfo;
const tangoConfigJsonOnly = !dependencies?.[name] && packageInfo;
const packageJsonIncluded = !!dependencies?.[name];
const tangoConfigJsonIncluded = !!packageInfo;
return [
hasUmd && (
@@ -330,21 +340,14 @@ function RenderItem({
</Tag>
</Tooltip>
),
unsafeUmd && (
<Tooltip title="依赖的 UMD 资源来自自建 unpkg 服务,稳定性不可靠,建议修改 tango.config.json 更换为正式 cdn 资源地址">
<Tag key="unsafeUmd" color="orange">
CDN
</Tag>
packageJsonIncluded && (
<Tooltip title="依赖在 package.json 中存在">
<Tag key="packageJson">package</Tag>
</Tooltip>
),
packageJsonOnly && (
<Tooltip title="此依赖package.json 中存在">
<Tag key="packageJsonOnly">package.json</Tag>
</Tooltip>
),
tangoConfigJsonOnly && (
<Tooltip title="此依赖只在 tango.config.json 中存在">
<Tag key="packageJsonOnly">tango.config.json</Tag>
tangoConfigJsonIncluded && (
<Tooltip title="此依赖在 tango.config.json 中存在">
<Tag key="tangoConfigJson">tango.config</Tag>
</Tooltip>
),
].filter((e) => e);
@@ -356,7 +359,7 @@ function RenderItem({
type !== DependencyItemType.
? [
<a key="upgrade" onClick={on}>
</a>,
<Popconfirm onConfirm={onDepDelete} title={`确认删除依赖 ${record.name}`}>
<a key="delete"></a>
@@ -366,7 +369,7 @@ function RenderItem({
? [
<Popconfirm
title={`确认升级 ${record.name}${basePackage.version} 吗?`}
onConfirm={onDepUpgrade}
onConfirm={() => onDepUpgrade({ version: basePackage.version })}
>
<a key="upgrade"></a>
</Popconfirm>,
@@ -394,28 +397,12 @@ function RenderItem({
/>
}
/>
<Modal
<DependencyConfigModal
open={open}
title={`升级依赖:${record.name}`}
record={record}
onCancel={onCloseModal}
onOk={onDepUpgrade}
>
<Text mr="6px">
&nbsp;
<Tooltip title="只提供正式版本列表,Beta版本依赖需要手动升级">
<QuestionCircleOutlined />
</Tooltip>
</Text>
<Select
style={{ width: 160 }}
showSearch
defaultValue={record.version}
options={versionList || []}
onChange={(value: string) => setVersion(value)}
placeholder="请选择要升级的版本号"
optionFilterProp="value"
/>
</Modal>
onOk={(event, { values }) => onDepUpgrade(values)}
/>
</List.Item>
);
}
@@ -447,111 +434,114 @@ interface DependencyModalProps {
templateBaseDependencies: Record<string, any>;
onBizDependencyAdd: Function;
onBaseDependencyAdd: Function;
record?: Record<string, any>;
}
function AddDependencyModal({
templateBaseDependencies,
onBizDependencyAdd,
onBaseDependencyAdd,
}: DependencyModalProps) {
record,
...rest
}: DependencyModalProps & ModalProps) {
// 依赖类型
const [type, setType] = useState<DependencyItemType>(DependencyItemType.);
const [open, { on, off }] = useBoolean();
const [form] = Form.useForm();
const workspace = useWorkspace();
const sandbox = useSandboxQuery();
const hasUpgraded = !!workspace.tangoConfigJson?.getValue('packages');
const onFinishCallback = (name?: string) => {
name && message.success(`${name} 添加成功`);
// FIXME: 修复沙箱添加组件后 HMR 失效的 bug,添加完刷新
sandbox.reload();
off();
form.resetFields();
};
const onOk = () => {
form.validateFields().then((values) => {
const { name, version } = values;
// 根据是否有 packages 字段,判断是否已升级为新的规范
const hasUpgraded = !!workspace.tangoConfigJson?.getValue('packages');
const onOk: DependencyConfigModalProps['onOk'] = (event, { values }) => {
const { type, name, version, umd, library, resources, designerResources } = values;
switch (type) {
case DependencyItemType.: {
workspace.updateDependency(name, version);
break;
}
// case DependencyItemType.业务组件: {
// const targetBizInfo = (bizList as any)?.find(
// (biz: { packageName: string }) => biz.packageName === name,
// );
// onBizDependencyAdd &&
// onBizDependencyAdd({
// ...values,
// detail: targetBizInfo,
// });
// break;
// }
case DependencyItemType.: {
const basePackage = templateBaseDependencies[name];
// 添加 package.json
workspace.updateDependency(
name,
basePackage.version || 'latest',
hasUpgraded
? {
package: basePackage.package,
}
: {},
);
// 配置已经合并为新版,不需要走旧的逻辑
if (!hasUpgraded) {
// 更新 tango.config.json baseDependencies
workspace.tangoConfigJson.setValue('baseDependencies', (targetValue) => {
const bases = targetValue || [];
(bases as string[]).push(name);
return bases;
});
// 更新 tango.config.json externals
workspace.tangoConfigJson.setValue('sandbox.externals', (targetValue) => {
targetValue[name] = basePackage?.global;
return targetValue;
});
// 更新 tango.config.json externalResources
workspace.tangoConfigJson
.setValue('sandbox.externalResources', (targetValue) => {
basePackage.resources.forEach((resource: string) => {
(targetValue as string[]).push(
resource.replace('{latest_version}', basePackage.version),
);
});
return targetValue;
})
.update();
}
onBaseDependencyAdd && onBaseDependencyAdd(basePackage);
break;
}
default:
break;
switch (type) {
case DependencyItemType.: {
workspace.updateDependency(
name,
version,
hasUpgraded && umd
? {
package: {
type: 'dependency',
...record?.package,
version,
library,
resources,
designerResources,
},
}
: {},
);
break;
}
onFinishCallback(values?.name);
});
};
// case DependencyItemType.业务组件: {
// const targetBizInfo = (bizList as any)?.find(
// (biz: { packageName: string }) => biz.packageName === name,
// );
// onBizDependencyAdd &&
// onBizDependencyAdd({
// ...values,
// detail: targetBizInfo,
// });
// break;
// }
case DependencyItemType.: {
const basePackage = templateBaseDependencies[name];
// 添加 package.json
workspace.updateDependency(
name,
basePackage.version || 'latest',
hasUpgraded
? {
package: basePackage.package,
}
: {},
);
const onValuesChange = ({ type: depType }: { type: DependencyItemType; name: string }) => {
if (depType) {
// type 变更触发
form.resetFields(['name', 'version']);
setType(depType);
// 配置已经合并为新版,不需要走旧的逻辑
if (!hasUpgraded) {
// 更新 tango.config.json baseDependencies
workspace.tangoConfigJson.setValue('baseDependencies', (targetValue) => {
const bases = targetValue || [];
(bases as string[]).push(name);
return bases;
});
// 更新 tango.config.json externals
workspace.tangoConfigJson.setValue('sandbox.externals', (targetValue) => {
targetValue[name] = basePackage?.global;
return targetValue;
});
// 更新 tango.config.json externalResources
workspace.tangoConfigJson
.setValue('sandbox.externalResources', (targetValue) => {
basePackage.resources.forEach((resource: string) => {
(targetValue as string[]).push(
resource.replace('{latest_version}', basePackage.version),
);
});
return targetValue;
})
.update();
}
onBaseDependencyAdd && onBaseDependencyAdd(basePackage);
break;
}
default:
break;
}
onFinishCallback(values?.name);
};
const onCloseModal = () => {
function onCloseModal() {
off();
form.resetFields(['name', 'version']);
};
}
return (
<Box px="l" py="m">
@@ -559,31 +549,155 @@ function AddDependencyModal({
<PlusOutlined />
</Button>
<Modal
width={560}
<DependencyConfigModal
open={open}
title="添加依赖"
title={record ? `修改依赖:${record.name}` : '添加依赖'}
onOk={onOk}
onCancel={onCloseModal}
destroyOnClose
{...rest}
/>
</Box>
);
}
type DependencyConfigModalProps = Omit<ModalProps, 'onOk'> & {
record?: Record<string, any>;
onOk: (
event: React.MouseEvent<HTMLElement, MouseEvent>,
data: {
values: Record<string, any>;
hasUpgraded: boolean;
},
) => any;
};
function ResourceFormList(props: Omit<FormListProps, 'children'>) {
return (
<Form.List {...props}>
{(fields, { add, remove }, { errors }) => (
<>
{fields.map((field) => (
<Form.Item key={field.key}>
<Box display="flex" alignItems="center">
<Form.Item
label="资源地址"
{...field}
rules={[{ required: true, whitespace: true, type: 'url' }]}
noStyle
>
<Input
placeholder="资源地址中的版本号可用 {{version}} 替代"
style={{ width: '100%' }}
/>
</Form.Item>
<Box width="20px" fontSize="18px" ml="6px" textAlign="center" flex="none">
<a onClick={() => remove(field.name)}>
<MinusCircleOutlined />
</a>
</Box>
</Box>
</Form.Item>
))}
<Button type="dashed" onClick={() => add()} block>
</Button>
<Form.ErrorList errors={errors} />
</>
)}
</Form.List>
);
}
function DependencyConfigModal({
record,
open,
onOk,
onCancel,
...rest
}: DependencyConfigModalProps) {
// 依赖类型
const [type, setType] = useState<DependencyItemType>(DependencyItemType.);
const [form] = Form.useForm();
const workspace = useWorkspace();
// 根据是否有 packages 字段,判断是否已升级为新的规范
const hasUpgraded = !!workspace.tangoConfigJson?.getValue('packages');
// const [versionList, setVersionList] = useState<Array<{ label: string; value: string }>>([]);
// const remoteServices = useRemoteServices();
// useEffect(() => {
// remoteServices?.DependencyService?.listPackageVersions({ packageName: record.name }).then(
// (data: any) => setVersionList((data || []).map((v: string) => ({ label: v, value: v }))),
// );
// }, [remoteServices, record]);
useEffect(() => {
if (open && form) {
form.resetFields();
if (record) {
setType(record?.type);
form.setFieldsValue({
...record,
...record?.package,
umd: !!record?.package?.resources?.length,
});
}
}
}, [record, open, form]);
const onSubmit = (event: React.MouseEvent<HTMLElement, MouseEvent>) => {
form.validateFields().then((values) => {
if (onOk) {
onOk(event, {
values,
hasUpgraded,
});
}
});
};
const onValuesChange = ({ type: depType }: { type: DependencyItemType; name: string }) => {
if (depType) {
// type 变更触发
form.resetFields();
form.setFieldValue('type', depType);
setType(depType);
}
};
return (
<>
<Modal
width={560}
open={open}
title={record ? `修改依赖:${record.name}` : '添加依赖'}
onOk={onSubmit}
onCancel={onCancel}
destroyOnClose
{...rest}
>
<Form
form={form}
layout="vertical"
autoComplete="off"
initialValues={{ type: DependencyItemType. }}
initialValues={{ type }}
onValuesChange={onValuesChange}
>
<Form.Item label="依赖类型" name="type">
<Radio.Group value={type}>
{/* <Radio.Button value={DependencyItemType.业务组件}>业务组件</Radio.Button> */}
<Radio.Button value={DependencyItemType.}></Radio.Button>
<Radio.Button value={DependencyItemType.}></Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item label="npm包名" name="name" rules={[{ required: true }]}>
<Input placeholder="输入依赖名" />
</Form.Item>
{!record && (
<>
<Form.Item label="依赖类型" name="type" rules={[{ required: true }]}>
<Radio.Group>
{/* <Radio.Button value={DependencyItemType.业务组件}>业务组件</Radio.Button> */}
<Radio.Button value={DependencyItemType.}></Radio.Button>
<Radio.Button value={DependencyItemType.}></Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item label="npm 包名" name="name" rules={[{ required: true }]}>
<Input placeholder="输入依赖名" />
</Form.Item>
</>
)}
<Form.Item
label="版本号"
name="version"
@@ -591,8 +705,43 @@ function AddDependencyModal({
>
<Input placeholder="输入版本号" />
</Form.Item>
{hasUpgraded && (
<Form.Item name="umd" valuePropName="checked">
<Checkbox>使 UMD </Checkbox>
</Form.Item>
)}
<Form.Item noStyle shouldUpdate>
{() =>
form.getFieldValue('umd') && (
<>
<Form.Item label="全局变量名" name="library">
<Input placeholder="输入 UMD 资源挂载在全局变量下的变量名" />
</Form.Item>
<Form.Item label="资源地址" required>
<ResourceFormList
name="resources"
rules={[
{
validator: async (rule, value) => {
if (!value?.length) {
throw new Error('资源地址不能为空');
}
},
},
]}
/>
</Form.Item>
{form.getFieldValue('type') === DependencyItemType. && (
<Form.Item label="设计器资源地址">
<ResourceFormList name="designerResources" />
</Form.Item>
)}
</>
)
}
</Form.Item>
</Form>
</Modal>
</Box>
</>
);
}
+17 -54
View File
@@ -1,21 +1,11 @@
import React, { useMemo } from 'react';
import cx from 'classnames';
import { Box, Text, css, HTMLCoralProps } from 'coral-system';
import {
ClusterOutlined,
FunctionOutlined,
ApiOutlined,
AppstoreOutlined,
BuildOutlined,
} from '@ant-design/icons';
import { Badge } from 'antd';
import { ReactComponentProps } from '@music163/tango-helpers';
import { observer, useDesigner } from '@music163/tango-context';
import { ResizableBox } from './resizable-box';
import { OutlinePanel } from './outline-panel';
import { DependencyPanel } from './dependency-panel';
import { VariablePanel } from './variable-panel';
import { DataSourcePanel } from './datasource-panel';
import { getWidget } from '../widgets';
const sidebarStyle = css`
position: relative;
@@ -51,7 +41,6 @@ export interface SidebarProps extends ReactComponentProps {
* 底部附加内容
*/
footer?: React.ReactNode;
builtinPanelMap?: Record<string, SidebarPanelItemProps>;
}
export interface SidebarPanelItemProps
@@ -79,54 +68,21 @@ export interface SidebarPanelItemProps
* 面板宽度
*/
width?: number;
/**
* 如果 key 匹配到内置组件的话,传递给子节点的属性
*/
widgetProps?: object;
}
const INTERNAL_SIDEBAR_PANEL_MAP: Record<string, SidebarPanelItemProps> = {
components: {
label: '组件',
title: '组件列表',
icon: <AppstoreOutlined />,
},
outline: {
label: '结构',
title: '结构',
icon: <BuildOutlined />,
children: <OutlinePanel />,
},
dependency: {
label: '依赖',
title: '项目依赖',
icon: <ClusterOutlined />,
children: <DependencyPanel />,
},
variables: {
label: '变量',
title: '变量管理',
icon: <FunctionOutlined />,
children: <VariablePanel />,
width: 440,
},
dataSource: {
label: '接口',
title: '数据源与接口',
icon: <ApiOutlined />,
children: <DataSourcePanel />,
width: 600,
},
};
function BaseSidebarPanel({
panelWidth: defaultPanelWidth = 280,
builtinPanelMap = INTERNAL_SIDEBAR_PANEL_MAP,
footer,
children,
}: SidebarProps) {
function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 280, footer, children }: SidebarProps) {
const items = useMemo(() => {
const ret: Record<React.Key, SidebarPanelItemProps> = {};
React.Children.forEach(children, (child) => {
if (child && React.isValidElement(child)) {
const widget = getWidget(['sidebar', child.key].join('.'));
const fallbackNode = widget ? React.createElement(widget, child.props.widgetProps) : null;
ret[child.key] = {
...builtinPanelMap[child.key],
children: fallbackNode,
...child.props,
};
}
@@ -203,7 +159,14 @@ function BaseSidebarPanel({
);
}
BaseSidebarPanel.Item = function ({ key, title, doc, isFloat, width }: SidebarPanelItemProps) {
BaseSidebarPanel.Item = function ({
key,
title,
doc,
isFloat,
width,
widgetProps,
}: SidebarPanelItemProps) {
return <></>;
};
@@ -6,12 +6,23 @@ import { FileAddOutlined, QuestionCircleOutlined } from '@ant-design/icons';
import { observer, useWorkspace, useWorkspaceData } from '@music163/tango-context';
import { EditableVariableTree, EditableVariableTreeProps } from '../components';
const defaultStoreTemplate = `
import { defineStore } from '@music163/tango-boot';
export default defineStore({
});
`;
export interface VariablePanelProps extends EditableVariableTreeProps {
wrapperHeight?: number | string;
newStoreTemplate?: string;
}
export const VariablePanel = observer(
({ wrapperHeight = '100%', ...restProps }: VariablePanelProps) => {
({
wrapperHeight = '100%',
newStoreTemplate = defaultStoreTemplate,
...restProps
}: VariablePanelProps) => {
const [isAdd, { on, off }] = useBoolean();
const workspace = useWorkspace();
const { storeVariables } = useWorkspaceData();
@@ -42,7 +53,7 @@ export const VariablePanel = observer(
existNames={storeNames}
onCancel={off}
onSubmit={(values) => {
workspace.addStoreModule(values.name, emptyStoreTemplate);
workspace.addStoreModule(values.name, newStoreTemplate);
off();
}}
/>
@@ -119,9 +130,3 @@ function AddStoreForm({ existNames = [], onCancel, onSubmit }: AddStoreFormProps
</Panel>
);
}
const emptyStoreTemplate = `
import { defineStore } from '@music163/tango-boot';
export default defineStore({
});
`;
+4 -17
View File
@@ -6,7 +6,7 @@ import { ISelectedItemData, isString, noop } from '@music163/tango-helpers';
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
import { IconButton } from '@music163/tango-ui';
import { getDragGhostElement } from '../helpers';
import { CopyNodeAction, DeleteNodeAction, ViewSourceAction } from '../selection-menu';
import { getWidget } from '../widgets';
/**
* 选择辅助工具的对齐方式
@@ -15,34 +15,21 @@ type SelectionHelperAlignType = 'top-right' | 'top-left' | 'inner-top-right';
const boundingOffset = 100; // 检测选中模块的工具栏是否溢出的偏移值
const internalSelectionToolsMap = {
source: <ViewSourceAction />,
copy: <CopyNodeAction />,
delete: <DeleteNodeAction />,
};
export const INTERNAL_SELECTION_TOOLS = ['source', 'create', 'copy', 'delete'];
export interface SelectionToolsProps {
/**
* 动作列表,内置的动作列表有 source-定位到源码, block-创建区块, copy-复制节点, delete-删除节点
*/
actions?: Array<string | React.ReactElement>;
builtinActionMap?: Record<string, React.ReactElement>;
}
export const SelectionTools = observer(
({
actions: actionsProp = INTERNAL_SELECTION_TOOLS,
builtinActionMap = internalSelectionToolsMap,
}: SelectionToolsProps) => {
({ actions: actionsProp = ['viewSource', 'copyNode', 'deleteNode'] }: SelectionToolsProps) => {
const workspace = useWorkspace();
const selectSource = workspace.selectSource;
const actions = actionsProp.map((item) => {
if (isString(item)) {
return builtinActionMap[item]
? React.cloneElement(builtinActionMap[item], { key: item })
: null;
const widget = getWidget(['selectionMenu', item].join('.'));
return widget ? React.createElement(widget, { key: item }) : null;
}
return item;
});
+2 -9
View File
@@ -10,21 +10,14 @@ import { DraggingMask } from './mask';
export interface ViewportProps extends HTMLCoralProps<'div'> {
selectionTools?: SelectionToolsProps['actions'];
builtinActionMap?: SelectionToolsProps['builtinActionMap'];
}
export function Viewport({
selectionTools,
builtinActionMap,
children,
className,
...rest
}: ViewportProps) {
export function Viewport({ selectionTools, children, className, ...rest }: ViewportProps) {
return (
<Box position="relative" height="100%" className={cx('Viewport', className)} {...rest}>
<ViewportBody>{children}</ViewportBody>
<div className="AuxTools">
<SelectionTools actions={selectionTools} builtinActionMap={builtinActionMap} />
<SelectionTools actions={selectionTools} />
<InsertionPrompt />
<DraggingMask />
</div>
@@ -21,7 +21,8 @@ export const ModeSwitchTool = observer(() => {
shape="ghost"
selected={designer.activeView === 'code'}
onClick={() => {
designer.setActiveView('code');
designer.setActiveView('code'); // 切换到源码视图
designer.setActiveSidebarPanel(''); // 关闭左侧面板
}}
tip="源码视图"
>
+5 -1
View File
@@ -11,7 +11,11 @@ export const PreviewTool = observer(() => {
shape="ghost"
selected={designer.isPreview}
onClick={() => {
designer.toggleIsPreview(!designer.isPreview);
const nextIsPreview = !designer.isPreview;
designer.toggleIsPreview(nextIsPreview);
if (nextIsPreview) {
designer.setActiveView('design');
}
}}
tip="预览"
>
+16 -17
View File
@@ -1,26 +1,13 @@
import React, { useMemo } from 'react';
import { Box, Group } from 'coral-system';
import { ReactComponentProps } from '@music163/tango-helpers';
import { HistoryTool } from './history';
import { ModeSwitchTool } from './mode-switch';
import { PreviewTool } from './preview';
import { RouteSwitchTool } from './route-switch';
import { TogglePanelTool } from './toggle-panel';
import { getWidget } from '../widgets';
export interface ToolbarProps {
children?: React.ReactElement | React.ReactElement[];
builtinToolMap?: Record<string, React.ReactNode>;
}
const builtinToolsMap = {
history: <HistoryTool />,
modeSwitch: <ModeSwitchTool />,
preview: <PreviewTool />,
routeSwitch: <RouteSwitchTool />,
togglePanel: <TogglePanelTool />,
};
export function Toolbar({ children, builtinToolMap = builtinToolsMap }: ToolbarProps) {
export function Toolbar({ children }: ToolbarProps) {
const [leftTools, centerTools, rightTools] = useMemo(() => {
const left: React.ReactNode[] = [];
const center: React.ReactNode[] = [];
@@ -28,7 +15,15 @@ export function Toolbar({ children, builtinToolMap = builtinToolsMap }: ToolbarP
let prevPlacement: string;
React.Children.forEach(children, (child: React.ReactElement, index) => {
let node = child.props.children || builtinToolMap[child.key];
let fallbackNode;
if (child.key) {
const Widget = getWidget(['toolbar', child.key].join('.'));
if (Widget) {
fallbackNode = React.createElement(Widget, child.props.widgetProps);
}
}
let node = child.props.children ?? fallbackNode ?? null;
if (!node) {
node = child; // separator
}
@@ -75,9 +70,13 @@ export function Toolbar({ children, builtinToolMap = builtinToolsMap }: ToolbarP
export interface ToolbarItemProps extends ReactComponentProps {
placement?: 'left' | 'center' | 'right';
children?: React.ReactElement;
/**
* 如果 key 匹配到内置组件的话,传递给子节点的属性
*/
widgetProps?: object;
}
function ToolbarItem({ placement, children }: ToolbarItemProps) {
function ToolbarItem({ placement, widgetProps, children }: ToolbarItemProps) {
return <div>{children}</div>;
}
+51
View File
@@ -0,0 +1,51 @@
import { getValue, setValue } from '@music163/tango-helpers';
import {
HistoryTool,
ModeSwitchTool,
PreviewTool,
RouteSwitchTool,
TogglePanelTool,
} from './toolbar';
import {
ComponentsPanel,
DataSourcePanel,
DependencyPanel,
OutlinePanel,
VariablePanel,
} from './sidebar';
import { CopyNodeAction, DeleteNodeAction, ViewSourceAction } from './selection-menu';
const widgets = {};
export function registerWidget(key: string, component: React.ComponentType<any>) {
if (!/^(toolbar|sidebar|selectionMenu)\.[a-zA-z]+$/.test(key)) {
throw new Error(
`Invalid widget key(${key}), should start with toolbar, sidebar, or selection-menu`,
);
}
setValue(widgets, key, component);
}
export function getWidget(key: string) {
if (!key) {
return;
}
return getValue(widgets, key);
}
registerWidget('toolbar.modeSwitch', ModeSwitchTool);
registerWidget('toolbar.preview', PreviewTool);
registerWidget('toolbar.history', HistoryTool);
registerWidget('toolbar.routeSwitch', RouteSwitchTool);
registerWidget('toolbar.togglePanel', TogglePanelTool);
registerWidget('sidebar.components', ComponentsPanel);
registerWidget('sidebar.outline', OutlinePanel);
registerWidget('sidebar.dependency', DependencyPanel);
registerWidget('sidebar.variables', VariablePanel);
registerWidget('sidebar.dataSource', DataSourcePanel);
registerWidget('selectionMenu.copyNode', CopyNodeAction);
registerWidget('selectionMenu.deleteNode', DeleteNodeAction);
registerWidget('selectionMenu.viewSource', ViewSourceAction);
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.1.4](https://github.com/netease/tango/compare/@music163/tango-helpers@0.1.3...@music163/tango-helpers@0.1.4) (2023-10-23)
### Bug Fixes
- enhance expSetter ([#43](https://github.com/netease/tango/issues/43)) ([5ebbb42](https://github.com/netease/tango/commit/5ebbb428fb3fb786d330ab01959028443338d315))
## [0.1.3](https://github.com/netease/tango/compare/@music163/tango-helpers@0.1.2...@music163/tango-helpers@0.1.3) (2023-09-18)
**Note:** Version bump only for package @music163/tango-helpers
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-helpers",
"version": "0.1.3",
"version": "0.1.4",
"description": "Shared types, helpers, and hooks of tango-apps",
"keywords": [
"shared",
+20 -5
View File
@@ -72,7 +72,7 @@ export function parseDndTrackId(str: string) {
return [parts[0], str];
}
type DndIdParsedType = {
interface DndIdParsedType {
/**
* 完整的 ID
*/
@@ -93,7 +93,7 @@ type DndIdParsedType = {
* 序号
*/
index?: string;
};
}
/**
* 解析 dnd id
@@ -178,7 +178,7 @@ export function isValidObjectString(str: string) {
return false;
}
const templatePattern = /^\{(.+)\}$/s;
const templatePattern = /^{(.+)}$/s;
/**
* 判断给定字符串是否为变量字符串
@@ -191,15 +191,30 @@ const templatePattern = /^\{(.+)\}$/s;
*/
export function isVariableString(str: string) {
// 先检查是否是简单的对象
// TODO: 是不是可以忽略这个检查
if (code2object(str)) {
return false;
}
// 排除简单对象后,用正则匹配
// 排除简单对象后,用正则匹配
return templatePattern.test(str);
}
/**
* 给输入代码加上花括号
* @example foo -> {foo}
* @example "hello" => {"hello"}
* @example () => {} => {() => {}}
*
* @param code 输入代码
* @returns 加上花括号后的代码
*/
export function wrapCode(code: string) {
if (isVariableString(code)) {
return code;
}
return `{${code}}`;
}
/**
* 是否为简单字符串,非变量字符串
* @param str
+2
View File
@@ -43,6 +43,8 @@ describe('string', () => {
expect(isVariableString('{"hello"}')).toBeTruthy();
expect(isVariableString('{ foo: "bar" }')).toBeFalsy();
expect(isVariableString('{{ foo: "bar" }}')).toBeTruthy();
expect(isVariableString('{() => {}}')).toBeTruthy();
expect(isVariableString('{() => { console.log(); }}')).toBeTruthy();
});
it('getVariableContent', () => {
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.1.7](https://github.com/netease/tango/compare/@music163/tango-sandbox@0.1.6...@music163/tango-sandbox@0.1.7) (2023-10-23)
**Note:** Version bump only for package @music163/tango-sandbox
## [0.1.6](https://github.com/netease/tango/compare/@music163/tango-sandbox@0.1.5...@music163/tango-sandbox@0.1.6) (2023-09-22)
**Note:** Version bump only for package @music163/tango-sandbox
## [0.1.5](https://github.com/netease/tango/compare/@music163/tango-sandbox@0.1.4...@music163/tango-sandbox@0.1.5) (2023-09-21)
**Note:** Version bump only for package @music163/tango-sandbox
## [0.1.4](https://github.com/netease/tango/compare/@music163/tango-sandbox@0.1.3...@music163/tango-sandbox@0.1.4) (2023-09-18)
**Note:** Version bump only for package @music163/tango-sandbox
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-sandbox",
"version": "0.1.4",
"version": "0.1.7",
"description": "sandbox of tango apps",
"author": "wwsun <ww.sun@outlook.com>",
"homepage": "",
@@ -29,8 +29,8 @@
},
"dependencies": {
"@ant-design/icons": "^4.8.0",
"@music163/tango-core": "^0.1.4",
"@music163/tango-helpers": "^0.1.3",
"@music163/tango-core": "^0.2.2",
"@music163/tango-helpers": "^0.1.4",
"crypto-js": "^4.1.1",
"lodash.isequal": "4.5.0",
"react-frame-component": "^5.2.4"
+22
View File
@@ -3,6 +3,28 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.2.6](https://github.com/netease/tango/compare/@music163/tango-setting-form@0.2.5...@music163/tango-setting-form@0.2.6) (2023-10-23)
### Bug Fixes
- enhance expSetter ([#43](https://github.com/netease/tango/issues/43)) ([5ebbb42](https://github.com/netease/tango/commit/5ebbb428fb3fb786d330ab01959028443338d315))
## [0.2.5](https://github.com/netease/tango/compare/@music163/tango-setting-form@0.2.4...@music163/tango-setting-form@0.2.5) (2023-10-12)
**Note:** Version bump only for package @music163/tango-setting-form
## [0.2.4](https://github.com/netease/tango/compare/@music163/tango-setting-form@0.2.3...@music163/tango-setting-form@0.2.4) (2023-10-09)
**Note:** Version bump only for package @music163/tango-setting-form
## [0.2.3](https://github.com/netease/tango/compare/@music163/tango-setting-form@0.2.2...@music163/tango-setting-form@0.2.3) (2023-09-22)
**Note:** Version bump only for package @music163/tango-setting-form
## [0.2.2](https://github.com/netease/tango/compare/@music163/tango-setting-form@0.2.1...@music163/tango-setting-form@0.2.2) (2023-09-21)
**Note:** Version bump only for package @music163/tango-setting-form
## [0.2.1](https://github.com/netease/tango/compare/@music163/tango-setting-form@0.2.0...@music163/tango-setting-form@0.2.1) (2023-09-18)
### Bug Fixes
+3 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-setting-form",
"version": "0.2.1",
"version": "0.2.6",
"description": "setting form of tango-apps",
"author": "wwsun <ww.sun@outlook.com>",
"homepage": "",
@@ -28,9 +28,8 @@
},
"dependencies": {
"@ant-design/icons": "^4.8.0",
"@music163/tango-core": "^0.1.4",
"@music163/tango-helpers": "^0.1.3",
"@music163/tango-ui": "^0.1.4",
"@music163/tango-helpers": "^0.1.4",
"@music163/tango-ui": "^0.1.7",
"antd": "^4.24.2",
"coral-system": "^1.0.5",
"mobx": "6.9.0",
+1 -1
View File
@@ -148,7 +148,7 @@ export function createFormItem(options: IFormItemCreateOptions) {
size="s"
shape="outline"
type="primary"
tooltip="切换为 JS 表达式"
tooltip={isVariable ? '关闭 JS 表达式' : '使用 JS 表达式'}
tooltipPlacement="left"
selected={isVariable}
onClick={() => toggleIsVariable()}
+11 -10
View File
@@ -11,9 +11,9 @@ export interface IFormModel {
onChange: (name: string, value: any, field?: Field) => void;
}
export type FormModelOptionsType = {
export interface FormModelOptionsType {
onChange?: IFormModel['onChange'];
};
}
export class FormModel implements IFormModel {
state = {};
@@ -141,21 +141,21 @@ class SubModel implements IFormModel {
}
}
type FieldCreateOptionsType = {
interface FieldCreateOptionsType {
parent: IFormModel;
name: string;
};
}
type FieldConfigType = {
interface FieldConfigType {
validate?: (value: any, field: Field, trigger: string) => string | Promise<any>;
};
}
type FieldStateType = {
interface FieldStateType {
error?: any;
validating?: boolean;
cancelValidation?: () => void;
[key: string]: any;
};
}
type FieldValidateTriggerType = '*' | 'blur' | 'change';
@@ -233,11 +233,12 @@ export class Field {
this.state.validating = false;
this.state.error = error;
this.parent?.onChange?.(this.name, this.value, this);
if (error) {
return error;
}
// 校验错误,不触发 onChange
this.parent?.onChange?.(this.name, this.value, this);
}),
);
};
+18
View File
@@ -3,6 +3,24 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.1.7](https://github.com/netease/tango/compare/@music163/tango-ui@0.1.6...@music163/tango-ui@0.1.7) (2023-10-23)
### Bug Fixes
- enhance expSetter ([#43](https://github.com/netease/tango/issues/43)) ([5ebbb42](https://github.com/netease/tango/commit/5ebbb428fb3fb786d330ab01959028443338d315))
## [0.1.6](https://github.com/netease/tango/compare/@music163/tango-ui@0.1.5...@music163/tango-ui@0.1.6) (2023-10-12)
### Bug Fixes
- refactor expSetter ([#41](https://github.com/netease/tango/issues/41)) ([d63517e](https://github.com/netease/tango/commit/d63517ecb936e4227e70c33e610664316625f4f4))
## [0.1.5](https://github.com/netease/tango/compare/@music163/tango-ui@0.1.4...@music163/tango-ui@0.1.5) (2023-10-09)
### Bug Fixes
- update tabs style ([46b5445](https://github.com/netease/tango/commit/46b54459ceb23f9df52b9eb40dc935bc872ba1b4))
## [0.1.4](https://github.com/netease/tango/compare/@music163/tango-ui@0.1.3...@music163/tango-ui@0.1.4) (2023-09-18)
**Note:** Version bump only for package @music163/tango-ui
+7 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-ui",
"version": "0.1.4",
"version": "0.1.7",
"description": "ui widgets of tango",
"keywords": [
"react",
@@ -34,14 +34,16 @@
},
"dependencies": {
"@ant-design/icons": "^4.8.0",
"@codemirror/autocomplete": "^6.7.1",
"@codemirror/lang-javascript": "^6.1.2",
"@codemirror/search": "^6.2.3",
"@music163/tango-helpers": "^0.1.3",
"@codemirror/autocomplete": "^6.10.2",
"@codemirror/lang-javascript": "^6.2.1",
"@codemirror/lint": "^6.4.2",
"@codemirror/search": "^6.5.4",
"@music163/tango-helpers": "^0.1.4",
"@uiw/react-codemirror": "^4.19.3",
"antd": "^4.24.2",
"antd-easy-form": "^1.1.3",
"coral-system": "^1.0.5",
"eslint-linter-browserify": "^8.51.0",
"react-json-view": "^1.21.3",
"react-monaco-editor-lite": "^1.2.3"
},
+4 -2
View File
@@ -9,7 +9,9 @@ export interface CopyClipboardProps {
* 拷贝成功后到回调
*/
onCopy?: () => void;
children?: React.ReactNode | ((copied: boolean) => React.ReactElement);
children?:
| React.ReactNode
| ((data: { copied: boolean; onClick: Function }) => React.ReactElement);
}
export function CopyClipboard({ onCopy, text, children }: CopyClipboardProps) {
@@ -37,7 +39,7 @@ export function CopyClipboard({ onCopy, text, children }: CopyClipboardProps) {
};
if (typeof children === 'function') {
return React.cloneElement(children(copied), {
return React.cloneElement(children({ copied, onClick }), {
onClick,
});
}
+26 -8
View File
@@ -1,10 +1,12 @@
import React, { useMemo } from 'react';
import { Box, HTMLCoralProps } from 'coral-system';
import CodeMirror, { ReactCodeMirrorProps } from '@uiw/react-codemirror';
import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
import { javascript, javascriptLanguage, esLint } from '@codemirror/lang-javascript';
import { CompletionContext } from '@codemirror/autocomplete';
import { syntaxTree } from '@codemirror/language';
import { linter, lintGutter } from '@codemirror/lint';
import * as eslint from 'eslint-linter-browserify';
import { getValue } from '@music163/tango-helpers';
import { Box, HTMLCoralProps } from 'coral-system';
const completePropertyAfter = ['PropertyName', '.', '?.'];
const dontCompleteIn = [
@@ -15,6 +17,17 @@ const dontCompleteIn = [
'PropertyDefinition',
];
const eslintConfig = {
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
},
env: {
browser: true,
},
rules: {},
};
function completeProperties(from: number, object: Object) {
const options = [];
for (const name in object) {
@@ -116,6 +129,10 @@ export interface InputCodeProps extends ReactCodeMirrorProps {
* 自定义补全的选项列表
*/
autoCompleteOptions?: string[];
/**
* 启用 ESLint
*/
enableESLint?: boolean;
}
export function InputCode({
@@ -126,6 +143,7 @@ export function InputCode({
autoCompleteOptions,
showLineNumbers,
showFoldGutter,
enableESLint = false,
...rest
}: InputCodeProps) {
const globalJavaScriptCompletions = useMemo(() => {
@@ -136,15 +154,16 @@ export function InputCode({
});
}, [autoCompleteContext, autoCompleteOptions]);
const { rootStyle, codeSetup } = useInputCode({ shape, status, showLineNumbers, showFoldGutter });
const extensions = [javascript({ jsx: true }), globalJavaScriptCompletions];
if (enableESLint) {
// @ts-ignore
extensions.push(lintGutter(), linter(esLint(new eslint.Linter(), eslintConfig)));
}
return (
<Box className="InputCode" display="flex" alignItems="center" overflow="hidden" {...rootStyle}>
<Box flex="1" overflow="auto">
<CodeMirror
extensions={[javascript({ jsx: true }), globalJavaScriptCompletions]}
basicSetup={codeSetup}
{...rest}
/>
<CodeMirror extensions={extensions} basicSetup={codeSetup} {...rest} />
</Box>
{suffix}
</Box>
@@ -166,7 +185,6 @@ function useInputCode({
border: 'solid',
borderColor: status === 'error' ? 'error.60' : 'line.normal',
borderRadius: 's',
px: 'm',
};
lineNumbers = false;
foldGutter = false;
+1 -1
View File
@@ -18,7 +18,7 @@ export function Tabs({
const tabBarStyle = useMemo(() => {
let ret = centered ? centerTabBarStyle : defaultTabBarStyle;
if (isTabBarSticky) {
ret = { ...ret, position: 'sticky', top: tabBarStickyOffset, zIndex: 1, background: '#fff' };
ret = { ...ret, position: 'sticky', top: tabBarStickyOffset, zIndex: 2, background: '#fff' };
}
return ret;
}, [centered, isTabBarSticky, tabBarStickyOffset]);
+4 -3
View File
@@ -7,11 +7,12 @@
"resolveJsonModule": true,
"strictFunctionTypes": true,
"paths": {
"@music163/tango-core": ["packages/core/src/index.ts"],
"@music163/tango-setting-form": ["packages/setting-form/src/index.ts"],
"@music163/tango-helpers": ["packages/helpers/src/index.ts"],
"@music163/tango": ["packages/designer/src/index.ts"],
"@music163/tango-core": ["packages/core/src/index.ts"],
"@music163/tango-context": ["packages/context/src/index.ts"],
"@music163/tango-setting-form": ["packages/setting-form/src/index.ts"],
"@music163/tango-ui": ["packages/ui/src/index.ts"],
"@music163/tango": ["packages/designer/src/index.ts"]
}
}
}
+60 -13
View File
@@ -1495,7 +1495,7 @@
exec-sh "^0.3.2"
minimist "^1.2.0"
"@codemirror/autocomplete@^6.0.0", "@codemirror/autocomplete@^6.7.1":
"@codemirror/autocomplete@^6.0.0":
version "6.9.0"
resolved "https://registry.npmmirror.com/@codemirror/autocomplete/-/autocomplete-6.9.0.tgz"
integrity sha512-Fbwm0V/Wn3BkEJZRhr0hi5BhCo5a7eBL6LYaliPjOSwCyfOpnjXY59HruSxOUNV+1OYer0Tgx1zRNQttjXyDog==
@@ -1505,6 +1505,16 @@
"@codemirror/view" "^6.6.0"
"@lezer/common" "^1.0.0"
"@codemirror/autocomplete@^6.10.2":
version "6.10.2"
resolved "https://registry.npmmirror.com/@codemirror/autocomplete/-/autocomplete-6.10.2.tgz#d3a7e2a4ddd1e8a2992b1b17d4e7692542c71052"
integrity sha512-3dCL7b0j2GdtZzWN5j7HDpRAJ26ip07R4NGYz7QYthIYMiX8I4E4TNrYcdTayPJGeVQtd/xe7lWU4XL7THFb/w==
dependencies:
"@codemirror/language" "^6.0.0"
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.17.0"
"@lezer/common" "^1.0.0"
"@codemirror/commands@^6.0.0", "@codemirror/commands@^6.1.0":
version "6.2.4"
resolved "https://registry.npmmirror.com/@codemirror/commands/-/commands-6.2.4.tgz"
@@ -1515,16 +1525,16 @@
"@codemirror/view" "^6.0.0"
"@lezer/common" "^1.0.0"
"@codemirror/lang-javascript@^6.1.2":
version "6.1.9"
resolved "https://registry.npmmirror.com/@codemirror/lang-javascript/-/lang-javascript-6.1.9.tgz"
integrity sha512-z3jdkcqOEBT2txn2a87A0jSy6Te3679wg/U8QzMeftFt+4KA6QooMwfdFzJiuC3L6fXKfTXZcDocoaxMYfGz0w==
"@codemirror/lang-javascript@^6.2.1":
version "6.2.1"
resolved "https://registry.npmmirror.com/@codemirror/lang-javascript/-/lang-javascript-6.2.1.tgz#8068d44365d13cdb044936fb4e3483301c12ef95"
integrity sha512-jlFOXTejVyiQCW3EQwvKH0m99bUYIw40oPmFjSX2VS78yzfe0HELZ+NEo9Yfo1MkGRpGlj3Gnu4rdxV1EnAs5A==
dependencies:
"@codemirror/autocomplete" "^6.0.0"
"@codemirror/language" "^6.6.0"
"@codemirror/lint" "^6.0.0"
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.0.0"
"@codemirror/view" "^6.17.0"
"@lezer/common" "^1.0.0"
"@lezer/javascript" "^1.0.0"
@@ -1549,7 +1559,16 @@
"@codemirror/view" "^6.0.0"
crelt "^1.0.5"
"@codemirror/search@^6.0.0", "@codemirror/search@^6.2.3":
"@codemirror/lint@^6.4.2":
version "6.4.2"
resolved "https://registry.npmmirror.com/@codemirror/lint/-/lint-6.4.2.tgz#c13be5320bde9707efdc94e8bcd3c698abae0b92"
integrity sha512-wzRkluWb1ptPKdzlsrbwwjYCPLgzU6N88YBAmlZi8WFyuiEduSd05MnJYNogzyc8rPK7pj6m95ptUApc8sHKVA==
dependencies:
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.0.0"
crelt "^1.0.5"
"@codemirror/search@^6.0.0":
version "6.5.1"
resolved "https://registry.npmmirror.com/@codemirror/search/-/search-6.5.1.tgz"
integrity sha512-4jupk4JwkeVbrN2pStY74q6OJEYqwosB4koA66nyLeVedadtX9MHI38j2vbYmnfDGurDApP3OZO46MrWalcjiQ==
@@ -1558,6 +1577,15 @@
"@codemirror/view" "^6.0.0"
crelt "^1.0.5"
"@codemirror/search@^6.5.4":
version "6.5.4"
resolved "https://registry.npmmirror.com/@codemirror/search/-/search-6.5.4.tgz#54005697bf581f7dccbbb4a0c34d3a7aa25a513a"
integrity sha512-YoTrvjv9e8EbPs58opjZKyJ3ewFrVSUzQ/4WXlULQLSDDr1nGPJ67mMXFNNVYwdFhybzhrzrtqgHmtpJwIF+8g==
dependencies:
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.0.0"
crelt "^1.0.5"
"@codemirror/state@^6.0.0", "@codemirror/state@^6.1.1", "@codemirror/state@^6.1.4", "@codemirror/state@^6.2.0":
version "6.2.1"
resolved "https://registry.npmmirror.com/@codemirror/state/-/state-6.2.1.tgz"
@@ -1582,6 +1610,15 @@
style-mod "^4.0.0"
w3c-keyname "^2.2.4"
"@codemirror/view@^6.17.0":
version "6.21.3"
resolved "https://registry.npmmirror.com/@codemirror/view/-/view-6.21.3.tgz#cf8e3ee6f08e06a6912f18bc90548b4b74badb7a"
integrity sha512-8l1aSQ6MygzL4Nx7GVYhucSXvW4jQd0F6Zm3v9Dg+6nZEfwzJVqi4C2zHfDljID+73gsQrWp9TgHc81xU15O4A==
dependencies:
"@codemirror/state" "^6.1.4"
style-mod "^4.1.0"
w3c-keyname "^2.2.4"
"@colors/colors@1.5.0":
version "1.5.0"
resolved "https://registry.npmmirror.com/@colors/colors/-/colors-1.5.0.tgz"
@@ -9294,6 +9331,11 @@ eslint-import-resolver-typescript@^3.6.0:
is-core-module "^2.11.0"
is-glob "^4.0.3"
eslint-linter-browserify@^8.51.0:
version "8.51.0"
resolved "https://registry.npmmirror.com/eslint-linter-browserify/-/eslint-linter-browserify-8.51.0.tgz#ab6a6a83b7ee0763b7f2eaa2d02fa4fc6b6e4ac1"
integrity sha512-N5wp3QVonbdDs6AImgiCjwpEtcApYCO2BI/lpjNYHT9MrqefL4uecPpyAxS0eF8Xuh8EFYH2sLjh5nLjpSrSsg==
eslint-module-utils@^2.7.4, eslint-module-utils@^2.8.0:
version "2.8.0"
resolved "https://registry.npmmirror.com/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz"
@@ -11185,9 +11227,9 @@ humanize-ms@^1.2.1:
dependencies:
ms "^2.0.0"
husky@^8.0.1:
husky@^8.0.3:
version "8.0.3"
resolved "https://registry.npmmirror.com/husky/-/husky-8.0.3.tgz"
resolved "https://registry.npmmirror.com/husky/-/husky-8.0.3.tgz#4936d7212e46d1dea28fef29bb3a108872cd9184"
integrity sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==
iconv-lite@0.4.24, iconv-lite@^0.4.24:
@@ -13043,10 +13085,10 @@ lines-and-columns@~2.0.3:
resolved "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-2.0.3.tgz"
integrity sha512-cNOjgCnLB+FnvWWtyRTzmB3POJ+cXxTA81LoW7u8JdmhfXzriropYwpjShnz1QLLWsQwY7nIxoDmcPTwphDK9w==
lint-staged@^13.1.0:
version "13.3.0"
resolved "https://registry.npmmirror.com/lint-staged/-/lint-staged-13.3.0.tgz"
integrity sha512-mPRtrYnipYYv1FEE134ufbWpeggNTo+O/UPzngoaKzbzHAthvR55am+8GfHTnqNRQVRRrYQLGW9ZyUoD7DsBHQ==
lint-staged@^14.0.1:
version "14.0.1"
resolved "https://registry.npmmirror.com/lint-staged/-/lint-staged-14.0.1.tgz#57dfa3013a3d60762d9af5d9c83bdb51291a6232"
integrity sha512-Mw0cL6HXnHN1ag0mN/Dg4g6sr8uf8sn98w2Oc1ECtFto9tvRF7nkXGJRbx8gPlHyoR0pLyBr2lQHbWwmUHe1Sw==
dependencies:
chalk "5.3.0"
commander "11.0.0"
@@ -18826,6 +18868,11 @@ style-mod@^4.0.0:
resolved "https://registry.npmmirror.com/style-mod/-/style-mod-4.0.3.tgz"
integrity sha512-78Jv8kYJdjbvRwwijtCevYADfsI0lGzYJe4mMFdceO8l75DFFDoqBhR1jVDicDRRaX4//g1u9wKeo+ztc2h1Rw==
style-mod@^4.1.0:
version "4.1.0"
resolved "https://registry.npmmirror.com/style-mod/-/style-mod-4.1.0.tgz#a313a14f4ae8bb4d52878c0053c4327fb787ec09"
integrity sha512-Ca5ib8HrFn+f+0n4N4ScTIA9iTOQ7MaGS1ylHcoVqW9J7w2w8PzN6g9gKmTYgGEBH8e120+RCmhpje6jC5uGWA==
style-to-object@0.3.0, style-to-object@^0.3.0:
version "0.3.0"
resolved "https://registry.npmmirror.com/style-to-object/-/style-to-object-0.3.0.tgz"