From bb95de0ab090f2b48044c19fb9940653945ea93a Mon Sep 17 00:00:00 2001 From: Filip Znachor Date: Mon, 13 May 2024 12:57:46 +0200 Subject: [PATCH] Added CSS export/download --- web/App.vue | 4 +- web/components/Components.vue | 72 ++++++++++++++++++++++++++++ web/components/Settings.vue | 17 +++++-- web/css/demo.css | 19 ++++++-- web/ts/Export.ts | 89 +++++++++++++++++++++++++++++++++++ web/ts/Shared.ts | 14 ++---- web/ts/Synergy.ts | 64 ++++++++++++++++--------- 7 files changed, 238 insertions(+), 41 deletions(-) create mode 100644 web/components/Components.vue create mode 100644 web/ts/Export.ts diff --git a/web/App.vue b/web/App.vue index 44ba0c2..603a712 100644 --- a/web/App.vue +++ b/web/App.vue @@ -3,7 +3,7 @@ import Settings from "./components/Settings.vue"; - + ./ts/synergy \ No newline at end of file + diff --git a/web/components/Components.vue b/web/components/Components.vue new file mode 100644 index 0000000..891c763 --- /dev/null +++ b/web/components/Components.vue @@ -0,0 +1,72 @@ + + + diff --git a/web/components/Settings.vue b/web/components/Settings.vue index b1216db..709c3d6 100644 --- a/web/components/Settings.vue +++ b/web/components/Settings.vue @@ -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,10 +16,17 @@ function update() { setVariables(variableValues); } +function applyPreset(preset: Preset) { + const variables = preset.getVariables() as ComboObject; + Object.keys(variables).forEach(key => { + variableValues[key] = variables[key]; + }); +} + ../ts/Synergy../ts/Styles../ts/Shared \ No newline at end of file + diff --git a/web/css/demo.css b/web/css/demo.css index 0d5c86c..56b2163 100644 --- a/web/css/demo.css +++ b/web/css/demo.css @@ -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) { @@ -41,4 +49,7 @@ 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;} \ No newline at end of file +::-webkit-color-swatch, ::-moz-color-swatch {border: 0;} + +a {color: var(--synergy-btn-primary-bg);} +a:hover {color: var(--synergy-btn-primary-bg-hover);} \ No newline at end of file diff --git a/web/ts/Export.ts b/web/ts/Export.ts new file mode 100644 index 0000000..b851375 --- /dev/null +++ b/web/ts/Export.ts @@ -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 +} diff --git a/web/ts/Shared.ts b/web/ts/Shared.ts index fe37e25..f1132c6 100644 --- a/web/ts/Shared.ts +++ b/web/ts/Shared.ts @@ -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(getDefaultVariables()); -export let variableValues = reactive(values); +export let selectedComponents = reactive(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); \ No newline at end of file diff --git a/web/ts/Synergy.ts b/web/ts/Synergy.ts index ac6e21b..5891ed3 100644 --- a/web/ts/Synergy.ts +++ b/web/ts/Synergy.ts @@ -1,38 +1,60 @@ 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"; export function getDefaultVariables() { return { - "border-width": "2px", + "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; +} \ No newline at end of file