Showing preview only (313K chars total). Download the full file or copy to clipboard to get everything.
Repository: lidojs/canva-clone
Branch: master
Commit: 195c80465814
Files: 55
Total size: 294.7 KB
Directory structure:
gitextract_2xb0w4ev/
├── README.md
├── index.html
├── package.json
├── src/
│ ├── assets/
│ │ └── .gitkeep
│ ├── constant/
│ │ ├── data.ts
│ │ └── text-effects.ts
│ ├── features/
│ │ └── design/
│ │ ├── components/
│ │ │ ├── LidoJSEditor.tsx
│ │ │ ├── editor-content/
│ │ │ │ ├── EditorContent.tsx
│ │ │ │ └── index.ts
│ │ │ ├── editor-header/
│ │ │ │ ├── EditorHeader.tsx
│ │ │ │ └── index.ts
│ │ │ ├── index.ts
│ │ │ ├── layer-settings/
│ │ │ │ ├── LayerSettings.tsx
│ │ │ │ └── index.ts
│ │ │ ├── preview/
│ │ │ │ ├── PreviewModal.tsx
│ │ │ │ └── index.ts
│ │ │ ├── sidebar/
│ │ │ │ ├── DrawContent.tsx
│ │ │ │ ├── FrameContent.tsx
│ │ │ │ ├── GraphicContent.tsx
│ │ │ │ ├── IframeContent.tsx
│ │ │ │ ├── ImageContent.tsx
│ │ │ │ ├── Photo.tsx
│ │ │ │ ├── QrCodeContent.tsx
│ │ │ │ ├── ShapeContent.tsx
│ │ │ │ ├── Sidebar.tsx
│ │ │ │ ├── TableContent.tsx
│ │ │ │ ├── TemplateContent.tsx
│ │ │ │ ├── TextContent.tsx
│ │ │ │ ├── UploadContent.tsx
│ │ │ │ ├── VideoContent.tsx
│ │ │ │ └── index.ts
│ │ │ └── tabs/
│ │ │ ├── TabList.tsx
│ │ │ └── index.ts
│ │ ├── config/
│ │ │ ├── iframe.tsx
│ │ │ ├── line.tsx
│ │ │ ├── qrCode.tsx
│ │ │ └── shape.tsx
│ │ └── pages/
│ │ ├── DesignPage.tsx
│ │ └── index.ts
│ ├── main.tsx
│ ├── pages/
│ │ └── Main.tsx
│ ├── shared/
│ │ ├── components/
│ │ │ ├── index.ts
│ │ │ └── masonry/
│ │ │ ├── Masonry.tsx
│ │ │ └── index.ts
│ │ ├── icons/
│ │ │ └── pencil/
│ │ │ ├── Highlighter.tsx
│ │ │ ├── Marker.tsx
│ │ │ └── Pencil.tsx
│ │ └── theme/
│ │ ├── index.ts
│ │ ├── palette.ts
│ │ └── theme.ts
│ ├── styles.css
│ └── utils/
│ ├── download.ts
│ └── thumbnail.ts
├── tsconfig.json
└── vite.config.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: README.md
================================================
<img width="960" alt="Screenshot 2023-04-22 101234" src="https://github.com/lidojs/canva-clone/assets/19285404/06249d78-3e6c-45a0-b14a-bd73c186fd84" />
# LidoJS - Powerful Design Tool for Creatives
LidoJS is a simple and user-friendly graphic design tool, that helps you create great designs quickly. Whether you need presentations, marketing materials, or social media graphics, LidoJS has what you need to bring your ideas to life.
The tool is developed from scratch with ReactJS, so it's easy to upgrade by yourself.
This code will show you how to integrate LidoJS into an application to make an online design editor application.
Explore the platform at [https://lidojs.com](https://lidojs.com) or meet us at [https://discord.gg/mBj7fqKpEM](https://discord.gg/mBj7fqKpEM).
## **Overview**
LidoJS makes designing simple and fun, offering a variety of tools to help designers, marketers, and content creators reach their goals. From creating slides to making graphics, LidoJS makes the whole process easier.
## **Demo**
[https://demo.lidojs.com](https://demo.lidojs.com).
After creating designs, you can download them in various formats, including PDF, PNG, and JPG. Download feature does not include in the packages, but you can see the output file below
[Open PDF file](https://s3.us-east-2.amazonaws.com/lidojs.com/output-from-templates.pdf)
## **Key Features**
- **Drag-and-Drop Interface:** Simplify the design process with an intuitive editor.
- **Customizable Templates:** Access a vast collection of pre-designed templates for presentations, posters, and social media.
- **Advanced Editing Tools:** Fine-tune your designs with precise editing options, including layering, masking, painting, shapes, frames, and color adjustments.
- **Export Options:** Download designs in various formats suitable for both print (PDF, PNG, JPG) and digital platforms (SVG, WebP).
Technical Highlights
## **Technical Highlights**
- **Proven Technology:** LidoJS uses the same advanced technical solutions as Canva, ensuring a reliable and high-performance design experience.
- **Built from Scratch:** Core features are developed entirely with React.js, without relying on third-party frameworks.
- **Unlimited Customization:** Enjoy total creative freedom with the ability to customize any element to fit your specific needs.
## **Usage Purpose**
LidoJS works for many design tasks, such as:
- **Presentations:** Craft professional slide decks with ease using customizable templates.
- **Marketing Campaigns:** Create eye-catching promotional materials for ads and social media.
- **Branding:** Develop cohesive brand assets such as logos and banners.
- **Content Creation:** Design visually compelling graphics for blogs, websites, and newsletters.
## **Trusted by Leading Brands**
LidoJS is a preferred design tool for top brands and organizations, including:
- **Sendsteps:** Engaging presentation solutions.
- **Menuzen:** Dynamic digital menu creation.
- **Designstripe:** Simplified design experiences.
- **Momentumstack:** Innovative technology and business services.
Start designing with LidoJS and experience the future of creative possibilities. Visit [https://lidojs.com](https://lidojs.com) to learn more and get started today.
## Disclaimer
This source code is developed entirely by our team from scratch. We have not used any third-party resources that require licensing.
Our product is designed to assist users in developing design applications efficiently. However, we do not guarantee any specific outcomes or take responsibility for how this software is used.
Use this code at your own risk. We are not liable for any damages or issues arising from its use.
================================================
FILE: index.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>LidoJS Design Editor</title>
<meta
content="Canva clone, design editor tool, graphic editor tool, image editor tool."
name="description"
/>
<meta
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover, interactive-widget=resizes-content"
name="viewport">
<meta content="website" property="og:type" />
<meta content="LidoJS Design Editor" property="og:title" />
<meta
content="Canva clone, design editor tool, graphic editor tool, image editor tool."
property="og:description"
/>
<link href="https://fonts.googleapis.com" rel="preconnect">
<link crossorigin href="https://fonts.gstatic.com" rel="preconnect">
<link
crossorigin
href="https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,300;0,400;0,600;0,700;1,400;1,600;1,700&display=swap"
rel="stylesheet">
</head>
<body>
<style>
html, body {
margin: 0;
font-family: 'Nunito', sans-serif;
color: #5E6278;
font-size: 14px;
}
html,
body {
overflow-wrap: break-word;
-webkit-hyphens: none;
hyphens: none;
word-break: break-word;
margin: 0;
display: flex;
flex-direction: column;
height: unset;
overscroll-behavior: none;
}
blockquote,
dl,
dd,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
figure,
p,
pre {
margin: 0;
}
button {
background-color: transparent;
background-image: none;
}
fieldset {
margin: 0;
padding: 0;
}
ol,
ul {
list-style: none;
margin: 0;
padding: 0;
}
body {
font-family: inherit;
line-height: inherit;
}
*,
::before,
::after {
box-sizing: border-box; /* 1 */
border-width: 0; /* 2 */
border-style: solid; /* 2 */
border-color: currentColor; /* 2 */
outline: none;
}
hr {
border-top-width: 1px;
}
img {
border-style: solid;
}
textarea {
resize: vertical;
}
input::placeholder,
textarea::placeholder {
opacity: 1;
color: #a1a1aa;
}
button,
[role='button'] {
cursor: pointer;
}
table {
border-collapse: collapse;
}
a {
color: inherit;
text-decoration: inherit;
}
button,
input,
optgroup,
select,
textarea {
padding: 0;
line-height: inherit;
color: inherit;
}
pre,
code,
kbd,
samp {
font-family: Menlo, Monaco, Consolas, 'Courier New', monospace;
}
img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
display: block;
vertical-align: middle;
}
img,
video {
max-width: 100%;
height: auto;
}
</style>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script src="./index.tsx" type="module"></script>
</body>
<% if(process.env.NODE_ENV !== 'development'){ %>
<script>
window["_fs_host"] = "fullstory.com";
window["_fs_script"] = "edge.fullstory.com/s/fs.js";
window["_fs_org"] = "o-1PBR16-na1";
window["_fs_namespace"] = "FS";
(function(m, n, e, t, l, o, g, y) {
if (e in m) {
if (m.console && m.console.log) {
m.console.log("FullStory namespace conflict. Please set window[\"_fs_namespace\"].");
}
return;
}
g = m[e] = function(a, b, s) {
g.q ? g.q.push([a, b, s]) : g._api(a, b, s);
};
g.q = [];
o = n.createElement(t);
o.async = 1;
o.crossOrigin = "anonymous";
o.src = "https://" + _fs_script;
y = n.getElementsByTagName(t)[0];
y.parentNode.insertBefore(o, y);
g.identify = function(i, v, s) {
g(l, { uid: i }, s);
if (v) g(l, v, s);
};
g.setUserVars = function(v, s) {
g(l, v, s);
};
g.event = function(i, v, s) {
g("event", { n: i, p: v }, s);
};
g.anonymize = function() {
g.identify(!!0);
};
g.shutdown = function() {
g("rec", !1);
};
g.restart = function() {
g("rec", !0);
};
g.log = function(a, b) {
g("log", [a, b]);
};
g.consent = function(a) {
g("consent", !arguments.length || a);
};
g.identifyAccount = function(i, v) {
o = "account";
v = v || {};
v.acctId = i;
g(o, v);
};
g.clearUserCookie = function() {
};
g.setVars = function(n, p) {
g("setVars", [n, p]);
};
g._w = {};
y = "XMLHttpRequest";
g._w[y] = m[y];
y = "fetch";
g._w[y] = m[y];
if (m[y]) m[y] = function() {
return g._w[y].apply(this, arguments);
};
g._v = "1.3.0";
})(window, document, window["_fs_namespace"], "script", "user");
</script>
<% } %>
</html>
================================================
FILE: package.json
================================================
{
"name": "@lidojs/react",
"version": "2.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview"
},
"dependencies": {
"@duyank/icons": "^0.1.2",
"@emotion/react": "11.14.0",
"@emotion/styled": "11.14.0",
"@lidojs/color-picker": "^2.0.0",
"@lidojs/design-core": "^2.0.0",
"@lidojs/design-editor": "^2.0.0",
"@lidojs/design-layers": "^2.0.0",
"@lidojs/design-screen": "^2.0.0",
"@lidojs/design-utils": "^2.0.0",
"@lidojs/draw": "^2.0.0",
"@lidojs/text-editor": "^2.0.0",
"@mui/material": "^6.4.5",
"axios": "^1.7.9",
"lodash": "^4.17.21",
"react": "^18.3.1",
"react-device-detect": "^2.2.3",
"react-dom": "catalog:",
"react-responsive-masonry": "^2.7.1",
"react-use": "^17.6.0",
"uuid": "^11.0.5"
},
"devDependencies": {
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.5",
"prop-types": "^15.8.1",
"vite-bundle-analyzer": "^0.17.0",
"vite-plugin-environment": "^1.1.3"
}
}
================================================
FILE: src/assets/.gitkeep
================================================
================================================
FILE: src/constant/data.ts
================================================
export const data = [
{
layers: {
ROOT: {
type: { resolvedName: 'RootLayer' },
props: {
boxSize: { width: 1640, height: 924 },
position: { x: 0, y: 0 },
rotate: 0,
color: 'rgb(255, 255, 255)',
image: null,
},
locked: false,
child: [
'd80fa039-b645-4678-89f2-fc0336958989',
'851673f7-8917-4b3e-95d0-3a81b6f638bb',
'af7b69ea-3060-46ff-914e-9833d66ddc1d',
'09a24f96-90bd-4e0d-b610-47d23de11c2b',
'2096d6ab-d78e-4acf-b509-170a3e6b97f5',
],
parent: null,
},
'd80fa039-b645-4678-89f2-fc0336958989': {
type: { resolvedName: 'ShapeLayer' },
props: {
shape: 'rectangle',
position: { x: -41, y: 652 },
boxSize: { width: 1766, height: 296, x: -41, y: 652 },
rotate: 0,
color: 'rgb(253, 235, 207)',
},
locked: false,
child: [],
parent: 'ROOT',
},
'851673f7-8917-4b3e-95d0-3a81b6f638bb': {
type: { resolvedName: 'TextLayer' },
props: {
doc: {
type: 'doc',
content: [
{
type: 'paragraph',
attrs: {
textAlign: 'center',
color: 'rgb(0, 0, 0)',
fontFamily: 'Agdasima',
fontSize: '42px',
lineHeight: 1.03,
letterSpacing: 0,
textTransform: '',
marginLeft: null,
indent: 0,
listType: '',
},
content: [
{
type: 'text',
marks: [
{ type: 'color', attrs: { color: 'rgb(0, 0, 0)' } },
],
text: 'LIDOJS',
},
],
},
{
type: 'paragraph',
attrs: {
textAlign: 'center',
color: 'rgb(0, 0, 0)',
fontFamily: 'Agdasima',
fontSize: '42px',
lineHeight: 1.03,
letterSpacing: 0,
textTransform: '',
marginLeft: null,
indent: 0,
listType: '',
},
content: [
{
type: 'text',
marks: [
{ type: 'color', attrs: { color: 'rgb(0, 0, 0)' } },
],
text: 'DESIGN EDITOR',
},
],
},
],
},
position: { x: 528.5031977930256, y: 276.6364429758855 },
boxSize: {
width: 536.3009995574356,
height: 144.98254063816975,
x: 523.1390728476821,
y: 259.9867549668875,
},
scale: 1.6664659843467788,
rotate: 0,
fonts: [
{
name: 'Agdasima',
fonts: [
{
urls: [
'https://fonts.gstatic.com/s/agdasima/v4/PN_zRfyxp2f1fUCgAMg6rzjb_-Da.ttf',
],
},
],
},
],
colors: ['rgb(0, 0, 0)'],
fontSizes: [42],
},
locked: false,
child: [],
parent: 'ROOT',
},
'af7b69ea-3060-46ff-914e-9833d66ddc1d': {
type: { resolvedName: 'TextLayer' },
props: {
doc: {
type: 'doc',
content: [
{
type: 'paragraph',
attrs: {
textAlign: 'center',
color: 'rgb(0, 0, 0)',
fontFamily: 'Oswald',
fontSize: '18px',
lineHeight: '1.4',
letterSpacing: 0,
textTransform: 'uppercase',
marginLeft: null,
indent: 0,
listType: '',
},
content: [
{
type: 'text',
marks: [
{ type: 'bold' },
{ type: 'color', attrs: { color: 'rgb(0, 0, 0)' } },
],
text: 'DEVELOPED WITH REACTJS',
},
],
},
],
},
position: { x: 665.6953642384103, y: 440.2251655629139 },
boxSize: {
width: 261.91666666666606,
height: 25,
x: 660.2715231788078,
y: 440.2251655629139,
},
scale: 1,
rotate: 0,
fonts: [
{
name: 'Oswald',
fonts: [
{
style: 'Bold',
urls: [
'https://lidojs-fonts.s3.us-east-2.amazonaws.com/Oswald/Oswald-Bold.woff2',
],
},
{
urls: [
'https://lidojs-fonts.s3.us-east-2.amazonaws.com/Oswald/Oswald-Regular.woff2',
],
},
],
},
],
colors: ['rgb(0, 0, 0)'],
fontSizes: [18],
effect: null,
},
locked: false,
child: [],
parent: 'ROOT',
},
'09a24f96-90bd-4e0d-b610-47d23de11c2b': {
type: { resolvedName: 'TextLayer' },
props: {
doc: {
type: 'doc',
content: [
{
type: 'paragraph',
attrs: {
textAlign: 'center',
color: 'rgb(0, 0, 0)',
fontFamily: 'Oswald',
fontSize: '18px',
lineHeight: '1.4',
letterSpacing: 0,
textTransform: 'uppercase',
marginLeft: null,
indent: 0,
listType: '',
},
content: [
{
type: 'text',
marks: [
{ type: 'bold' },
{ type: 'color', attrs: { color: 'rgb(0, 0, 0)' } },
],
text: 'DISCORD: https://discord.gg/mBj7fqKpEM',
},
],
},
],
},
position: { x: 587.4569536423838, y: 839.8609271523178 },
boxSize: {
width: 379.3868653421627,
height: 25,
x: 587.4569536423838,
y: 839.8609271523178,
},
scale: 1,
rotate: 0,
fonts: [
{
name: 'Oswald',
fonts: [
{
style: 'Bold',
urls: [
'https://lidojs-fonts.s3.us-east-2.amazonaws.com/Oswald/Oswald-Bold.woff2',
],
},
{
urls: [
'https://lidojs-fonts.s3.us-east-2.amazonaws.com/Oswald/Oswald-Regular.woff2',
],
},
],
},
],
colors: ['rgb(0, 0, 0)'],
fontSizes: [18],
effect: null,
},
locked: false,
child: [],
parent: 'ROOT',
},
'2096d6ab-d78e-4acf-b509-170a3e6b97f5': {
type: { resolvedName: 'TextLayer' },
props: {
doc: {
type: 'doc',
content: [
{
type: 'paragraph',
attrs: {
textAlign: 'center',
color: 'rgb(0, 0, 0)',
fontFamily: 'Oswald',
fontSize: '18px',
lineHeight: '1.4',
letterSpacing: 0,
textTransform: 'uppercase',
marginLeft: null,
indent: 0,
listType: '',
},
content: [
{
type: 'text',
marks: [
{ type: 'bold' },
{ type: 'color', attrs: { color: 'rgb(0, 0, 0)' } },
],
text: 'CONTACT: DUYANH980@GMAIL.COM',
},
],
},
],
},
position: { x: 634.1125827814567, y: 810.4900662251654 },
boxSize: {
width: 293.4994481236197,
height: 25,
x: 634.1125827814567,
y: 810.4900662251654,
},
scale: 1,
rotate: 0,
fonts: [
{
name: 'Oswald',
fonts: [
{
style: 'Bold',
urls: [
'https://lidojs-fonts.s3.us-east-2.amazonaws.com/Oswald/Oswald-Bold.woff2',
],
},
{
urls: [
'https://lidojs-fonts.s3.us-east-2.amazonaws.com/Oswald/Oswald-Regular.woff2',
],
},
],
},
],
colors: ['rgb(0, 0, 0)'],
fontSizes: [18],
effect: null,
},
locked: false,
child: [],
parent: 'ROOT',
},
},
},
];
================================================
FILE: src/constant/text-effects.ts
================================================
export const addAHeading = {
rootId: 'f2d33316-8857-4496-a0c7-3dcc9c4ff981',
layers: {
'f2d33316-8857-4496-a0c7-3dcc9c4ff981': {
type: { resolvedName: 'TextLayer' },
props: {
doc: {
type: 'doc',
content: [
{
type: 'paragraph',
attrs: {
textAlign: 'center',
color: 'rgb(0, 0, 0)',
fontFamily: 'Roboto',
fontSize: '68px',
lineHeight: '1.4',
letterSpacing: 0,
textTransform: '',
marginLeft: null,
indent: 0,
listType: '',
},
content: [
{
type: 'text',
marks: [
{
type: 'bold',
},
{
type: 'color',
attrs: {
color: 'rgb(0, 0, 0)',
},
},
],
text: 'Add a heading',
},
],
},
],
},
position: { x: 525.12102340009, y: 261.582179409994 },
boxSize: {
width: 536.3009995574356,
height: 95,
x: 523.1390728476821,
y: 259.9867549668875,
},
scale: 1,
rotate: 0,
fonts: [
{
name: 'Roboto',
fonts: [
{
style: 'Bold',
urls: [
'https://lidojs-fonts.s3.us-east-2.amazonaws.com/Roboto/Roboto-Bold.woff2',
],
},
{
style: 'Bold_Italic',
urls: [
'https://lidojs-fonts.s3.us-east-2.amazonaws.com/Roboto/Roboto-Bold.woff2',
],
},
{
style: 'Bold',
urls: [
'https://lidojs-fonts.s3.us-east-2.amazonaws.com/Roboto/Roboto-Bold.woff2',
],
},
{
urls: [
'https://lidojs-fonts.s3.us-east-2.amazonaws.com/Roboto/Roboto-Regular.woff2',
],
},
{
style: 'Italic',
urls: [
'https://lidojs-fonts.s3.us-east-2.amazonaws.com/Roboto/Roboto-Regular.woff2',
],
},
{
urls: [
'https://lidojs-fonts.s3.us-east-2.amazonaws.com/Roboto/Roboto-Regular.woff2',
],
},
],
},
],
colors: ['rgb(0, 0, 0)'],
fontSizes: [68],
},
locked: false,
child: [],
parent: 'ROOT',
},
},
};
export const addASubheading = {
rootId: '9cc89a8c-49d5-4f90-9964-f65bbe90db92',
layers: {
'9cc89a8c-49d5-4f90-9964-f65bbe90db92': {
type: { resolvedName: 'TextLayer' },
props: {
doc: {
type: 'doc',
content: [
{
type: 'paragraph',
attrs: {
textAlign: 'center',
color: 'rgb(0, 0, 0)',
fontFamily: 'Roboto',
fontSize: '38px',
lineHeight: '1.4',
letterSpacing: 0,
textTransform: '',
marginLeft: null,
indent: 0,
listType: '',
},
content: [
{
type: 'text',
marks: [
{
type: 'bold',
},
{
type: 'color',
attrs: {
color: 'rgb(0, 0, 0)',
},
},
],
text: 'Add a subheading',
},
],
},
],
},
position: { x: 519.12102340009, y: 365.582179409994 },
boxSize: {
width: 536.3009995574356,
height: 53,
x: 523.1390728476821,
y: 259.9867549668875,
},
scale: 1,
rotate: 0,
fonts: [
{
name: 'Roboto',
fonts: [
{
style: 'Bold',
urls: [
'https://lidojs-fonts.s3.us-east-2.amazonaws.com/Roboto/Roboto-Bold.woff2',
],
},
{
style: 'Bold_Italic',
urls: [
'https://lidojs-fonts.s3.us-east-2.amazonaws.com/Roboto/Roboto-Bold.woff2',
],
},
{
style: 'Bold',
urls: [
'https://lidojs-fonts.s3.us-east-2.amazonaws.com/Roboto/Roboto-Bold.woff2',
],
},
{
urls: [
'https://lidojs-fonts.s3.us-east-2.amazonaws.com/Roboto/Roboto-Regular.woff2',
],
},
{
style: 'Italic',
urls: [
'https://lidojs-fonts.s3.us-east-2.amazonaws.com/Roboto/Roboto-Regular.woff2',
],
},
{
urls: [
'https://lidojs-fonts.s3.us-east-2.amazonaws.com/Roboto/Roboto-Regular.woff2',
],
},
],
},
],
colors: ['rgb(0, 0, 0)'],
fontSizes: [38],
},
locked: false,
child: [],
parent: 'ROOT',
},
},
};
export const addABodyText = {
rootId: '3cace409-216f-4e0e-9449-9248901c8c94',
layers: {
'3cace409-216f-4e0e-9449-9248901c8c94': {
type: { resolvedName: 'TextLayer' },
props: {
doc: {
type: 'doc',
content: [
{
type: 'paragraph',
attrs: {
textAlign: 'center',
color: 'rgb(0, 0, 0)',
fontFamily: 'Roboto',
fontSize: '26px',
lineHeight: '1.4',
letterSpacing: 0,
textTransform: '',
marginLeft: null,
indent: 0,
listType: '',
},
content: [
{
type: 'text',
marks: [
{
type: 'color',
attrs: {
color: 'rgb(0, 0, 0)',
},
},
],
text: 'Add a little bit of body text',
},
],
},
],
},
position: { x: 508.99988653474736, y: 434.903672486454 },
boxSize: {
width: 536.3009995574356,
height: 36,
x: 523.1390728476821,
y: 259.9867549668875,
},
scale: 1,
rotate: 0,
fonts: [
{
name: 'Roboto',
fonts: [
{
style: 'Bold',
urls: [
'https://lidojs-fonts.s3.us-east-2.amazonaws.com/Roboto/Roboto-Bold.woff2',
],
},
{
style: 'Bold_Italic',
urls: [
'https://lidojs-fonts.s3.us-east-2.amazonaws.com/Roboto/Roboto-Bold.woff2',
],
},
{
style: 'Bold',
urls: [
'https://lidojs-fonts.s3.us-east-2.amazonaws.com/Roboto/Roboto-Bold.woff2',
],
},
{
urls: [
'https://lidojs-fonts.s3.us-east-2.amazonaws.com/Roboto/Roboto-Regular.woff2',
],
},
{
style: 'Italic',
urls: [
'https://lidojs-fonts.s3.us-east-2.amazonaws.com/Roboto/Roboto-Regular.woff2',
],
},
{
urls: [
'https://lidojs-fonts.s3.us-east-2.amazonaws.com/Roboto/Roboto-Regular.woff2',
],
},
],
},
],
colors: ['rgb(0, 0, 0)'],
fontSizes: [26],
},
locked: false,
child: [],
parent: 'ROOT',
},
},
};
================================================
FILE: src/features/design/components/LidoJSEditor.tsx
================================================
'use client';
import type { FontData } from '@lidojs/design-core';
import { Editor, type GetFontQuery, PageControl } from '@lidojs/design-editor';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { EditorContent } from './editor-content';
import { EditorHeader } from './editor-header';
import { LayerSettings } from './layer-settings';
import { PreviewModal } from './preview';
import { Sidebar } from './sidebar';
export const LidoJSEditor = ({
googleFontList,
}: { googleFontList: FontData[] }) => {
const leftSidebarRef = useRef<HTMLDivElement>(null);
const [openPreview, setOpenPreview] = useState(false);
const getFonts = useCallback(
async (query: GetFontQuery) => {
return googleFontList
.filter((i) => !query.q || i.name.toLowerCase().includes(query.q))
.slice(
Number.parseInt(query?.offset ?? '', 10),
Number.parseInt(query?.offset ?? '', 10) +
Number.parseInt(query?.limit || '', 10),
);
},
[googleFontList],
);
const [viewPortHeight, setViewPortHeight] = useState<number>();
useEffect(() => {
if (!window) return;
const windowHeight = () => {
setViewPortHeight(window.innerHeight);
};
window.addEventListener('resize', windowHeight);
windowHeight();
return () => {
window.removeEventListener('resize', windowHeight);
};
}, []);
const config = useMemo(
() => ({
assetPath: './assets',
frame: {
defaultImage: {
url: './assets/images/frame-placeholder.png',
width: 1200,
height: 800,
},
},
}),
[],
);
const uploadImage = async (file: File) => {
// TODO: to integrate with image manipulation then need update this
return new Promise<{ url: string; thumb: string }>((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
resolve({
url: reader.result as string,
thumb: reader.result as string,
});
};
reader.onerror = reject;
});
};
return (
<Editor config={config} getFonts={getFonts} uploadImage={uploadImage}>
<div
css={{
display: 'flex',
flexDirection: 'column',
width: '100vw',
height: '100vh',
maxHeight: viewPortHeight ? `${viewPortHeight}px` : 'auto',
}}
>
<EditorHeader openPreview={() => setOpenPreview(true)} />
{openPreview && <PreviewModal onClose={() => setOpenPreview(false)} />}
<div
css={{
display: 'flex',
flexDirection: 'row',
flex: 'auto',
overflow: 'auto',
background: '#EBECF0',
'@media (max-width: 900px)': {
flexDirection: 'column-reverse',
},
}}
>
<div
ref={leftSidebarRef}
css={{
display: 'flex',
background: 'white',
}}
>
<Sidebar />
</div>
<div
css={{
flexGrow: 1,
position: 'relative',
display: 'flex',
flexDirection: 'column',
overflow: 'auto',
}}
>
<LayerSettings />
<div
css={{
flexGrow: 1,
overflow: 'auto',
display: 'flex',
flexDirection: 'column',
}}
>
<EditorContent />
</div>
<div
css={{
height: 40,
background: '#fff',
borderTop: '1px solid rgba(57,76,96,.15)',
display: 'grid',
alignItems: 'center',
flexShrink: 0,
'@media (max-width: 900px)': {
display: 'none',
},
}}
>
<PageControl />
</div>
</div>
</div>
</div>
</Editor>
);
};
================================================
FILE: src/features/design/components/editor-content/EditorContent.tsx
================================================
import { DesignFrame } from '@lidojs/design-editor';
import { data } from '../../../../constant/data';
export const EditorContent = () => {
return <DesignFrame data={data} />;
};
================================================
FILE: src/features/design/components/editor-content/index.ts
================================================
export * from './EditorContent';
================================================
FILE: src/features/design/components/editor-header/EditorHeader.tsx
================================================
import ArrowClockwiseIcon from '@duyank/icons/regular/ArrowClockwise';
import ArrowCounterClockwiseIcon from '@duyank/icons/regular/ArrowCounterClockwise';
import GithubLogoIcon from '@duyank/icons/regular/GithubLogo';
import PlayCircleIcon from '@duyank/icons/regular/PlayCircle';
import { useEditor } from '@lidojs/design-editor';
import {
type ChangeEvent,
type ForwardRefRenderFunction,
forwardRef,
useRef,
} from 'react';
import { downloadObjectAsJson } from '../../../../utils/download';
interface HeaderLayoutProps {
openPreview: () => void;
}
const EditorHeaderForwardRef: ForwardRefRenderFunction<
HTMLDivElement,
HeaderLayoutProps
> = ({ openPreview }, ref) => {
const uploadRef = useRef<HTMLInputElement>(null);
const { actions, query } = useEditor();
const handleExport = () => {
downloadObjectAsJson('file', query.serialize());
};
const handleImport = (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = () => {
const fileContent = JSON.parse(reader.result as string);
actions.setData(fileContent);
};
reader.readAsText(file);
e.target.value = '';
}
};
return (
<div
ref={ref}
css={{
background: '#1E1E2D',
padding: '12px 32px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
'@media (max-width: 900px)': {
padding: 12,
},
}}
>
<div
css={{
color: '#3d8eff',
fontSize: 36,
}}
>
<div
css={{ color: 'white', height: 42, paddingTop: 6, paddingBottom: 6 }}
>
<a href="https://lidojs.com" rel="noreferrer" target="_blank">
<img
alt="LidoJs"
css={{ maxHeight: '100%' }}
src="./assets/logo.png"
/>
</a>
</div>
</div>
<div css={{ display: 'flex', alignItems: 'center', gap: 32 }}>
<div css={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div
css={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#fff',
background: '#3a3a4c',
width: 36,
height: 36,
borderRadius: '50%',
cursor: query.history.canUndo() ? 'pointer' : undefined,
opacity: query.history.canUndo() ? 1 : 0.5,
':hover': {
background: query.history.canUndo()
? 'rgba(58,58,76,0.5)'
: undefined,
},
}}
onClick={actions.history.undo}
>
<ArrowCounterClockwiseIcon />
</div>
<div
css={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#fff',
background: '#3a3a4c',
width: 36,
height: 36,
borderRadius: '50%',
cursor: query.history.canRedo() ? 'pointer' : undefined,
opacity: query.history.canRedo() ? 1 : 0.5,
':hover': {
background: query.history.canRedo()
? 'rgba(58,58,76,0.5)'
: undefined,
},
}}
onClick={actions.history.redo}
>
<ArrowClockwiseIcon />
</div>
</div>
<a
href="https://github.com/lidojs/canva-clone"
rel="noreferrer"
target="_blank"
>
<span
css={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#fff',
background: '#3a3a4c',
width: 36,
height: 36,
borderRadius: '50%',
cursor: 'pointer',
':hover': {
background: 'rgba(58,58,76,0.5)',
},
}}
>
<GithubLogoIcon />
</span>
</a>
<div
css={{
cursor: 'pointer',
color: '#fff',
fontWeight: 700,
':hover': {
textDecoration: 'underline',
},
'@media (max-width: 900px)': {
display: 'none',
},
}}
onClick={() => uploadRef.current?.click()}
>
<input
ref={uploadRef}
accept="application/json"
css={{ display: 'none' }}
type="file"
onChange={handleImport}
/>
Import
</div>
<div
css={{
cursor: 'pointer',
color: '#fff',
fontWeight: 700,
':hover': {
textDecoration: 'underline',
},
'@media (max-width: 900px)': {
display: 'none',
},
}}
onClick={() => handleExport()}
>
Export
</div>
<div
css={{
display: 'flex',
alignItems: 'center',
color: '#fff',
lineHeight: 1,
background: '#3a3a4c',
padding: '8px 14px',
borderRadius: 8,
cursor: 'pointer',
':hover': {
background: 'rgba(58,58,76,0.5)',
},
'@media (max-width: 900px)': {
display: 'none',
},
}}
onClick={openPreview}
>
<div css={{ marginRight: 4, fontSize: 20 }}>
<PlayCircleIcon />
</div>
Preview
</div>
</div>
</div>
);
};
export const EditorHeader = forwardRef(EditorHeaderForwardRef);
================================================
FILE: src/features/design/components/editor-header/index.ts
================================================
export * from './EditorHeader';
================================================
FILE: src/features/design/components/index.ts
================================================
export * from './LidoJSEditor';
================================================
FILE: src/features/design/components/layer-settings/LayerSettings.tsx
================================================
import {
LayerSettings as EditorLayerSettings,
useSelectedLayers,
} from '@lidojs/design-editor';
export const LayerSettings = () => {
const { selectedLayerIds } = useSelectedLayers();
return (
<div
css={{
background: 'white',
borderBottom: '1px solid rgba(57,76,96,.15)',
height: 50,
overflowX: 'auto',
flexShrink: 0,
'@media (max-width: 900px)': {
position: 'fixed',
bottom: 0,
left: 0,
right: 0,
background: '#fff',
display: selectedLayerIds.length > 0 ? 'flex' : 'none',
justifyContent: 'center',
zIndex: 20,
height: 72,
},
}}
>
<EditorLayerSettings />
</div>
);
};
================================================
FILE: src/features/design/components/layer-settings/index.ts
================================================
export * from './LayerSettings';
================================================
FILE: src/features/design/components/preview/PreviewModal.tsx
================================================
import XIcon from '@duyank/icons/regular/X';
import { Preview } from '@lidojs/design-editor';
import type { FC } from 'react';
interface PreviewModalProps {
onClose: () => void;
}
export const PreviewModal: FC<PreviewModalProps> = ({ onClose }) => {
return (
<div
css={{
position: 'fixed',
inset: 0,
zIndex: 1040,
background: 'rgba(13,18,22,.95)',
}}
>
<Preview />
<div
css={{
background: 'rgba(255,255,255,0.3)',
width: 60,
height: 60,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
position: 'fixed',
right: 24,
top: 24,
borderRadius: '50%',
fontSize: 36,
color: '#fff',
cursor: 'pointer',
}}
onClick={onClose}
>
<XIcon />
</div>
</div>
);
};
================================================
FILE: src/features/design/components/preview/index.ts
================================================
export * from './PreviewModal';
================================================
FILE: src/features/design/components/sidebar/DrawContent.tsx
================================================
import XBoldIcon from '@duyank/icons/bold/XBold';
import { useEditor } from '@lidojs/design-editor';
import { useDraw } from '@lidojs/draw';
import { type FC, useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { Highlighter } from '../../../../shared/icons/pencil/Highlighter';
import { Marker } from '../../../../shared/icons/pencil/Marker';
import { Pencil } from '../../../../shared/icons/pencil/Pencil';
export const DrawContent: FC<{ onClose: () => void }> = ({ onClose }) => {
const svgRef = useRef<SVGSVGElement>(null);
const boxRef = useRef<HTMLDivElement>(null);
const penColorRef = useRef('#0571d3');
const penWidthRef = useRef(5);
const scaleRef = useRef(1);
const transparencyRef = useRef(1);
const [size, setSize] = useState({ width: 0, height: 0 });
const { actions, scale, activePage, sidebar } = useEditor((state) => ({
scale: state.scale,
activePage: state.activePage,
sidebar: state.sidebar,
}));
useEffect(() => {
scaleRef.current = scale;
}, [scale]);
useDraw({
options: {
width: penWidthRef.current * scaleRef.current,
minWidth: 33 * scaleRef.current,
minHeight: 33 * scaleRef.current,
},
canStartDraw: (e) => {
const page = document.getElementById(`lidojs-page-${activePage}`);
if (!page) {
throw new Error("Can't find page");
}
const rect = page.getBoundingClientRect();
if (
e.x >= rect.x &&
e.x <= rect.x + rect.width &&
e.y >= rect.y &&
e.y <= rect.y + rect.height &&
!sidebar &&
boxRef.current
) {
boxRef.current.style.pointerEvents = 'auto';
boxRef.current.style.opacity = transparencyRef.current.toString();
return true;
}
return false;
},
onChange: (path) => {
if (!svgRef.current) return;
const svgPath = document.createElementNS(
'http://www.w3.org/2000/svg',
'path',
);
svgPath.setAttributeNS(null, 'd', path);
svgPath.setAttributeNS(null, 'fill', 'none');
svgPath.setAttributeNS(null, 'stroke', penColorRef.current);
svgPath.setAttributeNS(null, 'stroke-linecap', 'round');
svgPath.setAttributeNS(
null,
'stroke-width',
(penWidthRef.current * scaleRef.current).toString(),
);
svgRef.current.appendChild(svgPath);
},
onEnd: (path, boxSize, position) => {
const page = document.getElementById(`lidojs-page-${activePage}`);
if (page) {
const rect = page.getBoundingClientRect();
const p = {
x: (position.x - rect.x) / scaleRef.current,
y: (position.y - rect.y) / scaleRef.current,
};
actions.addDrawLayer(
{ path, color: penColorRef.current, width: penWidthRef.current },
{
width: boxSize.width / scaleRef.current,
height: boxSize.height / scaleRef.current,
},
p,
1 / scaleRef.current,
transparencyRef.current,
);
}
if (boxRef.current) {
boxRef.current.style.pointerEvents = 'none';
}
if (svgRef.current) {
svgRef.current.innerHTML = '';
}
},
});
useEffect(() => {
if (!window) return;
const { innerWidth: width, innerHeight: height } = window;
setSize({ width, height });
}, []);
return (
<>
<div
css={{
position: 'absolute',
left: 72,
top: 500,
zIndex: 10,
overflow: 'hidden',
width: 120,
height: 250,
paddingTop: 60,
paddingBottom: 60,
}}
>
<div
css={{
background: '#FFFFFF',
boxShadow:
'0px 0px 0px 1px rgba(64,87,109,.04),0px 6px 20px -4px rgba(64,87,109,.3)',
width: 30,
height: 30,
borderRadius: 9999,
zIndex: 10,
top: 0,
left: 30,
position: 'absolute',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
}}
onClick={() => onClose()}
>
<XBoldIcon />
</div>
<div
css={{
background: '#ffffff',
borderRadius: 20,
inset: 0,
top: 40,
right: 40,
bottom: 10,
position: 'absolute',
boxShadow:
'0px 0px 0px 1px rgba(64,87,109,.04),0px 6px 20px -4px rgba(64,87,109,.3)',
}}
/>
<div
css={{
display: 'flex',
flexDirection: 'column',
gap: 8,
position: 'relative',
}}
>
<div
css={{
color: 'rgb(5, 113, 211)',
marginLeft: '-30px',
cursor: 'pointer',
':hover': {
marginLeft: 0,
},
}}
onClick={() => {
penColorRef.current = '#0571d3';
penWidthRef.current = 5;
transparencyRef.current = 1;
}}
>
<Pencil width="100px" />
</div>
<div
css={{
color: 'rgb(231, 25, 31)',
marginLeft: '-30px',
cursor: 'pointer',
':hover': {
marginLeft: 0,
},
}}
onClick={() => {
penColorRef.current = '#e7171f';
penWidthRef.current = 10;
transparencyRef.current = 1;
}}
>
<Marker width="100px" />
</div>
<div
css={{
color: 'rgb(255, 242, 52)',
marginLeft: '-30px',
cursor: 'pointer',
':hover': {
marginLeft: 0,
},
}}
onClick={() => {
penColorRef.current = '#fff234';
penWidthRef.current = 20;
transparencyRef.current = 0.8;
}}
>
<Highlighter width="100px" />
</div>
</div>
</div>
{createPortal(
<div
ref={boxRef}
css={{ position: 'absolute', inset: 0 }}
style={{ pointerEvents: 'none' }}
>
<svg
ref={svgRef}
height={size.height}
viewBox={`0 0 ${size.width} ${size.height}`}
width={size.width}
xmlns="http://www.w3.org/2000/svg"
/>
</div>,
document.body,
)}
</>
);
};
================================================
FILE: src/features/design/components/sidebar/FrameContent.tsx
================================================
import XIcon from '@duyank/icons/regular/X';
import type { LayerId, LayerType, SerializedLayers } from '@lidojs/design-core';
import { useEditor } from '@lidojs/design-editor';
import axios from 'axios';
import { type FC, useState } from 'react';
import { isMobile } from 'react-device-detect';
import { useAsync } from 'react-use';
interface Frame {
id: string;
img: string;
clipPath: string;
width: number;
height: number;
}
export const FrameContent: FC<{ onClose: () => void }> = ({ onClose }) => {
const [frames, setFrames] = useState<Frame[]>([]);
const [isLoading, setIsLoading] = useState(true);
const { actions, query } = useEditor();
useAsync(async () => {
const response = await axios.get<Frame[]>('/frames');
setFrames(response.data);
setIsLoading(false);
}, []);
const addFrame = async (data: Frame) => {
actions.addFrameLayer(data, data.clipPath);
if (isMobile) {
onClose();
}
};
const handleDrag = (event: React.DragEvent, frame: Frame) => {
const { clientX, clientY } = event;
const pageSize = query.getPageSize(query.activePage());
const ratio = pageSize.width / pageSize.height;
const frameRatio = frame.width / frame.height;
const scale =
ratio > frameRatio
? (pageSize.height * 0.5) / frame.height
: (pageSize.width * 0.5) / frame.width;
const data: {
layer: LayerType;
data: { rootId: LayerId; layers: SerializedLayers };
} = {
layer: 'Frame',
data: {
rootId: frame.id,
layers: {
[frame.id]: {
type: {
resolvedName: 'FrameLayer',
},
props: {
clipPath: frame.clipPath,
position: {
x: 0,
y: 0,
},
boxSize: {
width: frame.width * scale,
height: frame.height * scale,
},
rotate: 0,
scale,
},
locked: false,
parent: 'ROOT',
child: [],
},
},
},
};
actions.startDragNDrop(data, { x: clientX, y: clientY });
event.dataTransfer.clearData('text/plain');
event.dataTransfer.setData('text/plain', JSON.stringify(data));
event.dataTransfer.setDragImage(new Image(), 0, 0);
};
return (
<div
css={{
width: '100%',
height: '100%',
flexDirection: 'column',
overflowY: 'auto',
display: 'flex',
}}
>
<div
css={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
height: 48,
borderBottom: '1px solid rgba(57,76,96,.15)',
padding: '0 20px',
}}
>
<p
css={{
lineHeight: '48px',
fontWeight: 600,
color: '#181C32',
flexGrow: 1,
}}
>
Frames
</p>
<div
css={{
fontSize: 20,
flexShrink: 0,
width: 32,
height: 32,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
onClick={onClose}
>
<XIcon />
</div>
</div>
<div
css={{ flexDirection: 'column', overflowY: 'auto', display: 'flex' }}
>
<div
css={{
flexGrow: 1,
overflowY: 'auto',
display: 'grid',
gridTemplateColumns: 'repeat(3,minmax(0,1fr))',
gridGap: 8,
padding: '16px',
}}
>
{isLoading && <div>Loading...</div>}
{frames.map((item, index) => (
<div
key={index}
css={{
cursor: 'pointer',
position: 'relative',
'-webkit-user-drag': 'element',
}}
onClick={() => addFrame(item)}
onDragStart={(e) => handleDrag(e, item)}
>
<div css={{ paddingBottom: '100%' }} />
<div
css={{
position: 'absolute',
top: 0,
left: 0,
height: '100%',
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<img
alt={item.img}
css={{
maxHeight: '100%',
maxWidth: '100%',
}}
src={item.img}
/>
</div>
</div>
))}
</div>
</div>
</div>
);
};
================================================
FILE: src/features/design/components/sidebar/GraphicContent.tsx
================================================
import MagnifyingGlassIcon from '@duyank/icons/regular/MagnifyingGlass';
import XIcon from '@duyank/icons/regular/X';
import { useEventCallback } from '@lidojs/design-core';
import { useEditor } from '@lidojs/design-editor';
import axios from 'axios';
import { type FC, type FormEvent, useEffect, useRef, useState } from 'react';
import { isMobile } from 'react-device-detect';
import { useAsync } from 'react-use';
import Masonry from '../../../../shared/components/masonry/Masonry';
export const GraphicContent: FC<{ onClose: () => void }> = ({ onClose }) => {
const qRef = useRef<HTMLInputElement>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const dataRef = useRef(false);
const [images, setImages] = useState<
{
id: string;
thumb: string;
downloadUrl: string;
}[]
>([]);
const [isLoading, setIsLoading] = useState(true);
const [keyword, setKeyword] = useState('');
const { actions } = useEditor();
const loadGraphicList = useEventCallback(async (offset: number) => {
dataRef.current = true;
setIsLoading(true);
const params = {
limit: '100',
offset: `${offset}`,
q: keyword,
};
const response = await axios.get<
{
id: string;
thumb: string;
downloadUrl: string;
}[]
>(`/graphics?${new URLSearchParams(params).toString()}`);
if (offset) {
setImages((prevState) => {
prevState.push(...response.data);
return prevState;
});
} else {
setImages(response.data);
}
setIsLoading(false);
if (response.data.length > 0) {
dataRef.current = false;
}
});
useAsync(async () => {
await loadGraphicList(0);
}, [loadGraphicList]);
const handleLoadMore = useEventCallback(async (e: Event) => {
const node = e.target as HTMLDivElement;
if (
node.scrollHeight - node.scrollTop - 80 <= node.clientHeight &&
!dataRef.current
) {
await loadGraphicList(images.length);
}
});
useEffect(() => {
const ele = scrollRef.current;
ele?.addEventListener('scroll', handleLoadMore);
return () => {
ele?.removeEventListener('scroll', handleLoadMore);
};
}, [handleLoadMore]);
const handleSearch = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (scrollRef.current) {
scrollRef.current.scrollTop = 0;
}
setKeyword(qRef.current?.value || '');
setTimeout(async () => {
await loadGraphicList(0);
});
};
const addGraphic = async (item: {
id: string;
thumb: string;
downloadUrl: string;
}) => {
const res = await axios.get(
`/graphics/download?url=${window.encodeURIComponent(item.downloadUrl)}`,
);
const file = res.data.file;
const parser = new DOMParser();
const ele = parser.parseFromString(file, 'text/xml')
.documentElement as unknown as SVGElement;
const viewBox = ele.getAttribute('viewBox')?.split(' ') || [];
const width =
viewBox.length === 4 ? +viewBox[2] : +(ele.getAttribute('width') || 100);
const height =
viewBox.length === 4 ? +viewBox[3] : +(ele.getAttribute('height') || 100);
const svgBlob = new Blob([ele.outerHTML], {
type: 'image/svg+xml;charset=utf-8',
});
const svgUrl = URL.createObjectURL(svgBlob);
actions.addSvgLayer(svgUrl, { width, height }, ele);
if (isMobile) {
onClose();
}
};
return (
<div
css={{
width: '100%',
height: '100%',
flexDirection: 'column',
overflowY: 'auto',
display: 'flex',
}}
>
<div
css={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
height: 48,
borderBottom: '1px solid rgba(57,76,96,.15)',
padding: '0 20px',
}}
>
<p
css={{
lineHeight: '48px',
fontWeight: 600,
color: '#181C32',
flexGrow: 1,
}}
>
Graphic
</p>
<div
css={{
fontSize: 20,
flexShrink: 0,
width: 32,
height: 32,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
onClick={onClose}
>
<XIcon />
</div>
</div>
<div
css={{
flexDirection: 'column',
overflowY: 'auto',
display: 'flex',
flexGrow: 1,
}}
>
<div
css={{
borderRadius: 4,
boxShadow: '0 0 0 1px rgba(43,59,74,.3)',
margin: 16,
}}
>
<div
css={{
height: 40,
borderRadius: 4,
padding: '0 12px',
display: 'flex',
alignItems: 'center',
}}
>
<div css={{ fontSize: 24, marginRight: 8, flexShrink: 0 }}>
<MagnifyingGlassIcon />
</div>
<form onSubmit={handleSearch}>
<input
ref={qRef}
css={{ width: '100%', height: '100%' }}
type="text"
/>
</form>
</div>
</div>
<div
ref={scrollRef}
css={{
flexGrow: 1,
overflowY: 'auto',
padding: '16px',
gridGap: 8,
}}
>
<Masonry columnsCount={4} gutter="20px">
{images.map((item) => (
<div
key={item.id}
css={{ cursor: 'pointer' }}
onClick={() => addGraphic(item)}
>
<img
key={item.id}
alt={item.thumb}
loading="lazy"
src={item.thumb}
/>
</div>
))}
</Masonry>
{isLoading && <div>Loading...</div>}
</div>
</div>
</div>
);
};
================================================
FILE: src/features/design/components/sidebar/IframeContent.tsx
================================================
import XIcon from '@duyank/icons/regular/X';
import type {
LayerId,
LayerType,
SerializedLayerTree,
SerializedLayers,
} from '@lidojs/design-core';
import { useEditor } from '@lidojs/design-editor';
import type { FC } from 'react';
import { isMobile } from 'react-device-detect';
import { iframeList } from '../../config/iframe';
export const IframeContent: FC<{ onClose: () => void }> = ({ onClose }) => {
const { actions } = useEditor();
const addIframe = async (elements: SerializedLayerTree) => {
actions.addLayerTree(elements);
if (isMobile) {
onClose();
}
};
const handleDrag = (
event: React.DragEvent,
elements: SerializedLayerTree,
) => {
const data: {
layer: LayerType;
data: { rootId: LayerId; layers: SerializedLayers };
} = {
layer: 'Image',
data: elements,
};
const { clientX, clientY } = event;
actions.startDragNDrop(data, { x: clientX, y: clientY });
event.dataTransfer.clearData('text/plain');
event.dataTransfer.setData('text/plain', JSON.stringify(data));
event.dataTransfer.setDragImage(new Image(), 0, 0);
};
return (
<div
css={{
width: '100%',
height: '100%',
flexDirection: 'column',
overflowY: 'auto',
display: 'flex',
}}
>
<div
css={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
height: 48,
borderBottom: '1px solid rgba(57,76,96,.15)',
padding: '0 20px',
}}
>
<p
css={{
lineHeight: '48px',
fontWeight: 600,
color: '#181C32',
flexGrow: 1,
}}
>
Widgets
</p>
<div
css={{
fontSize: 20,
flexShrink: 0,
width: 32,
height: 32,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
onClick={onClose}
>
<XIcon />
</div>
</div>
<div
css={{ flexDirection: 'column', overflowY: 'auto', display: 'flex' }}
>
<div
css={{
flexGrow: 1,
overflowY: 'auto',
display: 'grid',
gridTemplateColumns: 'repeat(3,minmax(0,1fr))',
gridGap: 8,
padding: '16px',
}}
>
{iframeList.map((item, index) => (
<div
key={index}
css={{
cursor: 'pointer',
position: 'relative',
'-webkit-user-drag': 'element',
}}
onClick={() => addIframe(item.elements[0])}
onDragStart={(e) => handleDrag(e, item.elements[0])}
>
<div css={{ paddingBottom: '100%' }} />
<div
css={{
position: 'absolute',
top: 0,
left: 0,
height: '100%',
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<img
alt={item.img}
css={{
maxHeight: '100%',
maxWidth: '100%',
}}
src={item.img}
/>
</div>
</div>
))}
</div>
</div>
</div>
);
};
================================================
FILE: src/features/design/components/sidebar/ImageContent.tsx
================================================
import MagnifyingGlassIcon from '@duyank/icons/regular/MagnifyingGlass';
import XIcon from '@duyank/icons/regular/X';
import {
type LayerId,
type LayerType,
type SerializedLayers,
useEventCallback,
} from '@lidojs/design-core';
import { useEditor } from '@lidojs/design-editor';
import axios from 'axios';
import type React from 'react';
import { type FC, type FormEvent, useEffect, useRef, useState } from 'react';
import { isMobile } from 'react-device-detect';
import Masonry from 'react-responsive-masonry';
import { useAsync } from 'react-use';
import { Photo } from './Photo';
export const ImageContent: FC<{ onClose: () => void }> = ({ onClose }) => {
const qRef = useRef<HTMLInputElement>(null);
const scrollRef = useRef<HTMLDivElement>(null);
const dataRef = useRef(false);
const [images, setImages] = useState<
{
id: string;
image: string;
thumb: string;
width: number;
height: number;
username: string;
name: string;
}[]
>([]);
const [isLoading, setIsLoading] = useState(true);
const [keyword, setKeyword] = useState('');
const { actions, query } = useEditor((state) => ({
dragNDrop: state.dragNDrop,
}));
const loadImageList = useEventCallback(async (offset = 0) => {
dataRef.current = true;
setIsLoading(true);
const params = {
limit: '40',
page: `${(images.length % 40) + 1}`,
q: keyword,
};
const response = await axios.get<
{
id: string;
image: string;
thumb: string;
width: number;
height: number;
username: string;
name: string;
}[]
>(`/images?${new URLSearchParams(params).toString()}`);
if (offset) {
setImages((prevState) => {
prevState.push(...response.data);
return prevState;
});
} else {
setImages(response.data);
}
setIsLoading(false);
if (response.data.length > 0) {
dataRef.current = false;
}
});
useAsync(async () => {
await loadImageList(0);
}, [loadImageList]);
useEffect(() => {
const handleLoadMore = async (e: Event) => {
const node = e.target as HTMLDivElement;
if (
node.scrollHeight - node.scrollTop - 80 <= node.clientHeight &&
!dataRef.current
) {
await loadImageList(images.length);
}
};
const ele = scrollRef.current;
ele?.addEventListener('scroll', handleLoadMore);
return () => {
ele?.removeEventListener('scroll', handleLoadMore);
};
}, [loadImageList, images]);
const handleSearch = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (scrollRef.current) {
scrollRef.current.scrollTop = 0;
}
setKeyword(qRef.current?.value || '');
setTimeout(async () => {
await loadImageList(0);
});
};
const addImage = async (item: {
id: string;
image: string;
thumb: string;
width: number;
height: number;
username: string;
name: string;
}) => {
actions.addImageLayer(
{ thumb: item.thumb, url: item.image },
{ width: item.width, height: item.height },
);
axios.put(`/images?id=${item.id}`);
if (isMobile) {
onClose();
}
};
const handleDrag = (
event: React.DragEvent,
item: {
id: string;
image: string;
thumb: string;
width: number;
height: number;
username: string;
name: string;
},
) => {
const { clientX, clientY } = event;
const pageSize = query.getPageSize(query.activePage());
const ratio = pageSize.width / pageSize.height;
const imgRatio = item.width / item.height;
const w =
ratio < imgRatio
? pageSize.width * 0.8
: pageSize.height * imgRatio * 0.8;
const h = w / imgRatio;
const data: {
layer: LayerType;
data: { rootId: LayerId; layers: SerializedLayers };
} = {
layer: 'Image',
data: {
rootId: item.id,
layers: {
[item.id]: {
type: {
resolvedName: 'ImageLayer',
},
props: {
image: {
url: item.image,
thumb: item.thumb,
boxSize: {
width: w,
height: h,
},
position: {
x: 0,
y: 0,
},
rotate: 0,
},
position: {
x: 0,
y: 0,
},
boxSize: {
width: w,
height: h,
},
rotate: 0,
},
locked: false,
parent: 'ROOT',
child: [],
},
},
},
};
actions.startDragNDrop(data, { x: clientX, y: clientY });
event.dataTransfer.clearData('text/plain');
event.dataTransfer.setData('text/plain', JSON.stringify(data));
event.dataTransfer.setDragImage(new Image(), 0, 0);
};
return (
<div
css={{
width: '100%',
height: '100%',
flexDirection: 'column',
overflowY: 'auto',
display: 'flex',
}}
>
<div
css={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
height: 48,
borderBottom: '1px solid rgba(57,76,96,.15)',
padding: '0 20px',
}}
>
<p
css={{
lineHeight: '48px',
fontWeight: 600,
color: '#181C32',
flexGrow: 1,
}}
>
Images
</p>
<div
css={{
fontSize: 20,
flexShrink: 0,
width: 32,
height: 32,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
onClick={onClose}
>
<XIcon />
</div>
</div>
<div
css={{
flexDirection: 'column',
overflowY: 'auto',
display: 'flex',
flexGrow: 1,
}}
>
<div
css={{
borderRadius: 4,
boxShadow: '0 0 0 1px rgba(43,59,74,.3)',
margin: 16,
}}
>
<div
css={{
height: 40,
borderRadius: 4,
padding: '0 12px',
display: 'flex',
alignItems: 'center',
}}
>
<div css={{ fontSize: 24, marginRight: 8, flexShrink: 0 }}>
<MagnifyingGlassIcon />
</div>
<form onSubmit={handleSearch}>
<input
ref={qRef}
css={{ width: '100%', height: '100%' }}
type="text"
/>
</form>
</div>
</div>
<div
ref={scrollRef}
css={{
flexGrow: 1,
overflowY: 'auto',
padding: '16px',
gridGap: 8,
}}
>
<Masonry columnsCount={2} gutter="8px">
{images.map((item) => (
<Photo
key={item.id}
image={item.thumb}
name={item.name}
username={item.username}
onClick={() => {
addImage(item);
}}
onDragStart={(e) => handleDrag(e, item)}
/>
))}
</Masonry>
{isLoading && <div>Loading...</div>}
</div>
</div>
<div css={{ flexShrink: 0, paddingLeft: 16, textAlign: 'center' }}>
Photos by
<a href="https://unsplash.com/" rel="noreferrer" target="_blank">
Unsplash
</a>
</div>
</div>
);
};
================================================
FILE: src/features/design/components/sidebar/Photo.tsx
================================================
import { type FC, type HTMLProps, memo, useState } from 'react';
type Props = {
image: string;
name: string;
username: string;
} & HTMLProps<HTMLDivElement>;
export const Photo: FC<Props> = memo(({ image, name, username, ...props }) => {
const [isShow, setIsShow] = useState(false);
return (
<div
css={{
cursor: 'pointer',
position: 'relative',
'-webkit-user-drag': 'element',
}}
{...props}
onMouseOut={() => setIsShow(false)}
onMouseOver={() => setIsShow(true)}
>
<img alt={image} loading="lazy" src={image} />
<p
css={{
display: isShow ? 'block' : 'none',
position: 'absolute',
bottom: 0,
fontSize: 10,
left: 0,
right: 0,
color: '#fff',
textAlign: 'center',
paddingTop: 12,
paddingBottom: 2,
paddingLeft: 4,
paddingRight: 4,
background:
'linear-gradient(0deg, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0.2) 80%, rgba(0, 0, 0, 0) 100%)',
}}
>
Photo by
<a
css={{ color: '#3d8eff' }}
href={`https://unsplash.com/@${username}?utm_source=lidojs&utm_medium=referral`}
rel="noreferrer"
target="_blank"
>
{name}
</a>
on
<a
href={`https://unsplash.com/@${username}?utm_source=lidojs&utm_medium=referral`}
rel="noreferrer"
target="_blank"
>
unsplash
</a>
</p>
</div>
);
});
================================================
FILE: src/features/design/components/sidebar/QrCodeContent.tsx
================================================
import XIcon from '@duyank/icons/regular/X';
import type {
LayerId,
LayerType,
SerializedLayerTree,
SerializedLayers,
} from '@lidojs/design-core';
import { useEditor } from '@lidojs/design-editor';
import type { FC } from 'react';
import { isMobile } from 'react-device-detect';
import { qrCodeList } from '../../config/qrCode';
export const QrCodeContent: FC<{ onClose: () => void }> = ({ onClose }) => {
const { actions } = useEditor();
const addQrCode = async (elements: SerializedLayerTree) => {
actions.addLayerTree(elements);
if (isMobile) {
onClose();
}
};
const handleDrag = (
event: React.DragEvent,
elements: SerializedLayerTree,
) => {
const data: {
layer: LayerType;
data: { rootId: LayerId; layers: SerializedLayers };
} = {
layer: 'Image',
data: elements,
};
const { clientX, clientY } = event;
actions.startDragNDrop(data, { x: clientX, y: clientY });
event.dataTransfer.clearData('text/plain');
event.dataTransfer.setData('text/plain', JSON.stringify(data));
event.dataTransfer.setDragImage(new Image(), 0, 0);
};
return (
<div
css={{
width: '100%',
height: '100%',
flexDirection: 'column',
overflowY: 'auto',
display: 'flex',
}}
>
<div
css={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
height: 48,
borderBottom: '1px solid rgba(57,76,96,.15)',
padding: '0 20px',
}}
>
<p
css={{
lineHeight: '48px',
fontWeight: 600,
color: '#181C32',
flexGrow: 1,
}}
>
QR Code
</p>
<div
css={{
fontSize: 20,
flexShrink: 0,
width: 32,
height: 32,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
onClick={onClose}
>
<XIcon />
</div>
</div>
<div
css={{ flexDirection: 'column', overflowY: 'auto', display: 'flex' }}
>
<div
css={{
flexGrow: 1,
overflowY: 'auto',
display: 'grid',
gridTemplateColumns: 'repeat(3,minmax(0,1fr))',
gridGap: 8,
padding: '16px',
}}
>
{qrCodeList.map((item, index) => (
<div
key={index}
css={{
cursor: 'pointer',
position: 'relative',
'-webkit-user-drag': 'element',
}}
onClick={() => addQrCode(item.elements[0])}
onDragStart={(e) => handleDrag(e, item.elements[0])}
>
<div css={{ paddingBottom: '100%' }} />
<div
css={{
position: 'absolute',
top: 0,
left: 0,
height: '100%',
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<img
alt={item.img}
css={{
maxHeight: '100%',
maxWidth: '100%',
}}
src={item.img}
/>
</div>
</div>
))}
</div>
</div>
</div>
);
};
================================================
FILE: src/features/design/components/sidebar/ShapeContent.tsx
================================================
import XIcon from '@duyank/icons/regular/X';
import type { LayerId, LayerType, SerializedLayers } from '@lidojs/design-core';
import { useEditor } from '@lidojs/design-editor';
import type { FC } from 'react';
import { isMobile } from 'react-device-detect';
import { v4 } from 'uuid';
import { type Line, lines } from '../../config/line';
import { type Shape, shapes } from '../../config/shape';
export const ShapeContent: FC<{ onClose: () => void }> = ({ onClose }) => {
const { actions, query } = useEditor();
const addLine = (props: Line['props']) => {
actions.addLineLayer({ props });
};
const addShape = (shape: Shape) => {
actions.addShapeLayer({
type: {
resolvedName: 'ShapeLayer',
},
props: {
shape: shape.type,
position: {
x: 0,
y: 400,
},
boxSize: {
width: shape.width,
height: shape.height,
},
rotate: 0,
color: '#5E6278',
},
});
if (isMobile) {
onClose();
}
};
const handleDrag = (event: React.DragEvent, shape: Shape) => {
const { clientX, clientY } = event;
const id = v4();
const data: {
layer: LayerType;
data: { rootId: LayerId; layers: SerializedLayers };
} = {
layer: 'Shape',
data: {
rootId: id,
layers: {
[id]: {
type: {
resolvedName: 'ShapeLayer',
},
locked: false,
parent: 'ROOT',
child: [],
props: {
shape: shape.type,
position: {
x: 0,
y: 400,
},
boxSize: {
width: shape.width,
height: shape.height,
},
rotate: 0,
color: '#5E6278',
},
},
},
},
};
actions.startDragNDrop(data, { x: clientX, y: clientY });
event.dataTransfer.clearData('text/plain');
event.dataTransfer.setData('text/plain', JSON.stringify(data));
event.dataTransfer.setDragImage(new Image(), 0, 0);
};
const handleDragLine = (event: React.DragEvent, line: Line) => {
const { clientX, clientY } = event;
const width = query.getPageSize(query.activePage()).width / 2;
const data: {
layer: LayerType;
data: { rootId: LayerId; layers: SerializedLayers };
} = {
layer: 'Line',
data: {
rootId: line.id,
layers: {
[line.id]: {
props: {
...line.props,
boxSize: {
width,
height: 4,
},
position: {
x: 0,
y: 0,
},
style: 'solid',
color: 'rgb(0, 0, 0)',
scale: 1,
rotate: 0,
},
type: {
resolvedName: 'LineLayer',
},
locked: false,
parent: 'ROOT',
child: [],
},
},
},
};
actions.startDragNDrop(data, { x: clientX, y: clientY });
event.dataTransfer.clearData('text/plain');
event.dataTransfer.setData('text/plain', JSON.stringify(data));
event.dataTransfer.setDragImage(new Image(), 0, 0);
};
return (
<div
css={{
width: '100%',
height: '100%',
flexDirection: 'column',
overflowY: 'auto',
display: 'flex',
}}
>
<div
css={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
height: 48,
borderBottom: '1px solid rgba(57,76,96,.15)',
padding: '0 20px',
}}
>
<p
css={{
lineHeight: '48px',
fontWeight: 600,
color: '#181C32',
flexGrow: 1,
}}
>
Shapes
</p>
<div
css={{
fontSize: 20,
flexShrink: 0,
width: 32,
height: 32,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
onClick={onClose}
>
<XIcon />
</div>
</div>
<div css={{ padding: '16px' }}>
<div css={{ padding: '8px 0', fontWeight: 700 }}>
Arrow
<div
css={{
display: 'inline-block',
marginLeft: 6,
background: '#fdebcf',
borderRadius: 9999,
fontSize: 9,
padding: '2px 4px',
}}
>
Business
</div>
</div>
<div
css={{
flexGrow: 1,
overflowY: 'auto',
display: 'grid',
gridTemplateColumns: 'repeat(4,minmax(0,1fr))',
gridGap: 8,
}}
>
{lines.map((l, idx) => (
<div
key={idx}
draggable
css={{
width: '100%',
paddingBottom: '100%',
position: 'relative',
cursor: 'pointer',
'-webkit-user-drag': 'element',
}}
onClick={() => addLine(l.props)}
onDragStart={(e) => handleDragLine(e, l)}
>
<div
css={{
position: 'absolute',
inset: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
{l.icon}
</div>
</div>
))}
</div>
<div css={{ padding: '8px 0', fontWeight: 700 }}>Shape</div>
<div
css={{
flexGrow: 1,
overflowY: 'auto',
display: 'grid',
gridTemplateColumns: 'repeat(4,minmax(0,1fr))',
gridGap: 8,
}}
>
{shapes.map((shape) => (
<div
key={shape.type}
css={{
width: '100%',
paddingBottom: '100%',
position: 'relative',
cursor: 'pointer',
'-webkit-user-drag': 'element',
}}
draggable={true}
onClick={() => addShape(shape)}
onDragStart={(e) => handleDrag(e, shape)}
>
{shape.icon}
</div>
))}
</div>
</div>
</div>
);
};
================================================
FILE: src/features/design/components/sidebar/Sidebar.tsx
================================================
import BrowserIcon from '@duyank/icons/regular/Browser';
import FrameCornersIcon from '@duyank/icons/regular/FrameCorners';
import ImageIcon from '@duyank/icons/regular/Image';
import LayoutIcon from '@duyank/icons/regular/Layout';
import PencilIcon from '@duyank/icons/regular/Pencil';
import PiggyBankIcon from '@duyank/icons/regular/PiggyBank';
import QrCodeIcon from '@duyank/icons/regular/QrCode';
import SquareIcon from '@duyank/icons/regular/Square';
import TableIcon from '@duyank/icons/regular/Table';
import TextTIcon from '@duyank/icons/regular/TextT';
import UploadIcon from '@duyank/icons/regular/Upload';
import VideoIcon from '@duyank/icons/regular/Video';
import { useEditor } from '@lidojs/design-editor';
import { useCallback, useState } from 'react';
import { SidebarTab } from '../tabs';
import { DrawContent } from './DrawContent';
import { FrameContent } from './FrameContent';
import { GraphicContent } from './GraphicContent';
import { IframeContent } from './IframeContent';
import { ImageContent } from './ImageContent';
import { QrCodeContent } from './QrCodeContent';
import { ShapeContent } from './ShapeContent';
import { TableContent } from './TableContent';
import { TemplateContent } from './TemplateContent';
import { TextContent } from './TextContent';
import { UploadContent } from './UploadContent';
import { VideoContent } from './VideoContent';
const tabs = [
{
name: 'Template',
icon: <LayoutIcon />,
},
{
name: 'Text',
icon: <TextTIcon />,
},
{
name: 'Shape',
icon: <SquareIcon />,
},
{
name: 'Table',
icon: <TableIcon />,
isBusiness: true,
},
{
name: 'Frame',
icon: <FrameCornersIcon />,
},
{
name: 'Image',
icon: <ImageIcon />,
},
{
name: 'Graphic',
icon: <PiggyBankIcon />,
},
{
name: 'QrCode',
icon: <QrCodeIcon />,
isBusiness: true,
},
{
name: 'Widgets',
icon: <BrowserIcon />,
},
{
name: 'Draw',
icon: <PencilIcon />,
isBusiness: true,
},
{
name: 'Video',
icon: <VideoIcon />,
},
{
name: 'Upload',
icon: <UploadIcon />,
},
];
export const Sidebar = () => {
const { actions } = useEditor();
const [tab, setTab] = useState<string | null>('Template');
const handleCloseTab = useCallback(() => {
setTab(null);
actions.setSidebar();
}, [actions]);
return (
<div
css={{
display: 'flex',
zIndex: 2,
position: 'relative',
backgroundColor: '#ffffff',
borderRight: '1px solid rgba(217, 219, 228, 0.6)',
}}
>
<div
css={{
display: 'flex',
}}
>
<SidebarTab
active={tab}
tabs={tabs}
onChange={(_, tab) => {
actions.setSidebar();
setTab(tab);
}}
/>
{tab && (
<div
css={{
width: tab === 'Draw' ? 0 : 360,
'@media (max-width: 900px)': {
width: '100%',
position: 'fixed',
bottom: 0,
left: 0,
top: 0,
background: '#fff',
},
}}
>
{tab === 'Template' && (
<TemplateContent
onClose={() => {
setTab(null);
actions.setSidebar();
}}
/>
)}
{tab === 'Text' && <TextContent onClose={handleCloseTab} />}
{tab === 'Frame' && <FrameContent onClose={handleCloseTab} />}
{tab === 'Image' && <ImageContent onClose={handleCloseTab} />}
{tab === 'Graphic' && <GraphicContent onClose={handleCloseTab} />}
{tab === 'QrCode' && <QrCodeContent onClose={handleCloseTab} />}
{tab === 'Widgets' && <IframeContent onClose={handleCloseTab} />}
{tab === 'Shape' && <ShapeContent onClose={handleCloseTab} />}
{tab === 'Table' && <TableContent onClose={handleCloseTab} />}
{tab === 'Video' && <VideoContent onClose={handleCloseTab} />}
{tab === 'Draw' && <DrawContent onClose={handleCloseTab} />}
<UploadContent
visibility={tab === 'Upload'}
onClose={handleCloseTab}
/>
</div>
)}
</div>
<div
css={{
width: 360,
position: 'absolute',
overflow: 'hidden',
top: 0,
left: 73,
height: '100%',
pointerEvents: 'none',
}}
id="settings"
/>
</div>
);
};
================================================
FILE: src/features/design/components/sidebar/TableContent.tsx
================================================
import XIcon from '@duyank/icons/regular/X';
import type { LayerId, LayerType, SerializedLayers } from '@lidojs/design-core';
import { useEditor } from '@lidojs/design-editor';
import type { FC } from 'react';
import { isMobile } from 'react-device-detect';
import { v4 } from 'uuid';
const tableLayer = {
type: {
resolvedName: 'TableLayer',
},
props: {
position: {
x: 458.2781364561637,
y: 229.60681114551085,
},
boxSize: {
width: 838.5314463859182,
height: 282,
x: 344.41332869685516,
y: 226.56694426649582,
},
rotate: 0,
scale: 1,
format: {
cellSpacing: 0,
cellPadding: 10,
rowHeight: [100, 120, 180, 202.38026124818578],
colWidth: [400, 400, 300, 252.3802612481859],
rows: [
{
index: 1,
height: 70,
},
{
index: 2,
height: 70,
},
{
index: 3,
height: 70,
},
{
index: 4,
height: 70,
},
],
columns: [
{
index: 1,
width: 226.64958360025628,
},
{
index: 2,
width: 209.83984625240237,
},
{
index: 3,
width: 200.19218449711718,
},
{
index: 4,
width: 199.8498320361424,
},
],
},
cells: [
{
value: {
type: 'doc',
content: [
{
type: 'paragraph',
attrs: {
textAlign: 'center',
color: 'rgb(84, 84, 84)',
fontFamily: 'Acme',
fontSize: '18px',
lineHeight: '1.4',
letterSpacing: 0,
textTransform: 'uppercase',
marginLeft: null,
indent: 0,
listType: '',
},
content: [
{
type: 'text',
marks: [
{
type: 'bold',
},
{
type: 'color',
attrs: {
color: 'rgb(84, 84, 84)',
},
},
],
text: 'Header 1',
},
],
},
],
},
row: 1,
col: 1,
background: 'rgb(255,255,255)',
border: {
top: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
left: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
right: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
bottom: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
},
},
{
value: {
type: 'doc',
content: [
{
type: 'paragraph',
attrs: {
textAlign: 'center',
color: 'rgb(84, 84, 84)',
fontFamily: 'Acme',
fontSize: '18px',
lineHeight: '1.4',
letterSpacing: 0,
textTransform: 'uppercase',
marginLeft: null,
indent: 0,
listType: '',
},
content: [
{
type: 'text',
marks: [
{
type: 'bold',
},
{
type: 'color',
attrs: {
color: 'rgb(84, 84, 84)',
},
},
],
text: 'Header 1',
},
],
},
],
},
row: 1,
col: 2,
background: 'rgb(255,255,255)',
border: {
top: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
left: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
right: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
bottom: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
},
},
{
value: {
type: 'doc',
content: [
{
type: 'paragraph',
attrs: {
textAlign: 'center',
color: 'rgb(84, 84, 84)',
fontFamily: 'Acme',
fontSize: '18px',
lineHeight: '1.4',
letterSpacing: 0,
textTransform: 'uppercase',
marginLeft: null,
indent: 0,
listType: '',
},
content: [
{
type: 'text',
marks: [
{
type: 'bold',
},
{
type: 'color',
attrs: {
color: 'rgb(84, 84, 84)',
},
},
],
text: 'Header 3',
},
],
},
],
},
row: 1,
col: 3,
background: 'rgb(255,255,255)',
border: {
top: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
left: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
right: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
bottom: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
},
},
{
value: {
type: 'doc',
content: [
{
type: 'paragraph',
attrs: {
textAlign: 'center',
color: 'rgb(84, 84, 84)',
fontFamily: 'Acme',
fontSize: '18px',
lineHeight: '1.4',
letterSpacing: 0,
textTransform: 'uppercase',
marginLeft: null,
indent: 0,
listType: '',
},
content: [
{
type: 'text',
marks: [
{
type: 'bold',
},
{
type: 'color',
attrs: {
color: 'rgb(84, 84, 84)',
},
},
],
text: 'Header 4',
},
],
},
],
},
row: 1,
col: 4,
background: 'rgb(255,255,255)',
border: {
top: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
left: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
right: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
bottom: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
},
},
{
value: {
type: 'doc',
content: [
{
type: 'paragraph',
attrs: {
textAlign: 'center',
color: 'rgb(0, 0, 0)',
fontFamily: 'Akatab',
fontSize: '10px',
lineHeight: '1.4',
letterSpacing: 0,
textTransform: 'uppercase',
marginLeft: null,
indent: 0,
listType: '',
},
content: [
{
type: 'text',
marks: [
{
type: 'color',
attrs: {
color: 'rgb(0, 0, 0)',
},
},
],
text: 'Content For Column 1',
},
],
},
],
},
row: 2,
col: 1,
background: 'rgb(255,255,255)',
border: {
top: {
width: 1,
color: 'rgb(228, 5, 5)',
style: 'solid',
},
left: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
right: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
bottom: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
},
},
{
value: {
type: 'doc',
content: [
{
type: 'paragraph',
attrs: {
textAlign: 'center',
color: 'rgb(0, 0, 0)',
fontFamily: 'Akatab',
fontSize: '10px',
lineHeight: '1.4',
letterSpacing: 0,
textTransform: 'uppercase',
marginLeft: null,
indent: 0,
listType: '',
},
content: [
{
type: 'text',
text: 'Content For Column 2',
},
],
},
],
},
row: 2,
col: 2,
background: 'rgb(255,255,255)',
border: {
top: {
width: 1,
color: 'rgb(96, 25, 211)',
style: 'solid',
},
left: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
right: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
bottom: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
},
},
{
value: {
type: 'doc',
content: [
{
type: 'paragraph',
attrs: {
textAlign: 'center',
color: 'rgb(0, 0, 0)',
fontFamily: 'Akatab',
fontSize: '10px',
lineHeight: '1.4',
letterSpacing: 0,
textTransform: 'uppercase',
marginLeft: null,
indent: 0,
listType: '',
},
content: [
{
type: 'text',
marks: [
{
type: 'color',
attrs: {
color: 'rgb(0, 0, 0)',
},
},
],
text: 'Content For Column 3',
},
],
},
],
},
row: 2,
col: 3,
background: 'rgb(255,255,255)',
border: {
top: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
left: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
right: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
bottom: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
},
},
{
row: 2,
col: 4,
value: {
type: 'doc',
content: [
{
type: 'paragraph',
attrs: {
textAlign: 'center',
color: 'rgb(0, 0, 0)',
fontFamily: 'Akatab',
fontSize: '10px',
lineHeight: '1.4',
letterSpacing: 0,
textTransform: 'uppercase',
marginLeft: null,
indent: 0,
listType: '',
},
content: [
{
type: 'text',
text: 'Content For Column 4',
},
],
},
],
},
background: 'rgb(255,255,255)',
border: {
top: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
left: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
right: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
bottom: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
},
},
{
row: 3,
col: 3,
value: {
type: 'doc',
content: [
{
type: 'paragraph',
attrs: {
textAlign: 'center',
color: 'rgb(0, 0, 0)',
fontFamily: 'Akatab',
fontSize: '10px',
lineHeight: '1.4',
letterSpacing: 0,
textTransform: 'uppercase',
marginLeft: null,
indent: 0,
listType: '',
},
content: [
{
type: 'text',
text: 'Content For Column 3',
},
],
},
],
},
background: 'rgb(255,255,255)',
border: {
top: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
left: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
right: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
bottom: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
},
},
{
row: 3,
col: 4,
value: {
type: 'doc',
content: [
{
type: 'paragraph',
attrs: {
textAlign: 'center',
color: 'rgb(0, 0, 0)',
fontFamily: 'Akatab',
fontSize: '10px',
lineHeight: '1.4',
letterSpacing: 0,
textTransform: 'uppercase',
marginLeft: null,
indent: 0,
listType: '',
},
content: [
{
type: 'text',
text: 'Content For Column 4',
},
],
},
],
},
background: 'rgb(255,255,255)',
border: {
top: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
left: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
right: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
bottom: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
},
},
{
value: {
type: 'doc',
content: [
{
type: 'paragraph',
attrs: {
textAlign: 'center',
color: 'rgb(0, 0, 0)',
fontFamily: 'Akatab',
fontSize: '10px',
lineHeight: '1.4',
letterSpacing: 0,
textTransform: 'uppercase',
marginLeft: null,
indent: 0,
listType: '',
},
content: [
{
type: 'text',
text: 'Content For Column 1',
},
],
},
],
},
row: 3,
col: 1,
background: 'rgb(255,255,255)',
border: {
top: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
left: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
right: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
bottom: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
},
},
{
value: {
type: 'doc',
content: [
{
type: 'paragraph',
attrs: {
textAlign: 'center',
color: 'rgb(0, 0, 0)',
fontFamily: 'Akatab',
fontSize: '10px',
lineHeight: '1.4',
letterSpacing: 0,
textTransform: 'uppercase',
marginLeft: null,
indent: 0,
listType: '',
},
content: [
{
type: 'text',
text: 'Content For Column 2',
},
],
},
],
},
row: 3,
col: 2,
background: 'rgb(255,255,255)',
border: {
top: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
left: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
right: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
bottom: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
},
},
{
value: {
type: 'doc',
content: [
{
type: 'paragraph',
attrs: {
textAlign: 'center',
color: 'rgb(0, 0, 0)',
fontFamily: 'Akatab',
fontSize: '10px',
lineHeight: '1.4',
letterSpacing: 0,
textTransform: 'uppercase',
marginLeft: null,
indent: 0,
listType: '',
},
content: [
{
type: 'text',
text: 'Content For Column 1',
},
],
},
],
},
col: 1,
row: 4,
background: 'rgb(255,255,255)',
border: {
top: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
left: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
right: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
bottom: {
width: 2,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
},
},
{
row: 4,
col: 2,
value: {
type: 'doc',
content: [
{
type: 'paragraph',
attrs: {
textAlign: 'center',
color: 'rgb(0, 0, 0)',
fontFamily: 'Akatab',
fontSize: '10px',
lineHeight: '1.4',
letterSpacing: 0,
textTransform: 'uppercase',
marginLeft: null,
indent: 0,
listType: '',
},
content: [
{
type: 'text',
text: 'Content For Column 2',
},
],
},
],
},
background: 'rgb(255,255,255)',
border: {
top: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
left: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
right: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
bottom: {
width: 2,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
},
},
{
row: 4,
col: 3,
value: {
type: 'doc',
content: [
{
type: 'paragraph',
attrs: {
textAlign: 'center',
color: 'rgb(0, 0, 0)',
fontFamily: 'Akatab',
fontSize: '10px',
lineHeight: '1.4',
letterSpacing: 0,
textTransform: 'uppercase',
marginLeft: null,
indent: 0,
listType: '',
},
content: [
{
type: 'text',
text: 'Content For Column 3',
},
],
},
],
},
background: 'rgb(255,255,255)',
border: {
top: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
left: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
right: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
bottom: {
width: 2,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
},
},
{
value: {
type: 'doc',
content: [
{
type: 'paragraph',
attrs: {
textAlign: 'center',
color: 'rgb(0, 0, 0)',
fontFamily: 'Akatab',
fontSize: '10px',
lineHeight: '1.4',
letterSpacing: 0,
textTransform: 'uppercase',
marginLeft: null,
indent: 0,
listType: '',
},
content: [
{
type: 'text',
text: 'Content For Column 4',
},
],
},
],
},
col: 4,
row: 4,
background: 'rgb(255,255,255)',
border: {
top: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
left: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
right: {
width: 1,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
bottom: {
width: 2,
color: 'rgb(0, 0, 0)',
style: 'solid',
},
},
},
],
fonts: [
{
name: 'Acme',
fonts: [
{
urls: [
'https://fonts.gstatic.com/s/acme/v25/RrQfboBx-C5_bx3Lb23lzLk.ttf',
],
},
],
},
{
name: 'Akatab',
fonts: [
{
style: 'Bold',
urls: [
'https://fonts.gstatic.com/s/akatab/v7/VuJzdNrK3Z7gqJE3gKLdPKNiaRpFvg.ttf',
],
},
{
urls: [
'https://fonts.gstatic.com/s/akatab/v7/VuJwdNrK3Z7gqJEPWIz5NIh-YA.ttf',
],
},
],
},
],
},
};
export const TableContent: FC<{ onClose: () => void }> = ({ onClose }) => {
const { actions } = useEditor();
const addTable = () => {
actions.addLayer(tableLayer);
if (isMobile) {
onClose();
}
};
const handleDrag = (event: React.DragEvent) => {
const { clientX, clientY } = event;
const id = v4();
const data: {
layer: LayerType;
data: { rootId: LayerId; layers: SerializedLayers };
} = {
layer: 'Table',
data: {
rootId: id,
layers: {
[id]: {
...tableLayer,
locked: false,
parent: 'ROOT',
child: [],
},
},
},
};
actions.startDragNDrop(data, { x: clientX, y: clientY });
event.dataTransfer.clearData('text/plain');
event.dataTransfer.setData('text/plain', JSON.stringify(data));
event.dataTransfer.setDragImage(new Image(), 0, 0);
};
return (
<div
css={{
width: '100%',
height: '100%',
flexDirection: 'column',
overflowY: 'auto',
display: 'flex',
}}
>
<div
css={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
height: 48,
borderBottom: '1px solid rgba(57,76,96,.15)',
padding: '0 20px',
}}
>
<p
css={{
lineHeight: '48px',
fontWeight: 600,
color: '#181C32',
flexGrow: 1,
}}
>
Table
</p>
<div
css={{
fontSize: 20,
flexShrink: 0,
width: 32,
height: 32,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
onClick={onClose}
>
<XIcon />
</div>
</div>
<div css={{ padding: '16px' }}>
<div
css={{ cursor: 'pointer', '-webkit-user-drag': 'element' }}
onClick={addTable}
onDragStart={(e) => handleDrag(e)}
>
<div>
<img src="/assets/images/table/table-1.png" alt="Table" />
</div>
</div>
</div>
</div>
);
};
================================================
FILE: src/features/design/components/sidebar/TemplateContent.tsx
================================================
import XIcon from '@duyank/icons/regular/X';
import type { SerializedPage } from '@lidojs/design-core';
import { useEditor } from '@lidojs/design-editor';
import axios from 'axios';
import { type FC, useState } from 'react';
import { isMobile } from 'react-device-detect';
import { useAsync } from 'react-use';
interface Template {
img: string;
elements: SerializedPage;
}
export const TemplateContent: FC<{ onClose: () => void }> = ({ onClose }) => {
const [templates, setTemplates] = useState<Template[]>([]);
const [isLoading, setIsLoading] = useState(true);
const { actions, activePage } = useEditor((state) => ({
activePage: state.activePage,
}));
useAsync(async () => {
const response = await axios.get<Template[]>('/templates');
setTemplates(response.data);
setIsLoading(false);
}, []);
const addPage = async (data: SerializedPage) => {
actions.setPage(activePage, data);
if (isMobile) {
onClose();
}
};
return (
<div
css={{
width: '100%',
height: '100%',
flexDirection: 'column',
overflowY: 'auto',
display: 'flex',
}}
>
<div
css={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
height: 48,
borderBottom: '1px solid rgba(57,76,96,.15)',
padding: '0 20px',
}}
>
<p
css={{
lineHeight: '48px',
fontWeight: 600,
color: '#181C32',
flexGrow: 1,
}}
>
Templates
</p>
<div
css={{
fontSize: 20,
flexShrink: 0,
width: 32,
height: 32,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
onClick={onClose}
>
<XIcon />
</div>
</div>
<div
css={{ flexDirection: 'column', overflowY: 'auto', display: 'flex' }}
>
<div
css={{
flexGrow: 1,
overflowY: 'auto',
display: 'grid',
gridTemplateColumns: 'repeat(2,minmax(0,1fr))',
gridGap: 8,
padding: '16px',
}}
>
{isLoading && <div>Loading...</div>}
{templates.map((item, index) => (
<div
key={index}
css={{ cursor: 'pointer' }}
onClick={() => addPage(item.elements)}
>
<img alt={item.img} loading="lazy" src={item.img} />
</div>
))}
</div>
</div>
</div>
);
};
================================================
FILE: src/features/design/components/sidebar/TextContent.tsx
================================================
import XIcon from '@duyank/icons/regular/X';
import type { LayerId, SerializedLayers } from '@lidojs/design-core';
import { useEditor } from '@lidojs/design-editor';
import axios from 'axios';
import { type FC, useState } from 'react';
import { isMobile } from 'react-device-detect';
import { useAsync } from 'react-use';
import {
addABodyText,
addAHeading,
addASubheading,
} from '../../../../constant/text-effects';
import { getThumbnail } from '../../../../utils/thumbnail';
interface Text {
img: string;
elements: {
rootId: LayerId;
layers: SerializedLayers;
};
}
export const TextContent: FC<{ onClose: () => void }> = ({ onClose }) => {
const { actions } = useEditor();
const [texts, setTexts] = useState<Text[]>([]);
const [isLoading, setIsLoading] = useState(true);
useAsync(async () => {
const response = await axios.get<Text[]>('/texts');
setTexts(response.data);
setIsLoading(false);
}, []);
const handleAddText = (data: {
rootId: LayerId;
layers: SerializedLayers;
}) => {
actions.addTextLayer(data);
if (isMobile) {
onClose();
}
};
const handleDrag = (
event: React.DragEvent,
data: { rootId: LayerId; layers: SerializedLayers },
) => {
const { clientX, clientY } = event;
actions.startDragNDrop({ layer: 'Text', data }, { x: clientX, y: clientY });
event.dataTransfer.clearData('text/plain');
event.dataTransfer.setData(
'text/plain',
JSON.stringify({ layer: 'Text', data }),
);
event.dataTransfer.setDragImage(new Image(), 0, 0);
};
return (
<div
css={{
width: '100%',
height: '100%',
flexDirection: 'column',
overflowY: 'auto',
display: 'flex',
}}
>
<div
css={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
height: 48,
borderBottom: '1px solid rgba(57,76,96,.15)',
padding: '0 20px',
}}
>
<p
css={{
lineHeight: '48px',
fontWeight: 600,
color: '#181C32',
flexGrow: 1,
}}
>
Text
</p>
<div
css={{
fontSize: 20,
flexShrink: 0,
width: 32,
height: 32,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
onClick={onClose}
>
<XIcon />
</div>
</div>
<div
css={{ flexDirection: 'column', overflowY: 'auto', display: 'flex' }}
>
<div
css={{
padding: 16,
display: 'flex',
gap: 8,
flexDirection: 'column',
}}
>
<div
css={{
fontSize: 28,
lineHeight: 1,
padding: '16px 16px',
fontWeight: 700,
background: '#EBECF0',
borderRadius: 4,
cursor: 'pointer',
'-webkit-user-drag': 'element',
}}
draggable={true}
onClick={() => handleAddText(addAHeading)}
onDragStart={(e) => handleDrag(e, addAHeading)}
>
Add a heading
</div>
<div
css={{
fontSize: 18,
lineHeight: 1,
padding: '16px',
fontWeight: 700,
background: '#EBECF0',
borderRadius: 4,
cursor: 'pointer',
'-webkit-user-drag': 'element',
}}
onClick={() => handleAddText(addASubheading)}
onDragStart={(e) => handleDrag(e, addASubheading)}
>
Add a subheading
</div>
<div
css={{
fontSize: 12,
lineHeight: 1,
padding: '16px',
fontWeight: 700,
background: '#EBECF0',
borderRadius: 4,
cursor: 'pointer',
'-webkit-user-drag': 'element',
}}
onClick={() => handleAddText(addABodyText)}
onDragStart={(e) => handleDrag(e, addABodyText)}
>
Add a little bit of body text
</div>
</div>
<div
css={{
flexGrow: 1,
overflowY: 'auto',
display: 'grid',
gridTemplateColumns: 'repeat(3,minmax(0,1fr))',
gridGap: 8,
padding: '16px',
}}
>
{isLoading && <div>Loading...</div>}
{texts.map(({ img, elements }, idx) => (
<div
key={idx}
css={{
cursor: 'pointer',
position: 'relative',
paddingBottom: '100%',
width: '100%',
'-webkit-user-drag': 'element',
}}
onClick={() => handleAddText(elements)}
onDragStart={(e) => handleDrag(e, elements)}
>
<img
alt={getThumbnail(img)}
css={{
position: 'absolute',
top: 0,
left: 0,
height: '100%',
width: '100%',
objectFit: 'cover',
}}
src={getThumbnail(img)}
/>
</div>
))}
</div>
</div>
</div>
);
};
================================================
FILE: src/features/design/components/sidebar/UploadContent.tsx
================================================
import XIcon from '@duyank/icons/regular/X';
import { useEditor } from '@lidojs/design-editor';
import { fetchSvgContent } from '@lidojs/design-utils';
import { type ChangeEvent, type FC, useRef, useState } from 'react';
import { isMobile } from 'react-device-detect';
interface UploadContentProps {
visibility: boolean;
onClose: () => void;
}
export const UploadContent: FC<UploadContentProps> = ({
visibility,
onClose,
}) => {
const inputFileRef = useRef<HTMLInputElement>(null);
const { actions } = useEditor();
const [images, setImages] = useState<
{ url: string; type: 'svg' | 'image' }[]
>([]);
const addImage = async (url: string) => {
if (!window) return;
const img = new Image();
img.onerror = (err) => window.alert(err);
img.src = url;
img.crossOrigin = 'anonymous';
img.onload = () => {
actions.addImageLayer(
{ url, thumb: url },
{ width: img.naturalWidth, height: img.naturalHeight },
);
if (isMobile) {
onClose();
}
};
};
const addSvg = async (url: string) => {
const ele = await fetchSvgContent(url);
const viewBox = ele.getAttribute('viewBox')?.split(' ') || [];
const width =
viewBox.length === 4 ? +viewBox[2] : +(ele.getAttribute('width') || 100);
const height =
viewBox.length === 4 ? +viewBox[3] : +(ele.getAttribute('height') || 100);
actions.addSvgLayer(url, { width, height }, ele);
if (isMobile) {
onClose();
}
};
const handleUpload = (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
setImages((prevState) => {
return prevState.concat([
{
url: reader.result as string,
type: file.type === 'image/svg+xml' ? 'svg' : 'image',
},
]);
});
};
reader.readAsDataURL(file);
}
};
return (
<div
css={{
width: '100%',
height: '100%',
flexDirection: 'column',
overflowY: 'auto',
display: visibility ? 'flex' : 'none',
}}
>
<div
css={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
height: 48,
borderBottom: '1px solid rgba(57,76,96,.15)',
padding: '0 20px',
}}
>
<p
css={{
lineHeight: '48px',
fontWeight: 600,
color: '#181C32',
flexGrow: 1,
}}
>
Upload Images
</p>
<div
css={{
fontSize: 20,
flexShrink: 0,
width: 32,
height: 32,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
onClick={onClose}
>
<XIcon />
</div>
</div>
<div
css={{
margin: 16,
background: '#3a3a4c',
borderRadius: 8,
color: '#fff',
padding: '8px 16px',
cursor: 'pointer',
textAlign: 'center',
}}
onClick={() => inputFileRef.current?.click()}
>
Upload
</div>
<input
ref={inputFileRef}
accept="image/*"
css={{ display: 'none' }}
type="file"
onChange={handleUpload}
/>
<div css={{ padding: '16px' }}>
<div
css={{
flexGrow: 1,
overflowY: 'auto',
display: 'grid',
gridTemplateColumns: 'repeat(2,minmax(0,1fr))',
gridGap: 8,
}}
>
{images.map((item, idx) => (
<div
key={idx}
css={{ cursor: 'pointer', position: 'relative' }}
onClick={() =>
item.type === 'image' ? addImage(item.url) : addSvg(item.url)
}
>
<div css={{ paddingBottom: '100%', height: 0 }} />
<div
css={{
position: 'absolute',
inset: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<img
alt={item.url}
css={{ maxHeight: '100%' }}
loading="lazy"
src={item.url}
/>
</div>
</div>
))}
</div>
</div>
</div>
);
};
================================================
FILE: src/features/design/components/sidebar/VideoContent.tsx
================================================
import XIcon from '@duyank/icons/regular/X';
import type { LayerId, LayerType, SerializedLayers } from '@lidojs/design-core';
import { useEditor } from '@lidojs/design-editor';
import { type FC, useState } from 'react';
import { isMobile } from 'react-device-detect';
import { useAsync } from 'react-use';
import { v4 } from 'uuid';
export const VideoContent: FC<{ onClose: () => void }> = ({ onClose }) => {
const [videos] = useState<
{ img: string; url: string; width: number; height: number }[]
>([
{
img: 'https://template.canva.com/EAFaarkqz_0/2/0/400w-IVVQCZOr1K4.jpg',
url: 'https://template.canva.com/EAFaarkqz_0/2/0/400w-xadNArxL6gA.mp4',
width: 400,
height: 334,
},
]);
const [isLoading] = useState(false);
const { actions, query } = useEditor();
useAsync(async () => {
//const response = await axios.get<{ img: string; url: string; width: number; height: number }[]>('/videos');
//setVideos(response.data);
//setIsLoading(false);
}, []);
const addVideo = ({
url,
width,
height,
}: {
url: string;
width: number;
height: number;
}) => {
actions.addVideoLayer({ url }, { width, height });
if (isMobile) {
onClose();
}
};
const handleDrag = (
event: React.DragEvent,
{ url, width, height }: (typeof videos)[number],
) => {
const { clientX, clientY } = event;
const pageSize = query.getPageSize(query.activePage());
const ratio = pageSize.width / pageSize.height;
const imgRatio = width / height;
const w =
ratio < imgRatio
? pageSize.width * 0.4
: pageSize.height * imgRatio * 0.4;
const h = w / imgRatio;
const id: LayerId = v4();
const data: {
layer: LayerType;
data: { rootId: LayerId; layers: SerializedLayers };
} = {
layer: 'Video',
data: {
rootId: id,
layers: {
[id]: {
type: {
resolvedName: 'VideoLayer',
},
props: {
video: {
url: url,
boxSize: {
width: w,
height: h,
},
position: {
x: 0,
y: 0,
},
rotate: 0,
},
position: {
x: 0,
y: 0,
},
boxSize: {
width: w,
height: h,
},
rotate: 0,
},
locked: false,
parent: 'ROOT',
child: [],
},
},
},
};
actions.startDragNDrop(data, { x: clientX, y: clientY });
event.dataTransfer.clearData('text/plain');
event.dataTransfer.setData('text/plain', JSON.stringify(data));
event.dataTransfer.setDragImage(new Image(), 0, 0);
};
return (
<div
css={{
width: '100%',
height: '100%',
flexDirection: 'column',
overflowY: 'auto',
display: 'flex',
}}
>
<div
css={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
height: 48,
borderBottom: '1px solid rgba(57,76,96,.15)',
padding: '0 20px',
}}
>
<p
css={{
lineHeight: '48px',
fontWeight: 600,
color: '#181C32',
flexGrow: 1,
}}
>
Images
</p>
<div
css={{
fontSize: 20,
flexShrink: 0,
width: 32,
height: 32,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
onClick={onClose}
>
<XIcon />
</div>
</div>
<div
css={{ flexDirection: 'column', overflowY: 'auto', display: 'flex' }}
>
<div
css={{
flexGrow: 1,
overflowY: 'auto',
display: 'grid',
gridTemplateColumns: 'repeat(3,minmax(0,1fr))',
padding: '16px',
gridGap: 8,
}}
>
{isLoading && <div>Loading...</div>}
{videos.map((item, idx) => (
<div
key={idx}
css={{
cursor: 'pointer',
position: 'relative',
paddingBottom: '100%',
width: '100%',
'-webkit-user-drag': 'element',
}}
onClick={() => addVideo(item)}
draggable={true}
onDragStart={(e) => handleDrag(e, item)}
>
<img
alt={item.img}
css={{
position: 'absolute',
top: 0,
left: 0,
height: '100%',
width: '100%',
objectFit: 'cover',
}}
loading="lazy"
src={item.img}
/>
</div>
))}
</div>
</div>
</div>
);
};
================================================
FILE: src/features/design/components/sidebar/index.ts
================================================
export * from './Sidebar';
================================================
FILE: src/features/design/components/tabs/TabList.tsx
================================================
import type { FC, ReactNode } from 'react';
interface SidebarTabProps {
tabs: {
name: string;
icon: ReactNode;
isBusiness?: boolean;
}[];
active: string | null;
onChange: (e: React.MouseEvent, tab: string) => void;
}
export const SidebarTab: FC<SidebarTabProps> = ({ tabs, active, onChange }) => {
const activeIdx = tabs.findIndex((tab) => tab.name === active);
return (
<div
css={{
color: '#5E6278',
borderRight: '1px solid rgba(217, 219, 228, 0.6)',
overflowY: 'auto',
'@media (max-width: 900px)': {
position: 'fixed',
bottom: 0,
left: 0,
right: 0,
background: '#fff',
display: 'flex',
justifyContent: 'center',
},
}}
>
<div
css={{
position: 'relative',
'@media (max-width: 900px)': {
display: 'flex',
overflowX: 'scroll',
},
}}
>
{activeIdx >= 0 && (
<div
css={{
background: '#fff',
width: 72,
height: 72,
position: 'absolute',
left: 0,
top: 0,
transform: `translateY(${activeIdx * 100}%)`,
'@media (max-width: 900px)': {
display: 'none',
},
}}
>
<div
css={{
position: 'absolute',
height: 8,
width: 8,
right: 0,
top: -8,
background:
'radial-gradient(circle closest-side,transparent 0,transparent 50%,#fff 0) 200% 200% /400% 400%',
}}
/>
<div
css={{
position: 'absolute',
height: 8,
width: 8,
right: 0,
bottom: -8,
transform: 'scaleY(-1)',
background:
'radial-gradient(circle closest-side,transparent 0,transparent 50%,#fff 0) 200% 200% /400% 400%',
}}
/>
</div>
)}
{tabs.map((tab, idx) => (
<div
key={idx}
css={{
color: idx === activeIdx ? '#009ef7' : undefined,
borderBottomRightRadius: idx === activeIdx - 1 ? 8 : 0,
borderTopRightRadius: idx === activeIdx + 1 ? 8 : 0,
position: 'relative',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
padding: '0 2px',
height: 72,
width: 72,
minWidth: 72,
minHeight: 72,
cursor: 'pointer',
':hover': {
color: '#009ef7',
},
}}
onClick={(e) => onChange(e, tab.name)}
>
<div css={{ fontSize: 24 }}>{tab.icon}</div>
<span css={{ fontSize: 10, lineHeight: 1.6, fontWeight: 600 }}>
{tab.name}
</span>
{tab.isBusiness && (
<div
css={{
position: 'absolute',
background: '#fdebcf',
borderRadius: 9999,
fontSize: 9,
padding: '2px 4px',
top: 0,
}}
>
Business
</div>
)}
</div>
))}
</div>
</div>
);
};
================================================
FILE: src/features/design/components/tabs/index.ts
================================================
export * from './TabList';
================================================
FILE: src/features/design/config/iframe.tsx
================================================
import type { LayerId, SerializedLayers } from '@lidojs/design-core';
export type IframeItem = {
elements: [
{
rootId: LayerId;
layers: SerializedLayers;
},
];
img: string;
};
export const iframeList: IframeItem[] = [
{
elements: [
{
rootId: '3f39efa2-4017-4c77-b3a9-c98456c629f0',
layers: {
'3f39efa2-4017-4c77-b3a9-c98456c629f0': {
type: { resolvedName: 'IframeLayer' },
props: {
url: 'https://api.wo-cloud.com/content/widget/?geoObjectKey=6112695&language=en®ion=US&timeFormat=HH:mm&windUnit=mph&systemOfMeasurement=imperial&temperatureUnit=fahrenheit',
position: { x: 45.586770981507925, y: 121.96449211646916 },
boxSize: {
width: 350.8264580369844,
height: 350.8264580369844,
},
rotate: 0,
scale: 1,
},
locked: false,
child: [],
parent: 'ROOT',
},
},
},
],
img: '/assets/images/weather/1.png',
},
];
================================================
FILE: src/features/design/config/line.tsx
================================================
import type { LayerId } from '@lidojs/design-core';
import type { DeepPartial, LineLayerProps } from '@lidojs/design-editor';
import type { ReactElement } from 'react';
import { v4 } from 'uuid';
export type Line = {
id: LayerId;
props: DeepPartial<LineLayerProps>;
icon: ReactElement;
};
export const lines: Line[] = [
{
id: v4(),
props: {
style: 'solid',
boxSize: {
height: 4,
},
color: 'rgb(94, 98, 120)',
},
icon: (
<svg
role="img"
aria-label="Solid"
fill="currentColor"
stroke="currentColor"
style={{ overflow: 'visible' }}
viewBox="0 -0.5 33 1"
xmlns="http://www.w3.org/2000/svg"
>
<line
fill="none"
strokeDasharray=""
strokeLinecap="butt"
x1="0.5"
x2="32.5"
/>
</svg>
),
},
{
id: v4(),
props: {
style: 'shortDashes',
boxSize: {
height: 4,
},
color: 'rgb(94, 98, 120)',
},
icon: (
<svg
role="img"
aria-label="Short Dashes"
fill="currentColor"
stroke="currentColor"
style={{ overflow: 'visible' }}
viewBox="0 -0.5 33 1"
xmlns="http://www.w3.org/2000/svg"
>
<line
fill="none"
strokeDasharray="3,1"
strokeLinecap="butt"
x1="0.5"
x2="32.5"
/>
</svg>
),
},
{
id: v4(),
props: {
style: 'dots',
boxSize: {
height: 4,
},
color: 'rgb(94, 98, 120)',
},
icon: (
<svg
role="img"
aria-label="Dots"
fill="currentColor"
stroke="currentColor"
style={{ overflow: 'visible' }}
viewBox="0 -0.5 33 1"
xmlns="http://www.w3.org/2000/svg"
>
<line
fill="none"
strokeDasharray="1,1"
strokeLinecap="butt"
x1="0.5"
x2="32.5"
/>
</svg>
),
},
{
id: v4(),
props: {
style: 'solid',
arrowStart: 'bar',
arrowEnd: 'bar',
boxSize: {
height: 4,
},
color: 'rgb(94, 98, 120)',
},
icon: (
<svg
role="img"
aria-label="Solid with Bar Arrows"
fill="currentColor"
stroke="currentColor"
style={{ overflow: 'visible' }}
viewBox="0 -0.5 33 1"
xmlns="http://www.w3.org/2000/svg"
>
<line
fill="none"
strokeDasharray=""
strokeLinecap="butt"
x1="1"
x2="32"
/>
<rect height="4" rx="0.5" stroke="none" width="1" x="0" y="-2" />
<g transform="translate(33)">
<rect height="4" rx="0.5" stroke="none" width="1" x="-1" y="-2" />
</g>
</svg>
),
},
{
id: v4(),
props: {
style: 'dots',
arrowStart: 'none',
arrowEnd: 'arrow',
boxSize: {
height: 4,
},
color: 'rgb(94, 98, 120)',
},
icon: (
<svg
role="img"
aria-label="Dots with Arrow End"
fill="currentColor"
stroke="currentColor"
style={{ overflow: 'visible' }}
viewBox="0 -0.5 33 1"
xmlns="http://www.w3.org/2000/svg"
>
<line
fill="none"
strokeDasharray="1,1"
strokeLinecap="butt"
x1="0.5"
x2="32.25"
/>
<g transform="translate(33)">
<path
d="M -2.5,-1.5,-0.5,0,-2.5,1.5 "
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
</svg>
),
},
{
id: v4(),
props: {
style: 'solid',
arrowStart: 'arrow',
arrowEnd: 'arrow',
boxSize: {
height: 4,
},
color: 'rgb(94, 98, 120)',
},
icon: (
<svg
role="img"
aria-label="Solid with Arrow Start and End"
fill="currentColor"
stroke="currentColor"
style={{ overflow: 'visible' }}
viewBox="0 -0.5 33 1"
xmlns="http://www.w3.org/2000/svg"
>
<line
fill="none"
strokeDasharray=""
strokeLinecap="butt"
x1="0.75"
x2="32.25"
/>
<path
d="M 2.5,-1.5,0.5,0,2.5,1.5 "
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
/>
<g transform="translate(33)">
<path
d="M -2.5,-1.5,-0.5,0,-2.5,1.5 "
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
</svg>
),
},
{
id: v4(),
props: {
style: 'solid',
arrowStart: 'outlineDiamond',
arrowEnd: 'outlineDiamond',
boxSize: {
height: 4,
},
color: 'rgb(94, 98, 120)',
},
icon: (
<svg
role="img"
aria-label="Dots"
fill="currentColor"
stroke="currentColor"
style={{ overflow: 'visible' }}
viewBox="0 -0.5 33 1"
xmlns="http://www.w3.org/2000/svg"
>
<line
fill="none"
strokeDasharray=""
strokeLinecap="butt"
x1="3.5"
x2="29.5"
/>
<path
d="M 0.5,0 l 1.5,-1.5 1.5,1.5 -1.5,1.5 Z"
fill="none"
strokeLinejoin="round"
/>
<g transform="translate(33)">
<path
d="M -0.5,0 l -1.5,-1.5 -1.5,1.5 1.5,1.5 Z"
fill="none"
strokeLinejoin="round"
/>
</g>
</svg>
),
},
{
id: v4(),
props: {
style: 'solid',
arrowStart: 'diamond',
arrowEnd: 'diamond',
boxSize: {
height: 4,
},
color: 'rgb(94, 98, 120)',
},
icon: (
<svg
role="img"
aria-label="Solid with Diamond Arrows"
fill="currentColor"
stroke="currentColor"
style={{ overflow: 'visible' }}
viewBox="0 -0.5 33 1"
xmlns="http://www.w3.org/2000/svg"
>
<line
fill="none"
strokeDasharray=""
strokeLinecap="butt"
x1="3.5"
x2="29.5"
/>
<path
d="M 0.5,0 l 1.5,-1.5 1.5,1.5 -1.5,1.5 Z"
fill="inherit"
strokeLinejoin="round"
/>
<g transform="translate(33)">
<path
d="M -0.5,0 l -1.5,-1.5 -1.5,1.5 1.5,1.5 Z"
fill="inherit"
strokeLinejoin="round"
/>
</g>
</svg>
),
},
{
id: v4(),
props: {
style: 'solid',
arrowStart: 'none',
arrowEnd: 'arrow',
boxSize: {
height: 4,
},
color: 'rgb(94, 98, 120)',
},
icon: (
<svg
role="img"
aria-label="Solid with Arrow End"
fill="currentColor"
stroke="currentColor"
style={{ overflow: 'visible' }}
viewBox="0 -0.5 33 1"
xmlns="http://www.w3.org/2000/svg"
>
<line
fill="none"
strokeDasharray=""
strokeLinecap="butt"
x1="0.5"
x2="32.25"
/>
<g transform="translate(33)">
<path
d="M -2.5,-1.5,-0.5,0,-2.5,1.5 "
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
</svg>
),
},
{
id: v4(),
props: {
style: 'solid',
arrowStart: 'none',
arrowEnd: 'triangle',
boxSize: {
height: 4,
},
color: 'rgb(94, 98, 120)',
},
icon: (
<svg
role="img"
aria-label="Solid with Triangle End"
fill="currentColor"
stroke="currentColor"
style={{ overflow: 'visible' }}
viewBox="0 -0.5 33 1"
xmlns="http://www.w3.org/2000/svg"
>
<line
fill="none"
strokeDasharray=""
strokeLinecap="butt"
x1="0.5"
x2="30"
/>
<g transform="translate(33)">
<path
d="M -2.5,-1.5,-0.5,0,-2.5,1.5 Z"
fill="inherit"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
</svg>
),
},
{
id: v4(),
props: {
style: 'solid',
arrowStart: 'circle',
arrowEnd: 'circle',
boxSize: {
height: 4,
},
color: 'rgb(94, 98, 120)',
},
icon: (
<svg
role="img"
aria-label="Solid with Circle Arrows"
fill="currentColor"
stroke="currentColor"
style={{ overflow: 'visible' }}
viewBox="0 -0.5 33 1"
xmlns="http://www.w3.org/2000/svg"
>
<line
fill="none"
strokeDasharray=""
strokeLinecap="butt"
x1="4"
x2="29"
/>
<circle cx="2" fill="inherit" r="1.5" />
<g transform="translate(33)">
<circle cx="-2" fill="inherit" r="1.5" />
</g>
</svg>
),
},
{
id: v4(),
props: {
style: 'solid',
arrowStart: 'square',
arrowEnd: 'square',
boxSize: {
height: 4,
},
color: 'rgb(94, 98, 120)',
},
icon: (
<svg
role="img"
aria-label="Solid with Square Arrows"
fill="currentColor"
stroke="currentColor"
style={{ overflow: 'visible' }}
viewBox="0 -0.5 33 1"
xmlns="http://www.w3.org/2000/svg"
>
<line
fill="none"
strokeDasharray=""
strokeLinecap="butt"
x1="4"
x2="29"
/>
<rect
fill="inherit"
height="3"
strokeLinejoin="round"
width="3"
x="0.5"
y="-1.5"
/>
<g transform="translate(33)">
<rect
fill="inherit"
height="3"
strokeLinejoin="round"
width="3"
x="-3.5"
y="-1.5"
/>
</g>
</svg>
),
},
{
id: v4(),
props: {
style: 'solid',
arrowStart: 'outlineSquare',
arrowEnd: 'outlineSquare',
boxSize: {
height: 4,
},
color: 'rgb(94, 98, 120)',
},
icon: (
<svg
role="img"
aria-label="Solid with Outline Square Arrows"
fill="currentColor"
stroke="currentColor"
style={{ overflow: 'visible' }}
viewBox="0 -0.5 33 1"
xmlns="http://www.w3.org/2000/svg"
>
<line
fill="none"
strokeDasharray=""
strokeLinecap="butt"
x1="4"
x2="29"
/>
<rect
fill="none"
height="3"
strokeLinejoin="round"
width="3"
x="0.5"
y="-1.5"
/>
<g transform="translate(33)">
<rect
fill="none"
height="3"
strokeLinejoin="round"
width="3"
x="-3.5"
y="-1.5"
/>
</g>
</svg>
),
},
{
id: v4(),
props: {
style: 'dots',
arrowStart: 'triangle',
arrowEnd: 'triangle',
boxSize: {
height: 4,
},
color: 'rgb(94, 98, 120)',
},
icon: (
<svg
role="img"
aria-label="Dots with Triangle Arrows"
fill="currentColor"
stroke="currentColor"
style={{ overflow: 'visible' }}
viewBox="0 -0.5 33 1"
xmlns="http://www.w3.org/2000/svg"
>
<line
fill="none"
strokeDasharray="1,1"
strokeLinecap="butt"
x1="3"
x2="30"
/>
<path
d="M 2.5,-1.5,0.5,0,2.5,1.5 Z"
fill="inherit"
strokeLinecap="round"
strokeLinejoin="round"
/>
<g transform="translate(33)">
<path
d="M -2.5,-1.5,-0.5,0,-2.5,1.5 Z"
fill="inherit"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
</svg>
),
},
{
id: v4(),
props: {
style: 'solid',
arrowStart: 'outlineCircle',
arrowEnd: 'outlineCircle',
boxSize: {
height: 4,
},
color: 'rgb(94, 98, 120)',
},
icon: (
<svg
role="img"
aria-label="Solid with Outline Circle Arrows"
fill="currentColor"
stroke="currentColor"
style={{ overflow: 'visible' }}
viewBox="0 -0.5 33 1"
xmlns="http://www.w3.org/2000/svg"
>
<line
fill="none"
strokeDasharray=""
strokeLinecap="butt"
x1="4"
x2="29"
/>
<circle cx="2" fill="none" r="1.5" />
<g transform="translate(33)">
<circle cx="-2" fill="none" r="1.5" />
</g>
</svg>
),
},
];
================================================
FILE: src/features/design/config/qrCode.tsx
================================================
import type { LayerId, SerializedLayers } from '@lidojs/design-core';
export type QrCodeItem = {
elements: [
{
rootId: LayerId;
layers: SerializedLayers;
},
];
img: string;
};
export const qrCodeList: QrCodeItem[] = [
{
elements: [
{
rootId: '3f39efa2-4017-4c77-b3a9-c98456c629f0',
layers: {
'3f39efa2-4017-4c77-b3a9-c98456c629f0': {
type: { resolvedName: 'QrCodeLayer' },
props: {
text: 'https://lidojs.com',
position: { x: 45.586770981507925, y: 121.96449211646916 },
boxSize: {
width: 350.8264580369844,
height: 350.8264580369844,
},
rotate: 0,
bgColor: 'rgb(255, 255, 255)',
textColor: 'rgb(30, 30, 45)',
logo: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAAAAAQACAYAAAB/HSuDAAAACXBIWXMAAC4jAAAuIwF4pT92AABbQ0lEQVR4nO3defzVc/7//8f73UJ7qBSpLNWEZBlqjKWyFJEYYxnrxzKYGR/MMGMZzAyGMZbMxzbMIDvjVyGERGRG2cmSCElpIS1apN6/P2bM12c+ltQ553nOeV6vl4t/Phc9X3cfb03nds55vWrq6urqAgAAAKhqtakHAAAAAMUnAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIQP3UA4CV8/7778cf//jH1DOK5sgjj4zu3bunngEAAFVDAIAKNWvWrLjssstSzyia3r17CwAAAFBAvgIAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyED91AMAAAAKZenSpTF58uR48803Y+rUqTF9+vSYPn16zJo1K+bOnfvvv5YuXRrLli2L+fPn//vXNm7cOBo2bBgREY0aNYpmzZr9+68111wzWrduHW3atIl27dpFmzZtolOnTtGxY8do0KBBqn9c+FYEAAAAoCJ98MEHMX78+HjhhRfi2WefjQkTJsS7774by5YtW6nz5s6d+61/TW1tbay33nqxwQYbRLdu3WKzzTaLzTbbLDbddNNo1qzZSu2AYhEAqshVV10VS5YsST2j4DbeeOPYddddU88AIuLee++Nt956K/WMotlwww1jzz33TD0DKIHBgwennlA0xx13XKy22mqpZxTFhx9+GCNHjoxHH300xowZE2+++WbqSbF8+fJ499134913341HH3303//3mpqa6NatW3z/+9//918bbbRRwqUQUVNXV1eXegSF0bJly5WqluXusMMOixtuuCH1jLLzwgsvxBZbbJF6RtEMGzYsBg0alHoG/2HQoEFx9913p55RNHvttVcMHz489QygBGpqalJPKJo5c+ZEy5YtU88omClTpsTtt98e9957b/z973+P5cuXp5600tZff/3Yfffdo3///rHTTjtFo0aNUk8iMz4BAAAAlJWFCxfGbbfdFjfddFM8/vjjUS3vWb799ttxxRVXxBVXXBFNmjSJPffcM370ox/FrrvuWrWf2qC8eAoAAABQFiZPnhw///nPY911142jjjoqxowZUzUv/v/TJ598ErfffnsMHDgw2rVrFyeddFK88cYbqWdR5QQAAAAgqYkTJ8bhhx8eXbp0iUsvvTQ+/vjj1JNKas6cOTF48ODo2rVr7LLLLjFy5MjUk6hSAgAAAJDE+++/H4cffnh069YthgwZstJ3768mo0aNit122y169uwZw4cPr9pPQJCGAAAAAJTU4sWL45xzzokuXbrEkCFDvMj9EuPHj4+99947tttuuxg/fnzqOVQJAQAAACiZJ598MjbffPM466yzYuHChannlL2///3v0bNnzzj44INj1qxZqedQ4QQAAACg6JYsWRK/+MUvYvvtt4+JEyemnlNxbrnllujWrVvccsstqadQwQQAAACgqN58883Ydttt45JLLvFx/1Xw4YcfxsEHHxw/+MEPYs6cOannUIEEAAAAoGjuu+++2HLLLeO5555LPaVqDB06NL773e/Gs88+m3oKFUYAAAAAiuLiiy+OgQMHxvz581NPqTqTJ0+O73//+3HrrbemnkIFEQAAAICCqqurixNOOCFOPvnkWL58eeo5VWvJkiVx0EEHxQUXXJB6ChVCAAAAAApm2bJlceSRR8af/vSn1FOycdppp8Xxxx/v/gp8IwEAAAAoiLq6ujj88MPj+uuvTz0lO5dffnmceuqpqWdQ5gQAAACgIH72s5/FzTffnHpGti688MI444wzUs+gjAkAAADAKvvd734XV155ZeoZ2fv9738f11xzTeoZlCkBAAAAWCV33HFHnH322aln8C8//elP45FHHkk9gzIkAAAAACvtmWeeif/6r/9KPYMv+Oyzz+KHP/xhvPfee6mnUGYEAAAAYKXMnTs39t9//1i0aFHqKfyHOXPmxIEHHhifffZZ6imUEQEAAABYKUcddVRMnjw59Qy+wpNPPhm/+93vUs+gjAgAAADAt3bDDTfEXXfdlXoG3+D3v/99PPfcc6lnUCYEAAAA4Fv54IMP4sQTT0w9gxWwbNmyOPLII30VgIgQAAAAgG/puOOOi7lz56aewQp64YUX4pJLLkk9gzIgAAAAACts5MiRMXz48NQz+JbOPffcmDFjRuoZJCYAAAAAK+Szzz6Lk046KfUMVsL8+fPj17/+deoZJFY/9QBg5ay55ppx2GGHpZ5RNB06dEg9AQD4D1deeWW8/vrrqWewkv7617/GSSedFBtvvHHqKSQiAECF6tChQ9xwww2pZwAAmVi0aFGcf/75qWewCurq6uJ3v/td3H777amnkIivAAAAAN/oz3/+c3zwwQepZ7CK7rzzznj11VdTzyARAQAAAPhaixcvjvPOOy/1DAqgrq7Ov8uMCQAAAMDXuvnmm2P27NmpZ1Agd955Z7z//vupZ5CAAAAAAHylurq6GDx4cOoZFNBnn30WV1xxReoZJCAAAAAAX2n06NHxyiuvpJ5Bgf35z3+OxYsXp55BiQkAAADAV/rLX/6SegJF8NFHH8Xw4cNTz6DEBAAAAOBLzZkzJ4YNG5Z6BkVy3XXXpZ5AiQkAAADAl7r11ltjyZIlqWdQJKNGjYopU6aknkEJCQAAAMCXuvPOO1NPoIjq6ur8O86MAAAAAPwfM2fOjLFjx6aeQZHdddddqSdQQvVTDwDIxfLly+Pdd9+Nt99+O955552YMWNGfPTRR/HRRx/FsmXL4uOPP46IiNVWWy0aNWoU9erVi2bNmkWrVq2idevW0aZNm1h33XWjS5cu0bJly6T/LPC5uXPnxrRp0+L999+PGTNmxIcffhhz5879919Lly6Nurq6mDt37r9/TZMmTaJBgwYREdGyZcto0aLFv/9q165dtGvXLtZZZ51o06ZN1NTUpPpHg+wNHTo0li9fnnoGRTZu3LiYMmVKdOjQIfUUSkAAACiSmTNnxuOPPx6PPfZYPPvss/Hyyy/HJ598UpCz11577fjOd74TW265ZWy33Xax3XbbRZs2bQpyNnyZyZMnx/PPPx8vvfRSvPnmmzFx4sSYNGlSzJs3r2jXbNCgQWywwQbRtWvX2GijjWLjjTeOLbbYIjbddNNo2LBh0a4L/NOIESNST6BEhg4dGieeeGLqGZSAAAAVatGiRTFx4sTUM4pm/fXXjxYtWqSe8a299957ceutt8bw4cNj3LhxUVdXV5TrzJgxI2bMmBFjxoyJSy+9NCIiNt5449hnn31in332iS222KIo1yUPixcvjqeeeirGjBkTY8eOjaeffvp/vYNfKkuXLo2JEyf+n9/r6tevH927d49tt902dtxxx9h+++2jbdu2Jd8H1WzJkiXx6KOPpp5BiTzwwAMCQCYEAKhQEydOrOoXecOGDYtBgwalnrFC6urq4oEHHoj/+Z//iQcffLBoL/q/yauvvhqvvvpqnHvuudG5c+f46U9/GkcccUQ0a9YsyR4qy9tvvx333ntvjBgxIh5//PGyvuv3Z599Fs8//3w8//zzccUVV0RExCabbBIDBgyIPfbYI7bddtuoV69e4pVQ2caOHRsLFy5MPYMSGTNmTCxatCgaNWqUegpF5iaAAKtg+PDhsfnmm8eAAQNi5MiRyV78/6dJkybFiSeeGOuuu2788pe//Pf9BeCL3nvvvbjwwgtj8803jw022CBOOOGEePjhh8v6xf9XeeWVV+LCCy+MHXbYIdq0aRPHHntsjB07tmz+m4RKM2rUqNQTKKElS5bEY489lnoGJSAAAKyECRMmxI477hh77713vPTSS6nnfKX58+fHH//4x9hwww3jsssui88++yz1JBL77LPP4q677oq+fftGx44d41e/+lW8+OKLqWcV1EcffRR//vOfY/vtt49OnTrFueeeGx988EHqWVBRnnzyydQTKLFHHnkk9QRKQAAA+BaWLVsW55xzTmy11Vbx+OOPp56zwj766KM48cQTo1evXvHqq6+mnkMCc+bMiXPOOSc6duwYP/zhD+PRRx/N4t3xKVOmxJlnnhkdOnSIAw44IJ555pnUk6DsffrppzF+/PjUMyixSvpzDStPAABYQR988EHsvPPOcdZZZ8Wnn36aes5KefbZZ2PrrbeOm2++OfUUSmTGjBlx6qmnxnrrrRdnnXVWTJs2LfWkJJYuXRp33HFHbL311tG/f/8YM2ZM6klQtp555pmK/CoQq+a5556LBQsWpJ5BkQkAACvg5Zdfjq233roqvh+3cOHCOOSQQ+LMM8/M4h3gXC1YsCDOPvvs2GCDDeIPf/hDwR5BWQ0efPDB6N27d/Tr16+sv8IDqTz33HOpJ5DAsmXL4qmnnko9gyITAAC+wdixY2O77baLqVOnpp5SUOeee24cdthhsWzZstRTKKC6urr461//Gp07d47f/e537uL9NR566KHYfPPN44gjjoiZM2emngNl4/nnn089gUTGjRuXegJF5jGAAF9j3LhxMWDAgJg3b17qKUVx0003xbJly+LGG2/02LQq8Nprr8WPf/zjGDt2bOopFaOuri6uv/76GD58eFx44YVx5JFHRk1NTepZkFQuAaBt27ax4447Rvfu3aNTp07RpEmTiPjnPRDee++9ePHFF2P06NHx/vvvJ15aOs8++2zqCRSZAADwFSZOnBj9+vWr2hf/n7v11lujtrY2brzxRi98KtSyZcvi/PPPj3POOadi70+R2pw5c+Loo4+Om266KYYMGRKdOnVKPQmSWLZsWbzyyiupZxTV7rvvHieffHLsuOOOUVv79R+IrqurizFjxsR5552XxaMR3Si1+vkKAMCXmDNnTgwcODDmzp2bekpJ3HzzzXHGGWeknsFKeOedd6J3795x5plnevFfAI8//nj06NEjbr311tRTIIl33nmnan8vadGiRQwdOjTuu+++6NOnzze++I+IqKmpid69e8fDDz8ct912WzRt2rQES9N57733YtasWalnUEQCAMCXOPzww+ONN95IPaOkzj///LjuuutSz+BbuOeee6JHjx4+8l9g8+bNi4MOOij+67/+KxYvXpx6DpTUxIkTU08oipYtW8YTTzwRe++990qfccABB8SYMWOiefPmBVxWfl5++eXUEygiAQDgP1x99dVxzz33pJ6RxHHHHef7fxWgrq4ufvOb38Ree+1V9V9RSemGG26I7bffPt57773UU6BkqjV+33rrrdG9e/dVPmfLLbeMu+66qwCLyle1fwUkdwIAwBe89957ccopp6Sekcynn34a++23XzZffahECxcujB/84Afx29/+NvWULDzzzDPx3e9+152xycZbb72VekLBHXHEEbHbbrsV7LxddtkljjzyyIKdV25ef/311BMoIgEA4AtOOOGEWLBgQeoZSU2ePDlOOOGE1DP4EnPmzIldd901hg0blnpKVmbOnBl9+vSJ+++/P/UUKLpqe+RtgwYN4uyzzy74uWeeeeYK3UOgEr322mupJ1BE1flTC7ASRo8e7YXVvwwZMiQefPDB1DP4gqlTp8Z2220XTz75ZOopWVq0aFEMHDgwbrnlltRToKimTJmSekJBDRo0KDp06FDwczt27Bh9+/Yt+LnloFrvA8E/CQAA8c/vVP/yl79MPaOsHH300fHJJ5+knkH888V/796949VXX009JWvLli2LQw89VASgqlXbJwD233//op3dr1+/op2d0rRp02LJkiWpZ1AkAgBARNx9991ufvcf3nvvvfjDH/6Qekb2pk2bFr17967K7+VWouXLl4sAVK1ly5bFzJkzU88oqB133LFoZ2+11VZFOzu1d999N/UEikQAALJXV1fnhmpf4aKLLqq6d4Mqydy5c2O33Xbz4r/MfB4BRowYkXoKFNSHH36YekJBrbPOOtGqVauind++ffuinZ3a22+/nXoCRSIAANm7//7744UXXkg9oywtWrQozjzzzNQzsvTpp5/GPvvsEy+99FLqKXyJ5cuXx3777Rf/+Mc/Uk+Bgqm2AFCM7/5/0ZprrlnU81MSAKqXAABkb/DgwaknlLWbbrop3nzzzdQzsnPEEUfE6NGjU8/ga3x+Y8DJkyenngIFMXv27NQTCmqNNdYo6vn16tUr6vkpTZ8+PfUEikQAALI2YcKEGDVqVOoZZW3ZsmVx0UUXpZ6RlYsvvth3zCvE7NmzY9CgQW6YSVX46KOPUk8oqIYNG6aeULE++OCD1BMoEgEAyNq1116bekJFuO6662LatGmpZ2ThkUce8USKCvPyyy/HEUccEXV1damnwCqptpA1b9681BMqlk8AVC8BAMjW4sWL46abbko9oyIsXbo0rrnmmtQzqt706dPjwAMPjOXLl6eewrd05513xhVXXJF6BqyShQsXpp5QUNX2lYZSmjFjRuoJFIkAAGRrxIgRMWfOnNQzKsY111wTS5cuTT2jatXV1cXhhx8es2bNSj2FlXTyySfHhAkTUs+AlbZgwYLUEwrqnXfe8cmclVRtj4Pk/xEAgGzdeuutqSdUlOnTp8djjz2WekbVGjx4cDz00EOpZ7AKlixZEgceeGAsWbIk9RRYKfPnz089oaDmz5/vMaorqdruB8H/IwAAWVqwYEHcd999qWdUnGr7eGi5ePPNN+OMM85IPYMCmDBhQpx77rmpZ8BKWbZsWeoJBff888+nnlCR5s2b5+toVUoAALL0wAMPxKeffpp6BkRdXV0cc8wxsWjRotRTKJALLrjAVwGgTPz9739PPaFi+ZpkdRIAgCzdfffdqSdAREQMGTIkRo8enXoGBfTZZ5/FUUcd5bvHUAb87/3K+/jjj1NPoAgEACA7y5cvjwcffDD1DIgFCxbEaaedlnoGRTBu3Li45ZZbUs+A7L399ts+kbOSqu2eEPyTAABk5/nnn/doIMrC+eefHx988EHqGRTJqaee6r4ZUAb+9re/pZ5Qkfz+VZ0EACA7Dz/8cOoJEFOmTIlLLrkk9QyK6P3334+LLroo9QxYYbW11fnS4Nprr43PPvss9YyK415J1ak6/ysH+Bpjx45NPQHi97//fSxevDj1DIrskksu8T1aKkbz5s1TTyiK6dOnx/Dhw1PPqDhz585NPYEiEACArCxfvtwdgUluypQpcd1116WeQQnMnTs3Bg8enHoGrJBq/QRARMSf/vSn1BMqjk8AVKfq/a8c4EtMmjTJY21I7g9/+EMsXbo09QxKZPDgwbFgwYLUM+AbtWrVKvWEonniiSfiscceSz2jonzyySepJ1AEAgCQlRdeeCH1BDI3Z86cGDJkSOoZlNDcuXPj+uuvTz0DvtFaa62VekJRnXbaaR7PSfYEACArL774YuoJZO4vf/mLd1UydNlll8Xy5ctTz4CvVe0B4KmnnhLjyJ4AAGTFJwBIafny5XH55ZennkECb731Vtx///2pZ8DXatu2beoJRffLX/7S41fJWv3UAwBKSQAgpYceeiimTJmSegaJ/OUvf4k99tgj9Qz4Sm3bto369etX9SPzPvzwwzj66KPj3nvvXeWz6tWrFz169CjAqvLUsmXL1BMoAgEAyMbs2bNj+vTpqWeQsRtuuCH1BBK67777YubMmdGmTZvUU+BL1a9fPzp06BCTJ09OPaWoRowYEZdddlmccMIJq3ROs2bNvLFAxfEVACAbb775ZuoJZGzOnDkxbNiw1DNI6LPPPotbbrkl9Qz4WhtssEHqCSVxyimnxLhx41LPgJLzCQAgG++8807qCck0adIkdtppp9hiiy2ic+fO0bx582jcuHHMmTMnpk2bFk8//XQ89NBDMXPmzNRTq9bw4cOze6Zy+/bto3///rHZZpvFeuutFy1atIgFCxbErFmz4pVXXolHHnkkuxtz3nHHHXHSSSelngFfaaONNopRo0alnlF0S5cujUGDBsXTTz8d7du3Tz0HSkYAALKRYwDo1KlTnH322bH//vtHo0aNvvbvXbp0aQwbNixOPfXUePvtt0u0MB9Dhw5NPaFkttpqqzjnnHOif//+UVNT87V/78svvxznnXde3HHHHSVal9a4ceNiypQp0aFDh9RT4EttuummqSeUzAcffBADBw6MsWPHRuPGjVPPgZLwFQAgG7kFgJNOOikmTpwYhx9++De++I+IaNCgQey3337xyiuvxOGHH178gRmZO3duPPTQQ6lnFF1NTU2ce+65MX78+Nhtt92+8cV/RET37t3j9ttvj/vuuy+bG04NHz489QT4SptvvnnqCSX1/PPPx7777htLly5NPQVKQgAAspFTADjvvPPikksuiYYNG37rX9uoUaO47rrr4tBDDy3Csjw9/PDDWXz8/4Ybbogzzjgjamu//R8vdt9993j88cdjzTXXLMKy8nL33XenngBfqUePHisU76rJAw88EEcffXTU1dWlngJFJwAA2cglAOy7775x+umnr9IZNTU18ec//zm6d+9eoFV5e/DBB1NPKLozzzxzlaNR9+7d429/+9tKBYRK8uSTT8bChQtTz4Av1bRp0+jcuXPqGSU3ZMiQ+MlPfiICUPWq+39hAb7go48+Sj2h6Bo3bhyDBw8uyFmrr756XHbZZQU5K3fV/vH/LbfcMs4+++yCnNW3b99VfjRXuVuyZEk8/vjjqWfAV9p+++1TT0ji6quvjlNPPTX1DCgqAQDIRg4B4Mgjj4x11123YOf16dMn+vTpU7DzcjRp0qSYMmVK6hlFdfHFF0e9evUKdt5ZZ50Va621VsHOK0c53GWdytW7d+/UE5K58MIL45RTTvFJAKqWAABkYd68ebFs2bLUM4ruiCOOKPiZbgi4ap588snUE4qqZ8+eBX+x0LJlyzjmmGMKema5GTt2bOoJ8JVy/QTA5y666CJfB6BqCQBAFmbNmpV6QtF16NChKHdv3nvvvaN+fU+NXVn/+Mc/Uk8oqmOPPbYo5/74xz8uyrnl4rnnnovFixenngFfqmPHjtGpU6fUM5K6+uqr45hjjsnizQPyIgAAWcjh4/99+/YtyrnNmjWLnj17FuXsHFRzAGjQoEHsvffeRTm7Y8eOVf1zt3Tp0njuuedSz4CvNHDgwNQTkrv22mvj0EMPFQGoKgIAkIUcAsBOO+1UtLN33nnnop1dzRYuXBivvPJK6hlFs80220SLFi2Kdn7//v2LdnY5ePrpp1NPgK8kAPzTrbfeGvvuu28Wj3IlDwIAkIVFixalnlB0xQwAxfp0QbV79dVXY/ny5alnFM2OO+5Y0een9tJLL6WeAF9phx12KGrgqyTDhw+Pfffd1+M7qQoCAEAVaNeuXbRr165o5/fq1ct9AFZCtb/A++53v1vU87faaquinp/ayy+/nHoCfKUGDRrEnnvumXpG2bj33ntjjz32EAGoeAIAkIV58+alnlBU3bt3L+r5DRs2jE022aSo16hG1R4Aiv1z17x58+jYsWNRr5HShAkTqvoTIlS+Qw45JPWEsvLoo4/GLrvsEnPnzk09BVaaAABkodr/kL3ZZpsV/Ro9evQo+jWqzZtvvpl6QtHU1NSU5MX5+uuvX/RrpLJo0aJ4//33U8+Ar7TTTjsV9dNllejvf/977LrrrlncW4jqJAAAWaj2AFDsd2IjoiiPGKx2kydPTj2haNq2bRsNGjQo+nU6dOhQ9GukVM0/I1S+evXqxcEHH5x6RtkZP3587Ljjjlk8YpjqIwAAWaj2rwCU4l3Sbt26Ff0a1aSuri7eeuut1DOKZu211y7JdVq3bl2S66QiAFDujjjiiNQTytKECRNihx12iKlTp6aeAt+KAABQBUrxEc1q/i52MUyfPr2qHxu1xhprVNV1UhEAKHff+c53ol+/fqlnlKXXX389evfuHVOmTEk9BVaYAABQBdq3b18V16gm1f7d7ubNm5fkOk2aNCnJdVKZMWNG6gnwjf77v/879YSy9dZbb8W2225b1fd8oboIAAAVbo011ojVV1+96Ndp1qxZyV70VQPfDS2Man/8ZLWHIqpD//79o3PnzqlnlK33338/dtxxR4/2pCIIAAAVrpQvytu2bVuya1W6adOmpZ5QFZo2bZp6QlEJRVSC2tra+NWvfpV6RlmbNm1a9O3bt+of/0rlEwAAKlyLFi1Kdq1GjRqV7FqVzgs7VoSfEyrFIYccEuutt17qGWVt9uzZsf3228e4ceNST4GvJAAAVLiampqSXava340tpGp/8gSFMWfOnNQTYIU0bNgwTj/99NQzyt68efNi1113jcceeyz1FPhSAgBAhWvZsmXJrlXt38cuJAGAFeHnhEpyxBFHeCLMCpg3b17svvvuMXr06NRT4P8QAABYYaWMDZVu/vz5qSdQAerq6mLBggWpZ8AKadiwYVxwwQWpZ1SERYsWRf/+/eOee+5JPQX+FwEAAIrAO7usqMWLF6eeACts//33j169eqWeURGWLl0a++67b9x5552pp8C/CQAAAAn5BACVpKamJi699NLUMyrG0qVL48ADD4xbbrkl9RSICAEAAIpiyZIlqScAFEWvXr3ikEMOST2jYixfvjwOPvjguOqqq1JPAQEAAIph0aJFqScAFM0ll1wSrVq1Sj2jovzkJz+JwYMHp55B5gQAAICEPvnkk9QT4Ftr1apVXHzxxalnVJyTTjrJjRRJSgAAAEho6dKlqSfASjn00ENj5513Tj2j4px22mlx2mmnpZ5BpgQAAABgpVx//fUeEbsSLrjggvjFL34RdXV1qaeQGQEAAABYKe3bt4+rr7469YyKdMkll8RPfvITEYCSEgAAAICVtv/++8fBBx+cekZFuvrqq+PQQw+NZcuWpZ5CJgQAAABglVx55ZXRuXPn1DMq0s033xwHH3xwfPrpp6mnkAEBAAAAWCXNmjWLoUOHRuPGjVNPqUi333577LvvviIARScAAAAAq2zTTTeNa6+9NvWMinXvvffGgAEDYuHChamnUMUEAAAAoCB+9KMfxfHHH596RsUaNWpU7LHHHjFv3rzUU6hSAgAAAFAwl156afTr1y/1jIr16KOPxi677BJz585NPYUqJAAAAAAFU69evbjzzjuje/fuqadUrPHjx8euu+4qAlBwAgAAAFBQzZs3jxEjRkTbtm1TT6lYIgDFIAAAAAAF16FDhxg5cmQ0b9489ZSKNX78+OjTp0/MmjUr9RSqhAAAAAAURY8ePeKBBx6IRo0apZ5SsZ5//vno27evCEBBCAAAAEDRbLvttjFs2LBo0KBB6ikVa8KECTFgwABfB2CVCQAAAEBR9evXL+666y4RYBU8/fTTMXDgwFi4cGHqKVQwAQAAACi6gQMHigCr6PHHH4999tknPv3009RTqFACAAAAUBIiwKp78MEH44ADDohly5alnkIFEgAAAICSGThwYAwdOtSNAVfBsGHD4qijjoq6urrUU6gwAgAAAFBSe+yxR4wYMSKaNWuWekrFuuGGG+Lss89OPYMKIwAAAAAl17dv33j44YejRYsWqadUrHPOOSf+8pe/pJ5BBREAAACAJHr27BlPPPFEtGrVKvWUinXMMcfEAw88kHoGFUIAAAAAkunevXv8/e9/j3XXXTf1lIq0fPny2HfffePZZ59NPYUKIAAAAABJde7cOcaOHRsbbrhh6ikVaeHChTFgwICYOnVq6imUOQEAAABIrlOnTvHYY4/FpptumnpKRZoxY0bstddesXDhwtRTKGMCAAAAUBbat28fo0ePjq222ir1lIr03HPPxZFHHunxgHwlAQAAACgbrVu3jkceeSS23Xbb1FMq0u233x7nn39+6hmUKQEAAAAoKy1atIiHH344dtppp9RTKtKvf/3ruOeee1LPoAwJAAAAQNlp3Lhx3H///bHnnnumnlJx6urq4qCDDopJkyalnkKZEQAAAICy1LBhwxg6dGj86Ec/Sj2l4ixYsCB++MMfxuLFi1NPoYwIAAAAQNmqX79+3HjjjfHjH/849ZSK8+KLL8bxxx+fegZlRAAAAADKWr169eLqq6+On//856mnVJy//OUv7gfAvwkAAABA2aupqYmLL744zjrrrNRTKs7RRx8ds2bNSj2DMiAAAAAAFeO3v/1t/PGPf0w9o6LMnDkzfvKTn6SeQRmon3oAAAAr5+OPP47BgwennlFwbdu2jWOPPTb1DMrYySefHE2aNImf/exnsXz58tRzKsJdd90V9957r6cqZE4AAACoUB9//HH89re/TT2j4Hr06CEA8I2OO+64aN68eRx66KEiwAr66U9/Gn369ImmTZumnkIivgIAAABUpIMOOiiGDRsWDRo0SD2lIrz33ntxzjnnpJ5BQgIAAABQsQYOHBj33XdfNGrUKPWUijB48OCYPHly6hkkIgAAAAAVbZdddokHH3wwmjdvnnpK2fv000/j5JNPTj2DRAQAAACg4m2//fYxatSoaNWqVeopZW/YsGHx1FNPpZ5BAgIAAABQFbbeeusYPXp0tGvXLvWUsnfaaaelnkACAgAAAFA1unfvHo8//nh06NAh9ZSy9thjj8UjjzySegYlJgAAAABVZaONNoonn3wyNtxww9RTyponAuRHAAAAAKpO+/bt47HHHotu3bqlnlK2xowZ414AmREAAACAqtS+ffsYM2ZMbLrppqmnlK3f//73qSdQQgIAAABQtVq3bh2jR4+O7373u6mnlKURI0bEpEmTUs+gRAQAAACgqrVu3TpGjRoV22yzTeopZaeuri4uv/zy1DMoEQEAAACoei1atIiHHnpIBPgS119/fSxYsCD1DEpAAAAAALLweQTwdYD/bf78+XHbbbelnkEJCAAAAEA2WrRoEffff78bA/6Hv/71r6knUAICAAAAkJXPbwwoAvw/48aNi1dffTX1DIpMAAAAALLzeQTYaKONUk8pGzfeeGPqCRSZAAAAAGSpdevW8fDDD8c666yTekpZ+Nvf/pZ6AkUmAAAAANnq1KlTjBw5Mlq0aJF6SnKTJ0+OZ599NvUMikgAAAAAsta9e/e47777olGjRqmnJHfXXXelnkARCQAAAED2vv/978edd94ZtbV5v0QaOXJk6gkUUd4/3QAAAP+yxx57xGWXXZZ6RlIvvPBCzJw5M/UMikQAAAAA+Jef/exncdxxx6WekdTDDz+cegJFIgAAAAB8wZ/+9KfYZZddUs9IZvTo0aknUCQCAAAAwBfUr18/7rzzzujatWvqKUn84x//SD2BIhEAAAAA/kPLli3j//v//r9o3Lhx6ikl9/rrr8fHH3+cegZFUD/1AAAAIL3BgwfH4MGDU88ouIsuuij23Xfflfq1m2yySfz5z3+OQw45pMCryltdXV2MHz8+dt1119RTKDABAAAAiI8//jjefffd1DMKbsGCBav06w8++OAYMWJE3HHHHQVaVBmee+45AaAK+QoAAADA17jqqquiXbt2qWeU1IQJE1JPoAgEAAAAgK+xxhprxOWXX556RkkJANVJAAAAAPgG++yzT+yxxx6pZ5TM66+/Hp999lnqGRSYAAAAALACLr744mjQoEHqGSWxZMmSmDJlSuoZFJgAAAAAsAK6dOkSxx13XOoZJfPOO++knkCBCQAAAAAr6NRTT43VV1899YySePvtt1NPoMAEAAAAgBXUrl27OOaYY1LPKAkBoPoIAAAAAN/CSSedFPXq1Us9o+imTp2aegIFJgAAAAB8Cx07doy999479YyimzFjRuoJFJgAAAAA8C39+Mc/Tj2h6GbPnp16AgUmAAAAAHxLO+20U7Rv3z71jKKaPn166gkUmAAAAADwLdXW1sZ+++2XekZR+QRA9REAAAAAVsKgQYNSTyiqJUuWxNKlS1PPoIAEAAAAgJWw7bbbRosWLVLPKKoFCxaknkABCQAAAAAroV69etGnT5/UM4pq7ty5qSdQQAIAAADAStpxxx1TTyiq+fPnp55AAQkAAAAAK6lnz56pJxTVsmXLUk+ggAQAAACAlbTllltGvXr1Us8omk8//TT1BApIAAAAAFhJq622WnTp0iX1jKJZuHBh6gkUkAAAAACwCjbZZJPUE2CFCAAAAACroGvXrqknwAoRAAAAAFbBhhtumHoCrBABAAAAYBV06NAh9QRYIQIAAADAKmjbtm3qCbBCBAAAAIBV0KpVq9QTiqZhw4apJ1BAAgAAAMAqqOYA0Lhx49QTKCABAAAAYBXUq1cvWrdunXpGUdTWeslYTfzbBAAAWEXV+imA5s2bp55AAQkAQBbUawCgmFZbbbXUE4qiWv+5cuVPxEAW1GsA+Hpu9rZqWrRokXpCUTRq1Cj1BApIAABghS1dujT1BKg63l2jXLjZG1+mWbNmqSdQQAIAACvsk08+ST0Bqo531yi0mpqalfp1TZo0KfCSvFRjQGnatGnUq1cv9QwKSAAAgCLwri6Qysp+FN3vW6umGr9C0bJly9QTKDABAKDCzZ8/v2TXWrx4ccmuVem8qwtUmmq9X87HH3+cekLFEgCqjwAAZKFa/1ATEbFkyZKSXUsAgMKrxncNSWf11Vdf6V8rXPKfBIDqIwAAWWjQoEHqCUWzYMGC1BP4Eqvyh3DyUo3fGyadVfkY/xprrFHAJeXDJwBWXtu2bVNPoMAEACAL1XwH27lz55bsWm4CuOLcTIsVVc2/P1F6K3sDwIjqDQAfffRRSa6zcOHCklynlFq3bp16AgUmAABZqOYbG82dOzeWL19ekmt5DOCK86KOFbHaaqu5wzYFtSof2V5zzTULN6SMzJgxoyTXKWWQLxWfAKg+AgCQhWr+XmNdXV3Mnj27JNeaN29eSa5TDar5vhMUjk+KfLlFixalnlCx1lprrZX+tdX6CYApU6aU5Dql+qRBKbVp0yb1BApMAACyUO1/yC5VAJgzZ05JrlMNmjZtmnoCFaBaX3CtqmL/nlaNL9Q+tyoBoLa2Nlq1alXANeVh4sSJJbnO1KlTS3KdUlpvvfVST6DABAAgC9V8E8CI0ry7UY0fbSwm35tkRVTji61C+Oijj4r6iNNZs2YV7ezUViUARESsvfbaBVpSPubMmRPTp08v6jVmzpxZlU/K6dChQ+oJFJgAAGSh2h9j88477xT9GqX6DmW18LFJVoQA8NXeeOONop399ttvF+3s1FY1AKy77roFWlJexo0bV9TzX3311aKen4oAUH0EACAL1f5x7Lfeeqvo1/jggw+Kfo1q4oVdYSxZsiT1hKJa1VDUokWLAi0pP08//XTRzn7llVeKdnZq7dq1S/rry9Xjjz9e1POff/75op6fQvPmzav695hcCQBAFurXr1/V9wF47bXXin6NadOmFf0a1aQaP0abQrXfDG5VA8CqPPKt3D366KNFO/upp54q2tmpderUaZV+/TrrrFOYIWXm/vvvL+r5Tz75ZFHPT2GDDTZIPYEiEACAbFTz1wBefvnlol+jFF8zqCbrrrtuVb84+/DDD0tynWJ+D7wcrOrHa6s5bI4cObIo36leunRpjBo1quDnlosNN9xwlX79+uuvX6Al5WXixInx4osvFuXsJUuWxMMPP1yUs1Pq2rVr6gkUgQAAZKOaA8CUKVOK/oJs0qRJRT2/2jRo0KBq30mLKF0AqPZ7T6xqAGjQoEHVPuZ03rx5MXTo0IKfe//998fHH39c8HPLxaq+a7uqAaGcXXvttUU596GHHqrKx+R26dIl9QSKQAAAslHNASAi4oknnijq+W+++WZRz69G1XzzpFLdRO3dd98tyXVS6dix4yqfUc1fN/njH/8YdXV1BT3zkksuKeh55aRp06ar/ASSav7Y9/XXX1+UR0z+6U9/KviZ5aBz586pJ1AEAgCQDQFg5dXV1cULL7xQtPOr1ap+F7ecLV68uCQvzkv1/O5UCvEzUs1PnHjhhRfipptuKth5w4cPL/rN4FIqxAu29dZbL+rXr1+ANeVn4cKFcfrppxf0zNGjR1ftV0o22WST1BMoAgEAyEa1B4D77ruvaGe/+eabVfnxxmKr9u9PPvvss0U9f/78+VX9yZN11lknmjVrtsrnrOo7vuXuxBNPjClTpqzyOVOnTo0f//jHBVhUvrbccstVPqNevXpV/c7vtddeG/fee29Bzvroo4/iyCOPLMhZ5aZevXqx8cYbp55BEQgAQDZW9dnI5W7ixInx+uuvF+Xsary7cSl069Yt9YSiGjt2bFHPf+KJJwr+8e9yUqhAVK2PbfvcnDlzYsCAAat0P4h33303dtppp5g1a1YBl5WfLbbYoiDnVPs7vz/60Y9i9OjRq3TG7NmzY7fddqvaG+R26dIlVl999dQzKAIBAMhG27ZtU08ouhtvvLEo51brxxuL7Tvf+U7qCUX14IMPFvX8kSNHFvX81Ar17lo137TtcxMmTIgtttjiW/9M1NXVxe233x5bb711vPHGG0VaVz622mqrgpyz6aabFuSccrVgwYLo379/nH/++bF06dJv9Wvr6upi6NCh0aNHjxg/fnyRFqbXo0eP1BMoEgEAyEY1f0/2c9ddd118+umnBT1zyZIlRX9+crXq2rVr1X6XNiLi1VdfjZdeeqkoZy9btizuuuuuopxdLgoVAKr9qyafmz59euy2227Ru3fvuOWWW7723fy33347Lr/88thiiy3iwAMPrPp3/iMiamtro3v37gU5q1DnlLOlS5fG6aefHhtssEH8/ve/jwkTJnzl37t8+fKYMGFCXHzxxdGjR4/4wQ9+ENOmTSvh2tIrVEyi/FTvn0oA/kO1f0w24p+PTLvuuuvi2GOPLdiZ9913X8yZM6dg5+VktdVWi+7du8fzzz+fekrRXHPNNXH55ZcX/Nx77703pk+fXvBzy0khvq8dkd+jusaMGRNjxoyJiIj27dtHx44do2nTprF06dKYP39+TJ48uWSPqSwn3bt3jyZNmhTkrJze/Z06dWqcccYZccYZZ0SzZs2ic+fO0bx582jUqFEsXLgwZs6cGe+8804sWrQo9dSS6tWrV+oJFIkAQNl79dVXY/DgwalnFM3GG28cu+66a+oZWaj2G2V97pxzzomDDz44mjZtuspn1dXVxYUXXliAVfnaaqutqjoA3HDDDXHWWWcV/BM2f/jDHwp6XrmpX79+bL755gU5q3PnzlFbWxvLly8vyHmVZOrUqTF16tTUM8rCzjvvXLCzNtxww1hrrbWyCynz58+P5557LvWM5OrVq+cTAFVMAKDsPf300/H000+nnlE0hx12mABQIuuss07qCSUxbdq0+NWvfhVXXHHFKp918803x7hx4wqwKl9bbbVV/OUvf0k9o2g++eST+M1vfhNXXnllwc4cOnRoPPXUUwU7rxx17969YDfYatiwYWy00UZZfMedr9a3b9+Cnrf11ltX/X04+HKbb755NGrUKPUMisQ9AIBs5PIJgIiIK6+8cpWfnT1p0qT42c9+VqBF+crhY5RXX311PProowU568MPP4yf/vSnBTmrnG2zzTYFPe973/teQc+jstSrVy922GGHgp7Zs2fPgp5H5dhuu+1ST6CIBAAgGw0bNswqAhx55JFx5513rtSvnTBhQvTp0yfmzZtX4FX52WyzzWKNNdZIPaOo6urqYr/99ovXXnttlc5ZuHBh7LnnnvHBBx8UaFn56t27d0HPEwDy9r3vfa8gX/v6Ii8C89WnT5/UEygiAQDISocOHVJPKJmlS5fG/vvvH6ecckp88sknK/RrFi1aFH/84x/ju9/9brz//vtFXpiH2tra2HbbbVPPKLrZs2fHdtttFw899NBK/frJkyfHdtttF//4xz8KvKw8CQAU0qBBgwp+5rbbbhsNGjQo+LmUt9ra2oL//kR5EQCArHTq1Cn1hJK76KKLYoMNNohf//rXMW7cuP/zmMDp06fHgw8+GMcff3ysv/768ctf/jKWLFmSaG11yuXdlI8++ij69esXBxxwwArf+PD999+PM844IzbZZJOqvlniF33nO9+Jtm3bFvTMTTbZJJo1a1bQM6kcP/zhDwt+ZuPGjX0NIENbbrlltGjRIvUMishNAIGs5BgAIiJmzpwZ5513Xpx33nkREbHmmmtGbW1tzJ8/34v9Eth9993j5JNPTj2jZO6444644447onPnztG3b9/o1q1brLPOOtGgQYOoq6uLmTNnxqRJk+LJJ5+M8ePHZ3f3+v79+xf8zHr16sUuu+wSQ4cOLfjZlLetttqqaJ9u22mnnWLs2LFFOZvyNGDAgNQTKDIBAMhK165dU08oCx999FHqCVnp1q1brL/++vH222+nnlJSkyZNikmTJqWeUXb22GOPopy75557CgAZ2nfffYt2dv/+/eO3v/1t0c6n/AgA1c9XAICsCACkUqwXfVSWZs2axfbbb1+Us3ffffeoqakpytmUp9ra2vjRj35UtPO32WabWHvttYt2PuVl7bXXjq222ir1DIpMAACy0q1bt9QTyNQ+++yTegJlYI899oiGDRsW5ew2bdr4znZmBgwYUNSb29bW1npHOCODBg2K2lovD6udf8NAVlq3bu3dDJLYYYcdol27dqlnkFgx362NiDjwwAOLej7l5dhjjy36NYrxhAHKUzFuJkn5EQCA7Gy55ZapJ5Ch2tpaL84yt8Yaa0S/fv2Keo2DDjqoaJ8woLx06tSpKDeU/E/9+vWLNdZYo+jXIa3WrVt7/F8mBAAgO1tssUXqCWTqoIMOSj2BhA444ICiP1d9rbXWir322quo16A8HHfccSX5uHbDhg1jv/32K/p1SGvfffeNevXqpZ5BCQgAQHa22Wab1BPI1JZbbhlbb7116hkkcswxx5TkOkceeWRJrkM6a621VvzkJz8p2fWK/dUV0jviiCNST6BEBAAgO9tuu23qCWSsFN/Zpfx873vfix49epTkWrvssosbnla5k08+OZo2bVqy622//fbRpUuXkl2P0urevXt897vfTT2DEhEAgOy0bt3aH2RI5oADDog111wz9QxKrJTv1tbW1sZpp51WsutRWi1atCh5SKypqYnjjjuupNekdHxqKC8CAJClHXfcMfUEMtW4ceP42c9+lnoGJdShQ4c44IADSnrNAw88MDp16lTSa1Iav/jFL6Jly5Ylv+7hhx8ejRs3Lvl1Ka4mTZrEYYcdlnoGJSQAAFkq9p244escf/zx0aRJk9QzKJFTTjkl6tevX9Jr1q9fP371q1+V9JoUX8eOHePkk09Ocu2WLVvGoYcemuTaFM/hhx+eJCiRjgAAZKlv374luXsyfJlWrVrFUUcdlXoGJdC2bdtkH6898sgjo3PnzkmuTXFceOGF0ahRo2TX/9WvfuVO8VWkpqYmjj/++NQzKDF/+gWytMYaa7gZIEn9+te/jmbNmqWeQZGdeeaZyV6wNWjQIC688MIk16bwdthhh+SP4+vUqZPHmVaRgQMHRteuXVPPoMQEACBbe++9d+oJZKxVq1bxy1/+MvUMiqhz587x4x//OOmGQYMGxe677550A6uufv368T//8z+pZ0RExOmnn+5TAFXi7LPPTj2BBAQAIFv77LNP6glk7qSTTor27dunnkGRXHjhhSX/7v+Xufzyy91zosKdddZZsdlmm6WeERERXbt2jWOOOSb1DFbRwIEDY4sttkg9gwQEACBbnTp1ip49e6aeUVHcAbqwmjRpEn/6059Sz6AIBgwYEIMGDUo9IyIi1l9//bjkkktSz2AlbbXVVmX3WMezzz47mjZtmnoGK6mmpiZ+85vfpJ5BIgIAkDV3NF5xrVu3jt69e6eeUXX23nvvGDBgQOoZFFCjRo3iiiuuSD3jfzn66KNjt912Sz2Db6lhw4Zxww03lMUnSb6oTZs2ceaZZ6aewUo6+OCDvfufMQEAyNr+++8fDRs2TD2jIhx++OHRoEGD1DOq0lVXXRUtWrRIPYMCueCCC6Jjx46pZ/wvNTU1ce2118Zaa62Vegrfwh/+8IfYdNNNU8/4Uj//+c9j8803Tz2Db2n11VeP8847L/UMEhIAgKyttdZaye+qXAlqamqS38ysmq233npx1VVXpZ5BAey6665l+1itddddN26//XaPQK0Qe++9d5xwwgmpZ3yl+vXrxzXXXOPnqcKceuqpsd5666WeQUL+iwWy99Of/jT1hLK35557xkYbbZR6RlU78MAD48ADD0w9g1Ww1lprxfXXXx81NTWpp3ylnXfeOc4///zUM/gG3bp1iyFDhpT1z1JExNZbbx2nn3566hmsoM6dO8epp56aegaJCQBA9nr16uVmgN/glFNOST0hC9dcc01ssskmqWewEmpra+OOO+6IddZZJ/WUb3TKKafED3/4w9Qz+AprrrlmDB8+PJo1a5Z6ygo5++yz43vf+17qGayAK6+8MlZbbbXUM0hMAACIKLs7LJeTnj17xnbbbZd6RhaaNm0aw4cPj5YtW6aewrd04YUXxk477ZR6xgqpqamJG264IXr16pV6Cv+hYcOGMXz48OjSpUvqKSusfv36ceutt8Yaa6yRegpf47jjjoudd9459QzKgAAAEP98Hq6bGX25c889N/WErGy00Ubxt7/9zQ0XK8gRRxwRv/jFL1LP+FYaN24c99xzT3Tr1i31FP6ltrY2br311th+++1TT/nWOnXqFHfddVfZPa2Af+ratWtcdNFFqWdQJgQAgPjnO2J//OMfU88oOzvuuKN3DBLYeeed46abbko9gxWw1157xTXXXJN6xkpp3bp1PPTQQ7HhhhumnkL88ytAP/jBD1LPWGl9+/aN//mf/0k9g/+w+uqrx2233RaNGzdOPYUyIQAA/MvOO+8ce+21V+oZZeWCCy5IPSFb+++/f1x55ZWpZ/A1+vTpE7feemvUq1cv9ZSV1r59+3jsscdEgMSuvPLKOPLII1PPWGXHHntsnHHGGaln8AV//etfY4sttkg9gzIiAAB8weDBg6NJkyapZ5SFQw891HeEEzvuuONEgDLVp0+fGDFiRFW8q/Z5BCjX581Xs9ra2hgyZEgcd9xxqacUzLnnnhsnnnhi6hlExK9+9av40Y9+lHoGZUYAAPiCTp06xaWXXpp6RnLNmjXz7n+ZOO644+Lqq6/2rO0yMmDAgKp58f+59u3bxxNPPBE77LBD6inZaNSoUQwbNiwOPfTQ1FMK7pJLLhEBEjvssMM88pMv5U8TAP/hqKOOioEDB6aekdTFF18c7dq1Sz2DfznmmGPi7rvv9umUMnDsscfG3XffXVUv/j/XsmXLePjhh+PYY49NPaXqtWnTJh599NGq/d+ampqauPTSS+P3v/996ilZ+uEPfxh//etfo6amJvUUypAAAPAfampqYsiQIdG5c+fUU5LYZZdd4qijjko9g/+wxx57xBNPPBHt27dPPSVLtbW1cdFFF8VVV11V0d/5/yYNGzaMq666Kq666ipPoiiSLbfcMsaPHx89e/ZMPaXoTjvttBgyZIhnz5fQ/vvvHzfffHNV/z7FqhEAAL5Ey5YtY/jw4dG8efPUU0pqrbXWiuuuu867BmVqiy22iOeffz522WWX1FOysvbaa8eoUaMq7lF/q+LYY4+NJ554ws0BC+yYY46JsWPHRseOHVNPKZlDDz00nnzyyejQoUPqKVXvhBNOiNtuuy0aNmyYegplTAAA+Aobb7xx3H///dGoUaPUU0rmpptu8g5zmWvVqlWMHDkyzj33XM/cLoFdd901XnjhhejTp0/qKSXXs2fPeOGFF3wiqADWWGONGDp0aFx99dVZ/W/K57baaqt49tlnq/YrD6nVq1cvLr300hg8eLCAzzcSAAC+xve///0YPnx4Fn9g+81vfhO77bZb6hmsgNra2jjjjDPi6aefjh49eqSeU5WaNWsW11xzTYwcOTLatm2bek4yTZs2jWuvvTbuuece7+CupJ122ileeuml2HvvvVNPSapVq1Zx9913x3XXXRfNmjVLPadqrL322jF69Gg3XWSFCQAA32DXXXeNkSNHVvXXAQ4++OA466yzUs/gW9p8883j6aefjnPOOSeLSFUqe+21V0yYMCGOPvpo76b9y5577hmvvfZanH766T5evIJat24dN954Yzz88MM+WfUF//Vf/xWvvPJK7LvvvqmnVLzdd989nn/+eU/v4FsRAABWwA477BCPP/54VX5vs2/fvu4WXMEaNGgQv/71r2PixIlxwAEHpJ5T0TbeeON48MEHY/jw4d7t/hKNGzeO8847L1566aXYa6+9Us8pWzU1NXH00UfH66+/HocccojfW7/EeuutF3/729/ioYceio033jj1nIrTokWLuP766+O+++7zxB6+NQEAYAX16NEjnn322aq6AVufPn3i3nvv9Y5eFVhvvfXitttui3/84x/Rr1+/1HMqyoYbbhjXX399vPjii7HrrrumnlP2unbtGsOHD4/x48f7WfsPAwYMiOeeey6uueaaWHPNNVPPKXu77LJLvPzyy3HjjTe64eQKqKmpiaOOOiomTpwYhx9+eOo5VCgBAOBbWGutteKBBx6ICy+8sOIfa9S/f/8YMWJEVT7PPGe9evWKkSNHxj/+8Y/Yc889vfv4Nbp16xbXX399vP7663H44Ye7qeK3tPXWW//7Z23QoEFZ/6ztsMMOMXbs2BgxYkRsvvnmqedUlNra2jjkkEPi9ddfjyFDhsRmm22WelJZ6tevXzz33HNx7bXXxtprr516DhVMAAD4lurVqxennHJKPPPMM9GrV6/Uc1bKf//3f8c999zjxX8V69WrV9xzzz3xxhtvxPHHH++mW/9SU1MTu+22W4wcOTJeeeUVL/wLoFevXjFs2LB466234qSTTqrq+6V8Ub169WLfffeNsWPHxpgxY+L73/9+6kkVrX79+nHooYfGiy++GI888kjstdde/tuMf96T5KmnnoqRI0eKSxSEAACwkjbddNP4+9//Hrfddlt06tQp9ZwVss4668SIESPisssuiwYNGqSeQwlstNFG8ac//Sk++OCDuOmmm2KnnXbK8p3azp07x3nnnRdTpkyJ+++/P/r165fl/x+Kaf31149LLrkkpk2bFjfccEPV/qy1bt06fvGLX8Sbb74Zf/vb37zwL4K+ffvG8OHDY+rUqXHxxRdn96mANdZYI0444YR45ZVXYvjw4dGzZ8/Uk6giAgDAKqipqYkDDjggJk6cGEOGDInu3bunnvSlVl999TjxxBPjlVdeiQEDBqSeQwKNGzeOgw8+OEaNGhVTp06NK6+8Mnbeeeeqfodtk002iV//+tfx7LPPxhtvvBGnn366u7GXQJMmTeKwww6LUaNGxZQpU+KCCy6IXr16VXQMWH311eOAAw6IESNGxPvvvx8XXXRRxYTfSrb22mvHz3/+83jxxRfjjTfeiD/84Q8V/7P0VRo2bBh77LFH3HzzzTFt2rQYPHiwGyRSFDV1dXV1qUdQGC1btoy5c+emnsG3dNhhh8UNN9zwrX/dCy+8EFtssUXhB5WJYcOGxaBBg1LPWCmPPfZY3HrrrXHXXXfFnDlzkm5p2rRpHHLIIXHaaafFeuutt8rnDRo0KO6+++4CLCtPe+21VwwfPjz1jJL6+OOP49FHH41HHnkkHn744XjjjTdST1pprVq1ij59+sQuu+wSO+20U2ywwQapJ/EFM2bMiPvuuy/uvffeGDNmTPLfH79Ju3bton///rH77rtHv379fI2mjHz44Yfx6KOPxqhRo2L06NExadKk1JNWSps2bWLnnXeO/v37x8CBA6NFixapJ5EBAaCKzJ07N/zrrDwNGzZcqe9hL1u2LObPn1+EReWhSZMmFf8R9U8//TRGjx4dDz30UIwaNSpefvnlkly3YcOGsf3228dBBx0U++23XzRp0qRgZ3/yySexdOnSgp1Xbho0aFDQ/39VopkzZ8a4cePiqaeeivHjx8fLL78cM2bMSD3r/2jSpElsvPHGsfXWW0evXr1im222iS5dulTlO4PVaPny5TFhwoR4/PHH44knnohnnnkmJk+enHRTp06domfPntGzZ8/o27dvbLbZZn6eKsTs2bPj6aefjnHjxsUzzzwTEyZMiHfffTf1rP9jo402im222Sa22Wab6N27t58xkhAAAErkww8/jOeeey6effbZeOGFF2LSpEkxefLk+Pjjj1fp3DZt2kSPHj2iR48e0bt37+jdu3f2L2IprNmzZ8eECRPitddei7fffjveeeedmDx5crz33nsxe/bsWL58eVGu27Jly1h33XWjU6dOsf7668f6668fXbp0iU033TQ6duzoD85VZv78+fHSSy/Fiy++GK+//nq8/fbb//55++STTwp2nXXWWSe6dOkSnTt3jq5du8Z3vvOd2HrrraNNmzYFuwbpLViwIF577bV47bXX4p133vn3X++9915Mnz69oD9TX9SiRYt//77VpUuX6NatW3Tt2jW6d+/u0ZCUBQEAILGPP/44pk6dGh9++GHMmjUrPvroo1i4cGF89tlnsWDBgn//fZ9/KqJZs2axzjrrxLrrrhvrrLNOtGrVKuF6crd8+fKYPXt2zJo1K2bNmhXz5s2L+fPnx4IFC2LRokX/5+f4c6uvvnqsvvrq0aBBg2jatGm0aNEimjdvHi1atIi2bdtG69ato2HDhgn+iShHs2fPjg8//PB//fV5PF28eHEsXrz4339v8+bNo7a2Nho3bhxNmjSJVq1aRZs2bWLttdeO1q1bV/ynyyiMRYsWxaxZs2LGjBkxf/78mDdvXsybNy8++eSTWLJkyVd+0rJZs2ZRr169WH311aNFixbRsmXLaNmyZay55prRvn17AZ6yJwAAAABABjwFAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIwP8PWIabnjgcWq0AAAAASUVORK5CYII=',
scale: 3.508264580369843,
},
locked: false,
child: [],
parent: 'ROOT',
},
},
},
],
img: '/assets/images/qr-code/1.png',
},
{
elements: [
{
rootId: '5c11f494-5d02-4d65-a816-c9669c519065',
layers: {
'5c11f494-5d02-4d65-a816-c9669c519065': {
type: { resolvedName: 'GroupLayer' },
props: {
position: { x: 23.478260869565247, y: 100.89354861209551 },
boxSize: { width: 395.04347826086956, height: 462 },
scale: 1,
rotate: 0,
},
locked: false,
child: [
'd78fb0c2-7a1c-4381-8571-c9871ccd9cac',
'3f39efa2-4017-4c77-b3a9-c98456c629f0',
'930dca01-95e0-4527-be58-ede3cc41bc2e',
],
parent: 'ROOT',
},
'd78fb0c2-7a1c-4381-8571-c9871ccd9cac': {
type: { resolvedName: 'SvgLayer' },
props: {
image:
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1OSIgaGVpZ2h0PSI2OSIgdmlld0JveD0iMCwtMC4wMDAwMDc2MjkzOTQ1MzEyNSwxNS40Nzk1NTMyMjI2NTYyNSwxOC4xNDQzNDA1MTUxMzY3MiIgdmVyc2lvbj0iMS4xIiB4bWw6c3BhY2U9InByZXNlcnZlIj4KICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNDcuNDQxODM1LC0xMTUuODQ5NjcpIj4KICAgIDxyZWN0IHdpZHRoPSIxNS40Nzk1NTEiIGhlaWdodD0iMTguMTQ0MzM5IiB4PSI0Ny40NDE4MzMiIHk9Ii0xMzMuOTk0IiB0cmFuc2Zvcm09InNjYWxlKDEsLTEpIiByeT0iMC40NTEwMTUxNyIvPgogICAgPHJlY3QgZmlsbD0iI2ZmZmZmZiIgc3Ryb2tlLXdpZHRoPSIwLjUyNTg1OyIgaWQ9ImNyYXlvbi11bmlxdWUtaWQtMzQtNCIgd2lkdGg9IjE0LjU5MTQzNyIgaGVpZ2h0PSIxNC41NTI4MjQiIHg9IjQ3LjkwMzUiIHk9Ii0xMzAuODUzMDkiIHRyYW5zZm9ybT0ic2NhbGUoMSwtMSkiIHJ5PSIwLjMxNjk5OTkxIi8+CiAgPC9nPgo8L3N2Zz4K',
position: { x: 0, y: 0 },
boxSize: { width: 395.04347826086956, height: 462 },
colors: ['rgb(0, 0, 0)', 'rgb(255, 255, 255)'],
rotate: 0,
},
locked: false,
child: [],
parent: '5c11f494-5d02-4d65-a816-c9669c519065',
},
'3f39efa2-4017-4c77-b3a9-c98456c629f0': {
type: { resolvedName: 'QrCodeLayer' },
props: {
text: 'https://lidojs.com',
position: { x: 22.108510111942678, y: 21.070943504373645 },
boxSize: {
width: 350.8264580369844,
height: 350.8264580369844,
x: 1082.586770981508,
y: 155.9961470707375,
},
rotate: 0,
bgColor: 'rgb(255, 255, 255)',
textColor: 'rgb(30, 30, 45)',
logo: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAAAAAQACAYAAAB/HSuDAAAACXBIWXMAAC4jAAAuIwF4pT92AABbQ0lEQVR4nO3defzVc/7//8f73UJ7qBSpLNWEZBlqjKWyFJEYYxnrxzKYGR/MMGMZzAyGMZbMxzbMIDvjVyGERGRG2cmSCElpIS1apN6/P2bM12c+ltQ553nOeV6vl4t/Phc9X3cfb03nds55vWrq6urqAgAAAKhqtakHAAAAAMUnAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIgAAAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyIAAAAAAABkQAAAAACADAgAAAABkQAAAAACADAgAAAAAkAEBAAAAADIgAAAAAEAGBAAAAADIQP3UA4CV8/7778cf//jH1DOK5sgjj4zu3bunngEAAFVDAIAKNWvWrLjssstSzyia3r17CwAAAFBAvgIAAAAAGRAAAAAAIAMCAAAAAGRAAAAAAIAMCAAAAACQAQEAAAAAMiAAAAAAQAYEAAAAAMiAAAAAAAAZEAAAAAAgAwIAAAAAZEAAAAAAgAwIAAAAAJABAQAAAAAyIAAAAABABgQAAAAAyED91AMAAAAKZenSpTF58uR48803Y+rUqTF9+vSYPn16zJo1K+bOnfvvv5YuXRrLli2L+fPn//vXNm7cOBo2bBgREY0aNYpmzZr9+68111wzWrduHW3atIl27dpFmzZtolOnTtGxY8do0KBBqn9c+FYEAAAAoCJ98MEHMX78+HjhhRfi2WefjQkTJsS7774by5YtW6nz5s6d+61/TW1tbay33nqxwQYbRLdu3WKzzTaLzTbbLDbddNNo1qzZSu2AYhEAqshVV10VS5YsST2j4DbeeOPYddddU88AIuLee++Nt956K/WMotlwww1jzz33TD0DKIHBgwennlA0xx13XKy22mqpZxTFhx9+GCNHjoxHH300xowZE2+++WbqSbF8+fJ499134913341HH3303//3mpqa6NatW3z/+9//918bbbRRwqUQUVNXV1eXegSF0bJly5WqluXusMMOixtuuCH1jLLzwgsvxBZbbJF6RtEMGzYsBg0alHoG/2HQoEFx9913p55RNHvttVcMHz489QygBGpqalJPKJo5c+ZEy5YtU88omClTpsTtt98e9957b/z973+P5cuXp5600tZff/3Yfffdo3///rHTTjtFo0aNUk8iMz4BAAAAlJWFCxfGbbfdFjfddFM8/vjjUS3vWb799ttxxRVXxBVXXBFNmjSJPffcM370ox/FrrvuWrWf2qC8eAoAAABQFiZPnhw///nPY911142jjjoqxowZUzUv/v/TJ598ErfffnsMHDgw2rVrFyeddFK88cYbqWdR5QQAAAAgqYkTJ8bhhx8eXbp0iUsvvTQ+/vjj1JNKas6cOTF48ODo2rVr7LLLLjFy5MjUk6hSAgAAAJDE+++/H4cffnh069YthgwZstJ3768mo0aNit122y169uwZw4cPr9pPQJCGAAAAAJTU4sWL45xzzokuXbrEkCFDvMj9EuPHj4+99947tttuuxg/fnzqOVQJAQAAACiZJ598MjbffPM466yzYuHChannlL2///3v0bNnzzj44INj1qxZqedQ4QQAAACg6JYsWRK/+MUvYvvtt4+JEyemnlNxbrnllujWrVvccsstqadQwQQAAACgqN58883Ydttt45JLLvFx/1Xw4YcfxsEHHxw/+MEPYs6cOannUIEEAAAAoGjuu+++2HLLLeO5555LPaVqDB06NL773e/Gs88+m3oKFUYAAAAAiuLiiy+OgQMHxvz581NPqTqTJ0+O73//+3HrrbemnkIFEQAAAICCqqurixNOOCFOPvnkWL58eeo5VWvJkiVx0EEHxQUXXJB6ChVCAAAAAApm2bJlceSRR8af/vSn1FOycdppp8Xxxx/v/gp8IwEAAAAoiLq6ujj88MPj+uuvTz0lO5dffnmceuqpqWdQ5gQAAACgIH72s5/FzTffnHpGti688MI444wzUs+gjAkAAADAKvvd734XV155ZeoZ2fv9738f11xzTeoZlCkBAAAAWCV33HFHnH322aln8C8//elP45FHHkk9gzIkAAAAACvtmWeeif/6r/9KPYMv+Oyzz+KHP/xhvPfee6mnUGYEAAAAYKXMnTs39t9//1i0aFHqKfyHOXPmxIEHHhifffZZ6imUEQEAAABYKUcddVRMnjw59Qy+wpNPPhm/+93vUs+gjAgAAADAt3bDDTfEXXfdlXoG3+D3v/99PPfcc6lnUCYEAAAA4Fv54IMP4sQTT0w9gxWwbNmyOPLII30VgIgQAAAAgG/puOOOi7lz56aewQp64YUX4pJLLkk9gzIgAAAAACts5MiRMXz48NQz+JbOPffcmDFjRuoZJCYAAAAAK+Szzz6Lk046KfUMVsL8+fPj17/+deoZJFY/9QBg5ay55ppx2GGHpZ5RNB06dEg9AQD4D1deeWW8/vrrqWewkv7617/GSSedFBtvvHHqKSQiAECF6tChQ9xwww2pZwAAmVi0aFGcf/75qWewCurq6uJ3v/td3H777amnkIivAAAAAN/oz3/+c3zwwQepZ7CK7rzzznj11VdTzyARAQAAAPhaixcvjvPOOy/1DAqgrq7Ov8uMCQAAAMDXuvnmm2P27NmpZ1Agd955Z7z//vupZ5CAAAAAAHylurq6GDx4cOoZFNBnn30WV1xxReoZJCAAAAAAX2n06NHxyiuvpJ5Bgf35z3+OxYsXp55BiQkAAADAV/rLX/6SegJF8NFHH8Xw4cNTz6DEBAAAAOBLzZkzJ4YNG5Z6BkVy3XXXpZ5AiQkAAADAl7r11ltjyZIlqWdQJKNGjYopU6aknkEJCQAAAMCXuvPOO1NPoIjq6ur8O86MAAAAAPwfM2fOjLFjx6aeQZHdddddqSdQQvVTDwDIxfLly+Pdd9+Nt99+O955552YMWNGfPTRR/HRRx/FsmXL4uOPP46IiNVWWy0aNWoU9erVi2bNmkWrVq2idevW0aZNm1h33XWjS5cu0bJly6T/LPC5uXPnxrRp0+L999+PGTNmxIcffhhz5879919Lly6Nurq6mDt37r9/TZMmTaJBgwYREdGyZcto0aLFv/9q165dtGvXLtZZZ51o06ZN1NTUpPpHg+wNHTo0li9fnnoGRTZu3LiYMmVKdOjQIfUUSkAAACiSmTNnxuOPPx6PPfZYPPvss/Hyyy/HJ598UpCz11577fjOd74TW265ZWy33Xax3XbbRZs2bQpyNnyZyZMnx/PPPx8vvfRSvPnmmzFx4sSYNGlSzJs3r2jXbNCgQWywwQbRtWvX2GijjWLjjTeOLbbYIjbddNNo2LBh0a4L/NOIESNST6BEhg4dGieeeGLqGZSAAAAVatGiRTFx4sTUM4pm/fXXjxYtWqSe8a299957ceutt8bw4cNj3LhxUVdXV5TrzJgxI2bMmBFjxoyJSy+9NCIiNt5449hnn31in332iS222KIo1yUPixcvjqeeeirGjBkTY8eOjaeffvp/vYNfKkuXLo2JEyf+n9/r6tevH927d49tt902dtxxx9h+++2jbdu2Jd8H1WzJkiXx6KOPpp5BiTzwwAMCQCYEAKhQEydOrOoXecOGDYtBgwalnrFC6urq4oEHHoj/+Z//iQcffLBoL/q/yauvvhqvvvpqnHvuudG5c+f46U9/GkcccUQ0a9YsyR4qy9tvvx333ntvjBgxIh5//PGyvuv3Z599Fs8//3w8//zzccUVV0RExCabbBIDBgyIPfbYI7bddtuoV69e4pVQ2caOHRsLFy5MPYMSGTNmTCxatCgaNWqUegpF5iaAAKtg+PDhsfnmm8eAAQNi5MiRyV78/6dJkybFiSeeGOuuu2788pe//Pf9BeCL3nvvvbjwwgtj8803jw022CBOOOGEePjhh8v6xf9XeeWVV+LCCy+MHXbYIdq0aRPHHntsjB07tmz+m4RKM2rUqNQTKKElS5bEY489lnoGJSAAAKyECRMmxI477hh77713vPTSS6nnfKX58+fHH//4x9hwww3jsssui88++yz1JBL77LPP4q677oq+fftGx44d41e/+lW8+OKLqWcV1EcffRR//vOfY/vtt49OnTrFueeeGx988EHqWVBRnnzyydQTKLFHHnkk9QRKQAAA+BaWLVsW55xzTmy11Vbx+OOPp56zwj766KM48cQTo1evXvHqq6+mnkMCc+bMiXPOOSc6duwYP/zhD+PRRx/N4t3xKVOmxJlnnhkdOnSIAw44IJ555pnUk6DsffrppzF+/PjUMyixSvpzDStPAABYQR988EHsvPPOcdZZZ8Wnn36aes5KefbZZ2PrrbeOm2++OfUUSmTGjBlx6qmnxnrrrRdnnXVWTJs2LfWkJJYuXRp33HFHbL311tG/f/8YM2ZM6klQtp555pmK/CoQq+a5556LBQsWpJ5BkQkAACvg5Zdfjq233roqvh+3cOHCOOSQQ+LMM8/M4h3gXC1YsCDOPvvs2GCDDeIPf/hDwR5BWQ0efPDB6N27d/Tr16+sv8IDqTz33HOpJ5DAsmXL4qmnnko9gyITAAC+wdixY2O77baLqVOnpp5SUOeee24cdthhsWzZstRTKKC6urr461//Gp07d47f/e537uL9NR566KHYfPPN44gjjoiZM2emngNl4/nnn089gUTGjRuXegJF5jGAAF9j3LhxMWDAgJg3b17qKUVx0003xbJly+LGG2/02LQq8Nprr8WPf/zjGDt2bOopFaOuri6uv/76GD58eFx44YVx5JFHRk1NTepZkFQuAaBt27ax4447Rvfu3aNTp07RpEmTiPjnPRDee++9ePHFF2P06NHx/vvvJ15aOs8++2zqCRSZAADwFSZOnBj9+vWr2hf/n7v11lujtrY2brzxRi98KtSyZcvi/PPPj3POOadi70+R2pw5c+Loo4+Om266KYYMGRKdOnVKPQmSWLZsWbzyyiupZxTV7rvvHieffHLsuOOOUVv79R+IrqurizFjxsR5552XxaMR3Si1+vkKAMCXmDNnTgwcODDmzp2bekpJ3HzzzXHGGWeknsFKeOedd6J3795x5plnevFfAI8//nj06NEjbr311tRTIIl33nmnan8vadGiRQwdOjTuu+++6NOnzze++I+IqKmpid69e8fDDz8ct912WzRt2rQES9N57733YtasWalnUEQCAMCXOPzww+ONN95IPaOkzj///LjuuutSz+BbuOeee6JHjx4+8l9g8+bNi4MOOij+67/+KxYvXpx6DpTUxIkTU08oipYtW8YTTzwRe++990qfccABB8SYMWOiefPmBVxWfl5++eXUEygiAQDgP1x99dVxzz33pJ6RxHHHHef7fxWgrq4ufvOb38Ree+1V9V9RSemGG26I7bffPt57773UU6BkqjV+33rrrdG9e/dVPmfLLbeMu+66qwCLyle1fwUkdwIAwBe89957ccopp6Sekcynn34a++23XzZffahECxcujB/84Afx29/+NvWULDzzzDPx3e9+152xycZbb72VekLBHXHEEbHbbrsV7LxddtkljjzyyIKdV25ef/311BMoIgEA4AtOOOGEWLBgQeoZSU2ePDlOOOGE1DP4EnPmzIldd901hg0blnpKVmbOnBl9+vSJ+++/P/UUKLpqe+RtgwYN4uyzzy74uWeeeeYK3UOgEr322mupJ1BE1flTC7ASRo8e7YXVvwwZMiQefPDB1DP4gqlTp8Z2220XTz75ZOopWVq0aFEMHDgwbrnlltRToKimTJmSekJBDRo0KDp06FDwczt27Bh9+/Yt+LnloFrvA8E/CQAA8c/vVP/yl79MPaOsHH300fHJJ5+knkH888V/796949VXX009JWvLli2LQw89VASgqlXbJwD233//op3dr1+/op2d0rRp02LJkiWpZ1AkAgBARNx9991ufvcf3nvvvfjDH/6Qekb2pk2bFr17967K7+VWouXLl4sAVK1ly5bFzJkzU88oqB133LFoZ2+11VZFOzu1d999N/UEikQAALJXV1fnhmpf4aKLLqq6d4Mqydy5c2O33Xbz4r/MfB4BRowYkXoKFNSHH36YekJBrbPOOtGqVauind++ffuinZ3a22+/nXoCRSIAANm7//7744UXXkg9oywtWrQozjzzzNQzsvTpp5/GPvvsEy+99FLqKXyJ5cuXx3777Rf/+Mc/Uk+Bgqm2AFCM7/5/0ZprrlnU81MSAKqXAABkb/DgwaknlLWbbrop3nzzzdQzsnPEEUfE6NGjU8/ga3x+Y8DJkyenngIFMXv27NQTCmqNNdYo6vn16tUr6vkpTZ8+PfUEikQAALI2YcKEGDVqVOoZZW3ZsmVx0UUXpZ6RlYsvvth3zCvE7NmzY9CgQW6YSVX46KOPUk8oqIYNG6aeULE++OCD1BMoEgEAyNq1116bekJFuO6662LatGmpZ2ThkUce8USKCvPyyy/HEUccEXV1damnwCqptpA1b9681BMqlk8AVC8BAMjW4sWL46abbko9oyIsXbo0rrnmmtQzqt706dPjwAMPjOXLl6eewrd05513xhVXXJF6BqyShQsXpp5QUNX2lYZSmjFjRuoJFIkAAGRrxIgRMWfOnNQzKsY111wTS5cuTT2jatXV1cXhhx8es2bNSj2FlXTyySfHhAkTUs+AlbZgwYLUEwrqnXfe8cmclVRtj4Pk/xEAgGzdeuutqSdUlOnTp8djjz2WekbVGjx4cDz00EOpZ7AKlixZEgceeGAsWbIk9RRYKfPnz089oaDmz5/vMaorqdruB8H/IwAAWVqwYEHcd999qWdUnGr7eGi5ePPNN+OMM85IPYMCmDBhQpx77rmpZ8BKWbZsWeoJBff888+nnlCR5s2b5+toVUoAALL0wAMPxKeffpp6BkRdXV0cc8wxsWjRotRTKJALLrjAVwGgTPz9739PPaFi+ZpkdRIAgCzdfffdqSdAREQMGTIkRo8enXoGBfTZZ5/FUUcd5bvHUAb87/3K+/jjj1NPoAgEACA7y5cvjwcffDD1DIgFCxbEaaedlnoGRTBu3Li45ZZbUs+A7L399ts+kbOSqu2eEPyTAABk5/nnn/doIMrC+eefHx988EHqGRTJqaee6r4ZUAb+9re/pZ5Qkfz+VZ0EACA7Dz/8cOoJEFOmTIlLLrkk9QyK6P3334+LLroo9QxYYbW11fnS4Nprr43PPvss9YyK415J1ak6/ysH+Bpjx45NPQHi97//fSxevDj1DIrskksu8T1aKkbz5s1TTyiK6dOnx/Dhw1PPqDhz585NPYEiEACArCxfvtwdgUluypQpcd1116WeQQnMnTs3Bg8enHoGrJBq/QRARMSf/vSn1BMqjk8AVKfq/a8c4EtMmjTJY21I7g9/+EMsXbo09QxKZPDgwbFgwYLUM+AbtWrVKvWEonniiSfiscceSz2jonzyySepJ1AEAgCQlRdeeCH1BDI3Z86cGDJkSOoZlNDcuXPj+uuvTz0DvtFaa62VekJRnXbaaR7PSfYEACArL774YuoJZO4vf/mLd1UydNlll8Xy5ctTz4CvVe0B4KmnnhLjyJ4AAGTFJwBIafny5XH55ZennkECb731Vtx///2pZ8DXatu2beoJRffLX/7S41fJWv3UAwBKSQAgpYceeiimTJmSegaJ/OUvf4k99tgj9Qz4Sm3bto369etX9SPzPvzwwzj66KPj3nvvXeWz6tWrFz169CjAqvLUsmXL1BMoAgEAyMbs2bNj+vTpqWeQsRtuuCH1BBK67777YubMmdGmTZvUU+BL1a9fPzp06BCTJ09OPaWoRowYEZdddlmccMIJq3ROs2bNvLFAxfEVACAbb775ZuoJZGzOnDkxbNiw1DNI6LPPPotbbrkl9Qz4WhtssEHqCSVxyimnxLhx41LPgJLzCQAgG++8807qCck0adIkdtppp9hiiy2ic+fO0bx582jcuHHMmTMnpk2bFk8//XQ89NBDMXPmzNRTq9bw4cOze6Zy+/bto3///rHZZpvFeuutFy1atIgFCxbErFmz4pVXXolHHnkkuxtz3nHHHXHSSSelngFfaaONNopRo0alnlF0S5cujUGDBsXTTz8d7du3Tz0HSkYAALKRYwDo1KlTnH322bH//vtHo0aNvvbvXbp0aQwbNixOPfXUePvtt0u0MB9Dhw5NPaFkttpqqzjnnHOif//+UVNT87V/78svvxznnXde3HHHHSVal9a4ceNiypQp0aFDh9RT4EttuummqSeUzAcffBADBw6MsWPHRuPGjVPPgZLwFQAgG7kFgJNOOikmTpwYhx9++De++I+IaNCgQey3337xyiuvxOGHH178gRmZO3duPPTQQ6lnFF1NTU2ce+65MX78+Nhtt92+8cV/RET37t3j9ttvj/vuuy+bG04NHz489QT4SptvvnnqCSX1/PPPx7777htLly5NPQVKQgAAspFTADjvvPPikksuiYYNG37rX9uoUaO47rrr4tBDDy3Csjw9/PDDWXz8/4Ybbogzzjgjamu//R8vdt9993j88cdjzTXXLMKy8nL33XenngBfqUePHisU76rJAw88EEcffXTU1dWlngJFJwAA2cglAOy7775x+umnr9IZNTU18ec//zm6d+9eoFV5e/DBB1NPKLozzzxzlaNR9+7d429/+9tKBYRK8uSTT8bChQtTz4Av1bRp0+jcuXPqGSU3ZMiQ+MlPfiICUPWq+39hAb7go48+Sj2h6Bo3bhyDBw8uyFmrr756XHbZZQU5K3fV/vH/LbfcMs4+++yCnNW3b99VfjRXuVuyZEk8/vjjqWfAV9p+++1TT0ji6quvjlNPPTX1DCgqAQDIRg4B4Mgjj4x11123YOf16dMn+vTpU7DzcjRp0qSYMmVK6hlFdfHFF0e9evUKdt5ZZ50Va621VsHOK0c53GWdytW7d+/UE5K58MIL45RTTvFJAKqWAABkYd68ebFs2bLUM4ruiCO
gitextract_2xb0w4ev/ ├── README.md ├── index.html ├── package.json ├── src/ │ ├── assets/ │ │ └── .gitkeep │ ├── constant/ │ │ ├── data.ts │ │ └── text-effects.ts │ ├── features/ │ │ └── design/ │ │ ├── components/ │ │ │ ├── LidoJSEditor.tsx │ │ │ ├── editor-content/ │ │ │ │ ├── EditorContent.tsx │ │ │ │ └── index.ts │ │ │ ├── editor-header/ │ │ │ │ ├── EditorHeader.tsx │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── layer-settings/ │ │ │ │ ├── LayerSettings.tsx │ │ │ │ └── index.ts │ │ │ ├── preview/ │ │ │ │ ├── PreviewModal.tsx │ │ │ │ └── index.ts │ │ │ ├── sidebar/ │ │ │ │ ├── DrawContent.tsx │ │ │ │ ├── FrameContent.tsx │ │ │ │ ├── GraphicContent.tsx │ │ │ │ ├── IframeContent.tsx │ │ │ │ ├── ImageContent.tsx │ │ │ │ ├── Photo.tsx │ │ │ │ ├── QrCodeContent.tsx │ │ │ │ ├── ShapeContent.tsx │ │ │ │ ├── Sidebar.tsx │ │ │ │ ├── TableContent.tsx │ │ │ │ ├── TemplateContent.tsx │ │ │ │ ├── TextContent.tsx │ │ │ │ ├── UploadContent.tsx │ │ │ │ ├── VideoContent.tsx │ │ │ │ └── index.ts │ │ │ └── tabs/ │ │ │ ├── TabList.tsx │ │ │ └── index.ts │ │ ├── config/ │ │ │ ├── iframe.tsx │ │ │ ├── line.tsx │ │ │ ├── qrCode.tsx │ │ │ └── shape.tsx │ │ └── pages/ │ │ ├── DesignPage.tsx │ │ └── index.ts │ ├── main.tsx │ ├── pages/ │ │ └── Main.tsx │ ├── shared/ │ │ ├── components/ │ │ │ ├── index.ts │ │ │ └── masonry/ │ │ │ ├── Masonry.tsx │ │ │ └── index.ts │ │ ├── icons/ │ │ │ └── pencil/ │ │ │ ├── Highlighter.tsx │ │ │ ├── Marker.tsx │ │ │ └── Pencil.tsx │ │ └── theme/ │ │ ├── index.ts │ │ ├── palette.ts │ │ └── theme.ts │ ├── styles.css │ └── utils/ │ ├── download.ts │ └── thumbnail.ts ├── tsconfig.json └── vite.config.ts
SYMBOL INDEX (18 symbols across 16 files)
FILE: src/features/design/components/editor-header/EditorHeader.tsx
type HeaderLayoutProps (line 14) | interface HeaderLayoutProps {
FILE: src/features/design/components/preview/PreviewModal.tsx
type PreviewModalProps (line 5) | interface PreviewModalProps {
FILE: src/features/design/components/sidebar/FrameContent.tsx
type Frame (line 9) | interface Frame {
FILE: src/features/design/components/sidebar/Photo.tsx
type Props (line 3) | type Props = {
FILE: src/features/design/components/sidebar/TemplateContent.tsx
type Template (line 9) | interface Template {
FILE: src/features/design/components/sidebar/TextContent.tsx
type Text (line 15) | interface Text {
FILE: src/features/design/components/sidebar/UploadContent.tsx
type UploadContentProps (line 7) | interface UploadContentProps {
FILE: src/features/design/components/tabs/TabList.tsx
type SidebarTabProps (line 3) | interface SidebarTabProps {
FILE: src/features/design/config/iframe.tsx
type IframeItem (line 3) | type IframeItem = {
FILE: src/features/design/config/line.tsx
type Line (line 6) | type Line = {
FILE: src/features/design/config/qrCode.tsx
type QrCodeItem (line 3) | type QrCodeItem = {
FILE: src/features/design/config/shape.tsx
type Shape (line 21) | type Shape = {
FILE: src/features/design/pages/DesignPage.tsx
type FontVariant (line 8) | type FontVariant =
FILE: src/pages/Main.tsx
function Page (line 3) | function Page() {
FILE: src/shared/components/masonry/Masonry.tsx
type MasonryProps (line 3) | interface MasonryProps {
type ColumnData (line 15) | interface ColumnData {
FILE: src/shared/theme/theme.ts
type Palette (line 7) | interface Palette {
type PaletteOptions (line 15) | interface PaletteOptions {
Condensed preview — 55 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (315K chars).
[
{
"path": "README.md",
"chars": 3691,
"preview": "<img width=\"960\" alt=\"Screenshot 2023-04-22 101234\" src=\"https://github.com/lidojs/canva-clone/assets/19285404/06249d78-"
},
{
"path": "index.html",
"chars": 4662,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\" />\n <title>LidoJS Design Editor</title>\n <meta\n con"
},
{
"path": "package.json",
"chars": 1049,
"preview": "{\n \"name\": \"@lidojs/react\",\n \"version\": \"2.0.0\",\n \"private\": true,\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"vi"
},
{
"path": "src/assets/.gitkeep",
"chars": 0,
"preview": ""
},
{
"path": "src/constant/data.ts",
"chars": 9555,
"preview": "export const data = [\n {\n layers: {\n ROOT: {\n type: { resolvedName: 'RootLayer' },\n props: {\n "
},
{
"path": "src/constant/text-effects.ts",
"chars": 8536,
"preview": "export const addAHeading = {\n rootId: 'f2d33316-8857-4496-a0c7-3dcc9c4ff981',\n layers: {\n 'f2d33316-8857-4496-a0c7-"
},
{
"path": "src/features/design/components/LidoJSEditor.tsx",
"chars": 4122,
"preview": "'use client';\n\nimport type { FontData } from '@lidojs/design-core';\nimport { Editor, type GetFontQuery, PageControl } fr"
},
{
"path": "src/features/design/components/editor-content/EditorContent.tsx",
"chars": 182,
"preview": "import { DesignFrame } from '@lidojs/design-editor';\nimport { data } from '../../../../constant/data';\n\nexport const Edi"
},
{
"path": "src/features/design/components/editor-content/index.ts",
"chars": 33,
"preview": "export * from './EditorContent';\n"
},
{
"path": "src/features/design/components/editor-header/EditorHeader.tsx",
"chars": 5924,
"preview": "import ArrowClockwiseIcon from '@duyank/icons/regular/ArrowClockwise';\nimport ArrowCounterClockwiseIcon from '@duyank/ic"
},
{
"path": "src/features/design/components/editor-header/index.ts",
"chars": 32,
"preview": "export * from './EditorHeader';\n"
},
{
"path": "src/features/design/components/index.ts",
"chars": 32,
"preview": "export * from './LidoJSEditor';\n"
},
{
"path": "src/features/design/components/layer-settings/LayerSettings.tsx",
"chars": 757,
"preview": "import {\n LayerSettings as EditorLayerSettings,\n useSelectedLayers,\n} from '@lidojs/design-editor';\n\nexport const Laye"
},
{
"path": "src/features/design/components/layer-settings/index.ts",
"chars": 33,
"preview": "export * from './LayerSettings';\n"
},
{
"path": "src/features/design/components/preview/PreviewModal.tsx",
"chars": 913,
"preview": "import XIcon from '@duyank/icons/regular/X';\nimport { Preview } from '@lidojs/design-editor';\nimport type { FC } from 'r"
},
{
"path": "src/features/design/components/preview/index.ts",
"chars": 32,
"preview": "export * from './PreviewModal';\n"
},
{
"path": "src/features/design/components/sidebar/DrawContent.tsx",
"chars": 6697,
"preview": "import XBoldIcon from '@duyank/icons/bold/XBold';\nimport { useEditor } from '@lidojs/design-editor';\nimport { useDraw } "
},
{
"path": "src/features/design/components/sidebar/FrameContent.tsx",
"chars": 4852,
"preview": "import XIcon from '@duyank/icons/regular/X';\nimport type { LayerId, LayerType, SerializedLayers } from '@lidojs/design-c"
},
{
"path": "src/features/design/components/sidebar/GraphicContent.tsx",
"chars": 6101,
"preview": "import MagnifyingGlassIcon from '@duyank/icons/regular/MagnifyingGlass';\nimport XIcon from '@duyank/icons/regular/X';\nim"
},
{
"path": "src/features/design/components/sidebar/IframeContent.tsx",
"chars": 3620,
"preview": "import XIcon from '@duyank/icons/regular/X';\nimport type {\n LayerId,\n LayerType,\n SerializedLayerTree,\n SerializedLa"
},
{
"path": "src/features/design/components/sidebar/ImageContent.tsx",
"chars": 7879,
"preview": "import MagnifyingGlassIcon from '@duyank/icons/regular/MagnifyingGlass';\nimport XIcon from '@duyank/icons/regular/X';\nim"
},
{
"path": "src/features/design/components/sidebar/Photo.tsx",
"chars": 1575,
"preview": "import { type FC, type HTMLProps, memo, useState } from 'react';\n\ntype Props = {\n image: string;\n name: string;\n user"
},
{
"path": "src/features/design/components/sidebar/QrCodeContent.tsx",
"chars": 3620,
"preview": "import XIcon from '@duyank/icons/regular/X';\nimport type {\n LayerId,\n LayerType,\n SerializedLayerTree,\n SerializedLa"
},
{
"path": "src/features/design/components/sidebar/ShapeContent.tsx",
"chars": 6632,
"preview": "import XIcon from '@duyank/icons/regular/X';\nimport type { LayerId, LayerType, SerializedLayers } from '@lidojs/design-c"
},
{
"path": "src/features/design/components/sidebar/Sidebar.tsx",
"chars": 4614,
"preview": "import BrowserIcon from '@duyank/icons/regular/Browser';\nimport FrameCornersIcon from '@duyank/icons/regular/FrameCorner"
},
{
"path": "src/features/design/components/sidebar/TableContent.tsx",
"chars": 26533,
"preview": "import XIcon from '@duyank/icons/regular/X';\nimport type { LayerId, LayerType, SerializedLayers } from '@lidojs/design-c"
},
{
"path": "src/features/design/components/sidebar/TemplateContent.tsx",
"chars": 2718,
"preview": "import XIcon from '@duyank/icons/regular/X';\nimport type { SerializedPage } from '@lidojs/design-core';\nimport { useEdit"
},
{
"path": "src/features/design/components/sidebar/TextContent.tsx",
"chars": 5560,
"preview": "import XIcon from '@duyank/icons/regular/X';\nimport type { LayerId, SerializedLayers } from '@lidojs/design-core';\nimpor"
},
{
"path": "src/features/design/components/sidebar/UploadContent.tsx",
"chars": 4655,
"preview": "import XIcon from '@duyank/icons/regular/X';\nimport { useEditor } from '@lidojs/design-editor';\nimport { fetchSvgContent"
},
{
"path": "src/features/design/components/sidebar/VideoContent.tsx",
"chars": 5196,
"preview": "import XIcon from '@duyank/icons/regular/X';\nimport type { LayerId, LayerType, SerializedLayers } from '@lidojs/design-c"
},
{
"path": "src/features/design/components/sidebar/index.ts",
"chars": 27,
"preview": "export * from './Sidebar';\n"
},
{
"path": "src/features/design/components/tabs/TabList.tsx",
"chars": 3591,
"preview": "import type { FC, ReactNode } from 'react';\n\ninterface SidebarTabProps {\n tabs: {\n name: string;\n icon: ReactNode"
},
{
"path": "src/features/design/components/tabs/index.ts",
"chars": 27,
"preview": "export * from './TabList';\n"
},
{
"path": "src/features/design/config/iframe.tsx",
"chars": 1096,
"preview": "import type { LayerId, SerializedLayers } from '@lidojs/design-core';\n\nexport type IframeItem = {\n elements: [\n {\n "
},
{
"path": "src/features/design/config/line.tsx",
"chars": 12930,
"preview": "import type { LayerId } from '@lidojs/design-core';\nimport type { DeepPartial, LineLayerProps } from '@lidojs/design-edi"
},
{
"path": "src/features/design/config/qrCode.tsx",
"chars": 107120,
"preview": "import type { LayerId, SerializedLayers } from '@lidojs/design-core';\n\nexport type QrCodeItem = {\n elements: [\n {\n "
},
{
"path": "src/features/design/config/shape.tsx",
"chars": 5121,
"preview": "import ArrowBottomIcon from '@duyank/icons/shape/ArrowBottom';\nimport ArrowLeftIcon from '@duyank/icons/shape/ArrowLeft'"
},
{
"path": "src/features/design/pages/DesignPage.tsx",
"chars": 1919,
"preview": "'use client';\n\nimport type { FontData } from '@lidojs/design-core';\nimport axios from 'axios';\nimport { useEffect, useSt"
},
{
"path": "src/features/design/pages/index.ts",
"chars": 30,
"preview": "export * from './DesignPage';\n"
},
{
"path": "src/main.tsx",
"chars": 293,
"preview": "import * as ReactDOM from 'react-dom/client';\nimport './styles.css';\nimport axios from 'axios';\nimport Page from './page"
},
{
"path": "src/pages/Main.tsx",
"chars": 116,
"preview": "import { DesignPage } from '../features/design/pages';\n\nexport default function Page() {\n return <DesignPage />;\n}\n"
},
{
"path": "src/shared/components/index.ts",
"chars": 27,
"preview": "export * from './masonry';\n"
},
{
"path": "src/shared/components/masonry/Masonry.tsx",
"chars": 4570,
"preview": "import React, { useCallback, useEffect, useRef, useState } from 'react';\n\ninterface MasonryProps {\n children: React.Rea"
},
{
"path": "src/shared/components/masonry/index.ts",
"chars": 27,
"preview": "export * from './Masonry';\n"
},
{
"path": "src/shared/icons/pencil/Highlighter.tsx",
"chars": 4286,
"preview": "import type { ComponentProps, FC } from 'react';\n\nexport const Highlighter: FC<ComponentProps<'svg'>> = (props) => {\n r"
},
{
"path": "src/shared/icons/pencil/Marker.tsx",
"chars": 4971,
"preview": "import type { ComponentProps, FC } from 'react';\n\nexport const Marker: FC<ComponentProps<'svg'>> = (props) => {\n return"
},
{
"path": "src/shared/icons/pencil/Pencil.tsx",
"chars": 8273,
"preview": "import type { ComponentProps, FC } from 'react';\n\nexport const Pencil: FC<ComponentProps<'svg'>> = (props) => {\n return"
},
{
"path": "src/shared/theme/index.ts",
"chars": 52,
"preview": "export * from './palette';\nexport * from './theme';\n"
},
{
"path": "src/shared/theme/palette.ts",
"chars": 3983,
"preview": "// Helper function to convert hex to rgb values\nconst hexToRgb = (hex: string) => {\n const result = /^#?([a-f\\d]{2})([a"
},
{
"path": "src/shared/theme/theme.ts",
"chars": 9726,
"preview": "'use client';\n\nimport { alpha, createTheme } from '@mui/material/styles';\nimport { palette, rgba } from './palette';\n\nde"
},
{
"path": "src/styles.css",
"chars": 1559,
"preview": "\nhtml, body {\n margin: 0;\n font-family: 'Nunito', sans-serif;\n color: #5E6278;\n font-size: 14px;\n}\n\nhtml,\nbo"
},
{
"path": "src/utils/download.ts",
"chars": 487,
"preview": "export const downloadObjectAsJson = (exportName: string, data: unknown) => {\n const dataStr = `data:text/json;charset=u"
},
{
"path": "src/utils/thumbnail.ts",
"chars": 353,
"preview": "export const getThumbnail = (url: string) => {\n const parts = url.split('/');\n\n // get the filename\n const fileName ="
},
{
"path": "tsconfig.json",
"chars": 766,
"preview": "{\n \"compilerOptions\": {\n \"jsx\": \"preserve\",\n \"allowJs\": false,\n \"strict\": true,\n \"jsxImportSource\": \"@emoti"
},
{
"path": "vite.config.ts",
"chars": 642,
"preview": "import react from '@vitejs/plugin-react';\nimport { defineConfig } from 'vite';\nimport { analyzer } from 'vite-bundle-ana"
}
]
About this extraction
This page contains the full source code of the lidojs/canva-clone GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 55 files (294.7 KB), approximately 114.2k tokens, and a symbol index with 18 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.