Created simple web app, added component selection & CSS download
This commit is contained in:
parent
43d4f32049
commit
ae9738d084
10 changed files with 240 additions and 89 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1 +1,4 @@
|
|||
node_modules
|
||||
dist
|
||||
.vercel
|
||||
pnpm-lock.yaml
|
||||
|
|
57
index.html
57
index.html
|
@ -8,7 +8,7 @@
|
|||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="content">
|
||||
<div class="content" id="app">
|
||||
|
||||
<header>
|
||||
<h1>
|
||||
|
@ -16,22 +16,61 @@
|
|||
</h1>
|
||||
<p>Simple framework with CSS-only UI components</p>
|
||||
<div class="colorselector">
|
||||
<input type="color" value="#865e3c" title="Main color">
|
||||
<input type="color" value="#63452c" title="Text color">
|
||||
<input type="color" value="#f9f7f4" title="Background color">
|
||||
<div class="config" title="Settings">
|
||||
<input type="color" v-model="data.preset.main" @change="applyPreset(data.preset);" title="Main color">
|
||||
<input type="color" v-model="data.preset.text" @change="applyPreset(data.preset);" title="Text color">
|
||||
<input type="color" v-model="data.preset.bg" @change="applyPreset(data.preset);" title="Background color">
|
||||
<div class="config" title="Settings" @click="settings = !settings;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M13.875 22h-3.75q-.375 0-.65-.25t-.325-.625l-.3-2.325q-.325-.125-.613-.3t-.562-.375l-2.175.9q-.35.125-.7.025t-.55-.425L2.4 15.4q-.2-.325-.125-.7t.375-.6l1.875-1.425Q4.5 12.5 4.5 12.337v-.674q0-.163.025-.338L2.65 9.9q-.3-.225-.375-.6t.125-.7l1.85-3.225q.175-.35.537-.438t.713.038l2.175.9q.275-.2.575-.375t.6-.3l.3-2.325q.05-.375.325-.625t.65-.25h3.75q.375 0 .65.25t.325.625l.3 2.325q.325.125.613.3t.562.375l2.175-.9q.35-.125.7-.025t.55.425L21.6 8.6q.2.325.125.7t-.375.6l-1.875 1.425q.025.175.025.338v.674q0 .163-.05.338l1.875 1.425q.3.225.375.6t-.125.7l-1.85 3.2q-.2.325-.563.438t-.712-.013l-2.125-.9q-.275.2-.575.375t-.6.3l-.3 2.325q-.05.375-.325.625t-.65.25Zm-1.825-6.5q1.45 0 2.475-1.025T15.55 12q0-1.45-1.025-2.475T12.05 8.5q-1.475 0-2.488 1.025T8.55 12q0 1.45 1.012 2.475T12.05 15.5Z"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<aside class="settings">
|
||||
<aside class="settings" :class="{ open: settings }">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="close" viewBox="0 0 24 24"><path fill="currentColor" d="M6.4 19L5 17.6l5.6-5.6L5 6.4L6.4 5l5.6 5.6L17.6 5L19 6.4L13.4 12l5.6 5.6l-1.4 1.4l-5.6-5.6L6.4 19Z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" @click="settings = false;" class="close" viewBox="0 0 24 24"><path fill="currentColor" d="M6.4 19L5 17.6l5.6-5.6L5 6.4L6.4 5l5.6 5.6L17.6 5L19 6.4L13.4 12l5.6 5.6l-1.4 1.4l-5.6-5.6L6.4 19Z"/></svg>
|
||||
|
||||
<b>Presets</b>
|
||||
|
||||
<div class="presets"></div>
|
||||
<div class="presets">
|
||||
<div v-for="p in data.presets" @click="data.preset = p; applyPreset(p);">
|
||||
<div :style="`background: ${p.main};`"></div>
|
||||
<div :style="`background: ${p.text};`"></div>
|
||||
<div :style="`background: ${p.bg};`"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<b>Component selection</b>
|
||||
|
||||
<form class="form" @submit.prevent="generateCSS">
|
||||
|
||||
<label class="cbox" v-for="p in data.parts">
|
||||
<input type="checkbox" v-model="p.enabled">
|
||||
<span>{{ p.name }}</span>
|
||||
</label>
|
||||
|
||||
<div class="btn-row">
|
||||
<button class="btn btn-primary" type="submit">
|
||||
Generate CSS
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<template v-if="data.results.length">
|
||||
|
||||
<b>CSS download</b>
|
||||
|
||||
<div class="btn-row">
|
||||
<a v-for="r in data.results" class="btn btn-primary btn-download" :download="r.name" :href="'data:text/plain;charset=utf-8,' + encodeURIComponent(r.css)">
|
||||
{{ r.name }}
|
||||
<small>
|
||||
{{ kbSize(r.size_gzip) }} (gzip) · {{ kbSize(r.size) }}
|
||||
</small>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
</aside>
|
||||
|
||||
|
@ -103,6 +142,6 @@
|
|||
</div>
|
||||
|
||||
</div>
|
||||
<script src="web/demo.js"></script>
|
||||
<script type="module" src="web/app.ts"></script>
|
||||
</body>
|
||||
</html>
|
21
package.json
Normal file
21
package.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "synergy",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"keywords": [],
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"petite-vue": "^0.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.4.5"
|
||||
}
|
||||
}
|
|
@ -17,6 +17,7 @@
|
|||
cursor: pointer;
|
||||
outline: none;
|
||||
transition: box-shadow .2s;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
|
|
10
tsconfig.json
Normal file
10
tsconfig.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2016",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
5
vite.config.ts
Normal file
5
vite.config.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
publicDir: "src/"
|
||||
});
|
49
web/app.ts
Normal file
49
web/app.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
import { Exporter, Preset, Theme } from "./lib";
|
||||
import { createApp, reactive } from "petite-vue";
|
||||
|
||||
export function applyPreset(p: Preset) {
|
||||
let theme = new Theme(p);
|
||||
theme.apply();
|
||||
let html = document.querySelector<HTMLElement>("html");
|
||||
if(html) html.style.background = p.siteBg ?? "";
|
||||
return theme;
|
||||
}
|
||||
|
||||
export let presets: Preset[] = [
|
||||
// {main: "#2ebdf5", text: "#ffffff", bg: "#040813"},
|
||||
// {main: "#f5b62e", text: "#ffffff", bg: "#040813"},
|
||||
// {main: "#FF6565", text: "#ffffff", bg: "#0f0413"},
|
||||
{main: "#9b8fe4", text: "#cfcef4", bg: "#090818", siteBg: "#100E22"},
|
||||
{main: "#337e2c", text: "#031601", bg: "#f3f7f2"},
|
||||
{main: "#1c71d8", text: "#030e1c", bg: "#ffffff"},
|
||||
{main: "#9141ac", text: "#613583", bg: "#f6edf7"},
|
||||
{main: "#a51d2d", text: "#3d3846", bg: "#f1e9e8"},
|
||||
{main: "#865e3c", text: "#63452c", bg: "#f9f7f4", siteBg: "#ffffff"}
|
||||
];
|
||||
|
||||
let pr = presets[Math.floor(Math.random() * presets.length)];
|
||||
|
||||
let data = reactive({
|
||||
theme: applyPreset(pr),
|
||||
preset: pr,
|
||||
parts: Exporter.parts,
|
||||
presets,
|
||||
results: []
|
||||
});
|
||||
|
||||
applyPreset(pr);
|
||||
|
||||
createApp({
|
||||
applyPreset(p: Preset) {
|
||||
data.results = [];
|
||||
applyPreset(p);
|
||||
},
|
||||
async generateCSS() {
|
||||
data.results = await Exporter.get(data.theme);
|
||||
},
|
||||
kbSize(value: number) {
|
||||
return `${Math.round(value/1024*100)/100} kB`;
|
||||
},
|
||||
data,
|
||||
settings: false
|
||||
}).mount("#app");
|
1
web/declaration.d.ts
vendored
Normal file
1
web/declaration.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
declare module '*.css';
|
|
@ -9,7 +9,8 @@ html, body {color: var(--synergy-text-color); font-family: Cantarell, ui-sans-se
|
|||
@media screen and (max-width: 700px) {
|
||||
.grid {grid-template-columns: 1fr; max-width: 500px;}
|
||||
}
|
||||
.form {display: flex; flex-direction: column; gap: 20px;}
|
||||
.form {display: flex; flex-direction: column; gap: 15px;}
|
||||
.form > * {margin: 0;}
|
||||
|
||||
header {margin-bottom: 70px; text-align: center;}
|
||||
header > * {margin: 30px 0;}
|
||||
|
@ -22,11 +23,15 @@ header h1 .color {background-clip: text; -webkit-background-clip: text; backgrou
|
|||
.colorselector div {display: flex; align-items: center; justify-content: center; box-shadow: 0 1px 3px #0004, inset 0 0 0 2px var(--synergy-border);}
|
||||
.colorselector svg {width: 30px;}
|
||||
|
||||
.settings {position: fixed; right: -600px; top: 0; width: 90%; max-width: 400px; height: 100%; box-shadow: 0 0 0 5px var(--synergy-border-active); border-radius: 40px 0 0 40px; padding: 40px; transition: all .3s; display: flex; flex-direction: column; gap: 15px; z-index: 100; background-color: var(--synergy-bg);}
|
||||
.settings {position: fixed; right: -600px; top: 0; width: 90%; max-width: 400px; height: 100%; box-shadow: 0 0 0 5px var(--synergy-border-active); border-radius: 40px 0 0 40px; padding: 40px; transition: all .3s; display: flex; flex-direction: column; gap: 20px; z-index: 100; background-color: var(--synergy-bg); overflow-y: auto;}
|
||||
.settings.open {right: 0;}
|
||||
|
||||
.settings b {margin-top: 20px;}
|
||||
|
||||
.presets {display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 15px;}
|
||||
.presets > * {height: 50px; border-radius: var(--synergy-border-radius); box-shadow: 0 1px 3px #0004; display: flex; overflow: hidden; cursor: pointer;}
|
||||
.presets > * > * {flex: 1;}
|
||||
|
||||
.settings .close {width: 64px; height: 64px; cursor: pointer; padding: 20px; position: absolute; right: 0; top: 0; z-index: 10;}
|
||||
|
||||
.btn.btn-download small {font-weight: 400; font-size: 12px; display: block;}
|
|
@ -1,78 +1,94 @@
|
|||
import { reactive } from "petite-vue";
|
||||
|
||||
var style = document.createElement("style");
|
||||
|
||||
addEventListener("load", () => {
|
||||
|
||||
let cs = document.querySelectorAll(".colorselector > *");
|
||||
cs.forEach(c => c.addEventListener("change", updateColors));
|
||||
document.querySelector("head").appendChild(style);
|
||||
updateColors();
|
||||
showPresets();
|
||||
const p = presets[Math.floor(Math.random() * presets.length)];
|
||||
applyPreset(p);
|
||||
|
||||
document.querySelector(".colorselector .config").addEventListener("click", toggleSettings);
|
||||
document.querySelector(".settings .close").addEventListener("click", toggleSettings);
|
||||
document.querySelector("head")!.appendChild(style);
|
||||
|
||||
});
|
||||
|
||||
function updateColors() {
|
||||
let cs = document.querySelectorAll(".colorselector > *");
|
||||
let p = {main: cs[0].value, text: cs[1].value, bg: cs[2].value};
|
||||
console.log(p);
|
||||
let t = new Theme(p);
|
||||
t.apply();
|
||||
export namespace Exporter {
|
||||
|
||||
export let parts = reactive([
|
||||
{name: "Buttons", file: "button", enabled: true},
|
||||
{name: "Fields", file: "input", enabled: true},
|
||||
{name: "Toggles", file: "toggle", enabled: true},
|
||||
{name: "Checkboxes and radios", file: "checkbox", enabled: true},
|
||||
]);
|
||||
|
||||
interface Result {
|
||||
name: string,
|
||||
css: string,
|
||||
size: number,
|
||||
size_gzip: number
|
||||
}
|
||||
|
||||
function toggleSettings() {
|
||||
document.querySelector(".settings").classList.toggle("open");
|
||||
export async function get(theme: Theme) {
|
||||
|
||||
let cssParts = [theme.generate()];
|
||||
for(let p of parts) {
|
||||
if(p.enabled) {
|
||||
let value = await (await fetch(`./${p.file}.css`)).text();
|
||||
value = `/* ${p.name} */\n\n${value}`;
|
||||
cssParts.push(value);
|
||||
}
|
||||
}
|
||||
let css = cssParts.join("\n\n/* ------------------- */\n\n");
|
||||
|
||||
let results: Result[] = [];
|
||||
await addResult(results, "synergy.min.css", minify(css));
|
||||
await addResult(results, "synergy.css", css);
|
||||
|
||||
return results;
|
||||
|
||||
}
|
||||
|
||||
let presets = [
|
||||
{main: "#2ebdf5", text: "#ffffff", bg: "#040813"},
|
||||
{main: "#f5b62e", text: "#ffffff", bg: "#040813"},
|
||||
{main: "#FF6565", text: "#ffffff", bg: "#0f0413"},
|
||||
{main: "#9b8fe4", text: "#cfcef4", bg: "#090818", siteBg: "#100E22"},
|
||||
{main: "#337e2c", text: "#031601", bg: "#f3f7f2"},
|
||||
{main: "#1c71d8", text: "#030e1c", bg: "#ffffff"},
|
||||
{main: "#9141ac", text: "#613583", bg: "#f6edf7"},
|
||||
{main: "#a51d2d", text: "#3d3846", bg: "#f1e9e8"},
|
||||
{main: "#865e3c", text: "#63452c", bg: "#f9f7f4", siteBg: "#ffffff"}
|
||||
];
|
||||
|
||||
function showPresets() {
|
||||
let presetsEl = document.querySelector(".presets");
|
||||
presets.forEach(p => {
|
||||
let el = document.createElement("div");
|
||||
el.innerHTML = `
|
||||
<div style="background-color: ${p.main};"></div>
|
||||
<div style="background-color: ${p.text};"></div>
|
||||
<div style="background-color: ${p.bg};"></div>`;
|
||||
el.addEventListener("click", () => {
|
||||
applyPreset(p);
|
||||
});
|
||||
presetsEl.appendChild(el);
|
||||
async function addResult(results: Result[], name: string, css: string) {
|
||||
results.push({
|
||||
name,
|
||||
css,
|
||||
size: getSize(css),
|
||||
size_gzip: await getCompressedSize(css)
|
||||
});
|
||||
}
|
||||
|
||||
function applyPreset(p) {
|
||||
let cs = document.querySelectorAll(".colorselector > *");
|
||||
cs[0].value = p.main;
|
||||
cs[1].value = p.text;
|
||||
cs[2].value = p.bg;
|
||||
updateColors();
|
||||
let html = document.querySelector("html");
|
||||
html.style = "";
|
||||
if(p.siteBg) html.style.background = p.siteBg;
|
||||
async function 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;
|
||||
}
|
||||
|
||||
class Color {
|
||||
function getSize(content: string) {
|
||||
return (new TextEncoder().encode(content)).length
|
||||
}
|
||||
|
||||
r;
|
||||
g;
|
||||
b;
|
||||
a;
|
||||
function 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, "");
|
||||
}
|
||||
|
||||
constructor(r, g, b, a = 1) {
|
||||
}
|
||||
|
||||
export interface Preset {
|
||||
main: string,
|
||||
text: string,
|
||||
bg: string,
|
||||
siteBg?: string
|
||||
}
|
||||
|
||||
export class Color {
|
||||
|
||||
r: number;
|
||||
g: number;
|
||||
b: number;
|
||||
a: number;
|
||||
|
||||
constructor(r: number, g: number, b: number, a: number = 1) {
|
||||
this.r = r;
|
||||
this.g = g;
|
||||
this.b = b;
|
||||
|
@ -83,11 +99,12 @@ class Color {
|
|||
return new Color(this.r, this.g, this.b, this.a);
|
||||
}
|
||||
|
||||
static fromHex(hex) {
|
||||
return new Color(...this.hexToRgb(hex));
|
||||
static fromHex(hex: string) {
|
||||
let [r, g, b] = this.hexToRgb(hex);
|
||||
return new Color(r, g, b);
|
||||
}
|
||||
|
||||
static hexToRgb(hex) {
|
||||
static hexToRgb(hex: string) {
|
||||
hex = hex.replace(/^#/, '');
|
||||
const r = parseInt(hex.slice(0, 2), 16) / 255;
|
||||
const g = parseInt(hex.slice(2, 4), 16) / 255;
|
||||
|
@ -99,8 +116,8 @@ class Color {
|
|||
return `rgba(${this.r*255}, ${this.g*255}, ${this.b*255}, ${this.a})`;
|
||||
}
|
||||
|
||||
contrast(otherColor) {
|
||||
const getRelativeLuminance = (rgb) => {
|
||||
contrast(otherColor: Color) {
|
||||
const getRelativeLuminance = (rgb: number) => {
|
||||
const sRGB = rgb / 255;
|
||||
return sRGB <= 0.03928 ? sRGB / 12.92 : Math.pow((sRGB + 0.055) / 1.055, 2.4);
|
||||
};
|
||||
|
@ -114,19 +131,19 @@ class Color {
|
|||
return (contrastRatio*100)-100;
|
||||
}
|
||||
|
||||
equals(otherColor) {
|
||||
equals(otherColor: Color) {
|
||||
return this.r == otherColor.r && this.g == otherColor.g && this.b == otherColor.b && this.a == otherColor.a;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Theme {
|
||||
export class Theme {
|
||||
|
||||
main;
|
||||
text;
|
||||
bg;
|
||||
main: Color;
|
||||
text: Color;
|
||||
bg: Color;
|
||||
|
||||
constructor(opt) {
|
||||
constructor(opt: Preset) {
|
||||
|
||||
this.main = Color.fromHex(opt.main);
|
||||
this.text = Color.fromHex(opt.text);
|
||||
|
@ -165,16 +182,16 @@ class Theme {
|
|||
if(this.main.contrast(this.bg) < .3) alert("Contrast between main color and the background is low!");
|
||||
if(this.bg.contrast(this.text) < .3) alert("Contrast between text color and the background is low!");
|
||||
|
||||
let styles = [`:root {${variables.join("")}}`];
|
||||
let styles = [`:root {\n${variables.join("\n")}\n}`];
|
||||
|
||||
let btnColor = this.getBtnColor(this.main, this.text);
|
||||
if(btnColor != this.text) styles.push(`.btn.btn-primary {${this.var("text-color", btnColor.rgbFormat())}}`);
|
||||
if(btnColor != this.text) styles.push(`.btn.btn-primary {\n${this.var("text-color", btnColor.rgbFormat())}\n}`);
|
||||
|
||||
return styles.join("\n");
|
||||
return styles.join("\n\n");
|
||||
|
||||
}
|
||||
|
||||
getBtnColor(main, text) {
|
||||
getBtnColor(main: Color, text: Color) {
|
||||
let white = new Color(1, 1, 1);
|
||||
let black = new Color(0, 0, 0);
|
||||
let cText = main.contrast(text);
|
||||
|
@ -182,14 +199,14 @@ class Theme {
|
|||
return cWhite > .3 ? white : cText > .3 ? text : black;
|
||||
}
|
||||
|
||||
cArgb(color, alpha = 1) {
|
||||
cArgb(color: Color, alpha: number = 1) {
|
||||
let c = color.clone();
|
||||
c.a = alpha;
|
||||
return c.rgbFormat();
|
||||
}
|
||||
|
||||
var(name, value) {
|
||||
return `--synergy-${name}: ${value};`;
|
||||
var(name: string, value: string) {
|
||||
return `\t--synergy-${name}: ${value};`;
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue