Add web frontend panel skeleton & Vue environment
7
web-frontend/.editorconfig
Normal file
@@ -0,0 +1,7 @@
|
||||
[*.{js,jsx,ts,tsx,vue}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
max_line_length = 100
|
||||
23
web-frontend/.eslintrc.js
Normal file
@@ -0,0 +1,23 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
"plugin:vue/vue3-essential",
|
||||
"@vue/airbnb",
|
||||
"@vue/typescript/recommended",
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020,
|
||||
},
|
||||
rules: {
|
||||
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
|
||||
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
|
||||
"no-tabs": 0,
|
||||
"max-len": 0,
|
||||
"linebreak-style": ["error", "unix"],
|
||||
indent: ["error", "tab"],
|
||||
quotes: ["error", "double"],
|
||||
},
|
||||
};
|
||||
12919
web-frontend/package-lock.json
generated
Normal file
@@ -1,11 +1,14 @@
|
||||
{
|
||||
"name": "graphite-web-frontend",
|
||||
"version": "0.1.0",
|
||||
"description": "Graphite's web app frontend pathfinder. Planned to be replaced by a Rust native GUI framework in the future.",
|
||||
"description": "Graphite's web app frontend. Planned to be replaced by a Rust native GUI framework in the future.",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"build": "webpack",
|
||||
"start": "webpack serve"
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint",
|
||||
"webpack-build": "webpack",
|
||||
"webpack-start": "webpack serve"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -15,10 +18,40 @@
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://www.graphite.design",
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^2.33.0",
|
||||
"@typescript-eslint/parser": "^2.33.0",
|
||||
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||
"@vue/cli-plugin-typescript": "~4.5.0",
|
||||
"@vue/cli-service": "~4.5.0",
|
||||
"@vue/compiler-sfc": "^3.0.0",
|
||||
"@vue/eslint-config-airbnb": "^5.0.2",
|
||||
"@vue/eslint-config-typescript": "^5.0.2",
|
||||
"@wasm-tool/wasm-pack-plugin": "^1.3.3",
|
||||
"html-webpack-plugin": "^5.1.0",
|
||||
"webpack": "^5.21.2",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-import": "^2.20.2",
|
||||
"eslint-plugin-vue": "^7.0.0-0",
|
||||
"html-webpack-plugin": "^4.5.1",
|
||||
"lint-staged": "^9.5.0",
|
||||
"sass": "^1.26.5",
|
||||
"sass-loader": "^8.0.2",
|
||||
"typescript": "~3.9.3",
|
||||
"vue-svg-loader": "^0.16.0",
|
||||
"vue-template-compiler": "^2.6.12",
|
||||
"webpack": "^4.45.0",
|
||||
"webpack-cli": "^4.5.0",
|
||||
"webpack-dev-server": "^3.11.2"
|
||||
},
|
||||
"gitHooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,vue,ts,tsx}": [
|
||||
"vue-cli-service lint",
|
||||
"git add"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.0.0",
|
||||
"vue-class-component": "^8.0.0-0"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
web-frontend/public/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
web-frontend/public/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
web-frontend/public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
9
web-frontend/public/browserconfig.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="/mstile-150x150.png"/>
|
||||
<TileColor>#ffffff</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
||||
BIN
web-frontend/public/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
web-frontend/public/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
web-frontend/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
24
web-frontend/public/index.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<meta name="msapplication-TileColor" content="#da532c">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<title>
|
||||
<%= htmlWebpackPlugin.options.title %>
|
||||
</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>JavaScript is required</noscript>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
BIN
web-frontend/public/mstile-150x150.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
19
web-frontend/public/site.webmanifest
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "",
|
||||
"short_name": "",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
28
web-frontend/src/App.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<MainWindow />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Options, Vue } from "vue-class-component";
|
||||
import MainWindow from "./components/window/MainWindow.vue";
|
||||
|
||||
@Options({
|
||||
components: {
|
||||
MainWindow,
|
||||
},
|
||||
})
|
||||
export default class App extends Vue {}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
html, body, #app {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
font-family: "Segoe UI", Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
color: #ddd;
|
||||
background: #222;
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
60
web-frontend/src/components/footer/FooterBar.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div class="status-area footer-third">
|
||||
<div>
|
||||
<span>File: 1.8 MB | Memory: 137 MB | Scratch: 0.7/12.3 GB</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hint-area footer-third">
|
||||
<InputHint :inputMouse="'LMBDrag'">Box Select</InputHint>
|
||||
<InputHint :inputKeys="['G']">Grab Selection</InputHint>
|
||||
<InputHint :inputKeys="['R']">Rotate Selection</InputHint>
|
||||
<InputHint :inputKeys="['S']">Scale Selection</InputHint>
|
||||
</div>
|
||||
<div class="version-area footer-third">
|
||||
<div>
|
||||
<span>Graphite 0.1.0</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.footer-third {
|
||||
display: flex;
|
||||
flex: 1 1 100%;
|
||||
|
||||
&:nth-child(1) {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
&.status-area div, &.version-area {
|
||||
margin: 0 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { Options, Vue } from "vue-class-component";
|
||||
import LayoutRow from "../layout/LayoutRow.vue";
|
||||
import LayoutCol from "../layout/LayoutCol.vue";
|
||||
import InputHint from "./InputHint.vue";
|
||||
|
||||
@Options({
|
||||
components: {
|
||||
LayoutRow,
|
||||
LayoutCol,
|
||||
InputHint,
|
||||
},
|
||||
props: {},
|
||||
})
|
||||
export default class FooterBar extends Vue {}
|
||||
</script>
|
||||
144
web-frontend/src/components/footer/InputHint.vue
Normal file
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<div class="input-hint">
|
||||
<span class="input-key" v-for="inputKey in inputKeys" :key="inputKey">
|
||||
{{inputKey}}
|
||||
</span>
|
||||
<span class="input-mouse" v-if="inputMouse">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" v-html="getMouseIconInnerSVG"></svg>
|
||||
</span>
|
||||
<span class="hint-text">
|
||||
<slot></slot>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.input-hint {
|
||||
height: 100%;
|
||||
margin: 0 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.input-key, .input-mouse {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.input-key {
|
||||
font-family: "Consolas", monospace;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
color: #000;
|
||||
background: #fff;
|
||||
border: 2px;
|
||||
border-style: solid;
|
||||
border-color: #999;
|
||||
border-radius: 4px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.input-mouse {
|
||||
font-size: 0;
|
||||
|
||||
.primary {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.secondary {
|
||||
fill: #888;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { Options, Vue } from "vue-class-component";
|
||||
|
||||
export enum MouseInputInteraction {
|
||||
"None" = "None",
|
||||
"LMB" = "LMB",
|
||||
"RMB" = "RMB",
|
||||
"MMB" = "MMB",
|
||||
"ScrollUp" = "ScrollUp",
|
||||
"ScrollDown" = "ScrollDown",
|
||||
"Drag" = "Drag",
|
||||
"LMBDrag" = "LMBDrag",
|
||||
"RMBDrag" = "RMBDrag",
|
||||
"MMBDrag" = "MMBDrag",
|
||||
}
|
||||
|
||||
@Options({
|
||||
components: {},
|
||||
props: {
|
||||
inputKeys: { type: Array, default: [] },
|
||||
inputMouse: { type: String },
|
||||
},
|
||||
computed: {
|
||||
getMouseIconInnerSVG() {
|
||||
switch (this.inputMouse) {
|
||||
case MouseInputInteraction.None: return `
|
||||
<path style="fill:#888888;" d="M9,7c0,0.55-0.45,1-1,1l0,0C7.45,8,7,7.55,7,7V4.5c0-0.55,0.45-1,1-1l0,0c0.55,0,1,0.45,1,1V7z" />
|
||||
<path style="fill:#888888;" d="M10,2c1.1,0,2,0.9,2,2v6c0,2.21-1.79,4-4,4s-4-1.79-4-4V4c0-1.1,0.9-2,2-2H10 M10,1H6
|
||||
C4.35,1,3,2.35,3,4v6c0,2.76,2.24,5,5,5s5-2.24,5-5V4C13,2.35,11.65,1,10,1L10,1z" />`;
|
||||
case MouseInputInteraction.LMB: return `
|
||||
<path style="fill:#FFFFFF;" d="M8,1H6C4.35,1,3,2.35,3,4v4h5V1z" />
|
||||
<path style="fill:#888888;" d="M10,1H9v1h1c1.1,0,2,0.9,2,2v6c0,2.21-1.79,4-4,4s-4-1.79-4-4V9H3v1c0,2.76,2.24,5,5,5s5-2.24,5-5V4
|
||||
C13,2.35,11.65,1,10,1z" />`;
|
||||
case MouseInputInteraction.RMB: return `
|
||||
<path class="secondary" d="M8,1h2c1.65,0,3,1.35,3,3v4H8V1z" />
|
||||
<path class="primary" d="M6,1h1v1H6C4.9,2,4,2.9,4,4v6c0,2.21,1.79,4,4,4s4-1.79,4-4V9h1v1c0,2.76-2.24,5-5,5s-5-2.24-5-5V4
|
||||
C3,2.35,4.35,1,6,1z" />`;
|
||||
case MouseInputInteraction.MMB: return `
|
||||
<path style="fill:#FFFFFF;" d="M9,7c0,0.55-0.45,1-1,1l0,0C7.45,8,7,7.55,7,7V4.5c0-0.55,0.45-1,1-1l0,0c0.55,0,1,0.45,1,1V7z" />
|
||||
<path style="fill:#888888;" d="M10,2c1.1,0,2,0.9,2,2v6c0,2.21-1.79,4-4,4s-4-1.79-4-4V4c0-1.1,0.9-2,2-2H10 M10,1H6
|
||||
C4.35,1,3,2.35,3,4v6c0,2.76,2.24,5,5,5s5-2.24,5-5V4C13,2.35,11.65,1,10,1L10,1z" />`;
|
||||
case MouseInputInteraction.ScrollUp: return `
|
||||
<polygon style="fill:#FFFFFF;" points="10.5,4 8,2 5.5,4 5.5,2 8,0 10.5,2 " />
|
||||
<polygon style="fill:#FFFFFF;" points="10.5,8 8,6 5.5,8 5.5,6 8,4 10.5,6 " />
|
||||
<path style="fill:#888888;" d="M11.5,1.42v1.28C11.8,3.06,12,3.5,12,4v6c0,2.21-1.79,4-4,4s-4-1.79-4-4V4c0-0.5,0.2-0.94,0.5-1.29
|
||||
V1.42C3.61,1.94,3,2.9,3,4v6c0,2.76,2.24,5,5,5s5-2.24,5-5V4C13,2.9,12.39,1.94,11.5,1.42z" />`;
|
||||
case MouseInputInteraction.ScrollDown: return `
|
||||
<polygon style="fill:#FFFFFF;" points="5.5,4 8,6 10.5,4 10.5,6 8,8 5.5,6 " />
|
||||
<polygon style="fill:#FFFFFF;" points="5.5,0 8,2 10.5,0 10.5,2 8,4 5.5,2 " />
|
||||
<path style="fill:#888888;" d="M11.5,1.42v1.28C11.8,3.06,12,3.5,12,4v6c0,2.21-1.79,4-4,4s-4-1.79-4-4V4c0-0.5,0.2-0.94,0.5-1.29
|
||||
V1.42C3.61,1.94,3,2.9,3,4v6c0,2.76,2.24,5,5,5s5-2.24,5-5V4C13,2.9,12.39,1.94,11.5,1.42z" />`;
|
||||
case MouseInputInteraction.Drag: return `
|
||||
<path style="fill:#888888;" d="M8,7c0,0.55-0.45,1-1,1l0,0C6.45,8,6,7.55,6,7V4.5c0-0.55,0.45-1,1-1l0,0c0.55,0,1,0.45,1,1V7z" />
|
||||
<path style="fill:#FFFFFF;" d="M11,16c-0.18,0-0.36-0.1-0.45-0.28c-0.12-0.25-0.02-0.55,0.22-0.67C10.87,15.01,13,13.88,13,11V6
|
||||
c0-0.28,0.22-0.5,0.5-0.5S14,5.72,14,6v5c0,3.52-2.66,4.89-2.78,4.95C11.15,15.98,11.08,16,11,16z" />
|
||||
<path style="fill:#FFFFFF;" d="M14.5,15c-0.13,0-0.26-0.05-0.35-0.15c-0.19-0.19-0.2-0.51,0-0.7C14.17,14.12,15,13.2,15,11V8
|
||||
c0-0.28,0.22-0.5,0.5-0.5S16,7.72,16,8v3c0,2.68-1.1,3.81-1.15,3.85C14.76,14.95,14.63,15,14.5,15z" />
|
||||
<path style="fill:#888888;" d="M9,2c1.1,0,2,0.9,2,2v6c0,2.21-1.79,4-4,4s-4-1.79-4-4V4c0-1.1,0.9-2,2-2H9 M9,1H5C3.35,1,2,2.35,2,4
|
||||
v6c0,2.76,2.24,5,5,5s5-2.24,5-5V4C12,2.35,10.65,1,9,1L9,1z" />`;
|
||||
case MouseInputInteraction.LMBDrag: return `
|
||||
<path style="fill:#FFFFFF;" d="M11,16c-0.18,0-0.36-0.1-0.45-0.28c-0.12-0.25-0.02-0.55,0.22-0.67C10.87,15.01,13,13.88,13,11V6
|
||||
c0-0.28,0.22-0.5,0.5-0.5S14,5.72,14,6v5c0,3.52-2.66,4.89-2.78,4.95C11.15,15.98,11.08,16,11,16z" />
|
||||
<path style="fill:#FFFFFF;" d="M14.5,15c-0.13,0-0.26-0.05-0.35-0.15c-0.19-0.19-0.2-0.51,0-0.7C14.17,14.12,15,13.2,15,11V8
|
||||
c0-0.28,0.22-0.5,0.5-0.5S16,7.72,16,8v3c0,2.68-1.1,3.81-1.15,3.85C14.76,14.95,14.63,15,14.5,15z" />
|
||||
<path style="fill:#FFFFFF;" d="M7,1H5C3.35,1,2,2.35,2,4v4h5V1z" />
|
||||
<path style="fill:#888888;" d="M9,1H8v1h1c1.1,0,2,0.9,2,2v6c0,2.21-1.79,4-4,4s-4-1.79-4-4V9H2v1c0,2.76,2.24,5,5,5s5-2.24,5-5V4
|
||||
C12,2.35,10.65,1,9,1z" />`;
|
||||
case MouseInputInteraction.RMBDrag: return `
|
||||
<path style="fill:#FFFFFF;" d="M11,16c-0.18,0-0.36-0.1-0.45-0.28c-0.12-0.25-0.02-0.55,0.22-0.67C10.87,15.01,13,13.88,13,11V6
|
||||
c0-0.28,0.22-0.5,0.5-0.5S14,5.72,14,6v5c0,3.52-2.66,4.89-2.78,4.95C11.15,15.98,11.08,16,11,16z" />
|
||||
<path style="fill:#FFFFFF;" d="M14.5,15c-0.13,0-0.26-0.05-0.35-0.15c-0.19-0.19-0.2-0.51,0-0.7C14.17,14.12,15,13.2,15,11V8
|
||||
c0-0.28,0.22-0.5,0.5-0.5S16,7.72,16,8v3c0,2.68-1.1,3.81-1.15,3.85C14.76,14.95,14.63,15,14.5,15z" />
|
||||
<path style="fill:#FFFFFF;" d="M7,1h2c1.65,0,3,1.35,3,3v4H7V1z" />
|
||||
<path style="fill:#888888;" d="M5,1h1v1H5C3.9,2,3,2.9,3,4v6c0,2.21,1.79,4,4,4s4-1.79,4-4V9h1v1c0,2.76-2.24,5-5,5s-5-2.24-5-5V4
|
||||
C2,2.35,3.35,1,5,1z" />`;
|
||||
case MouseInputInteraction.MMBDrag: return `
|
||||
<path style="fill:#FFFFFF;" d="M8,7c0,0.55-0.45,1-1,1l0,0C6.45,8,6,7.55,6,7V4.5c0-0.55,0.45-1,1-1l0,0c0.55,0,1,0.45,1,1V7z" />
|
||||
<path style="fill:#FFFFFF;" d="M11,16c-0.18,0-0.36-0.1-0.45-0.28c-0.12-0.25-0.02-0.55,0.22-0.67C10.87,15.01,13,13.88,13,11V6
|
||||
c0-0.28,0.22-0.5,0.5-0.5S14,5.72,14,6v5c0,3.52-2.66,4.89-2.78,4.95C11.15,15.98,11.08,16,11,16z" />
|
||||
<path style="fill:#FFFFFF;" d="M14.5,15c-0.13,0-0.26-0.05-0.35-0.15c-0.19-0.19-0.2-0.51,0-0.7C14.17,14.12,15,13.2,15,11V8
|
||||
c0-0.28,0.22-0.5,0.5-0.5S16,7.72,16,8v3c0,2.68-1.1,3.81-1.15,3.85C14.76,14.95,14.63,15,14.5,15z" />
|
||||
<path style="fill:#888888;" d="M9,2c1.1,0,2,0.9,2,2v6c0,2.21-1.79,4-4,4s-4-1.79-4-4V4c0-1.1,0.9-2,2-2H9 M9,1H5C3.35,1,2,2.35,2,4
|
||||
v6c0,2.76,2.24,5,5,5s5-2.24,5-5V4C12,2.35,10.65,1,9,1L9,1z" />`;
|
||||
default: return "";
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
export default class InputHint extends Vue {}
|
||||
</script>
|
||||
55
web-frontend/src/components/header/FileMenu.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div class="entry">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16">
|
||||
<path d="M5.5,10.2c0.1,0.2,0,0.3-0.1,0.4c-0.2,0.1-0.3,0-0.4-0.1c0,0,0,0,0,0L2.7,6.7c-0.1-0.2,0-0.3,0.1-0.4c0.2-0.1,0.3,0,0.4,0.1
|
||||
L5.5,10.2z M14.9,14.9L14.9,14.9C14.9,14.9,14.9,15,14.9,14.9C14.8,15,14.8,15,14.9,14.9L14.9,14.9c-0.5,0.2-1,0.4-1.5,0.5
|
||||
c-0.5,0.1-1.1,0.2-1.6,0.2c-0.5,0.1-1.1,0.1-1.6,0c-0.5,0-1.1-0.1-1.6-0.1l-1.6-0.1c-1.7,0-3.9-0.1-4.4-0.1s-1.1-0.2-1.1-0.3
|
||||
c0-0.1,0.3-0.2,0.6-0.2c0.5-0.1,1.1-0.2,1.7-0.3c0.6-0.1,2-0.2,4.1-0.2c0.8,0,1.8,0,1.8,0c0.6,0,1,0,1.9,0c0.7,0,1.1,0,1.2,0
|
||||
c0.3,0,0.6,0.1,0.9,0.2l-2.6-1.9c-0.2,0.1-0.4,0.2-0.7,0.2H4.5c-0.5,0-0.9-0.2-1.1-0.6l-2.9-5c-0.2-0.4-0.2-0.9,0-1.3l2.9-5
|
||||
C3.6,0.5,4,0.3,4.5,0.3h5.8c0.5,0,0.9,0.2,1.1,0.6l2.9,5l0,0l0,0c0,0.1,0.1,0.2,0.1,0.3c0,0,0,0.1,0,0.1c0,0.1,0,0.1,0,0.2
|
||||
c0,0,0.7,7.2,0.7,7.8v0C15.2,14.6,15.1,14.8,14.9,14.9L14.9,14.9z M4.3,1.9L4.2,2l3.6,6.2c0.4-0.1,0.9-0.2,1.4-0.2l0.9-1.3l1.6-0.2
|
||||
c0.2-0.4,0.5-0.8,0.8-1.1l-2.1-3.7c-0.1-0.2-0.3-0.3-0.5-0.3H4.8C4.6,1.6,4.4,1.7,4.3,1.9z M9.4,11.6l-2-1.4C7.4,10.1,7.3,10,7.3,10
|
||||
l0,0L3.5,3.3l-1.7,3c-0.1,0.2-0.1,0.4,0,0.6l2.6,4.5c0.1,0.2,0.3,0.3,0.5,0.3L9.4,11.6z M13.5,11.3l-0.5-4.6c-0.3,0.3-0.5,0.7-0.7,1
|
||||
l-1.6,0.2L9.8,9.2c-0.4,0-0.8,0-1.3,0.1l3.7,2.7C12.5,11.6,13,11.3,13.5,11.3L13.5,11.3z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="entry"><span>File</span></div>
|
||||
<div class="entry"><span>Edit</span></div>
|
||||
<div class="entry"><span>Comp</span></div>
|
||||
<div class="entry"><span>View</span></div>
|
||||
<div class="entry"><span>Help</span></div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
|
||||
svg {
|
||||
fill: #ddd;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #555;
|
||||
|
||||
svg {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
span {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { Options, Vue } from "vue-class-component";
|
||||
|
||||
@Options({
|
||||
components: {},
|
||||
props: {},
|
||||
})
|
||||
export default class FileMenu extends Vue {}
|
||||
</script>
|
||||
51
web-frontend/src/components/header/HeaderBar.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div class="header-third">
|
||||
<FileMenu />
|
||||
</div>
|
||||
<div class="header-third">
|
||||
<WindowTitle />
|
||||
</div>
|
||||
<div class="header-third">
|
||||
<WindowButtons :maximized="true" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.header-third {
|
||||
display: flex;
|
||||
flex: 1 1 100%;
|
||||
|
||||
&:nth-child(1) {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { Options, Vue } from "vue-class-component";
|
||||
import LayoutRow from "../layout/LayoutRow.vue";
|
||||
import LayoutCol from "../layout/LayoutCol.vue";
|
||||
import FileMenu from "./FileMenu.vue";
|
||||
import WindowTitle from "./WindowTitle.vue";
|
||||
import WindowButtons from "./WindowButtons.vue";
|
||||
|
||||
@Options({
|
||||
components: {
|
||||
LayoutRow,
|
||||
LayoutCol,
|
||||
FileMenu,
|
||||
WindowTitle,
|
||||
WindowButtons,
|
||||
},
|
||||
props: {},
|
||||
})
|
||||
export default class HeaderBar extends Vue {}
|
||||
</script>
|
||||
54
web-frontend/src/components/header/WindowButtons.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div class="button minimize" title="Minimize">
|
||||
<svg width="10" height="10" viewBox="0 0 10 10">
|
||||
<rect y="4" width="10" height="1" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="button maximize" title="Maximize" v-if="maximized === false">
|
||||
<svg width="10" height="10" viewBox="0 0 10 10">
|
||||
<path d="M10,0v10H0V0H10z M9,1H1v8h8V1z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="button restore-down" title="Restore Down" v-if="maximized === true">
|
||||
<svg width="10" height="10" viewBox="0 0 10 10">
|
||||
<path d="M10,8H8v2H0V2h2V0h8V8z M7,3H1v6h6V3z M9,1H3v1h5v5h1V1z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="button close" title="Close">
|
||||
<svg width="10" height="10" viewBox="0 0 10 10">
|
||||
<polygon points="10,0.7 9.3,0 5,4.3 0.7,0 0,0.7 4.3,5 0,9.3 0.7,10 5,5.7 9.3,10 10,9.3 5.7,5" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
|
||||
svg {
|
||||
fill: #ddd;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #555;
|
||||
|
||||
svg {
|
||||
fill: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { Options, Vue } from "vue-class-component";
|
||||
|
||||
@Options({
|
||||
components: {},
|
||||
props: {
|
||||
maximized: { type: Boolean, default: false },
|
||||
},
|
||||
})
|
||||
export default class WindowButtons extends Vue {}
|
||||
</script>
|
||||
23
web-frontend/src/components/header/WindowTitle.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<div class="window-title">
|
||||
<span>X-35B.gdd* - Graphite</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.window-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { Options, Vue } from "vue-class-component";
|
||||
|
||||
@Options({
|
||||
components: {},
|
||||
props: {},
|
||||
})
|
||||
export default class WindowTitle extends Vue {}
|
||||
</script>
|
||||
23
web-frontend/src/components/layout/LayoutCol.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<div :class="['layout-col']">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.layout-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { Options, Vue } from "vue-class-component";
|
||||
|
||||
@Options({
|
||||
components: {},
|
||||
props: {},
|
||||
})
|
||||
export default class LayoutCol extends Vue {}
|
||||
</script>
|
||||
23
web-frontend/src/components/layout/LayoutRow.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<div :class="['layout-row']">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.layout-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-grow: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { Options, Vue } from "vue-class-component";
|
||||
|
||||
@Options({
|
||||
components: {},
|
||||
props: {},
|
||||
})
|
||||
export default class LayoutRow extends Vue {}
|
||||
</script>
|
||||
146
web-frontend/src/components/panel-system/DockablePanel.vue
Normal file
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<div class="panel">
|
||||
<div class="tab-bar" :class="{ 'constant-widths': tabConstantWidths }">
|
||||
<div class="tab" :class="{ active: tabIndex === tabActiveIndex }" v-for="(tabLabel, tabIndex) in tabLabels" :key="tabLabel">
|
||||
<span>{{tabLabel}}</span>
|
||||
<button v-if="tabCloseButtons">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-content">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.panel {
|
||||
background: #111;
|
||||
border-radius: 8px;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.tab-bar {
|
||||
flex-direction: row;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
|
||||
&.constant-widths .tab {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
height: 100%;
|
||||
padding: 0 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
|
||||
&.active {
|
||||
background: #333;
|
||||
border-radius: 8px 8px 0 0;
|
||||
position: relative;
|
||||
|
||||
&::before, &::after {
|
||||
content: "";
|
||||
width: 16px;
|
||||
height: 8px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
box-shadow: #333;
|
||||
}
|
||||
|
||||
&::before {
|
||||
left: -16px;
|
||||
border-bottom-right-radius: 8px;
|
||||
box-shadow: 8px 0 0 0 #333;
|
||||
}
|
||||
|
||||
&::after {
|
||||
right: -16px;
|
||||
border-bottom-left-radius: 8px;
|
||||
box-shadow: -8px 0 0 0 #333;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
flex: 1 1 100%;
|
||||
overflow-x: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
// Required because https://stackoverflow.com/a/21611191/775283
|
||||
height: 100%;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
button {
|
||||
flex: 0 0 auto;
|
||||
outline: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: none;
|
||||
color: #ddd;
|
||||
font-weight: bold;
|
||||
font-size: 10px;
|
||||
border-radius: 2px;
|
||||
margin-left: 8px;
|
||||
|
||||
&:hover {
|
||||
background: #555;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.active) + .tab:not(.active) {
|
||||
margin-left: 1px;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -1px;
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
background: #444;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-of-type:not(.active) {
|
||||
margin-right: 1px;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: -1px;
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
background: #444;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
background: #333;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { Options, Vue } from "vue-class-component";
|
||||
|
||||
@Options({
|
||||
components: {},
|
||||
props: {
|
||||
tabConstantWidths: { type: Boolean, default: false },
|
||||
tabCloseButtons: { type: Boolean, default: false },
|
||||
tabLabels: { type: Array, required: true },
|
||||
tabActiveIndex: { type: Number, required: true },
|
||||
},
|
||||
})
|
||||
export default class DockablePanel extends Vue {}
|
||||
</script>
|
||||
56
web-frontend/src/components/panel-system/PanelArea.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<LayoutRow class="dockable-grid-subdivision">
|
||||
<LayoutCol class="dockable-grid-subdivision" style="flex-grow: 1597;">
|
||||
<DockablePanel :tabCloseButtons="true" :tabConstantWidths="true" :tabLabels="['X-35B*', 'Document 2', 'Document 3', 'Document 4', 'Document 5']" :tabActiveIndex="0" />
|
||||
</LayoutCol>
|
||||
<LayoutCol class="dockable-grid-resize-gutter"></LayoutCol>
|
||||
<LayoutCol class="dockable-grid-subdivision" style="flex-grow: 319;">
|
||||
<LayoutRow class="dockable-grid-subdivision">
|
||||
<DockablePanel :tabLabels="['Properties', 'Typography', 'Colors']" :tabActiveIndex="0" />
|
||||
</LayoutRow>
|
||||
<LayoutRow class="dockable-grid-resize-gutter"></LayoutRow>
|
||||
<LayoutRow class="dockable-grid-subdivision">
|
||||
<DockablePanel :tabLabels="['Layers']" :tabActiveIndex="0" />
|
||||
</LayoutRow>
|
||||
<LayoutRow class="dockable-grid-resize-gutter"></LayoutRow>
|
||||
<LayoutRow class="dockable-grid-subdivision" style="flex-grow: 0; height: 0;">
|
||||
<DockablePanel :tabLabels="['Minimap', 'Brushes', 'Links']" :tabActiveIndex="0" />
|
||||
</LayoutRow>
|
||||
</LayoutCol>
|
||||
</LayoutRow>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.dockable-grid-subdivision {
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
.dockable-grid-resize-gutter {
|
||||
flex: 0 0 4px;
|
||||
|
||||
&.layout-row {
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
&.layout-col {
|
||||
cursor: ew-resize;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { Options, Vue } from "vue-class-component";
|
||||
import LayoutRow from "../layout/LayoutRow.vue";
|
||||
import LayoutCol from "../layout/LayoutCol.vue";
|
||||
import DockablePanel from "./DockablePanel.vue";
|
||||
|
||||
@Options({
|
||||
components: {
|
||||
LayoutRow,
|
||||
LayoutCol,
|
||||
DockablePanel,
|
||||
},
|
||||
props: {},
|
||||
})
|
||||
export default class PanelArea extends Vue {}
|
||||
</script>
|
||||
55
web-frontend/src/components/window/MainWindow.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<LayoutCol class="main-window">
|
||||
<LayoutRow :class="['header-bar']">
|
||||
<HeaderBar />
|
||||
</LayoutRow>
|
||||
<LayoutRow :class="['panel-container']">
|
||||
<PanelArea />
|
||||
</LayoutRow>
|
||||
<LayoutRow :class="['footer-bar']">
|
||||
<FooterBar />
|
||||
</LayoutRow>
|
||||
</LayoutCol>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.main-window {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.header-bar {
|
||||
height: 28px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.panel-container {
|
||||
flex: 1 1 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.footer-bar {
|
||||
height: 24px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { Options, Vue } from "vue-class-component";
|
||||
import LayoutRow from "../layout/LayoutRow.vue";
|
||||
import LayoutCol from "../layout/LayoutCol.vue";
|
||||
import HeaderBar from "../header/HeaderBar.vue";
|
||||
import PanelArea from "../panel-system/PanelArea.vue";
|
||||
import FooterBar from "../footer/FooterBar.vue";
|
||||
|
||||
@Options({
|
||||
components: {
|
||||
LayoutRow,
|
||||
LayoutCol,
|
||||
HeaderBar,
|
||||
PanelArea,
|
||||
FooterBar,
|
||||
},
|
||||
props: {},
|
||||
})
|
||||
export default class MainWindow extends Vue {}
|
||||
</script>
|
||||
4
web-frontend/src/main.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { createApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
|
||||
createApp(App).mount("#app");
|
||||
6
web-frontend/src/shims-vue.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/* eslint-disable */
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
40
web-frontend/tsconfig.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "esnext",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"importHelpers": true,
|
||||
"moduleResolution": "node",
|
||||
"experimentalDecorators": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"types": [
|
||||
"webpack-env"
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue",
|
||||
"tests/**/*.ts",
|
||||
"tests/**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
@@ -1,23 +1,23 @@
|
||||
const path = require('path');
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");
|
||||
|
||||
module.exports = {
|
||||
entry: "./main.js",
|
||||
output: {
|
||||
path: path.resolve(__dirname, "dist"),
|
||||
filename: "main.js",
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({ title: 'Graphite' }),
|
||||
new WasmPackPlugin({
|
||||
crateDirectory: path.resolve(__dirname, "..", "packages", "wasm-bindings"),
|
||||
outDir: path.resolve(__dirname, "pkg"),
|
||||
}),
|
||||
],
|
||||
mode: "development",
|
||||
devtool: 'source-map',
|
||||
experiments: {
|
||||
syncWebAssembly: true,
|
||||
},
|
||||
};
|
||||
const path = require("path");
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");
|
||||
|
||||
module.exports = {
|
||||
entry: "./main.js",
|
||||
output: {
|
||||
path: path.resolve(__dirname, "dist"),
|
||||
filename: "main.js",
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({ title: "Graphite" }),
|
||||
new WasmPackPlugin({
|
||||
crateDirectory: path.resolve(__dirname, "..", "packages", "wasm-bindings"),
|
||||
outDir: path.resolve(__dirname, "pkg"),
|
||||
}),
|
||||
],
|
||||
mode: "development",
|
||||
devtool: "source-map",
|
||||
// experiments: {
|
||||
// syncWebAssembly: true,
|
||||
// },
|
||||
};
|
||||
|
||||