Added CSS export/download

This commit is contained in:
Filip Znachor 2024-05-13 12:57:46 +02:00
parent b830194366
commit bb95de0ab0
7 changed files with 238 additions and 41 deletions

View file

@ -3,7 +3,7 @@
import Settings from "./components/Settings.vue"; import Settings from "./components/Settings.vue";
</script> </script>
<template> <template>
<div> <div>
@ -116,4 +116,4 @@ import Settings from "./components/Settings.vue";
</div> </div>
</template>./ts/synergy </template>

View 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>

View file

@ -3,8 +3,9 @@
import { variables } from "../ts/Synergy"; import { variables } from "../ts/Synergy";
import { Icon } from "@iconify/vue"; import { Icon } from "@iconify/vue";
import { Preset } from "../ts/Preset"; import { Preset } from "../ts/Preset";
import { variableValues, applyVariables } from "../ts/Shared"; import { variableValues } from "../ts/Shared";
import { setVariables } from "../ts/Styles"; import { setVariables } from "../ts/Styles";
import Components from "./Components.vue";
const variableTypeIcon = { const variableTypeIcon = {
color: "ic:outline-color-lens", color: "ic:outline-color-lens",
@ -15,10 +16,17 @@ function update() {
setVariables(variableValues); setVariables(variableValues);
} }
function applyPreset(preset: Preset) {
const variables = preset.getVariables() as ComboObject;
Object.keys(variables).forEach(key => {
variableValues[key] = variables[key];
});
}
</script> </script>
<template> <template>
<div class="settings"> <div class="settings">
<div> <div>
@ -47,11 +55,12 @@ function update() {
<div> <div>
<h2>Presets</h2> <h2>Presets</h2>
<div class="presets"> <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 :style="`background-color: ${c?.hexFormat()}`" v-for="c in preset.colors"></div>
</div> </div>
</div> </div>
<Components />
</div> </div>
</div> </div>
</template>../ts/Synergy../ts/Styles../ts/Shared </template>

View file

@ -1,5 +1,5 @@
html {background: var(--synergy-site-bg);} 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;} *, *::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;} 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 h2 {text-align: center;}
.settings .variables {display: grid; gap: 10px 20px; align-items: center; grid-template-columns: max-content 1fr;} .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;} .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 > * {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 .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) { @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) { @media screen and (max-width: 500px) {
@ -41,4 +49,7 @@ header h1 .color {background-clip: text; -webkit-background-clip: text; backgrou
.inp.color {width: 40px;} .inp.color {width: 40px;}
.inp input[type=color] {padding: 0; border: 0;} .inp input[type=color] {padding: 0; border: 0;}
::-webkit-color-swatch, ::-moz-color-swatch {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
View 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
}

View file

@ -1,19 +1,13 @@
import { reactive, watch } from "vue"; import { reactive, watch } from "vue";
import { getDefaultVariables } from "./Synergy"; import { getDefaultComponents, getDefaultVariables } from "./Synergy";
import { setVariables } from "./Styles"; 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, () => { watch(() => variableValues, () => {
setVariables(variableValues); setVariables(variableValues);
}, {deep: true}); }, {deep: true});
setVariables(values); setVariables(variableValues);
export function applyVariables(variables: ComboObject) {
Object.keys(variables).forEach(key => {
variableValues[key] = variables[key];
});
}

View file

@ -1,38 +1,60 @@
import { Preset } from "./Preset"; import { Preset } from "./Preset";
export let variables: Variable[] = [ export const variables: Variable[] = [
{name: "border", type: "color"}, {name: "border", type: "color", requiredBy: ["input", "checkbox", "toggle", "tabs"]},
{name: "border-active", type: "color"}, {name: "border-active", type: "color", requiredBy: ["input", "checkbox", "toggle", "tabs"]},
{name: "border-width", type: "number"}, {name: "border-width", type: "number", requiredBy: ["input", "checkbox", "toggle"]},
{name: "border-radius", type: "number"}, {name: "border-radius", type: "number", requiredBy: ["input", "button", "checkbox", "tabs"]},
{name: "focus-highlight", type: "color"}, {name: "focus-highlight", type: "color", requiredBy: ["input", "button", "checkbox"]},
{name: "tab-highlight", type: "color"}, {name: "tab-highlight", type: "color", requiredBy: ["tabs"]},
{name: "tab-bar-height", type: "number"}, {name: "tab-bar-height", type: "number", requiredBy: ["tabs"]},
{name: "label", type: "color"}, {name: "label", type: "color", requiredBy: ["input"]},
{name: "label-active", type: "color"}, {name: "label-active", type: "color", requiredBy: ["input"]},
{name: "btn-primary-bg", type: "color"}, {name: "btn-primary-bg", type: "color", requiredBy: ["button"]},
{name: "btn-primary-bg-hover", type: "color"}, {name: "btn-primary-bg-hover", type: "color", requiredBy: ["button"]},
{name: "btn-primary-bg-active", type: "color"}, {name: "btn-primary-bg-active", type: "color", requiredBy: ["button"]},
{name: "btn-bg", type: "color"}, {name: "btn-bg", type: "color", requiredBy: ["button"]},
{name: "btn-bg-hover", type: "color"}, {name: "btn-bg-hover", type: "color", requiredBy: ["button"]},
{name: "btn-bg-active", type: "color"}, {name: "btn-bg-active", type: "color", requiredBy: ["button"]},
{name: "text-color", type: "color"}, {name: "text-color", type: "color", requiredBy: ["input", "button"]},
{name: "bg", type: "color"}, {name: "bg", type: "color", requiredBy: ["input", "checkbox", "toggle"]},
{name: "site-bg", type: "color"} {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 { export interface Variable {
name: string, 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"; export type VariableType = "color" | "number";
export function getDefaultVariables() { export function getDefaultVariables() {
return { return {
"border-width": "2px", "border-width": "2px",
"border-radius": ".375rem", "border-radius": ".375rem",
"tab-bar-height": "3px", "tab-bar-height": "2px",
...Preset.getRandom().getVariables() ...Preset.getRandom().getVariables()
}; };
} }
export function getDefaultComponents() {
let res: ComboObject = {};
components.forEach(c => res[c.id] = true);
return res;
}