feat: add header of idraw design

This commit is contained in:
chenshenhai 2023-05-27 11:23:27 +08:00
parent 026052d93a
commit f834f010e0
22 changed files with 482 additions and 55 deletions

View file

@ -10,5 +10,8 @@
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[less]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}

148
packages/design/dev/data.ts Normal file
View file

@ -0,0 +1,148 @@
import type { DesignData } from '../src';
const data: DesignData = {
components: [
{
uuid: 'demo-xxx-001',
type: 'component',
name: 'demo',
x: 50,
y: 50,
w: 100,
h: 100,
desc: {
bgColor: '#1f1f1f',
children: [
{
uuid: 'group-001-0014',
type: 'circle',
x: -40,
y: 0,
w: 100,
h: 100,
desc: {
bgColor: '#f44336'
}
},
{
uuid: 'group-001-0015',
type: 'circle',
x: -20,
y: 0,
w: 100,
h: 100,
desc: {
bgColor: '#ff9800'
}
},
{
uuid: 'group-001-0016',
type: 'circle',
x: 0,
y: 0,
w: 100,
h: 100,
desc: {
bgColor: '#ffc106'
}
},
{
uuid: 'group-001-0017',
type: 'circle',
x: 20,
y: 0,
w: 100,
h: 100,
desc: {
bgColor: '#cddc39'
}
},
{
uuid: 'group-001-0018',
type: 'circle',
x: 40,
y: 0,
w: 100,
h: 100,
desc: {
bgColor: '#4caf50'
}
}
]
}
},
{
uuid: 'demo-xxx-002',
type: 'component',
name: 'demo',
x: 50,
y: 50,
w: 100,
h: 100,
desc: {
bgColor: '#f0f0f0',
children: [
{
uuid: 'group-001-0014',
type: 'circle',
x: -40,
y: 0,
w: 100,
h: 100,
desc: {
bgColor: '#f44336'
}
},
{
uuid: 'group-001-0015',
type: 'circle',
x: -20,
y: 0,
w: 100,
h: 100,
desc: {
bgColor: '#ff9800'
}
},
{
uuid: 'group-001-0016',
type: 'circle',
x: 0,
y: 0,
w: 100,
h: 100,
desc: {
bgColor: '#ffc106'
}
},
{
uuid: 'group-001-0017',
type: 'circle',
x: 20,
y: 0,
w: 100,
h: 100,
desc: {
bgColor: '#cddc39'
}
},
{
uuid: 'group-001-0018',
type: 'circle',
x: 40,
y: 0,
w: 100,
h: 100,
desc: {
bgColor: '#4caf50'
}
}
]
}
}
],
modules: [],
pages: []
};
export default data;

View file

@ -1,8 +1,26 @@
import React from 'react';
import React, { useState, useEffect } from 'react';
import { createRoot } from 'react-dom/client';
import { Design } from '../src/index';
const dom = document.querySelector('#lab') as HTMLDivElement;
const root = createRoot(dom);
root.render(<Design />);
const App = () => {
// const style = { margin: 0, padding: 0 }
// const [width, setWidth] = useState<number>(window.innerWidth);
// const [height, setHeight] = useState<number>(window.innerHeight);
// useEffect(() => {
// window.addEventListener('resize', () => {
// setWidth(window.innerWidth);
// setHeight(window.innerHeight);
// });
// }, []);
const style = { margin: 40 };
const width = 1000;
const height = 600;
return <Design width={width} height={height} style={style} />;
};
root.render(<App />);

View file

@ -0,0 +1,24 @@
import { createContext } from 'react';
import { DesignData } from './types';
export interface DesignContext {
data: DesignData;
}
export function createDesignData(): DesignData {
return {
components: [],
modules: [],
pages: []
};
}
export function createDesignContextValue(opts?: { data?: DesignData }): DesignContext {
return {
data: opts?.data || createDesignData()
};
}
const Context = createContext<DesignContext>(createDesignContextValue());
export const Provider = Context.Provider;

View file

@ -1,11 +1,19 @@
@import "../../css/variable.less";
@import '../../css/variable.less';
@mod-name: ~'@{prefix}-icon';
.@{mod-name} {
display: flex;
text-align: center;
font-size: 16;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 16;
}
color: inherit;
font-style: normal;
line-height: 0;
text-align: center;
text-transform: none;
svg {
justify-content: center;
}
}

View file

@ -6,3 +6,4 @@
@import "./modules/header.less";
@import "./modules/sketch.less";
@import "./modules/toolbar.less";
@import "./modules/panel-layer.less";

View file

@ -1 +1,16 @@
@import "../variable.less";
@import '../variable.less';
@mod-header: ~'@{prefix}-mod-header';
.@{mod-header} {
display: flex;
align-items: center;
justify-content: space-between;
height: 100%;
box-sizing: border-box;
padding: 0 20px;
.@{mod-header}-theme-switch {
display: flex;
}
}

View file

@ -0,0 +1,23 @@
@import "../variable.less";
@mod-panel-layer: ~'@{prefix}-mod-panel-layer';
.@{mod-panel-layer} {
height: 100%;
width: 100%;
display: flex;
flex-flow: column;
.@{mod-panel-layer}-header {
height: 40px;
}
.@{mod-panel-layer}-content {
flex: 1;
}
.@{mod-panel-layer}-footer {
height: 40px;
}
}

View file

@ -2,7 +2,10 @@
@mod-sketch: ~'@{prefix}-mod-sketch';
// @mod-sketch-header-height: 36px;
.@{mod-sketch} {
display: flex;
position: relative;
overflow: hidden;
box-shadow: 0 0 0 1px #0000001a,0px 0px .5px #0000002e, 0px 3px 8px #0000001a, 0px 1px 3px #0000001a;
@ -15,4 +18,20 @@
min-width: 400px;
transform: translateX(-50%);
}
.@{mod-sketch}-header {
position: absolute;
top: 0;
left: 0;
right: 0;
// height: @mod-sketch-header-height;
}
.@{mod-sketch}-content {
position: absolute;
// top: @mod-sketch-header-height;
bottom: 0;
left: 0;
right: 0;
}
}

View file

@ -0,0 +1,17 @@
import React from 'react';
import classnames from 'classnames';
import { iconClassName } from './util';
import type { IconProps } from './util';
const Dark = (props: IconProps) => {
const { className, style } = props;
return (
<span className={classnames([iconClassName, className])} style={style}>
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor">
<path d="M516.266667 938.666667h-38.4c-234.666667-21.333333-405.333333-230.4-384-465.066667 17.066667-204.8 179.2-366.933333 384-384 17.066667 0 34.133333 8.533333 42.666666 21.333333 8.533333 12.8 8.533333 34.133333-4.266666 46.933334-85.333333 115.2-59.733333 273.066667 55.466666 358.4 89.6 68.266667 213.333333 68.266667 302.933334 0 12.8-8.533333 29.866667-12.8 46.933333-4.266667 12.8 8.533333 21.333333 25.6 21.333333 42.666667-8.533333 115.2-64 217.6-153.6 290.133333-81.066667 59.733333-174.933333 93.866667-273.066666 93.866667zM396.8 187.733333c-123.733333 42.666667-213.333333 153.6-221.866667 290.133334-17.066667 187.733333 119.466667 354.133333 307.2 371.2 89.6 8.533333 179.2-17.066667 247.466667-76.8 46.933333-38.4 81.066667-89.6 102.4-145.066667-106.666667 38.4-226.133333 21.333333-320-46.933333-119.466667-93.866667-166.4-251.733333-115.2-392.533334z"></path>
</svg>
</span>
);
};
export default Dark;

View file

@ -0,0 +1,17 @@
import React from 'react';
import classnames from 'classnames';
import { iconClassName } from './util';
import type { IconProps } from './util';
const Light = (props: IconProps) => {
const { className, style } = props;
return (
<span className={classnames([iconClassName, className])} style={style}>
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor">
<path d="M512 768c-141.376 0-256-114.624-256-256s114.624-256 256-256 256 114.624 256 256-114.624 256-256 256z m0-85.333333a170.666667 170.666667 0 1 0 0-341.333334 170.666667 170.666667 0 0 0 0 341.333334zM469.333333 85.333333a42.666667 42.666667 0 1 1 85.333334 0v85.333334a42.666667 42.666667 0 1 1-85.333334 0V85.333333z m0 768a42.666667 42.666667 0 1 1 85.333334 0v85.333334a42.666667 42.666667 0 1 1-85.333334 0v-85.333334zM85.333333 554.666667a42.666667 42.666667 0 1 1 0-85.333334h85.333334a42.666667 42.666667 0 1 1 0 85.333334H85.333333z m768 0a42.666667 42.666667 0 1 1 0-85.333334h85.333334a42.666667 42.666667 0 1 1 0 85.333334h-85.333334zM161.834667 222.165333a42.666667 42.666667 0 0 1 60.330666-60.330666l64 64a42.666667 42.666667 0 0 1-60.330666 60.330666l-64-64z m576 576a42.666667 42.666667 0 0 1 60.330666-60.330666l64 64a42.666667 42.666667 0 0 1-60.330666 60.330666l-64-64z m-515.669334 64a42.666667 42.666667 0 0 1-60.330666-60.330666l64-64a42.666667 42.666667 0 0 1 60.330666 60.330666l-64 64z m576-576a42.666667 42.666667 0 0 1-60.330666-60.330666l64-64a42.666667 42.666667 0 0 1 60.330666 60.330666l-64 64z"></path>
</svg>
</span>
);
};
export default Light;

View file

@ -0,0 +1,20 @@
import React from 'react';
import classnames from 'classnames';
import { iconClassName } from './util';
import type { IconProps } from './util';
const More = (props: IconProps) => {
const { className, style } = props;
return (
<span className={classnames([iconClassName, className])} style={style}>
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor">
<path d="M512 42.666667a469.333333 469.333333 0 1 0 469.333333 469.333333A469.333333 469.333333 0 0 0 512 42.666667z m0 864a394.666667 394.666667 0 1 1 394.666667-394.666667 395.146667 395.146667 0 0 1-394.666667 394.666667z"></path>
<path d="M304.906667 512m-66.666667 0a66.666667 66.666667 0 1 0 133.333333 0 66.666667 66.666667 0 1 0-133.333333 0Z"></path>
<path d="M512 512m-66.666667 0a66.666667 66.666667 0 1 0 133.333334 0 66.666667 66.666667 0 1 0-133.333334 0Z"></path>
<path d="M719.093333 512m-66.666666 0a66.666667 66.666667 0 1 0 133.333333 0 66.666667 66.666667 0 1 0-133.333333 0Z"></path>
</svg>
</span>
);
};
export default More;

View file

@ -1,25 +1,31 @@
import React, { useEffect, useState } from 'react';
import { Sketch } from './modules';
import type { SketchProps } from './modules';
import { Provider, createDesignContextValue } from './context';
import type { DesignContext } from './context';
import type { DesignData } from './types';
import './css/index.less';
export const Design = () => {
const [width, setWidth] = useState<number>(1000);
const [height, setHeight] = useState<number>(600);
export type DesignProps = SketchProps & {
data?: DesignData;
locale?: string; // TODO
};
// const [width, setWidth] = useState<number>(window.innerWidth);
// const [height, setHeight] = useState<number>(window.innerHeight);
// useEffect(() => {
// window.addEventListener('resize', () => {
// const width = window.innerWidth;
// const height = window.innerHeight;
// setWidth(width);
// setHeight(height);
// });
// }, []);
export const Design = (props: DesignProps) => {
const { width = 1000, height = 600, style, className, data } = props;
const [contextValue, setContextValue] = useState<DesignContext>(createDesignContextValue({ data }));
useEffect(() => {
if (data) {
setContextValue({ ...contextValue, ...{ data } });
}
}, [data]);
return (
<div style={{ position: 'fixed', left: 0, right: 0, width, height }}>
<Sketch width={width} height={height} />
</div>
<Provider value={contextValue}>
<Sketch width={width} height={height} style={style} className={className} />
</Provider>
);
};
export * from './types';

View file

@ -2,9 +2,8 @@ import React from 'react';
import type { CSSProperties } from 'react';
import classnames from 'classnames';
import { createPrefixName } from '../../css';
import './index.less';
const modName = 'mod-sketch';
const modName = 'mod-xxx';
const prefixName = createPrefixName(modName);

View file

@ -0,0 +1,31 @@
import React from 'react';
import type { CSSProperties } from 'react';
import classnames from 'classnames';
import Switch from 'antd/es/switch';
import { createPrefixName } from '../../css';
import IconDark from '../../icons/dark';
import IconLight from '../../icons/light';
const modName = 'mod-header';
const prefixName = createPrefixName(modName);
export interface ModProps {
className?: string;
style?: CSSProperties;
}
export const Header = (props: ModProps) => {
const { className, style } = props;
return (
<div style={style} className={classnames(prefixName(), className)}>
<span>Header</span>
<Switch
className={prefixName('theme', 'switch')}
checkedChildren={<IconLight style={{ height: '100%' }} />}
unCheckedChildren={<IconDark style={{ height: '100%' }} />}
defaultChecked
/>
</div>
);
};

View file

@ -0,0 +1,24 @@
import React from 'react';
import type { CSSProperties } from 'react';
import classnames from 'classnames';
import { createPrefixName } from '../../css';
const modName = 'mod-panel-layer';
const prefixName = createPrefixName(modName);
export interface PanelLayerProps {
className?: string;
style?: CSSProperties;
}
export const PanelLayer = (props: PanelLayerProps) => {
const { className, style } = props;
return (
<div style={style} className={classnames(prefixName(), className)}>
<div className={prefixName('header')}>header</div>
<div className={prefixName('content')}>Panel Layer</div>
<div className={prefixName('footer')}>footer</div>
</div>
);
};

View file

@ -5,8 +5,11 @@ import { calcElementsContextSize } from '@idraw/util';
import Drawer from 'antd/es/drawer';
import { getData } from '../../data';
import { Toolbar } from '../toolbar';
import { PanelLayer } from '../panel-layer';
import { Header } from '../header';
import type { CSSProperties } from 'react';
import { createPrefixName } from '../../css';
import { HEADER_HEIGHT } from './layout';
const modName = 'mod-sketch';
const siderWidth = 200;
@ -24,18 +27,19 @@ export const Sketch = (props: SketchProps) => {
const ref = useRef<HTMLDivElement>(null);
const refCore = useRef<Core | null>(null);
const refLeftDOM = useRef<HTMLDivElement | null>(null);
const refRighttDOM = useRef<HTMLDivElement | null>(null);
const { className, style, width, height } = props;
const data = getData();
const devicePixelRatio = window.devicePixelRatio;
const [openLayer, setOpenLayer] = useState<boolean>(false);
const [openSetting, setOpenSetting] = useState<boolean>(false);
const [openLeftSider, setOpenLeftSider] = useState<boolean>(false);
const [openRightSider, setOpenRightSider] = useState<boolean>(false);
useEffect(() => {
if (ref?.current) {
const options = {
width,
height,
height: height - HEADER_HEIGHT,
devicePixelRatio
};
const core = new Core(ref.current, options);
@ -55,40 +59,48 @@ export const Sketch = (props: SketchProps) => {
const contextSize = calcElementsContextSize(data.elements, { viewWidth: width, viewHeight: height });
core.resize({
width,
height,
height: height - HEADER_HEIGHT,
devicePixelRatio,
...contextSize
});
}, [height, width]);
return (
<div className={classnames(prefixName(), className)} style={style}>
<div ref={ref}></div>
<div className={classnames(prefixName(), className)} style={{ ...style, ...{ width, height, padding: 0 } }}>
<div className={prefixName('header')} style={{ height: HEADER_HEIGHT }}>
<Header />
</div>
<div ref={ref} className={prefixName('content')} style={{ top: HEADER_HEIGHT }}></div>
<div ref={refLeftDOM}></div>
<div ref={refRighttDOM}></div>
<Toolbar
className={prefixName('toolbar-position')}
openLayer={openLayer}
openSetting={openSetting}
openLeftSider={openLeftSider}
openRightSider={openRightSider}
onClickToggleLayer={() => {
setOpenLayer(openLayer ? false : true);
setOpenLeftSider(openLeftSider ? false : true);
}}
onClickToggleSetting={() => {
setOpenSetting(openSetting ? false : true);
setOpenRightSider(openRightSider ? false : true);
}}
/>
<div ref={refLeftDOM}></div>
<Drawer
title="left Drawer"
// title="left Drawer"
placement="left"
closable={false}
onClose={() => {
console.log('on close left');
setOpenLayer(false);
setOpenLeftSider(false);
}}
mask={false}
open={openLayer}
open={openLeftSider}
getContainer={() => {
return refLeftDOM.current as HTMLDivElement;
}}
width={siderWidth}
bodyStyle={{
padding: 0
}}
rootStyle={{
position: 'absolute',
top: 0,
@ -96,21 +108,19 @@ export const Sketch = (props: SketchProps) => {
left: 0
}}
>
<p>Some contents...</p>
<p>Some contents...</p>
<p>Some contents...</p>
<PanelLayer />
</Drawer>
<Drawer
title="right Drawer"
placement="right"
onClose={() => {
console.log('on close right');
setOpenSetting(false);
setOpenRightSider(false);
}}
mask={false}
open={openSetting}
open={openRightSider}
getContainer={() => {
return refLeftDOM.current as HTMLDivElement;
return refRighttDOM.current as HTMLDivElement;
}}
width={siderWidth}
rootStyle={{

View file

@ -0,0 +1 @@
export const HEADER_HEIGHT = 36;

View file

@ -10,6 +10,7 @@ import IconHand from '../../icons/hand';
import IconScale from '../../icons/scale';
import IconLayer from '../../icons/layer';
import IconSetting from '../../icons/setting';
import IconMore from '../../icons/more';
const RadioButton = Radio.Button;
const RadioGroup = Radio.Group;
@ -20,21 +21,21 @@ const prefixName = createPrefixName(modName);
export interface ToolbarProps {
className?: string;
style?: CSSProperties;
openLayer: boolean;
openSetting: boolean;
openLeftSider: boolean;
openRightSider: boolean;
onClickToggleLayer?: () => void;
onClickToggleSetting?: () => void;
}
export const Toolbar = (props: ToolbarProps) => {
const { className, style, openLayer, openSetting, onClickToggleLayer, onClickToggleSetting } = props;
const { className, style, openLeftSider, openRightSider, onClickToggleLayer, onClickToggleSetting } = props;
const [mode, setMode] = useState<string>('select');
const iconStyle = { fontSize: 20 };
return (
<div style={style} className={classnames(prefixName(), className)}>
<div className={prefixName('left')}>
<Button shape="circle" type={openLayer ? 'primary' : 'default'} icon={<IconLayer style={iconStyle} />} onClick={onClickToggleLayer} />
<Button shape="circle" type={openLeftSider ? 'primary' : 'default'} icon={<IconLayer style={iconStyle} />} onClick={onClickToggleLayer} />
</div>
<RadioGroup className={classnames(prefixName('middle'), prefixName('mode-switch'))} value={mode} onChange={(e) => setMode(e.target.value)}>
<RadioButton value="select">
@ -49,9 +50,12 @@ export const Toolbar = (props: ToolbarProps) => {
<RadioButton value="scale">
<IconScale style={iconStyle} />
</RadioButton>
<RadioButton value="more">
<IconMore style={iconStyle} />
</RadioButton>
</RadioGroup>
<div className={prefixName('right')}>
<Button shape="circle" type={openSetting ? 'primary' : 'default'} icon={<IconSetting style={iconStyle} />} onClick={onClickToggleSetting} />
<Button shape="circle" type={openRightSider ? 'primary' : 'default'} icon={<IconSetting style={iconStyle} />} onClick={onClickToggleSetting} />
</div>
</div>
);

View file

@ -0,0 +1,34 @@
import type { Element, ElementType, ElementSize, ElementBaseDesc } from '@idraw/types';
export type DesignComponent = Omit<ElementSize, 'angle'> & {
uuid: string;
type: 'component';
name?: string;
desc?: ElementBaseDesc & {
children: Array<Element<ElementType> | DesignComponent>;
};
};
export type DesignModule = Omit<ElementSize, 'angle'> & {
uuid: string;
type: 'module';
name?: string;
desc?: ElementBaseDesc & {
children: Array<DesignComponent>;
};
};
export type DesignPage = Omit<ElementSize, 'angle'> & {
uuid: string;
type: 'page';
name?: string;
desc: ElementBaseDesc & {
children: Array<DesignModule>;
};
};
export interface DesignData {
components: DesignComponent[];
modules: DesignModule[];
pages: DesignPage[];
}

View file

@ -0,0 +1 @@
export * from './data';

View file

@ -6,7 +6,7 @@ export interface ElementSize {
angle?: number;
}
interface ElementBaseDesc {
export interface ElementBaseDesc {
borderWidth?: number;
borderColor?: string;
borderRadius?: number;
@ -14,13 +14,17 @@ interface ElementBaseDesc {
shadowOffsetX?: number;
shadowOffsetY?: number;
shadowBlur?: number;
}
interface ElementRectDesc extends ElementBaseDesc {
color?: string;
bgColor?: string;
}
// interface ElementRectDesc extends ElementBaseDesc {
// // color?: string;
// // bgColor?: string;
// }
type ElementRectDesc = ElementBaseDesc;
interface ElemenTextDesc extends ElementBaseDesc {
text: string;
color: string;