Added CSS export/download
This commit is contained in:
parent
b830194366
commit
bb95de0ab0
7 changed files with 238 additions and 41 deletions
|
@ -116,4 +116,4 @@ import Settings from "./components/Settings.vue";
|
|||
|
||||
</div>
|
||||
|
||||
</template>./ts/synergy
|
||||
</template>
|
||||
|
|
72
web/components/Components.vue
Normal file
72
web/components/Components.vue
Normal file
|
@ -0,0 +1,72 @@
|
|||
<script lang="ts" setup>
|
||||
|
||||
import { reactive } from 'vue';
|
||||
import { Export, Result } from "../ts/Export";
|
||||
import { components } from "../ts/Synergy";
|
||||
import { variableValues, selectedComponents } from "../ts/Shared";
|
||||
|
||||
let data = reactive<Data>({
|
||||
results: [],
|
||||
vars: null
|
||||
});
|
||||
|
||||
interface Data {
|
||||
results: Result[],
|
||||
vars: string | null
|
||||
}
|
||||
|
||||
async function exportCss() {
|
||||
let exp = new Export(variableValues, selectedComponents);
|
||||
data.results = await exp.process();
|
||||
data.vars = exp.vars;
|
||||
}
|
||||
|
||||
function kbSize(value: number) {
|
||||
return `${Math.round(value/1024*100)/100} kB`;
|
||||
}
|
||||
|
||||
function download(name: string, str: string) {
|
||||
let a = document.createElement("a");
|
||||
a.download = name;
|
||||
a.href=`data:text/plain;charset=utf-8,${encodeURIComponent(str)}`;
|
||||
a.click();
|
||||
}
|
||||
|
||||
function showVars() {
|
||||
console.log(data.vars);
|
||||
alert("Check console logs!");
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2>Components</h2>
|
||||
<div class="components">
|
||||
<form class="box" @submit.prevent="exportCss">
|
||||
<div class="item" v-for="component in components">
|
||||
<label class="cbox">
|
||||
<input type="checkbox" v-model="selectedComponents[component.id]">
|
||||
<span>{{ component.name }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn btn-primary" type="submit">
|
||||
Generate CSS
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="box export" v-if="data.results.length && data.vars">
|
||||
<b>CSS Export</b>
|
||||
<div class="btn-row">
|
||||
<button class="btn btn-primary" v-for="r in data.results" @click="download(r.name, r.css)">
|
||||
{{ r.name }}
|
||||
<small>{{ kbSize(r.size_gzip) }} kB (gzip) · {{ kbSize(r.size) }} kB</small>
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<a href="#" @click.prevent="showVars">Show Synergy variables</a> ·
|
||||
<a href="#" @click.prevent="download('variables.css', data.vars)">download</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -3,8 +3,9 @@
|
|||
import { variables } from "../ts/Synergy";
|
||||
import { Icon } from "@iconify/vue";
|
||||
import { Preset } from "../ts/Preset";
|
||||
import { variableValues, applyVariables } from "../ts/Shared";
|
||||
import { variableValues } from "../ts/Shared";
|
||||
import { setVariables } from "../ts/Styles";
|
||||
import Components from "./Components.vue";
|
||||
|
||||
const variableTypeIcon = {
|
||||
color: "ic:outline-color-lens",
|
||||
|
@ -15,6 +16,13 @@ function update() {
|
|||
setVariables(variableValues);
|
||||
}
|
||||
|
||||
function applyPreset(preset: Preset) {
|
||||
const variables = preset.getVariables() as ComboObject;
|
||||
Object.keys(variables).forEach(key => {
|
||||
variableValues[key] = variables[key];
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -47,11 +55,12 @@ function update() {
|
|||
<div>
|
||||
<h2>Presets</h2>
|
||||
<div class="presets">
|
||||
<div v-for="preset in Preset.presets" @click="applyVariables(preset.getVariables())">
|
||||
<div v-for="preset in Preset.presets" @click="applyPreset(preset)">
|
||||
<div :style="`background-color: ${c?.hexFormat()}`" v-for="c in preset.colors"></div>
|
||||
</div>
|
||||
</div>
|
||||
<Components />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>../ts/Synergy../ts/Styles../ts/Shared
|
||||
</template>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
html {background: var(--synergy-site-bg);}
|
||||
html, body {color: var(--synergy-text-color); font-family: Cantarell, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Open Sans, Helvetica Neue, Arial, Noto Sans, Roboto, sans-serif; font-size: 18px; overflow-x: hidden;}
|
||||
html, body {color: var(--synergy-text-color); font-family: Cantarell, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Open Sans, Helvetica Neue, Arial, Noto Sans, Roboto, sans-serif; font-size: 18px; margin: 0;}
|
||||
|
||||
*, *::before, *::after {box-sizing: border-box;}
|
||||
|
||||
|
@ -9,7 +9,8 @@ header h1 {font-size: 50px;}
|
|||
header h1 .color {background-clip: text; -webkit-background-clip: text; background-image: linear-gradient(10deg, var(--synergy-border-active), var(--synergy-border)); color: transparent;}
|
||||
|
||||
|
||||
.settings {margin: 70px 0; display: grid; grid-template-columns: 1fr 1fr; gap: 25px;}
|
||||
.settings {margin: 70px 0; display: grid; grid-template-columns: 1fr 1fr; gap: 40px; align-items: start;}
|
||||
.settings > * {position: sticky; top: 0;}
|
||||
.settings h2 {text-align: center;}
|
||||
.settings .variables {display: grid; gap: 10px 20px; align-items: center; grid-template-columns: max-content 1fr;}
|
||||
.settings .variables > * {display: flex; gap: 10px; align-items: center;}
|
||||
|
@ -18,8 +19,15 @@ header h1 .color {background-clip: text; -webkit-background-clip: text; backgrou
|
|||
.settings .presets > * {display: flex; border-radius: 10px; overflow: hidden; cursor: pointer; box-shadow: 0 1px 2px 0 rgba(0, 0, 0, .1);}
|
||||
.settings .presets > * > * {height: 100px; flex: 1;}
|
||||
|
||||
.settings .components .item {margin-bottom: 10px;}
|
||||
.settings .components .btn-row {margin-top: 15px;}
|
||||
.settings .components .box {margin-top: 15px; background-color: var(--synergy-tab-highlight); padding: 15px; border-radius: var(--synergy-border-radius);}
|
||||
.settings .components .btn small {display: block; font-weight: 400; font-size: 11px; margin-top: 3px;}
|
||||
.settings .components .export .btn-row {margin: 15px 0;}
|
||||
|
||||
@media screen and (max-width: 850px) {
|
||||
.settings {display: flex; flex-direction: column-reverse;}
|
||||
.settings {display: flex; flex-direction: column-reverse; align-items: normal;}
|
||||
.settings > * {position: static;}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
|
@ -42,3 +50,6 @@ header h1 .color {background-clip: text; -webkit-background-clip: text; backgrou
|
|||
.inp.color {width: 40px;}
|
||||
.inp input[type=color] {padding: 0; border: 0;}
|
||||
::-webkit-color-swatch, ::-moz-color-swatch {border: 0;}
|
||||
|
||||
a {color: var(--synergy-btn-primary-bg);}
|
||||
a:hover {color: var(--synergy-btn-primary-bg-hover);}
|
89
web/ts/Export.ts
Normal file
89
web/ts/Export.ts
Normal file
|
@ -0,0 +1,89 @@
|
|||
import { Variable, components, variables } from "./Synergy";
|
||||
|
||||
export class Export {
|
||||
|
||||
private variables: ComboObject;
|
||||
private components: ComboObject;
|
||||
|
||||
public vars: string | null = null;
|
||||
public css: Result[] = [];
|
||||
|
||||
constructor(variables: ComboObject, components: ComboObject) {
|
||||
this.variables = variables;
|
||||
this.components = components;
|
||||
}
|
||||
|
||||
async process() {
|
||||
|
||||
this.vars = this.getVariables();
|
||||
let cssParts = [this.vars];
|
||||
|
||||
for (let c of components) {
|
||||
if (!this.components[c.id]) continue;
|
||||
let value = await (await fetch(`./${c.id}.css`)).text();
|
||||
value = `/* ${c.name} */\n\n${value}`;
|
||||
cssParts.push(value);
|
||||
}
|
||||
let css = cssParts.join("\n\n/* ------------------- */\n\n");
|
||||
|
||||
this.css = [];
|
||||
await this.addResult("synergy.min.css", this.minify(css));
|
||||
await this.addResult("synergy.css", css);
|
||||
|
||||
return this.css;
|
||||
|
||||
}
|
||||
|
||||
getVariables() {
|
||||
let root: string[] = [];
|
||||
for (let v of variables) {
|
||||
if (!this.required(v)) continue;
|
||||
root.push(`\t--synergy-${v.name}: ${this.variables[v.name]};`);
|
||||
}
|
||||
return `:root {\n${root.join("\n")}\n}`;
|
||||
}
|
||||
|
||||
required(variable: Variable) {
|
||||
if (!variable.requiredBy) return true;
|
||||
for (let id of variable.requiredBy) {
|
||||
if (this.components[id]) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async addResult(name: string, css: string) {
|
||||
this.css.push({
|
||||
name,
|
||||
css,
|
||||
size: this.getSize(css),
|
||||
size_gzip: await this.getCompressedSize(css)
|
||||
});
|
||||
}
|
||||
|
||||
async getCompressedSize(content: string) {
|
||||
let ds = new CompressionStream("gzip");
|
||||
let blob = new Blob([content]);
|
||||
let compressedStream = blob.stream().pipeThrough(ds);
|
||||
return (await new Response(compressedStream).blob()).size;
|
||||
}
|
||||
|
||||
getSize(content: string) {
|
||||
return (new TextEncoder().encode(content)).length
|
||||
}
|
||||
|
||||
minify(value: string) {
|
||||
return value
|
||||
.replace(/([^0-9a-zA-Z\.#])\s+/g, "$1")
|
||||
.replace(/\s([^0-9a-zA-Z\.#]+)/g, "$1")
|
||||
.replace(/;}/g, "}")
|
||||
.replace(/\/\*.*?\*\//g, "");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export interface Result {
|
||||
name: string,
|
||||
css: string,
|
||||
size: number,
|
||||
size_gzip: number
|
||||
}
|
|
@ -1,19 +1,13 @@
|
|||
import { reactive, watch } from "vue";
|
||||
import { getDefaultVariables } from "./Synergy";
|
||||
import { getDefaultComponents, getDefaultVariables } from "./Synergy";
|
||||
import { setVariables } from "./Styles";
|
||||
|
||||
let values = getDefaultVariables();
|
||||
export let variableValues = reactive<ComboObject>(getDefaultVariables());
|
||||
|
||||
export let variableValues = reactive<ComboObject>(values);
|
||||
export let selectedComponents = reactive<ComboObject>(getDefaultComponents());
|
||||
|
||||
watch(() => variableValues, () => {
|
||||
setVariables(variableValues);
|
||||
}, {deep: true});
|
||||
|
||||
setVariables(values);
|
||||
|
||||
export function applyVariables(variables: ComboObject) {
|
||||
Object.keys(variables).forEach(key => {
|
||||
variableValues[key] = variables[key];
|
||||
});
|
||||
}
|
||||
setVariables(variableValues);
|
|
@ -1,29 +1,45 @@
|
|||
import { Preset } from "./Preset";
|
||||
|
||||
export let variables: Variable[] = [
|
||||
{name: "border", type: "color"},
|
||||
{name: "border-active", type: "color"},
|
||||
{name: "border-width", type: "number"},
|
||||
{name: "border-radius", type: "number"},
|
||||
{name: "focus-highlight", type: "color"},
|
||||
{name: "tab-highlight", type: "color"},
|
||||
{name: "tab-bar-height", type: "number"},
|
||||
{name: "label", type: "color"},
|
||||
{name: "label-active", type: "color"},
|
||||
{name: "btn-primary-bg", type: "color"},
|
||||
{name: "btn-primary-bg-hover", type: "color"},
|
||||
{name: "btn-primary-bg-active", type: "color"},
|
||||
{name: "btn-bg", type: "color"},
|
||||
{name: "btn-bg-hover", type: "color"},
|
||||
{name: "btn-bg-active", type: "color"},
|
||||
{name: "text-color", type: "color"},
|
||||
{name: "bg", type: "color"},
|
||||
export const variables: Variable[] = [
|
||||
{name: "border", type: "color", requiredBy: ["input", "checkbox", "toggle", "tabs"]},
|
||||
{name: "border-active", type: "color", requiredBy: ["input", "checkbox", "toggle", "tabs"]},
|
||||
{name: "border-width", type: "number", requiredBy: ["input", "checkbox", "toggle"]},
|
||||
{name: "border-radius", type: "number", requiredBy: ["input", "button", "checkbox", "tabs"]},
|
||||
{name: "focus-highlight", type: "color", requiredBy: ["input", "button", "checkbox"]},
|
||||
{name: "tab-highlight", type: "color", requiredBy: ["tabs"]},
|
||||
{name: "tab-bar-height", type: "number", requiredBy: ["tabs"]},
|
||||
{name: "label", type: "color", requiredBy: ["input"]},
|
||||
{name: "label-active", type: "color", requiredBy: ["input"]},
|
||||
{name: "btn-primary-bg", type: "color", requiredBy: ["button"]},
|
||||
{name: "btn-primary-bg-hover", type: "color", requiredBy: ["button"]},
|
||||
{name: "btn-primary-bg-active", type: "color", requiredBy: ["button"]},
|
||||
{name: "btn-bg", type: "color", requiredBy: ["button"]},
|
||||
{name: "btn-bg-hover", type: "color", requiredBy: ["button"]},
|
||||
{name: "btn-bg-active", type: "color", requiredBy: ["button"]},
|
||||
{name: "text-color", type: "color", requiredBy: ["input", "button"]},
|
||||
{name: "bg", type: "color", requiredBy: ["input", "checkbox", "toggle"]},
|
||||
{name: "site-bg", type: "color"}
|
||||
];
|
||||
|
||||
export const components: Component[] = [
|
||||
{name: "Button", id: "button"},
|
||||
{name: "Input", id: "input"},
|
||||
{name: "Checkbox & radio", id: "checkbox"},
|
||||
{name: "Tabs", id: "tabs"},
|
||||
{name: "Toggle", id: "toggle"},
|
||||
];
|
||||
|
||||
export interface Variable {
|
||||
name: string,
|
||||
type: VariableType
|
||||
type: VariableType,
|
||||
requiredBy?: ComponentID[]
|
||||
}
|
||||
|
||||
export type ComponentID = "button" | "input" | "checkbox" | "tabs" | "toggle";
|
||||
|
||||
export interface Component {
|
||||
name: string,
|
||||
id: ComponentID
|
||||
}
|
||||
|
||||
export type VariableType = "color" | "number";
|
||||
|
@ -32,7 +48,13 @@ export function getDefaultVariables() {
|
|||
return {
|
||||
"border-width": "2px",
|
||||
"border-radius": ".375rem",
|
||||
"tab-bar-height": "3px",
|
||||
"tab-bar-height": "2px",
|
||||
...Preset.getRandom().getVariables()
|
||||
};
|
||||
}
|
||||
|
||||
export function getDefaultComponents() {
|
||||
let res: ComboObject = {};
|
||||
components.forEach(c => res[c.id] = true);
|
||||
return res;
|
||||
}
|
Loading…
Reference in a new issue