Compare commits

...

14 commits

29 changed files with 825 additions and 587 deletions

View file

@ -4,159 +4,11 @@
<title>Synergy UI</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="web/demo.css">
<link rel="stylesheet" href="web/css/demo.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="content" id="app">
<header>
<h1>
<span class="color">Synergy</span> UI
</h1>
<p>Simple framework with CSS-only UI components</p>
<form autocomplete="off" class="colorselector">
<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>
</form>
</header>
<aside class="settings" :class="{ open: settings }">
<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 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>
<div class="grid">
<form class="form">
<div class="inp">
<input type="text" placeholder=" ">
<label>Name</label>
</div>
<div class="inp">
<input type="text" placeholder=" ">
<label>Surname</label>
</div>
<div class="inp select">
<select>
<option disabled>Disabled</option>
<option>Guest</option>
<option>User</option>
<option>Administrator</option>
</select>
<label>Role</label>
</div>
<div class="inp">
<textarea placeholder=" ">Some very long text...</textarea>
<label>Long text</label>
</div>
<div class="toggle-text">
<label for="toggle1">Toggle me! I'm a toggle.</label>
<div class="toggle">
<input id="toggle1" type="checkbox">
<div class="indicator"></div>
</div>
</div>
<div class="cbox-row">
<label class="cbox">
<input type="radio" name="radios" checked>
<span>Radio 1</span>
</label>
<label class="cbox">
<input type="radio" name="radios">
<span>Radio 2</span>
</label>
</div>
<label class="cbox">
<input type="checkbox">
<span>Check me!</span>
</label>
<div class="tabs offset">
<label>
<input type="radio" name="tabs" checked>
<div>Home</div>
</label>
<label>
<input type="radio" name="tabs">
<div>Account</div>
</label>
<label>
<input type="radio" name="tabs">
<div>Settings</div>
</label>
</div>
<div class="btn-row">
<button class="btn btn-primary">
Send
</button>
<button type="reset" class="btn">
Reset
</button>
</div>
</form>
</div>
</div>
<script type="module" src="web/app.ts"></script>
<div id="app"></div>
<script type="module" src="web/ts/App.ts"></script>
</body>
</html>

View file

@ -1,21 +1,23 @@
{
"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"
}
"name": "synergy",
"version": "1.0.0",
"description": "",
"main": "index.js",
"keywords": [],
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"author": "",
"license": "ISC",
"dependencies": {
"@iconify/vue": "^4.1.2",
"vue": "^3.4.26"
},
"devDependencies": {
"typescript": "^5.0.2",
"@vitejs/plugin-vue": "^5.0.4",
"vite": "^4.4.5"
}
}

View file

@ -1,22 +0,0 @@
:root {
--synergy-border: #865e3c66;
--synergy-border-active: #865e3c;
--synergy-border-width: 2px;
--synergy-border-radius: .375rem;
--synergy-focus-highlight: #865e3c3f;
--synergy-tab-highlight: #865e3c19;
--synergy-label: #9d7d61;
--synergy-label-active: #865e3c;
--synergy-btn-primary-bg: #9d7d61;
--synergy-btn-primary-bg-active: #c0ab98;
--synergy-btn-primary-bg-hover: #865e3c;
--synergy-btn-bg: #e1dedb;
--synergy-btn-bg-active: #c8c5c2;
--synergy-btn-bg-hover: #d9d6d2;
--synergy-text-color: #63452c;
--synergy-bg: #f9f7f4;
}
.btn.btn-primary {
--synergy-text-color: #ffffff;
}

View file

@ -6,18 +6,16 @@
.btn {
background-color: var(--synergy-btn-bg);
padding: 9px .8rem;
padding: 9px .8em;
border: 0;
color: var(--synergy-text-color);
font: inherit;
font-weight: 700;
font-size: 1rem;
font-size: 1em;
line-height: 1.25;
border-radius: var(--synergy-border-radius);
transition: .2s background-color;
transition: transform .1s;
cursor: pointer;
outline: none;
transition: box-shadow .2s;
text-decoration: none;
}
@ -30,20 +28,21 @@
}
.btn:active {
background-color: var(--synergy-btn-bg-active);
transform: scale(.97);
}
.btn.btn-primary {
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, .1);
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, .2);
--synergy-btn-bg: var(--synergy-btn-primary-bg);
--synergy-btn-bg-hover: var(--synergy-btn-primary-bg-hover);
--synergy-btn-bg-active: var(--synergy-btn-primary-bg-active);
--synergy-text-color: var(--synergy-btn-primary-text-color);
}
.btn:focus-visible {
box-shadow: 0 0 0 5px var(--synergy-focus-highlight);
outline: 5px solid var(--synergy-btn-focus-highlight);
background-color: var(--synergy-btn-bg-hover);
}
.btn.btn-primary:focus-visible {
box-shadow: 0 0 0 5px var(--synergy-focus-highlight);
outline: 5px solid var(--synergy-focus-highlight);
}

View file

@ -25,12 +25,12 @@ label.cbox > input + * {
label.cbox > input + *::before {
content: "";
display: block;
width: 1rem;
height: 1rem;
width: 1em;
height: 1em;
border-radius: 50%;
aspect-ratio: 1 / 1;
border: var(--synergy-border-width) solid var(--synergy-border);
transition: background .1s, box-shadow .2s;
transition: background .1s, box-shadow .2s, border-color .1s;
background: var(--synergy-bg);
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, .1);
grid-area: box;
@ -38,10 +38,11 @@ label.cbox > input + *::before {
label.cbox > input:checked + *::before {
background: var(--synergy-border-active);
border-color: transparent;
}
label.cbox > input:focus-visible + *::before {
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, .1), 0 0 0 8px var(--synergy-focus-highlight);
outline: 5px solid var(--synergy-focus-highlight);
}
label.cbox > input[type=checkbox] + *::before {

32
src/fancy_input.css Normal file
View file

@ -0,0 +1,32 @@
/* Label */
.inp.inp-fancy label {
position: absolute;
top: 7px;
left: 5px;
font-size: 1em;
font-weight: 700;
transform-origin: 0 0;
transform: translate3d(0, 0, 0);
transition: all .2s ease;
pointer-events: none;
background: var(--synergy-bg);
padding: 3px 8px;
border-radius: 10px;
line-height: 1.2em;
color: var(--synergy-label);
margin: var(--synergy-border-width);
}
.inp.inp-fancy :is(
:is(input, textarea, select):not(:placeholder-shown) + label,
:is(input, textarea):not(:-ms-input-placeholder) + label,
:is(input, textarea):not(:-moz-placeholder-shown) + label
),
.inp.inp-fancy :is(input, textarea):focus + label {
color: var(--synergy-label-active);
transform: scale(.8);
top: -.7em;
}

View file

@ -3,45 +3,14 @@
width: 100%;
}
/* Label */
.inp label {
position: absolute;
top: 7px;
left: 5px;
font-size: 1rem;
font-weight: 700;
transform-origin: 0 0;
transform: translate3d(0, 0, 0);
transition: all .2s ease;
pointer-events: none;
background: var(--synergy-bg);
padding: 3px 8px;
border-radius: 10px;
line-height: 1.2rem;
color: var(--synergy-label);
margin: var(--synergy-border-width);
}
.inp :is(
:is(input, textarea, select):not(:placeholder-shown) + label,
:is(input, textarea):not(:-ms-input-placeholder) + label,
:is(input, textarea):not(:-moz-placeholder-shown) + label
),
.inp :is(input, textarea):focus + label {
color: var(--synergy-label-active);
transform: scale(.8);
top: -.7rem;
}
/* Common styles */
.inp :is(input, textarea, select) {
.inp :is(input, textarea, select), .inp:is(input, textarea, select) {
display: block;
width: 100%;
font-family: inherit;
padding: 10px 12px;
font-size: 1rem;
font-size: 1em;
font-weight: 500;
color: var(--synergy-text-color);
transition: border-color 0.15s ease;
@ -52,7 +21,7 @@
margin: 0px;
}
.inp :is(input, textarea, select):focus {
:is(.inp :is(input, textarea, select), .inp:is(input, textarea, select)):focus {
outline: none;
border-color: var(--synergy-border-active);
}
@ -64,23 +33,23 @@
padding-right: 40px;
}
.inp.select::after {
.inp.select::before {
content: "";
border: 2px solid var(--synergy-text-color);
border: 1.5px solid var(--synergy-text-color);
border-top: 0;
border-left: 0;
display: block;
position: absolute;
top: 50%;
margin-top: -8.7px;
top: var(--synergy-border-width);
margin-top: .74em;
right: 15px;
padding: 5px;
padding: 4.5px;
transform: rotate(45deg);
pointer-events: none;
}
.inp textarea {
.inp textarea, textarea.inp {
height: 100px;
max-width: 100%;
resize: vertical;
}
}

View file

@ -1,13 +1,12 @@
.tabs {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.tabs input {
position: absolute;
opacity: 0;
pointer-events: none;
visibility: none;
}
.tabs label {
@ -16,7 +15,7 @@
.tabs div {
position: relative;
padding: 10px 0;
padding: 10px 10px;
font-weight: 700;
transition: color .2s;
}
@ -24,11 +23,11 @@
.tabs div::after {
content: "";
position: absolute;
width: 70%;
height: var(--synergy-border-width);
width: 60%;
height: var(--synergy-tab-bar-height);
background-color: var(--synergy-border-active);
bottom: 0;
left: 15%;
bottom: 0;
left: 20%;
transition: all .2s;
opacity: 0;
transform: scaleX(0);
@ -40,8 +39,8 @@
content: "";
position: absolute;
top: 13%;
left: -8px;
width: calc(100% + 16px);
left: 0;
width: 100%;
height: 75%;
opacity: 0;
border-radius: var(--synergy-border-radius);
@ -62,6 +61,24 @@
transform: none;
}
.tabs.offset {
margin: 0 8px;
.tabs.tabs-no-padding {
margin: 0 -10px;
}
.tabs.tabs-full div {
transition: background-color .2s;
}
.tabs.tabs-full div::after {
left: 0;
width: 100%;
border-radius: 0;
}
.tabs.tabs-full div::before {
display: none;
}
.tabs.tabs-full :is(input:focus-visible + div, div:hover) {
background-color: var(--synergy-tab-highlight);
}

View file

@ -5,8 +5,8 @@
border-radius: 100px;
padding: 3px;
display: flex;
max-width: 2.6rem;
height: 1.5rem;
max-width: 2.6em;
height: 1.5em;
width: 100%;
position: relative;
}
@ -30,7 +30,7 @@
}
.toggle .indicator::after {
transition: all .2s;
transition: background-color .2s;
margin-left: auto;
display: inline-block;
height: 100%;
@ -48,8 +48,8 @@
background: var(--synergy-border-active);
}
.toggle input:focus-visible + .indicator::after {
box-shadow: 0 0 10px 0 var(--synergy-bg), 0 0 0 10px var(--synergy-focus-highlight);
.toggle input:focus-visible + .indicator:after {
outline: 10px solid var(--synergy-focus-highlight);
}
@ -59,7 +59,7 @@
display: grid;
align-items: center;
gap: 20px;
grid-template-columns: 1fr 2.6rem;
grid-template-columns: 1fr 2.6em;
}
.toggle-text label {

View file

@ -1,4 +1,3 @@
@import url("src/_config.css");
@import url("src/button.css");
@import url("src/input.css");
@import url("src/toggle.css");

View file

@ -1,5 +1,9 @@
import vuePlugin from '@vitejs/plugin-vue';
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
vuePlugin()
],
publicDir: "src/"
});

29
web/App.vue Normal file
View file

@ -0,0 +1,29 @@
<script setup lang="ts">
import Header from "./components/Header.vue";
import Settings from "./components/Settings.vue";
import Preview from "./components/Preview.vue";
</script>
<template>
<div>
<Header />
<section class="sec-bg" id="settings">
<div class="inner">
<Settings />
</div>
</section>
<section>
<div class="inner" id="preview">
<Preview />
</div>
</section>
</div>
</template>

View file

@ -1,49 +0,0 @@
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 = [];
data.theme = 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");

View file

@ -0,0 +1,71 @@
<script lang="ts" setup>
import { reactive } from 'vue';
import { Export, Result } from "../ts/Export";
import { variableValues, components } 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, components);
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="component.selected">
<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) }} (gzip) · {{ kbSize(r.size) }}</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>

21
web/components/Header.vue Normal file
View file

@ -0,0 +1,21 @@
<script setup lang="ts">
import Logo from "./Logo.vue";
</script>
<template>
<header>
<div class="inner">
<div class="grid">
<Logo />
<h1>Simple framework with CSS-only <span class="color">UI components</span></h1>
<p>Modern ready-to-use components made only with CSS.</p>
<div class="btn-row" style="margin: 30px 0;">
<a href="#settings" class="btn btn-primary">Get started</a>
<a href="#preview" class="btn">Preview</a>
</div>
</div>
</div>
</header>
</template>

12
web/components/Logo.vue Normal file
View file

@ -0,0 +1,12 @@
<template>
<svg class="logo" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" viewBox="0 0 74.08 18.52">
<defs>
<linearGradient id="a">
<stop offset="0" style="stop-color:var(--synergy-border);stop-opacity:1"/>
<stop offset="1" style="stop-color:var(--synergy-border-active);stop-opacity:1"/>
</linearGradient>
<linearGradient xlink:href="#a" id="b" x1="48.66" x2="46.25" y1="1.83" y2="22.35" gradientTransform="translate(-.05 .57)" gradientUnits="userSpaceOnUse"/>
</defs>
<path d="M13.03 5.65c-3.06.08-6.19 1.7-7.72 5.95 3.22-3.41 6.6-6.18 11.34-3.53-2.83.58-4.62 2.51-5.67 4.58-2.94 5.75-6.72 2.05-8.41-.3 1.76 7.13 7.4 5.82 9.62 1.32 1.47-2.97 4.5-5.9 8.75-3.04-.15-2.65-3.97-5.08-7.9-4.98Zm20.01 3.91c-2.16 0-3.6 1.11-3.6 2.8 0 3.1 4.74 2.09 4.74 3.93 0 .65-.57 1.07-1.7 1.07-.82 0-1.82-.2-2.7-.57l-.49 1.36c.97.43 2.07.7 3.12.7 2.21 0 3.54-1.08 3.54-2.78 0-3.26-4.77-2.13-4.77-3.95 0-.7.64-1.08 1.76-1.08.76 0 1.58.14 2.22.39l.48-1.35a6.58 6.58 0 0 0-2.6-.52Zm-13.46 1.59c-1.9 0-3.87 1.45-4.99 3.7-1.47 2.97-4.48 5.9-8.74 3.04.26 4.7 12.13 8.75 15.62-.97-3.22 3.41-6.6 6.18-11.33 3.53 2.83-.58 4.61-2.51 5.67-4.58 2.94-5.75 6.72-2.05 8.41.3-.88-3.56-2.73-5.02-4.64-5.02zm34.47 1.11c-1.89 0-3.15 1.36-3.15 3.32 0 2.04 1.32 3.27 3.48 3.27.75 0 1.55-.14 2.24-.43l-.36-1.16c-.51.21-1.07.33-1.6.33-1.15 0-1.84-.53-2.04-1.56h4.1c.04-.26.07-.66.07-.96 0-1.72-1.06-2.8-2.74-2.8zm11.97 0c-1.74 0-2.94 1.33-2.94 3.35 0 1.97 1.12 3.24 2.73 3.24.7 0 1.29-.24 1.75-.67v.52c0 1.1-.6 1.67-1.73 1.67a4.8 4.8 0 0 1-1.75-.38l-.33 1.25c.68.32 1.37.48 2.09.48 2.02 0 3.35-1.24 3.35-3.09V12.4H68l-.2.58a2.35 2.35 0 0 0-1.79-.7zm-18.73.03a3 3 0 0 0-2.04.8l-.29-.7h-1.18v6.33h1.65v-4.5a2 2 0 0 1 1.29-.49c.8 0 1.23.46 1.23 1.32v3.67h1.66v-3.9c0-1.57-.88-2.53-2.32-2.53zm14.33.05c-.76 0-1.46.37-1.94 1l-.16-.95h-1.31v6.33h1.65v-4.1c.38-.46.92-.72 1.5-.72.27 0 .57.05.8.13l.4-1.52c-.28-.1-.6-.17-.94-.17zm-25.3.05 2.45 6.33-1.14 2.83h1.78l1.02-2.83 2.44-6.33H41.2l-1.58 4.82-1.48-4.82Zm33.78 0 2.46 6.33-1.15 2.83h1.79l1.01-2.83 2.44-6.33H75l-1.58 4.82-1.49-4.82zm-16.16 1.16c.82 0 1.29.52 1.29 1.46h-2.66c.1-.96.57-1.46 1.37-1.46zm12.32.12c.55 0 1 .23 1.3.66v2.58c-.29.35-.71.55-1.23.55-.96 0-1.57-.76-1.57-1.96 0-1.12.56-1.83 1.5-1.83z" style="opacity:1;fill:url(#b);stroke-width:.342417" transform="translate(-2.57 -4.99)"/>
</svg>
</template>

View file

@ -0,0 +1,81 @@
<template>
<div class="preview">
<form class="form" @submit.prevent>
<div class="inp inp-fancy">
<input type="text" placeholder=" ">
<label>Name</label>
</div>
<div class="inp inp-fancy">
<input type="text" placeholder=" ">
<label>Surname</label>
</div>
<div class="inp inp-fancy select">
<select>
<option disabled>Disabled</option>
<option>Guest</option>
<option>User</option>
<option>Administrator</option>
</select>
<label>Role</label>
</div>
<div class="inp inp-fancy">
<textarea placeholder=" ">Some very long text...</textarea>
<label>Long text</label>
</div>
<div class="toggle-text">
<label for="toggle1">Toggle me! I'm a toggle.</label>
<div class="toggle">
<input id="toggle1" type="checkbox">
<div class="indicator"></div>
</div>
</div>
<div class="cbox-row">
<label class="cbox">
<input type="radio" name="radios" checked>
<span>Radio 1</span>
</label>
<label class="cbox">
<input type="radio" name="radios">
<span>Radio 2</span>
</label>
</div>
<label class="cbox">
<input type="checkbox">
<span>Check me!</span>
</label>
<div class="tabs">
<label>
<input type="radio" name="tabs" checked>
<div>Home</div>
</label>
<label>
<input type="radio" name="tabs">
<div>Account</div>
</label>
<label>
<input type="radio" name="tabs">
<div>Settings</div>
</label>
</div>
<div class="btn-row">
<button class="btn btn-primary">
Send
</button>
<button type="reset" class="btn">
Reset
</button>
</div>
</form>
</div>
</template>

View file

@ -0,0 +1,64 @@
<script setup lang="ts">
import { variables } from "../ts/Synergy";
import { Icon } from "@iconify/vue";
import { Preset } from "../ts/Preset";
import { variableValues } from "../ts/Shared";
import { setVariables } from "../ts/Styles";
import Components from "./Components.vue";
const variableTypeIcon = {
color: "ic:outline-color-lens",
number: "mdi:numeric"
};
function update() {
setVariables(variableValues);
}
function applyPreset(preset: Preset) {
const variables = preset.getColorVariables() as ComboObject;
Object.keys(variables).forEach(key => {
variableValues[key] = variables[key];
});
}
</script>
<template>
<div class="settings">
<div>
<h2>Variables</h2>
<div class="variables">
<template v-for="v in variables">
<label :for="v.name">
<Icon :icon="variableTypeIcon[v.type]" />
{{ v.name }}
</label>
<div v-if="v.type == 'color'">
<div class="colori" :style="{ background: variableValues[v.name] }" />
<div class="inp inp-small">
<input v-model="variableValues[v.name]" type="string" :id="v.name" @change="update">
</div>
</div>
<div v-else class="inp inp-small">
<input v-model="variableValues[v.name]" type="string" :id="v.name" @change="update">
</div>
</template>
</div>
</div>
<div>
<h2>Presets</h2>
<div class="presets">
<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>

63
web/css/demo.css Normal file
View file

@ -0,0 +1,63 @@
:root {
--site-padding: 40px;
}
@media screen and (max-width: 700px) {
:root {
--site-padding: 20px;
}
}
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; margin: 0;}
*, *::before, *::after {box-sizing: border-box;}
:is(header, section, footer) > .inner {margin: 70px auto; padding: 0 var(--site-padding); max-width: 1000px;}
.logo {max-width: 200px;}
header > * {margin: 15px 0;}
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 .grid {max-width: 450px;}
.sec-bg {background-color: var(--synergy-tab-highlight); padding: .1px; border: var(--synergy-border-width) solid var(--synergy-border); border-right-width: 0; border-left-width: 0;}
.settings {display: grid; grid-template-columns: 1fr 1fr; gap: 40px; align-items: start;}
.settings > * {position: sticky; top: 0;}
.settings h2 {text-align: center;}
.settings > * > :first-child {margin-top: 0;}
.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 .presets {display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 10px;}
.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-bg); border: var(--synergy-border-width) solid var(--synergy-border); 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; align-items: normal;}
.settings > * {position: static;}
}
@media screen and (max-width: 500px) {
.settings .variables {display: block;}
.settings .variables label {margin: 20px 0 10px;}
}
.form {display: flex; flex-direction: column; gap: 15px; max-width: 450px;}
.form > * {margin: 0;}
.colori {aspect-ratio: 1 / 1; height: 34px; border-radius: var(--synergy-border-radius);}
a {color: inherit;}
/* Synergy expansions */
.inp.inp-small input {padding: 5px 9px; font-size: .9em;}

View file

@ -1 +1,5 @@
declare module '*.css';
declare module '*.css';
declare interface ComboObject {
[U: string]: any
};

View file

@ -1,37 +0,0 @@
html {background: var(--synergy-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;}
*, *::before, *::after {box-sizing: border-box;}
.content {padding: 20px; margin: auto; max-width: 1000px;}
.grid {display: grid; grid-template-columns: 1fr 1fr; gap: 20px; align-items: center;}
@media screen and (max-width: 700px) {
.grid {grid-template-columns: 1fr; max-width: 500px;}
}
.form {display: flex; flex-direction: column; gap: 15px;}
.form > * {margin: 0;}
header {margin-bottom: 70px; text-align: center;}
header > * {margin: 30px 0;}
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;}
.colorselector {display: flex; gap: 10px; justify-content: center;}
.colorselector > * {border: 0; padding: 0; cursor: pointer; width: 50px; height: 50px; border-radius: 50%; overflow: hidden; box-shadow: 0 1px 3px #0004;}
::-webkit-color-swatch, ::-moz-color-swatch {border: 0;}
.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: 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: 11px; display: block;}

View file

@ -1,235 +0,0 @@
import { reactive } from "petite-vue";
var style = document.createElement("style");
addEventListener("load", () => {
document.querySelector("head")!.appendChild(style);
});
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},
{name: "Tabs", file: "tabs", enabled: true},
]);
interface Result {
name: string,
css: string,
size: number,
size_gzip: number
}
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;
}
async function addResult(results: Result[], name: string, css: string) {
results.push({
name,
css,
size: getSize(css),
size_gzip: await getCompressedSize(css)
});
}
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;
}
function getSize(content: string) {
return (new TextEncoder().encode(content)).length
}
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, "");
}
}
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;
this.a = a;
}
clone() {
return new Color(this.r, this.g, this.b, this.a);
}
static fromHex(hex: string) {
let [r, g, b] = this.hexToRgb(hex);
return new Color(r, g, b);
}
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;
const b = parseInt(hex.slice(4, 6), 16) / 255;
return [r, g, b];
}
rgbFormat() {
let rgb = `${this.r*255}, ${this.g*255}, ${this.b*255}`;
return this.a == 1 ? `rgb(${rgb})` : `rgba(${rgb}, ${this.a})`;
}
hexFormat() {
let hex = `#${(1 << 24 | (this.r*255) << 16 | (this.g*255) << 8 | (this.b*255)).toString(16).slice(1)}`;
return this.a != 1 ? `${hex}${(Math.floor(this.a * 255).toString(16).padStart(2, '0'))}` : hex;
}
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);
};
const luminance1 = getRelativeLuminance(this.r) * 0.2126 +
getRelativeLuminance(this.g) * 0.7152 +
getRelativeLuminance(this.b) * 0.0722;
const luminance2 = getRelativeLuminance(otherColor.r) * 0.2126 +
getRelativeLuminance(otherColor.g) * 0.7152 +
getRelativeLuminance(otherColor.b) * 0.0722;
const contrastRatio = (Math.max(luminance1, luminance2) + 0.05) / (Math.min(luminance1, luminance2) + 0.05);
return (contrastRatio*100)-100;
}
equals(otherColor: Color) {
return this.r == otherColor.r && this.g == otherColor.g && this.b == otherColor.b;
}
mix(color: Color, ratio: number): Color {
const r = Math.round(this.r*255 * (1 - ratio) + color.r*255 * ratio);
const g = Math.round(this.g*255 * (1 - ratio) + color.g*255 * ratio);
const b = Math.round(this.b*255 * (1 - ratio) + color.b*255 * ratio);
return new Color(r/255, g/255, b/255);
}
}
export class Theme {
main: Color;
text: Color;
bg: Color;
constructor(opt: Preset) {
this.main = Color.fromHex(opt.main);
this.text = Color.fromHex(opt.text);
this.bg = Color.fromHex(opt.bg);
}
apply() {
style.innerHTML = this.generate();
}
generate() {
let variables = [];
variables.push(this.var("border", this.cAlpha(this.main, .4)));
variables.push(this.var("border-active", this.cAlpha(this.main)));
variables.push(this.var("border-width", "2px"));
variables.push(this.var("border-radius", ".375rem"))
variables.push(this.var("focus-highlight", this.cAlpha(this.main, .25)));
variables.push(this.var("tab-highlight", this.cAlpha(this.main, .1)));
variables.push(this.var("label", this.cMix(this.main, this.bg, .8)));
variables.push(this.var("label-active", this.cMix(this.main, this.bg)));
variables.push(this.var("btn-primary-bg", this.cMix(this.main, this.bg, .8)));
variables.push(this.var("btn-primary-bg-active", this.cMix(this.main, this.bg, .5)));
variables.push(this.var("btn-primary-bg-hover", this.cMix(this.main, this.bg)));
let btnBg = this.bg.mix(new Color(.6, .6, .6), .8).mix(this.main, .1);
variables.push(this.var("btn-bg", this.cMix(btnBg, this.bg, .3)));
variables.push(this.var("btn-bg-active", this.cMix(btnBg, this.bg, .6)));
variables.push(this.var("btn-bg-hover", this.cMix(btnBg, this.bg, .4)));
variables.push(this.var("text-color", this.text.hexFormat()));
variables.push(this.var("bg", this.bg.hexFormat()));
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 {\n${variables.join("\n")}\n}`];
let btnColor = this.getBtnColor(this.main, this.text);
if(btnColor != this.text) styles.push(`.btn.btn-primary {\n${this.var("text-color", btnColor.hexFormat())}\n}`);
return styles.join("\n\n");
}
getBtnColor(main: Color, text: Color) {
let white = new Color(1, 1, 1);
let black = new Color(0, 0, 0);
let cText = main.contrast(text);
let cWhite = main.contrast(white);
return cWhite > .3 ? white : cText > .3 ? text : black;
}
cMix(color: Color, color2: Color, ratio: number = 1) {
let c = color2.mix(color, ratio);
return c.hexFormat();
}
cAlpha(color: Color, alpha: number = 1) {
let c = color.clone();
c.a = alpha;
return c.hexFormat();
}
var(name: string, value: string) {
return `\t--synergy-${name}: ${value};`;
}
}

4
web/ts/App.ts Normal file
View file

@ -0,0 +1,4 @@
import { createApp } from "vue";
import App from "../App.vue";
let app = createApp(App).mount("#app");

72
web/ts/Color.ts Normal file
View file

@ -0,0 +1,72 @@
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;
this.a = a;
}
clone() {
return new Color(this.r, this.g, this.b, this.a);
}
static fromHex(hex: string) {
let [r, g, b] = this.hexToRgb(hex);
return new Color(r, g, b);
}
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;
const b = parseInt(hex.slice(4, 6), 16) / 255;
return [r, g, b];
}
alpha(alpha: number) {
return new Color(this.r, this.g, this.b, alpha);
}
rgbFormat() {
let rgb = `${this.r*255}, ${this.g*255}, ${this.b*255}`;
return this.a == 1 ? `rgb(${rgb})` : `rgba(${rgb}, ${this.a})`;
}
hexFormat() {
let hex = `#${(1 << 24 | (this.r*255) << 16 | (this.g*255) << 8 | (this.b*255)).toString(16).slice(1)}`;
return this.a != 1 ? `${hex}${(Math.floor(this.a * 255).toString(16).padStart(2, '0'))}` : hex;
}
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);
};
const luminance1 = getRelativeLuminance(this.r) * 0.2126 +
getRelativeLuminance(this.g) * 0.7152 +
getRelativeLuminance(this.b) * 0.0722;
const luminance2 = getRelativeLuminance(otherColor.r) * 0.2126 +
getRelativeLuminance(otherColor.g) * 0.7152 +
getRelativeLuminance(otherColor.b) * 0.0722;
const contrastRatio = (Math.max(luminance1, luminance2) + 0.05) / (Math.min(luminance1, luminance2) + 0.05);
return (contrastRatio*100)-100;
}
equals(otherColor: Color) {
return this.r == otherColor.r && this.g == otherColor.g && this.b == otherColor.b;
}
mix(color: Color, ratio: number): Color {
const r = Math.round(this.r*255 * (1 - ratio) + color.r*255 * ratio);
const g = Math.round(this.g*255 * (1 - ratio) + color.g*255 * ratio);
const b = Math.round(this.b*255 * (1 - ratio) + color.b*255 * ratio);
return new Color(r/255, g/255, b/255);
}
}

92
web/ts/Export.ts Normal file
View file

@ -0,0 +1,92 @@
import { Component, Variable, variables } from "./Synergy";
export class Export {
private variables: ComboObject;
private components: Component[];
public vars: string | null = null;
public css: Result[] = [];
constructor(variables: ComboObject, components: Component[]) {
this.variables = variables;
this.components = components;
}
async process() {
let cssParts = [];
let selectedComponents = new Set<string>();
for (let c of this.components) {
if (!c.selected) continue;
let value = await (await fetch(`./${c.id}.css`)).text();
value = `/* ${c.name} */\n\n${value}`;
cssParts.push(value);
selectedComponents.add(c.id);
}
let css = cssParts.join("\n\n/* ------------------- */\n\n");
this.vars = this.getVariables(selectedComponents);
cssParts.unshift(this.vars);
this.css = [];
await this.addResult("synergy.min.css", this.minify(css));
await this.addResult("synergy.css", css);
return this.css;
}
getVariables(selectedComponents: Set<string>) {
let root: string[] = [];
for (let v of variables) {
if (!this.required(v, selectedComponents)) continue;
root.push(`\t--synergy-${v.name}: ${this.variables[v.name]};`);
}
return `:root {\n${root.join("\n")}\n}`;
}
required(variable: Variable, selectedComponents: Set<string>) {
if (!variable.requiredBy) return true;
for (let id of variable.requiredBy) {
if (selectedComponents.has(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
}

105
web/ts/Preset.ts Normal file
View file

@ -0,0 +1,105 @@
import { Color } from "./Color";
export class Preset {
static readonly presets: Preset[] = [
new Preset({main: "#337e2c", text: "#031601", bg: "#f3f7f2"}),
new Preset({main: "#1c71d8", text: "#030e1c", bg: "#ffffff"}),
new Preset({main: "#9141ac", text: "#613583", bg: "#ffffff", "site-bg": "#f6edf7"}),
new Preset({main: "#a51d2d", text: "#3d3846", bg: "#f6f2f1", "site-bg": "#f1e9e8"}),
new Preset({main: "#865e3c", text: "#63452c", bg: "#f9f7f4", "site-bg": "#ffffff"}),
new Preset({main: "#F45662", text: "#c4aeae", bg: "#181218"}),
new Preset({main: "#E66993", text: "#dddddd", bg: "#18181b"}),
new Preset({main: "#ffab9b", text: "#E8C3BC", bg: "#191e23"}),
new Preset({main: "#9b8fe4", text: "#cfcef4", bg: "#090818", "site-bg": "#100E22"}),
];
readonly colors: Colors<Color>;
constructor(colors: Colors<Color | string>) {
this.colors = Preset.convertColors(colors);
}
private selectColor(colors: ColorType | ColorType[]): Color {
let input = Array.isArray(colors) ? colors : [colors];
for (let key of input) {
let val = this.colors[key];
if (val) return val;
}
return this.colors["main"];
}
public getColorVariables() {
const cMain = this.selectColor("main");
const cBg = this.selectColor("bg");
const cSiteBg = this.selectColor(["site-bg", "bg"]);
const cText = this.selectColor("text");
const cGray = new Color(.6, .6, .6);
const cBtn = cBg.mix(cGray, .6).mix(cMain, .1);
const colors = {
"border": cMain.mix(cBg, .6),
"border-active": cMain,
"focus-highlight": cMain.alpha(.25),
"tab-highlight": cMain.alpha(.1),
"label": cMain.mix(cBg, .2),
"label-active": cMain,
"btn-primary-bg": cMain.mix(cBg, .2),
"btn-primary-bg-hover": cMain.mix(cBg, .1),
"btn-primary-text-color": this.cSelectContrast(cMain, cText, cBg, Color.fromHex("#fff"), Color.fromHex("#000")),
"btn-focus-highlight": cBtn.alpha(.25),
"btn-bg": cBtn.mix(cBg, .7),
"btn-bg-hover": cBtn.mix(cBg, .45),
"text-color": cText,
"bg": cBg,
"site-bg": cSiteBg
};
return this.convertColorsToHex(colors);
}
private cSelectContrast(color: Color, ...colors: Color[]) {
let bestContrast = 0;
let bestColor;
for (const otherColor of colors) {
const contrast = color.contrast(otherColor);
if (contrast > bestContrast) {
bestContrast = contrast;
bestColor = otherColor;
}
}
return bestColor;
}
private convertColorsToHex(colors: ComboObject) {
const hexColors: ComboObject = {};
for (const key in colors) {
if (colors.hasOwnProperty(key)) {
hexColors[key] = colors[key].hexFormat();
}
}
return hexColors;
}
public static convertColors(input: Colors<Color | string>) {
return Object.entries(input).reduce<Partial<Colors<Color>>>((acc, [key, value]) => {
acc[key as ColorType] = value instanceof Color ? value : Color.fromHex(value);
return acc;
}, {}) as Colors<Color>;
}
public static getRandom() {
return this.presets[Math.floor(Math.random() * this.presets.length)];
}
}
export type AcceptedColor = Color | string;
// TODO: add more colors, use them if available and more specific
export interface Colors<T> {
main: T,
text: T,
bg: T,
"site-bg"?: T
}
type ColorType = keyof Colors<any>;

13
web/ts/Shared.ts Normal file
View file

@ -0,0 +1,13 @@
import { reactive, watch } from "vue";
import { components as synergyComponents, getDefaultVariables, Component } from "./Synergy";
import { setVariables } from "./Styles";
export let variableValues = reactive<ComboObject>(getDefaultVariables());
export let components = reactive<Component[]>(synergyComponents);
watch(() => variableValues, () => {
setVariables(variableValues);
}, {deep: true});
setVariables(variableValues);

19
web/ts/Styles.ts Normal file
View file

@ -0,0 +1,19 @@
let style = document.createElement("style");
addEventListener("load", () => {
document.querySelector("head")!.appendChild(style);
});
function parseVariables(variables: ComboObject) {
let vars: string[] = [];
Object.keys(variables).forEach(key => {
if (variables[key] == null) return;
vars.push(`--synergy-${key}: ${variables[key]};`);
});
return vars.join("");
}
export function setVariables(variables: ComboObject) {
const vars = parseVariables(variables);
style.innerHTML = `:root {${vars}}`;
}

56
web/ts/Synergy.ts Normal file
View file

@ -0,0 +1,56 @@
import { Preset } from "./Preset";
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-text-color", type: "color", requiredBy: ["button"]},
{name: "btn-focus-highlight", type: "color", requiredBy: ["button"]},
{name: "btn-bg", type: "color", requiredBy: ["button"]},
{name: "btn-bg-hover", 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", selected: true},
{name: "Input", id: "input", selected: true},
{name: "Checkbox & radio", id: "checkbox", selected: true},
{name: "Fancy input", id: "fancy_input"},
{name: "Tabs", id: "tabs", selected: true},
{name: "Toggle", id: "toggle", selected: true},
];
export interface Variable {
name: string,
type: VariableType,
requiredBy?: ComponentID[]
}
export type ComponentID = "button" | "input" | "fancy_input" | "checkbox" | "tabs" | "toggle";
export interface Component {
name: string,
id: ComponentID,
selected?: boolean
}
export type VariableType = "color" | "number";
export function getDefaultVariables() {
return {
"border-width": "2px",
"border-radius": ".375rem",
"tab-bar-height": "2px",
...Preset.getRandom().getColorVariables()
};
}