feat(themes): theme builder app updates

* theme-builder wip

* Theme Builder updates
- new CSS variable support
- realtime color updating with alpha support (rgb generation)
- auto tint/shade/contrast generation
- auto step generation
- CSS variable highlighting (forward and backwards)
- Colourlovers Palette search (via local proxy)
This commit is contained in:
Ross Gerbasi
2018-02-08 08:17:23 -06:00
committed by Adam Bradley
parent 9e8a0c127a
commit cdba38d004
29 changed files with 1811 additions and 390 deletions

View File

@ -40,7 +40,8 @@
"test.watch": "jest --watch --no-cache",
"build-test-cmp": "stencil build --dev --config scripts/test-components/stencil.config.js",
"theme-app-build": "stencil build --dev --config scripts/theme-builder/stencil.config.js",
"theme-builder": "npm run theme-app-build && sd concurrent \"stencil build --dev --watch\" \"stencil-dev-server\" \"npm run theme-server\" ",
"theme-builder": "sd concurrent \"npm run theme-app-build\" \"stencil build --dev --watch\" \"stencil-dev-server\" \"npm run theme-server\" ",
"theme-builder:dev": "sd concurrent \"npm run theme-app-build -- --watch\" \"stencil build --dev --watch\" \"stencil-dev-server\" \"npm run theme-server\" ",
"theme-server": "node scripts/theme-builder/server.js",
"tslint": "tslint --project .",
"tslint-fix": "tslint --project . --fix",

View File

@ -20,7 +20,8 @@ function requestHandler(request, response) {
if (parsedUrl.pathname === '/data') {
requestDataHandler(response);
} else if (parsedUrl.pathname === '/color') {
requestColorHandler(request, response);
} else if (parsedUrl.pathname === '/save-css') {
requestSaveCssHandler(parsedUrl, response);
@ -82,6 +83,29 @@ function requestDataHandler(response) {
}
}
function requestColorHandler (req, res) {
res.setHeader('Content-Type', 'application/json');
const params = {} = req.url.split('?').filter((e,i) => i > 0)[0].split('&').map(e => {let [name, value]=e.split('='); return {[name]:value}})[0],
url = `http://www.colourlovers.com/api/palettes?format=json&keywords=${params.search}`;
// console.log (`Proxy: ${url}`, params);
http.get(url, (resp) => {
let data = '';
resp.on('data', (chunk) => {
data += chunk;
});
// The whole response has been received. Print out the result.
resp.on('end', () => {
res.end(data);
});
}).on("error", (err) => {
console.log("Error: " + err.message);
res.end('{success: false}');
});
}
function requestSaveCssHandler (parsedUrl, response) {
try {

View File

@ -4,6 +4,9 @@
* and imports for stencil collections that might be configured in your stencil.config.js file
*/
import {
Color,
} from './components/Color';
import {
AppPreview as AppPreview
@ -32,38 +35,7 @@ declare global {
cssText?: string;
demoMode?: string;
demoUrl?: string;
}
}
}
import {
ColorSelector as ColorSelector
} from './components/color-selector/color-selector';
declare global {
interface HTMLColorSelectorElement extends ColorSelector, HTMLElement {
}
var HTMLColorSelectorElement: {
prototype: HTMLColorSelectorElement;
new (): HTMLColorSelectorElement;
};
interface HTMLElementTagNameMap {
"color-selector": HTMLColorSelectorElement;
}
interface ElementTagNameMap {
"color-selector": HTMLColorSelectorElement;
}
namespace JSX {
interface IntrinsicElements {
"color-selector": JSXElements.ColorSelectorAttributes;
}
}
namespace JSXElements {
export interface ColorSelectorAttributes extends HTMLAttributes {
isRgb?: boolean;
property?: string;
value?: string;
hoverProperty?: string;
}
}
}
@ -186,9 +158,43 @@ declare global {
}
namespace JSXElements {
export interface ThemeSelectorAttributes extends HTMLAttributes {
propertiesUsed?: string[];
themeData?: { name: string }[];
}
}
}
import {
VariableSelector as VariableSelector
} from './components/variable-selector/variable-selector';
declare global {
interface HTMLVariableSelectorElement extends VariableSelector, HTMLElement {
}
var HTMLVariableSelectorElement: {
prototype: HTMLVariableSelectorElement;
new (): HTMLVariableSelectorElement;
};
interface HTMLElementTagNameMap {
"variable-selector": HTMLVariableSelectorElement;
}
interface ElementTagNameMap {
"variable-selector": HTMLVariableSelectorElement;
}
namespace JSX {
interface IntrinsicElements {
"variable-selector": JSXElements.VariableSelectorAttributes;
}
}
namespace JSXElements {
export interface VariableSelectorAttributes extends HTMLAttributes {
isRgb?: boolean;
property?: string;
type?: 'color' | 'percent';
value?: Color | string | number;
}
}
}
declare global { namespace JSX { interface StencilJSX {} } }

View File

@ -0,0 +1,216 @@
interface RGB {
r: number,
g: number,
b: number
}
interface HSL {
h: number,
s: number,
l: number
}
export declare interface ColorStep {
id: string,
color: Color
}
export declare interface ColorStepDefinition {
color?: Color,
increments: number[]
}
function componentToHex (c) {
const hex = c.toString(16);
return hex.length == 1 ? `0${hex}` : hex;
}
function expandHex (hex: string): string {
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
hex = hex.replace(shorthandRegex, function (_m, r, g, b) {
return r + r + g + g + b + b;
});
return `#${hex.replace('#', '')}`;
}
function hexToRGB (hex: string): RGB {
hex = expandHex(hex);
hex = hex.replace('#', '');
const intValue: number = parseInt(hex, 16);
return {
r: (intValue >> 16) & 255,
g: (intValue >> 8) & 255,
b: intValue & 255
};
}
function hslToRGB ({h, s, l}: HSL): RGB {
h = h / 360;
s = s / 100;
l = l / 100;
if (s == 0) {
return {
r: l,
g: l,
b: l
};
}
const hue2rgb = function hue2rgb (p, q, t) {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
},
q = l < 0.5 ? l * (1 + s) : l + s - l * s,
p = 2 * l - q,
r = hue2rgb(p, q, h + (1 / 3)),
g = hue2rgb(p, q, h),
b = hue2rgb(p, q, h - (1 / 3));
return {
r: Math.round(r * 255),
g: Math.round(g * 255),
b: Math.round(b * 255)
};
}
function mixColors (color: Color, mixColor: Color, weight: number = .5): RGB {
const colorRGB: RGB = color.rgb,
mixColorRGB: RGB = mixColor.rgb,
mixColorWeight = 1 - weight;
return {
r: Math.round(weight * mixColorRGB.r + mixColorWeight * colorRGB.r),
g: Math.round(weight * mixColorRGB.g + mixColorWeight * colorRGB.g),
b: Math.round(weight * mixColorRGB.b + mixColorWeight * colorRGB.b)
};
}
function rgbToHex ({r, g, b}: RGB) {
return '#' + componentToHex(r) + componentToHex(g) + componentToHex(b);
}
function rgbToHSL ({r, g, b}: RGB): HSL {
r = Math.max(Math.min(r / 255, 1), 0);
g = Math.max(Math.min(g / 255, 1), 0);
b = Math.max(Math.min(b / 255, 1), 0);
const max = Math.max(r, g, b),
min = Math.min(r, g, b),
l = (max + min) / 2;
let d, h, s;
if (max !== min) {
d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
if (max === r) {
h = (g - b) / d + (g < b ? 6 : 0);
} else if (max === g) {
h = (b - r) / d + 2;
} else {
h = (r - g) / d + 4;
}
h = h / 6;
} else {
h = s = 0;
}
return {
h: Math.round(h * 360),
s: Math.round(s * 100),
l: Math.round(l * 100)
};
}
function rgbToYIQ ({r, g, b}: RGB): number {
return ((r * 299) + (g * 587) + (b * 114)) / 1000;
}
export class Color {
readonly hex: string;
readonly rgb: RGB;
readonly hsl: HSL;
readonly yiq: number;
public static isColor (value: string): Boolean {
if (/rgb\((\d{1,3}), ?(\d{1,3}), ?(\d{1,3})\)/.test(value)) return true;
return /(^#[0-9a-fA-F]+)/.test(value.trim());
}
constructor (value: string | RGB | HSL) {
if (typeof(value) === 'string' && /rgb\(/.test(value)) {
const matches = /rgb\((\d{1,3}), ?(\d{1,3}), ?(\d{1,3})\)/.exec(value);
value = {r: parseInt(matches[0]), g: parseInt(matches[1]), b: parseInt(matches[2])};
} else if (typeof(value) === 'string' && /hsl\(/.test(value)) {
const matches = /hsl\((\d{1,3}), ?(\d{1,3}%), ?(\d{1,3}%)\)/.exec(value);
value = {h: parseInt(matches[0]), s: parseInt(matches[1]), l: parseInt(matches[2])};
}
if (typeof(value) === 'string') {
value = value.replace(/\s/g, '');
this.hex = expandHex(value);
this.rgb = hexToRGB(this.hex);
this.hsl = rgbToHSL(this.rgb);
} else if ('r' in value && 'g' in value && 'b' in value) {
this.rgb = <RGB>value;
this.hex = rgbToHex(this.rgb);
this.hsl = rgbToHSL(this.rgb);
} else if ('h' in value && 's' in value && 'l' in value) {
this.hsl = <HSL>value;
this.rgb = hslToRGB(this.hsl);
this.hex = rgbToHex(this.rgb);
} else {
return null;
}
this.yiq = rgbToYIQ(this.rgb);
}
contrast (threshold: number = 128): Color {
return new Color((this.yiq >= threshold ? '#000' : '#fff'));
}
shiftLightness (percent: number): Color {
const hsl: HSL = Object.assign({}, this.hsl, {
l: this.hsl.l * percent
});
return new Color(hsl);
}
tint (percent: number = .1): Color {
percent = 1 + percent;
return this.shiftLightness(percent);
}
shade (percent: number = .1): Color {
percent = 1 - percent;
return this.shiftLightness(percent);
}
steps (from: ColorStepDefinition = {increments: [.2, .3, .5, .75]}): ColorStep[] {
const steps: ColorStep[] = [],
mixColor: Color = from.color || new Color((this.yiq > 128 ? '#000' : '#fff'));
for (let i = 1; i <= from.increments.length; i++) {
const {r, g, b} = mixColors(this, mixColor, from.increments[i - 1]);
steps.push({
id: (i * 100).toString(),
color: new Color({r,g,b})
});
}
return steps;
}
toList (): string {
const {r, g, b}: RGB = this.rgb;
return `${r},${g},${b}`;
}
}

View File

@ -0,0 +1,56 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { Component, Prop, Watch } from '@stencil/core';
let AppPreview = class AppPreview {
onCssTextChange() {
console.log('AppPreview onCssTextChange');
this.applyStyles();
}
applyStyles() {
if (this.iframe && this.iframe.contentDocument && this.iframe.contentDocument.documentElement) {
const iframeDoc = this.iframe.contentDocument;
const themerStyleId = 'themer-style';
let themerStyle = iframeDoc.getElementById(themerStyleId);
if (!themerStyle) {
themerStyle = iframeDoc.createElement('style');
themerStyle.id = themerStyleId;
iframeDoc.documentElement.appendChild(themerStyle);
}
themerStyle.innerHTML = this.cssText;
}
}
onIframeLoad() {
this.applyStyles();
}
render() {
const url = `${this.demoUrl}?ionicplatform=${this.demoMode}`;
return [
h("div", null,
h("iframe", { src: url, ref: el => this.iframe = el, onLoad: this.onIframeLoad.bind(this) }))
];
}
};
__decorate([
Prop()
], AppPreview.prototype, "demoUrl", void 0);
__decorate([
Prop()
], AppPreview.prototype, "demoMode", void 0);
__decorate([
Prop()
], AppPreview.prototype, "cssText", void 0);
__decorate([
Watch('cssText')
], AppPreview.prototype, "onCssTextChange", null);
AppPreview = __decorate([
Component({
tag: 'app-preview',
styleUrl: 'app-preview.css',
shadow: true
})
], AppPreview);
export { AppPreview };

View File

@ -1,4 +1,5 @@
import { Component, Prop, Watch } from '@stencil/core';
import { Component, Event, EventEmitter, Listen, Prop, Watch } from '@stencil/core';
import { Color } from '../Color';
@Component({
@ -11,7 +12,11 @@ export class AppPreview {
@Prop() demoUrl: string;
@Prop() demoMode: string;
@Prop() cssText: string;
@Prop() hoverProperty: string;
@Event() propertiesUsed: EventEmitter;
iframe: HTMLIFrameElement;
hasIframeListener: boolean = false;
@Watch('cssText')
onCssTextChange () {
@ -20,6 +25,22 @@ export class AppPreview {
this.applyStyles();
}
@Watch('hoverProperty')
onHoverPropertyChange () {
const el = this.iframe.contentDocument.documentElement;
el.style.cssText = '';
if (this.hoverProperty) {
const computed = window.getComputedStyle(el),
value = computed.getPropertyValue(this.hoverProperty);
if (Color.isColor(value)) {
el.style.setProperty(this.hoverProperty, '#ff0000');
} else {
el.style.setProperty(this.hoverProperty, parseFloat(value) > .5 ? '.1': '1');
}
}
}
applyStyles () {
if (this.iframe && this.iframe.contentDocument && this.iframe.contentDocument.documentElement) {
const iframeDoc = this.iframe.contentDocument;
@ -30,14 +51,66 @@ export class AppPreview {
themerStyle = iframeDoc.createElement('style');
themerStyle.id = themerStyleId;
iframeDoc.documentElement.appendChild(themerStyle);
const applicationStyle = iframeDoc.createElement('style');
iframeDoc.documentElement.appendChild(applicationStyle);
applicationStyle.innerHTML = 'html.theme-property-searching body * { pointer-events: auto !important}'
}
themerStyle.innerHTML = this.cssText;
}
}
onIframeMouseMove (ev) {
if (ev.ctrlKey) {
const el: HTMLElement = this.iframe.contentDocument.documentElement;
if (!el.classList.contains('theme-property-searching')) {
el.classList.add('theme-property-searching');
}
const sheets = (this.iframe.contentDocument.styleSheets),
items: Element[] = Array.from(ev.currentTarget.querySelectorAll(':hover')),
properties = [];
items.forEach(item => {
for (let i in sheets) {
const sheet: CSSStyleSheet = sheets[i] as CSSStyleSheet,
rules = sheet.rules || sheet.cssRules;
for (let r in rules) {
const rule: CSSStyleRule = rules[r] as CSSStyleRule;
if (item.matches(rule.selectorText)) {
const matches = rule.cssText.match(/(--ion.+?),/mgi);
if (matches) {
properties.push(...matches.map(match => match.replace(',', '')));
}
}
}
}
});
this.propertiesUsed.emit({
properties: Array.from(new Set(properties)).filter(prop => !/(-ios-|-md-)/.test(prop))
})
}
}
@Listen('body:keyup', {capture: true})
onKeyUp (ev: KeyboardEvent) {
if (!ev.ctrlKey) {
const el: HTMLElement = this.iframe.contentDocument.documentElement;
el.classList.remove('theme-property-searching');
this.propertiesUsed.emit({
properties: []
})
}
}
onIframeLoad () {
this.applyStyles();
this.iframe.contentDocument.documentElement.addEventListener('mousemove', this.onIframeMouseMove.bind(this));
}
render () {

View File

@ -1,78 +0,0 @@
import { Component, Event, EventEmitter, Prop } from '@stencil/core';
import { isValidColorValue } from '../helpers';
@Component({
tag: 'color-selector',
styleUrl: 'color-selector.css',
shadow: true
})
export class ColorSelector {
@Prop() property: string;
@Prop({ mutable: true }) value: string;
@Prop() isRgb: boolean;
isValid: boolean;
onChange(ev) {
if (this.isRgb) {
this.value = hexToRgb(ev.currentTarget.value);
} else {
this.value = ev.currentTarget.value;
}
this.colorChange.emit({
property: this.property,
value: this.value
});
}
@Event() colorChange: EventEmitter;
render() {
const value = this.value.trim().toLowerCase();
const hex = rgbToHex(value);
return [
<section class={isValidColorValue(value) ? 'valid' : 'invalid'}>
<div class='color-square'>
<input type='color' value={hex} onInput={this.onChange.bind(this)} tabindex='-1' />
</div>
<div class='color-value'>
<input type='text' value={value} onInput={this.onChange.bind(this)} />
</div>
<div class='property-label'>
{this.property}
</div>
</section>
];
}
}
function rgbToHex(value: string) {
if (value.indexOf('rgb') === -1) {
return value;
}
var c = value.replace(/[\sa-z\(\);]+/gi, '').split(',');
c = c.map(s => parseInt(s, 10).toString(16).replace(/^([a-z\d])$/i, '0$1'));
return '#' + c[0] + c[1] + c[2];
}
function hexToRgb(c: any) {
if (c.indexOf('#') === -1) {
return c;
}
c = c.replace(/#/, '');
c = c.length % 6 ? c.replace(/(.)(.)(.)/, '$1$1$2$2$3$3') : c;
c = parseInt(c, 16);
var a = parseFloat(a) || null;
const r = (c >> 16) & 255;
const g = (c >> 8) & 255;
const b = (c >> 0) & 255;
return `rgb${a ? 'a' : ''}(${[r, g, b, a].join().replace(/,$/, '')})`;
}

View File

@ -0,0 +1,79 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { Component, Prop } from '@stencil/core';
import { STORED_THEME_KEY, deleteCssUrl, getThemeUrl, saveCssUrl } from '../helpers';
let CssText = class CssText {
submitUpdate(ev) {
ev.stopPropagation();
ev.preventDefault();
this.saveCss(this.themeName, this.cssText);
}
saveCss(themeName, cssText) {
const url = saveCssUrl(themeName, cssText);
fetch(url).then(rsp => {
return rsp.text().then(txt => {
console.log('theme server response:', txt);
});
}).catch(err => {
console.log(err);
});
}
createNew(ev) {
ev.stopPropagation();
ev.preventDefault();
const name = prompt(`New theme name:`);
if (name) {
const themeName = name.split('.')[0].trim().toLowerCase();
if (themeName.length) {
console.log('createNew themeName', themeName);
localStorage.setItem(STORED_THEME_KEY, themeName);
this.saveCss(themeName, this.cssText);
}
}
}
deleteTheme(ev) {
ev.stopPropagation();
ev.preventDefault();
const shouldDelete = confirm(`Sure you want to delete "${this.themeName}"?`);
if (shouldDelete) {
const url = deleteCssUrl(this.themeName);
fetch(url).then(rsp => {
return rsp.text().then(txt => {
console.log('theme server response:', txt);
});
}).catch(err => {
console.log(err);
});
localStorage.removeItem(STORED_THEME_KEY);
}
}
render() {
return [
h("h1", null, getThemeUrl(this.themeName)),
h("div", null,
h("textarea", { readOnly: true, spellcheck: 'false' }, this.cssText)),
h("div", null,
h("button", { type: 'button', onClick: this.submitUpdate.bind(this) }, "Save Theme"),
h("button", { type: 'button', onClick: this.createNew.bind(this) }, "Create"),
h("button", { type: 'button', onClick: this.deleteTheme.bind(this) }, "Delete"))
];
}
};
__decorate([
Prop()
], CssText.prototype, "themeName", void 0);
__decorate([
Prop()
], CssText.prototype, "cssText", void 0);
CssText = __decorate([
Component({
tag: 'css-text',
styleUrl: 'css-text.css',
shadow: true
})
], CssText);
export { CssText };

View File

@ -1,14 +1,14 @@
import { Component, Prop } from '@stencil/core';
import { STORED_THEME_KEY, deleteCssUrl, getThemeUrl, saveCssUrl } from '../helpers';
import { Component, Element, Prop } from '@stencil/core';
import { deleteCssUrl, getThemeUrl, saveCssUrl, STORED_THEME_KEY } from '../helpers';
@Component({
tag: 'css-text',
styleUrl: 'css-text.css',
shadow: true
styleUrl: 'css-text.css'
})
export class CssText {
@Element() el: HTMLElement;
@Prop() themeName: string;
@Prop() cssText: string;
@ -70,17 +70,18 @@ export class CssText {
}
render () {
return [
<h1>
{getThemeUrl(this.themeName)}
</h1>,
<div>
<textarea readOnly spellcheck='false'>{this.cssText}</textarea>
<textarea readOnly spellcheck="false">{this.cssText}</textarea>
</div>,
<div>
<button type='button' onClick={this.submitUpdate.bind(this)}>Save Theme</button>
<button type='button' onClick={this.createNew.bind(this)}>Create</button>
<button type='button' onClick={this.deleteTheme.bind(this)}>Delete</button>
<button type="button" onClick={this.submitUpdate.bind(this)}>Save Theme</button>
<button type="button" onClick={this.createNew.bind(this)}>Create</button>
<button type="button" onClick={this.deleteTheme.bind(this)}>Delete</button>
</div>
];
}

View File

@ -0,0 +1,47 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { Component, Event, Prop } from '@stencil/core';
let DemoSelection = class DemoSelection {
onChangeUrl(ev) {
this.demoUrlChange.emit(ev.currentTarget.value);
}
onChangeMode(ev) {
this.demoModeChange.emit(ev.currentTarget.value);
}
render() {
return [
h("div", null,
h("select", { onChange: this.onChangeUrl.bind(this) }, this.demoData.map(d => h("option", { value: d.url, selected: d.url === this.demoUrl }, d.name))),
h("select", { onChange: this.onChangeMode.bind(this) },
h("option", { value: 'md', selected: 'md' === this.demoMode }, "md"),
h("option", { value: 'ios', selected: 'ios' === this.demoMode }, "ios")))
];
}
};
__decorate([
Prop()
], DemoSelection.prototype, "demoData", void 0);
__decorate([
Prop()
], DemoSelection.prototype, "demoUrl", void 0);
__decorate([
Prop()
], DemoSelection.prototype, "demoMode", void 0);
__decorate([
Event()
], DemoSelection.prototype, "demoUrlChange", void 0);
__decorate([
Event()
], DemoSelection.prototype, "demoModeChange", void 0);
DemoSelection = __decorate([
Component({
tag: 'demo-selection',
styleUrl: 'demo-selection.css',
shadow: true
})
], DemoSelection);
export { DemoSelection };

View File

@ -1,6 +1,7 @@
export const SERVER_DOMAIN = `http://localhost:5454`;
export const DATA_URL = `${SERVER_DOMAIN}/data`;
export const COLOR_URL = `${SERVER_DOMAIN}/color`;
export const SAVE_CSS_URL = `${SERVER_DOMAIN}/save-css`;
export const DELETE_CSS_URL = `${SERVER_DOMAIN}/delete-css`;
export const CSS_THEME_FILE_PATH = `/src/themes/css`;
@ -21,50 +22,3 @@ export function getThemeUrl(themeName: string) {
export const STORED_DEMO_URL_KEY = 'theme-builder-demo-url';
export const STORED_DEMO_MODE_KEY = 'theme-builder-demo-mode';
export const STORED_THEME_KEY = 'theme-builder-theme-url';
export function cleanCssValue(value: string) {
if (typeof value === 'string') {
value = (value || '').trim().toLowerCase().replace(/ /g, '');
if (value.length) {
if (value.charAt(0) === '#') {
return cleanHexValue(value);
}
if (value.charAt(0) === 'r') {
return cleanRgb(value);
}
}
}
return '';
}
function cleanHexValue(value: string) {
return '#' + value.substr(1).split('').map(c => {
return /[a-f]|[0-9]/.test(c) ? c : '';
}).join('').substr(0, 6);
}
function cleanRgb(value: string) {
return value.split('').map(c => {
return /[rgba0-9\,\.\(\)]/.test(c) ? c : '';
}).join('');
}
export function isValidColorValue(value: string) {
if (value) {
if (value.charAt(0) === '#') {
const rxValidHex = /^#[0-9a-f]{6}$/i;
return rxValidHex.test(value);
}
if (value.charAt(0) === 'r') {
const rxValidRgb = /([R][G][B][A]?[(]\s*([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\s*,\s*([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])\s*,\s*([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])(\s*,\s*((0\.[0-9]{1})|(1\.0)|(1)))?[)])/i;
return rxValidRgb.test(value);
}
}
return false;
}

View File

@ -0,0 +1,88 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { Component, Listen, State } from '@stencil/core';
import { DATA_URL, STORED_DEMO_MODE_KEY, STORED_DEMO_URL_KEY } from '../helpers';
let ThemeBuilder = class ThemeBuilder {
constructor() {
this.cssText = '';
this.themeName = '';
}
componentWillLoad() {
return fetch(DATA_URL).then(rsp => {
return rsp.json().then(data => {
this.demoData = data.demos;
this.themeData = data.themes;
this.initUrl();
});
}).catch(err => {
console.log('ThemeBuilder componentWillLoad', err);
});
}
initUrl() {
console.log('ThemeBuilder initUrl');
const storedUrl = localStorage.getItem(STORED_DEMO_URL_KEY);
const defaultUrl = this.demoData[0].url;
this.demoUrl = storedUrl || defaultUrl;
const storedMode = localStorage.getItem(STORED_DEMO_MODE_KEY);
const defaultMode = 'md';
this.demoMode = storedMode || defaultMode;
}
onDemoUrlChange(ev) {
this.demoUrl = ev.detail;
localStorage.setItem(STORED_DEMO_URL_KEY, this.demoUrl);
}
onDemoModeChange(ev) {
this.demoMode = ev.detail;
localStorage.setItem(STORED_DEMO_MODE_KEY, this.demoMode);
}
onThemeCssChange(ev) {
this.cssText = ev.detail.cssText;
this.themeName = ev.detail.themeName;
console.log('ThemeBuilder themeCssChange', this.themeName);
}
render() {
return [
h("main", null,
h("section", { class: 'preview-column' },
h("demo-selection", { demoData: this.demoData, demoUrl: this.demoUrl, demoMode: this.demoMode }),
h("app-preview", { demoUrl: this.demoUrl, demoMode: this.demoMode, cssText: this.cssText })),
h("section", { class: 'selector-column' },
h("theme-selector", { themeData: this.themeData })),
h("section", null,
h("css-text", { themeName: this.themeName, cssText: this.cssText })))
];
}
};
__decorate([
State()
], ThemeBuilder.prototype, "demoUrl", void 0);
__decorate([
State()
], ThemeBuilder.prototype, "demoMode", void 0);
__decorate([
State()
], ThemeBuilder.prototype, "cssText", void 0);
__decorate([
State()
], ThemeBuilder.prototype, "themeName", void 0);
__decorate([
Listen('demoUrlChange')
], ThemeBuilder.prototype, "onDemoUrlChange", null);
__decorate([
Listen('demoModeChange')
], ThemeBuilder.prototype, "onDemoModeChange", null);
__decorate([
Listen('themeCssChange')
], ThemeBuilder.prototype, "onThemeCssChange", null);
ThemeBuilder = __decorate([
Component({
tag: 'theme-builder',
styleUrl: 'theme-builder.css',
shadow: true
})
], ThemeBuilder);
export { ThemeBuilder };

View File

@ -15,6 +15,8 @@ export class ThemeBuilder {
@State() demoUrl: string;
@State() demoMode: string;
@State() cssText: string = '';
@State() hoverProperty: string;
@State() propertiesUsed: string[];
@State() themeName: string = '';
componentWillLoad() {
@ -60,17 +62,31 @@ export class ThemeBuilder {
console.log('ThemeBuilder themeCssChange', this.themeName);
}
@Listen('propertyHoverStart')
onPropertyHoverStart(ev) {
this.hoverProperty = ev.detail.property;
}
@Listen('propertyHoverStop')
onPropertyHoverStop() {
this.hoverProperty = undefined;
}
@Listen('propertiesUsed')
onPropertiesUsed(ev) {
this.propertiesUsed = ev.detail.properties;
}
render() {
return [
<main>
<section class='preview-column'>
<demo-selection demoData={this.demoData} demoUrl={this.demoUrl} demoMode={this.demoMode}></demo-selection>
<app-preview demoUrl={this.demoUrl} demoMode={this.demoMode} cssText={this.cssText}></app-preview>
<app-preview demoUrl={this.demoUrl} demoMode={this.demoMode} cssText={this.cssText} hoverProperty={this.hoverProperty}></app-preview>
</section>
<section class='selector-column'>
<theme-selector themeData={this.themeData}></theme-selector>
<theme-selector themeData={this.themeData} propertiesUsed={this.propertiesUsed}></theme-selector>
</section>
<section>

View File

@ -6,3 +6,147 @@ select {
section {
margin: 10px;
}
.palettes {
display: flex;
flex-direction: column;
justify-content: space-between;
}
.palette {
position: relative;
display: flex;
flex-direction: row;
width: 100%;
height: 75px;
padding: 8px 0;
}
.palette:after {
content: attr(data-title);
position: absolute;
pointer-events: none;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
text-align: center;
justify-content: center;
align-items: center;
font-size: 28px;
opacity: .2;
}
.palette .color {
position: relative;
flex: 1;
padding: 0 8px;
}
.color:after {
position: absolute;
top: 0;
content: "#" attr(data-color);
pointer-events: none;
font-size: 12px;
text-shadow: rgba(255, 255, 255, .5) 1px 1px;
}
.color-buttons {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
align-content: center;
display: none;
}
.color:hover .color-buttons {
display: flex;
}
.color-buttons button {
outline: none;
border: none;
font-size: 12px;
margin: 1px;
padding: 4px;
cursor: pointer;
opacity: .5;
text-shadow: rgba(255, 255, 255, .5) 1px 1px;
}
.color-buttons button.primary {
background-color: var(--ion-color-primary);
}
.color-buttons button.secondary {
background-color: var(--ion-color-secondary);
}
.color-buttons button.tertiary {
background-color: var(--ion-color-tertiary);
}
.color-buttons button.success {
background-color: var(--ion-color-success);
}
.color-buttons button.warning {
background-color: var(--ion-color-warning);
}
.color-buttons button.danger {
background-color: var(--ion-color-danger);
}
.color-buttons button.light {
background-color: var(--ion-color-light);
}
.color-buttons button.medium {
background-color: var(--ion-color-medium);
}
.color-buttons button.dark {
background-color: var(--ion-color-dark);
}
.color-buttons button.background {
background-color: var(--ion-background-color);
}
.color-buttons button.text {
background-color: var(--ion-text-color);
}
.color-buttons button:hover {
background-color: rgba(161, 60, 68, 0.53);
}
.top-bar {
display: flex;
padding: 8px;
flex-direction: row;
justify-content: space-between;
}
.search-button {
margin-left: 6px;
}
.search-toggle, .search-button {
background-color: rgb(89, 124, 155);
border: none;
color: white;
padding: 4px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 11px;
}

View File

@ -0,0 +1,130 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { Component, Event, Listen, Prop, State } from '@stencil/core';
import { THEME_VARIABLES } from '../../theme-variables';
import * as Helpers from '../helpers';
console.log(Helpers);
let ThemeSelector = class ThemeSelector {
constructor() {
this.themeVariables = [];
}
onChangeUrl(ev) {
this.themeName = ev.currentTarget.value;
localStorage.setItem(Helpers.STORED_THEME_KEY, this.themeName);
this.loadThemeCss();
}
componentWillLoad() {
const storedThemeName = localStorage.getItem(Helpers.STORED_THEME_KEY);
const defaultThemeName = this.themeData[0].name;
this.themeName = storedThemeName || defaultThemeName;
this.loadThemeCss();
}
loadThemeCss() {
console.log('ThemeSelector loadThemeCss');
const themeUrl = Helpers.getThemeUrl(this.themeName);
return fetch(themeUrl).then(rsp => {
return rsp.text().then(css => {
this.parseCss(css);
this.generateCss();
});
});
}
parseCss(css) {
console.log('ThemeSelector parseCss');
const themer = document.getElementById('themer');
themer.innerHTML = css;
const computed = window.getComputedStyle(document.body);
this.themeVariables = THEME_VARIABLES.map(themeVariable => {
const value = (computed.getPropertyValue(themeVariable.property) || PLACEHOLDER_COLOR);
return {
property: themeVariable.property.trim(),
value: value,
type: themeVariable.type,
computed: themeVariable.computed,
isRgb: value.indexOf('rgb') > -1
};
});
}
generateCss() {
console.log('ThemeSelector generateCss', this.themeName);
const c = [];
c.push(`/** ${this.themeName} theme **/`);
c.push(`\n`);
c.push(':root {');
this.themeVariables.forEach(themeVariable => {
themeVariable.value = Helpers.cleanCssValue(themeVariable.value);
c.push(` ${themeVariable.property}: ${themeVariable.value};`);
});
c.push('}');
const cssText = c.join('\n');
this.themeCssChange.emit({
cssText: cssText,
themeName: this.themeName
});
}
onColorChange(ev) {
console.log('ThemeSelector colorChange');
this.themeVariables = this.themeVariables.map(themeVariable => {
let value = themeVariable.value;
if (ev.detail.property === themeVariable.property) {
value = ev.detail.value;
}
return {
property: themeVariable.property,
value: value,
type: themeVariable.type,
computed: themeVariable.computed,
isRgb: themeVariable.isRgb
};
});
this.themeVariables
.filter(themeVariable => !!themeVariable.computed)
.forEach(themeVariable => {
const computed = themeVariable.computed || {}, fn = computed.fn, params = computed.params;
if (Helpers[fn]) {
themeVariable.value = Helpers[fn].apply(fn, params);
}
else {
console.log(`Unknown Helpers Function '${fn}'`);
}
});
this.generateCss();
}
render() {
return [
h("div", null,
h("select", { onChange: this.onChangeUrl.bind(this) }, this.themeData.map(d => h("option", { value: d.name, selected: this.themeName === d.name }, d.name))),
h("section", null, this.themeVariables
.filter(d => !d.computed)
.map(d => h("variable-selector", { property: d.property, value: d.value, isRgb: d.isRgb, type: d.type }))))
];
}
};
__decorate([
State()
], ThemeSelector.prototype, "themeName", void 0);
__decorate([
State()
], ThemeSelector.prototype, "themeVariables", void 0);
__decorate([
Prop()
], ThemeSelector.prototype, "themeData", void 0);
__decorate([
Event()
], ThemeSelector.prototype, "themeCssChange", void 0);
__decorate([
Listen('colorChange')
], ThemeSelector.prototype, "onColorChange", null);
ThemeSelector = __decorate([
Component({
tag: 'theme-selector',
styleUrl: 'theme-selector.css',
shadow: true
})
], ThemeSelector);
export { ThemeSelector };
const PLACEHOLDER_COLOR = `#ff00ff`;

View File

@ -1,48 +1,60 @@
import { Component, Event, EventEmitter, Listen, Prop, State } from '@stencil/core';
import { STORED_THEME_KEY, cleanCssValue, getThemeUrl } from '../helpers';
import { Component, Element, Event, EventEmitter, Listen, Prop, State } from '@stencil/core';
import { THEME_VARIABLES } from '../../theme-variables';
import { Color, ColorStep } from '../Color';
import { COLOR_URL, getThemeUrl, STORED_THEME_KEY } from '../helpers';
interface ThemeVariable {
property: string;
value?: Color | number | string;
computed?: string;
}
const PLACEHOLDER_COLOR = '#ff00ff';
@Component({
tag: 'theme-selector',
styleUrl: 'theme-selector.css',
shadow: true
styleUrl: 'theme-selector.css'
})
export class ThemeSelector {
@Element() el: HTMLThemeSelectorElement;
@State() themeName: string;
@State() themeVariables: { property: string; value?: string; isRgb?: boolean; }[] = [];
@State() themeVariables: ThemeVariable[] = [];
@State() searchMode: boolean;
@State() palettes: any[];
@Prop() propertiesUsed: string[] = [];
@Prop() themeData: { name: string }[];
@Event() themeCssChange: EventEmitter;
@Event() propertyHoverStart: EventEmitter;
@Event() propertyHoverStop: EventEmitter;
private currentHoveredProperty: string;
onChangeUrl(ev) {
async onChangeUrl (ev) {
this.themeName = ev.currentTarget.value;
localStorage.setItem(STORED_THEME_KEY, this.themeName);
this.loadThemeCss();
await this.loadThemeCss();
}
componentWillLoad() {
async componentWillLoad () {
const storedThemeName = localStorage.getItem(STORED_THEME_KEY);
const defaultThemeName = this.themeData[0].name;
this.themeName = storedThemeName || defaultThemeName;
this.loadThemeCss();
await this.loadThemeCss();
}
loadThemeCss() {
async loadThemeCss () {
console.log('ThemeSelector loadThemeCss');
const themeUrl = getThemeUrl(this.themeName);
return fetch(themeUrl).then(rsp => {
return rsp.text().then(css => {
const css = await fetch(themeUrl).then(r => r.text());
this.parseCss(css);
this.generateCss();
});
});
}
parseCss (css: string) {
@ -54,12 +66,12 @@ export class ThemeSelector {
const computed = window.getComputedStyle(document.body);
this.themeVariables = THEME_VARIABLES.map(themeVariable => {
const value = (computed.getPropertyValue(themeVariable.property) || PLACEHOLDER_COLOR).trim().toLowerCase();
return {
const value = (computed.getPropertyValue(themeVariable.property) || PLACEHOLDER_COLOR);
return Object.assign({}, themeVariable, {
property: themeVariable.property.trim(),
value: value,
isRgb: value.indexOf('rgb') > -1
};
value: themeVariable.computed ? value : (!Color.isColor(value) ? parseFloat(value) : new Color(value))
});
});
}
@ -72,8 +84,8 @@ export class ThemeSelector {
c.push(':root {');
this.themeVariables.forEach(themeVariable => {
themeVariable.value = cleanCssValue(themeVariable.value);
c.push(` ${themeVariable.property}: ${themeVariable.value};`);
const value = themeVariable.value;
c.push(` ${themeVariable.property}: ${value instanceof Color ? value.hex : value};`);
});
c.push('}');
@ -85,40 +97,200 @@ export class ThemeSelector {
});
}
hoverProperty () {
const targets: Element[] = Array.from(this.el.querySelectorAll(':hover')),
selector: Element = targets.find(target => {
return target.tagName.toLowerCase() === 'variable-selector';
});
if (selector) {
const property = (selector as HTMLVariableSelectorElement).property;
if (this.currentHoveredProperty !== property) {
this.propertyHoverStop.emit({
property: this.currentHoveredProperty
});
this.currentHoveredProperty = property;
this.propertyHoverStart.emit({
property: this.currentHoveredProperty
});
}
}
}
@Listen('colorChange')
onColorChange (ev) {
console.log('ThemeSelector colorChange');
this.themeVariables = this.themeVariables.map(themeVariable => {
let value = themeVariable.value;
if (ev.detail.property === themeVariable.property) {
value = ev.detail.value;
this.changeColor(ev.detail.property, ev.detail.value);
}
return {
property: themeVariable.property,
value: value,
isRgb: themeVariable.isRgb
};
changeColor (property: string, value: Color | string) {
this.themeVariables = this.themeVariables.map(themeVariable => {
if (property === themeVariable.property) {
return Object.assign({}, themeVariable, {
value: value instanceof Color ? value : themeVariable.value instanceof Color ? new Color(value) : value
});
}
return themeVariable;
});
this.themeVariables
.filter(themeVariable => !!themeVariable.computed)
.forEach(themeVariable => {
const computed = themeVariable.computed,
referenceVariable = this.themeVariables.find(themeVariable => themeVariable.property === computed),
value = referenceVariable.value;
if (value instanceof Color) {
themeVariable.value = value.toList();
}
});
this.generateCss();
}
@Listen('generateColors')
onGenerateColors (ev) {
const color: Color = ev.detail.color,
steps: Boolean = ev.detail.steps,
property = ev.detail.property;
if (color && property) {
if (steps) {
const steps: ColorStep[] = color.steps();
steps.forEach((step: ColorStep) => {
const themeVariable: ThemeVariable = this.themeVariables.find((variable: ThemeVariable) => variable.property === `${property}-step-${step.id}`);
themeVariable && (themeVariable.value = step.color);
});
} else {
const tint: ThemeVariable = this.themeVariables.find((variable: ThemeVariable) => variable.property === `${property}-tint`),
shade: ThemeVariable = this.themeVariables.find((variable: ThemeVariable) => variable.property === `${property}-shade`),
contrast: ThemeVariable = this.themeVariables.find((variable: ThemeVariable) => variable.property === `${property}-contrast`);
tint && (tint.value = color.tint());
shade && (shade.value = color.shade());
contrast && (contrast.value = color.contrast());
}
this.generateCss();
//TODO: Figure out why we need this typed to any
(this.el as any).forceUpdate();
}
}
@Listen('body:keydown')
onKeyDown (ev: MouseEvent) {
if (ev.ctrlKey) {
this.hoverProperty();
}
}
@Listen('body:keyup')
onKeyUp (ev: KeyboardEvent) {
if (this.currentHoveredProperty && !ev.ctrlKey) {
this.propertyHoverStop.emit({
property: this.currentHoveredProperty
});
this.currentHoveredProperty = null;
}
}
@Listen('mousemove')
onMouseMove (ev: MouseEvent) {
if (ev.ctrlKey) {
this.hoverProperty();
}
}
onSearchInput (ev: KeyboardEvent) {
if (ev.keyCode == 13) {
this.search();
}
}
toggleSearchMode () {
this.searchMode = !this.searchMode;
}
async search () {
const input: HTMLInputElement = this.el.querySelector('#searchInput') as HTMLInputElement,
value = input.value;
input.value = '';
try {
this.palettes = await fetch(`${COLOR_URL}?search=${value}&stuff=poop`).then(r => r.json()) || [];
} catch (e) {
this.palettes = [];
}
}
onColorClick (ev: MouseEvent) {
console.log(ev);
let target: HTMLElement = ev.currentTarget as HTMLElement;
const property = target.getAttribute('data-property');
while (target && !target.classList.contains('color')) {
target = target.parentElement as HTMLElement;
}
const color = target.getAttribute('data-color');
this.changeColor(property, color);
}
render () {
const
onColorClick = this.onColorClick.bind(this),
variables = <section>
{
this.themeVariables
.filter(d => !d.computed)
.map(d => <variable-selector class={this.propertiesUsed.indexOf(d.property) >= 0 ? 'used' : ''}
property={d.property} value={d.value}></variable-selector>)
}
</section>,
search = <section>
<div>
<input type="text" id="searchInput" onKeyUp={this.onSearchInput.bind(this)}/>
<button class="search-button" onClick={this.search.bind(this)}>Search</button>
</div>
<div class="palettes">
{
(this.palettes || []).map((d: any) => <div class="palette" data-title={d.title}>
{(d.colors || []).map((c: string) => <div class="color" data-color={`#${c}`}
style={{backgroundColor: `#${c}`}}>
<div class="color-buttons">
<button onClick={onColorClick} data-property="--ion-color-primary" class="primary">p</button>
<button onClick={onColorClick} data-property="--ion-color-secondary" class="secondary">s</button>
<button onClick={onColorClick} data-property="--ion-color-tertiary" class="tertiary">t</button>
<button onClick={onColorClick} data-property="--ion-color-success" class="success">ss</button>
<button onClick={onColorClick} data-property="--ion-color-warning" class="warning">w</button>
<button onClick={onColorClick} data-property="--ion-color-danger" class="danger">d</button>
<button onClick={onColorClick} data-property="--ion-color-light" class="light">l</button>
<button onClick={onColorClick} data-property="--ion-color-medium" class="medium">m</button>
<button onClick={onColorClick} data-property="--ion-color-dark" class="dark">dk</button>
<button onClick={onColorClick} data-property="--ion-background-color" class="background">bg</button>
<button onClick={onColorClick} data-property="--ion-text-color" class="text">txt</button>
</div>
</div>)}
</div>)
}
</div>
</section>;
return [
<div>
<div class="top-bar">
<select onChange={this.onChangeUrl.bind(this)}>
{this.themeData.map(d => <option value={d.name} selected={this.themeName === d.name}>{d.name}</option>)}
</select>
<section>
{this.themeVariables.map(d => <color-selector property={d.property} value={d.value} isRgb={d.isRgb}></color-selector>)}
</section>
<button type="button" class="search-toggle"
onClick={this.toggleSearchMode.bind(this)}>{this.searchMode ? 'Close' : 'Open'} Search
</button>
</div>
{this.searchMode ? search : variables}
</div>
];
}
}
const PLACEHOLDER_COLOR = `#ff00ff`;
};

View File

@ -10,6 +10,7 @@ section {
.property-label {
font-family: Courier New, Courier, monospace;
white-space: nowrap;
cursor: pointer;
flex: 1;
padding-left: 10px;
@ -37,3 +38,7 @@ input[type="color"]::-webkit-color-swatch-wrapper {
input[type="color"]::-webkit-color-swatch {
border: 1px solid black;
}
:host(.used) section {
background-color: var(--variable-selector-color, rgba(255, 149, 243, 0.91));
}

View File

@ -0,0 +1,98 @@
import { Component, Element, Event, EventEmitter, Listen, Method, Prop } from '@stencil/core';
import { Color } from '../Color';
@Component({
tag: 'variable-selector',
styleUrl: 'variable-selector.css',
shadow: true
})
export class VariableSelector {
@Element() el: HTMLElement;
@Prop() property: string;
@Prop() type: 'color' | 'percent';
@Prop({mutable: true}) value: Color | string | number;
@Prop() isRgb: boolean;
@Event() colorChange: EventEmitter;
@Event() generateColors: EventEmitter;
@Method()
getProperty () {
return this.property;
}
onChange (ev) {
const input: HTMLInputElement = ev.currentTarget,
value = ev.currentTarget.value;
if (input.type === 'color') {
this.value = new Color(value);
} else if (input.type === 'text') {
if (Color.isColor(value)) {
this.value = new Color(value);
} else {
return;
}
} else if (input.type === 'range') {
this.value = value / 100;
}
this.colorChange.emit({
property: this.property,
value: this.value
});
}
@Listen('dblclick')
onMouseUp (ev) {
if (ev.altKey) {
const color = this.value as Color;
if (/(primary|secondary|tertiary|success|warning|danger|light|medium|dark)$/.test(this.property)) {
this.generateColors.emit({
color,
property: this.property
});
} else if (/(^--ion-background-color$|^--ion-text-color$)/.test(this.property)) {
this.generateColors.emit({
color,
steps: true,
property: this.property
});
}
}
}
render () {
if (this.value instanceof Color || this.value == null) {
const color = this.value && this.value as Color,
value = color.hex, {r, g, b} = color.rgb;
this.el.style.setProperty('--variable-selector-color', `rgba(${r}, ${g}, ${b}, .5`);
return [
<section class={value ? 'valid' : 'invalid'}>
<div class="color-square">
<input type="color" value={value} onInput={this.onChange.bind(this)} tabindex="-1"/>
</div>
<div class="color-value">
<input type="text" value={value} onChange={this.onChange.bind(this)}/>
</div>
<div class="property-label">
{this.property}
</div>
</section>
];
}
const value = parseFloat(this.value as string);
return [
<section>
<div class="property-value">
<input type="range" value={value * 100} min="0" max="100" step="1" onInput={this.onChange.bind(this)}/>
</div>
<div class="property-label">
{this.property}
</div>
</section>
];
}
}

View File

@ -12,8 +12,6 @@
<style id="themer"></style>
</head>
<body>
<theme-builder></theme-builder>
</body>
</html>

View File

@ -0,0 +1,310 @@
export const THEME_VARIABLES = [
{
property: '--ion-alpha-lowest',
type: 'percent'
},
{
property: '--ion-alpha-low',
type: 'percent'
},
{
property: '--ion-alpha-medium',
type: 'percent'
},
{
property: '--ion-alpha-high',
type: 'percent'
},
{
property: '--ion-alpha-highest',
type: 'percent'
},
{
property: '--ion-color-primary'
},
{
property: '--ion-color-primary-contrast'
},
{
property: '--ion-color-primary-rgb',
computed: {
fn: 'colorToRGBList',
params: [
'--ion-color-primary'
]
}
},
{
property: '--ion-color-primary-shade'
},
{
property: '--ion-color-primary-tint'
},
{
property: '--ion-color-secondary'
},
{
property: '--ion-color-secondary-contrast'
},
{
property: '--ion-color-secondary-rgb',
computed: {
fn: 'colorToRGBList',
params: [
'--ion-color-secondary'
]
}
},
{
property: '--ion-color-secondary-shade'
},
{
property: '--ion-color-secondary-tint'
},
{
property: '--ion-color-tertiary'
},
{
property: '--ion-color-tertiary-contrast'
},
{
property: '--ion-color-tertiary-rgb',
computed: {
fn: 'colorToRGBList',
params: [
'--ion-color-tertiary'
]
}
},
{
property: '--ion-color-tertiary-shade'
},
{
property: '--ion-color-tertiary-tint'
},
{
property: '--ion-color-success'
},
{
property: '--ion-color-success-contrast'
},
{
property: '--ion-color-success-rgb',
computed: {
fn: 'colorToRGBList',
params: [
'--ion-color-success'
]
}
},
{
property: '--ion-color-success-shade'
},
{
property: '--ion-color-success-tint'
},
{
property: '--ion-color-warning'
},
{
property: '--ion-color-warning-contrast'
},
{
property: '--ion-color-warning-rgb',
computed: {
fn: 'colorToRGBList',
params: [
'--ion-color-warning'
]
}
},
{
property: '--ion-color-warning-shade'
},
{
property: '--ion-color-warning-tint'
},
{
property: '--ion-color-danger'
},
{
property: '--ion-color-danger-contrast'
},
{
property: '--ion-color-danger-rgb',
computed: {
fn: 'colorToRGBList',
params: [
'--ion-color-danger'
]
}
},
{
property: '--ion-color-danger-shade'
},
{
property: '--ion-color-danger-tint'
},
{
property: '--ion-color-light'
},
{
property: '--ion-color-light-contrast'
},
{
property: '--ion-color-light-rgb',
computed: {
fn: 'colorToRGBList',
params: [
'--ion-color-light'
]
}
},
{
property: '--ion-color-light-shade'
},
{
property: '--ion-color-light-tint'
},
{
property: '--ion-color-medium'
},
{
property: '--ion-color-medium-contrast'
},
{
property: '--ion-color-medium-rgb',
computed: {
fn: 'colorToRGBList',
params: [
'--ion-color-medium'
]
}
},
{
property: '--ion-color-medium-shade'
},
{
property: '--ion-color-medium-tint'
},
{
property: '--ion-color-dark'
},
{
property: '--ion-color-dark-contrast'
},
{
property: '--ion-color-dark-rgb',
computed: {
fn: 'colorToRGBList',
params: [
'--ion-color-dark'
]
}
},
{
property: '--ion-color-dark-shade'
},
{
property: '--ion-color-dark-tint'
},
{
property: '--ion-backdrop-color'
},
{
property: '--ion-background-color'
},
{
property: '--ion-background-color-rgb',
computed: {
fn: 'colorToRGBList',
params: [
'--ion-background-color'
]
}
},
{
property: '--ion-background-color-step-100'
},
{
property: '--ion-background-color-step-200'
},
{
property: '--ion-background-color-step-300'
},
{
property: '--ion-background-color-step-400'
},
{
property: '--ion-border-color'
},
{
property: '--ion-box-shadow-color'
},
{
property: '--ion-text-color'
},
{
property: '--ion-text-color-rgb',
computed: {
fn: 'colorToRGBList',
params: [
'--ion-text-color'
]
}
},
{
property: '--ion-text-color-step-100'
},
{
property: '--ion-text-color-step-200'
},
{
property: '--ion-text-color-step-300'
},
{
property: '--ion-text-color-step-400'
},
{
property: '--ion-tabbar-background-color'
},
{
property: '--ion-tabbar-border-color'
},
{
property: '--ion-tabbar-text-color'
},
{
property: '--ion-tabbar-text-color-active'
},
{
property: '--ion-toolbar-background-color'
},
{
property: '--ion-toolbar-border-color'
},
{
property: '--ion-toolbar-color-active'
},
{
property: '--ion-toolbar-color-inactive'
},
{
property: '--ion-toolbar-text-color'
},
{
property: '--ion-item-background-color'
},
{
property: '--ion-item-background-color-active'
},
{
property: '--ion-item-border-color'
},
{
property: '--ion-item-text-color'
},
{
property: '--ion-placeholder-text-color'
}
];

View File

@ -1,151 +1,250 @@
export const THEME_VARIABLES = [
export const THEME_VARIABLES: {property: string, type?: 'percent' | 'color', computed?: string}[] = [
{
property: '--ion-alpha-lowest'
},
{
property: '--ion-alpha-low'
},
{
property: '--ion-alpha-medium'
},
{
property: '--ion-alpha-high'
},
{
property: '--ion-alpha-highest'
},
{
property: '--ion-color-primary'
},
{
property: '--ion-color-primary-tint'
},
{
property: '--ion-color-primary-shade'
},
{
property: '--ion-color-primary-contrast'
},
{
property: '--ion-color-primary-rgb',
computed: '--ion-color-primary'
},
{
property: '--ion-color-primary-shade'
},
{
property: '--ion-color-primary-tint'
},
{
property: '--ion-color-secondary'
},
{
property: '--ion-color-secondary-tint'
},
{
property: '--ion-color-secondary-shade'
},
{
property: '--ion-color-secondary-contrast'
},
{
property: '--ion-color-secondary-rgb',
computed: '--ion-color-secondary'
},
{
property: '--ion-color-secondary-shade'
},
{
property: '--ion-color-secondary-tint'
},
{
property: '--ion-color-tertiary'
},
{
property: '--ion-color-tertiary-tint'
},
{
property: '--ion-color-tertiary-shade'
},
{
property: '--ion-color-tertiary-contrast'
},
{
property: '--ion-color-tertiary-rgb',
computed: '--ion-color-tertiary'
},
{
property: '--ion-color-tertiary-shade'
},
{
property: '--ion-color-tertiary-tint'
},
{
property: '--ion-color-success'
},
{
property: '--ion-color-success-tint'
},
{
property: '--ion-color-success-shade'
},
{
property: '--ion-color-success-contrast'
},
{
property: '--ion-color-success-rgb',
computed: '--ion-color-success'
},
{
property: '--ion-color-success-shade'
},
{
property: '--ion-color-success-tint'
},
{
property: '--ion-color-warning'
},
{
property: '--ion-color-warning-tint'
},
{
property: '--ion-color-warning-shade'
},
{
property: '--ion-color-warning-contrast'
},
{
property: '--ion-color-warning-rgb',
computed: '--ion-color-warning'
},
{
property: '--ion-color-warning-shade'
},
{
property: '--ion-color-warning-tint'
},
{
property: '--ion-color-danger'
},
{
property: '--ion-color-danger-tint'
},
{
property: '--ion-color-danger-shade'
},
{
property: '--ion-color-danger-contrast'
},
{
property: '--ion-color-danger-rgb',
computed: '--ion-color-danger'
},
{
property: '--ion-color-danger-shade'
},
{
property: '--ion-color-danger-tint'
},
{
property: '--ion-color-light'
},
{
property: '--ion-color-light-tint'
},
{
property: '--ion-color-light-shade'
},
{
property: '--ion-color-light-contrast'
},
{
property: '--ion-color-light-rgb',
computed: '--ion-color-light'
},
{
property: '--ion-color-light-shade'
},
{
property: '--ion-color-light-tint'
},
{
property: '--ion-color-medium'
},
{
property: '--ion-color-medium-tint'
},
{
property: '--ion-color-medium-shade'
},
{
property: '--ion-color-medium-contrast'
},
{
property: '--ion-color-medium-rgb',
computed: '--ion-color-medium'
},
{
property: '--ion-color-medium-shade'
},
{
property: '--ion-color-medium-tint'
},
{
property: '--ion-color-dark'
},
{
property: '--ion-color-dark-tint'
},
{
property: '--ion-color-dark-shade'
},
{
property: '--ion-color-dark-contrast'
},
{
property: '--ion-text-color'
property: '--ion-color-dark-rgb',
computed: '--ion-color-dark'
},
{
property: '--ion-text-color-alt'
property: '--ion-color-dark-shade'
},
{
property: '--ion-color-dark-tint'
},
{
property: '--ion-backdrop-color'
},
{
property: '--ion-background-color'
},
{
property: '--ion-background-color-alt'
property: '--ion-background-color-rgb',
computed: '--ion-background-color'
},
{
property: '--ion-toolbar-background-color'
property: '--ion-background-color-step-100'
},
{
property: '--ion-background-color-step-200'
},
{
property: '--ion-background-color-step-300'
},
{
property: '--ion-background-color-step-400'
},
{
property: '--ion-border-color'
},
{
property: '--ion-box-shadow-color'
},
{
property: '--ion-text-color'
},
{
property: '--ion-text-color-rgb',
computed: '--ion-text-color'
},
{
property: '--ion-text-color-step-100'
},
{
property: '--ion-text-color-step-200'
},
{
property: '--ion-text-color-step-300'
},
{
property: '--ion-text-color-step-400'
},
{
property: '--ion-tabbar-background-color'
},
{
property: '--ion-tabbar-border-color'
},
{
property: '--ion-tabbar-text-color'
},
{
property: '--ion-tabbar-text-color-active'
},
{
property: '--ion-toolbar-background-color'
},
{
property: '--ion-toolbar-border-color'
},
{
property: '--ion-toolbar-color-active'
},
{
property: '--ion-toolbar-color-inactive'
},
{
property: '--ion-toolbar-text-color'
},
{
property: '--ion-item-background-color'
},
{
property: '--ion-item-background-color-alt'
property: '--ion-item-background-color-active'
},
{
property: '--ion-border-color'
property: '--ion-item-border-color'
},
{
property: '--ion-backdrop-color'
property: '--ion-item-text-color'
},
{
property: '--ion-box-shadow-color'
},
property: '--ion-placeholder-text-color'
}
];

View File

@ -8,4 +8,4 @@ exports.devServer = {
root: '../../',
watchGlob: 'src/**',
openUrl: '/theme-builder'
}
};

View File

@ -156,7 +156,7 @@ $alert-ios-button-border-width: $hairlines-width !default;
$alert-ios-button-border-style: solid !default;
/// @prop - Border color of the alert button
$alert-ios-button-border-color: $background-ios-color-step-100 !default;
$alert-ios-button-border-color: ion-color-alpha($text-ios-color-value, text-ios-color, $alpha-low) !default;
/// @prop - Border radius of the alert button
$alert-ios-button-border-radius: 0 !default;

View File

@ -34,6 +34,13 @@
opacity: $button-ios-opacity-hover;
}
a[disabled],
button[disabled],
.button[disabled] {
opacity: $button-ios-opacity-disabled;
}
// iOS Default Button Color Mixin
// --------------------------------------------------

View File

@ -57,6 +57,9 @@ $button-ios-opacity-hover: .8 !default;
/// @prop - Background color of the focused button
$button-ios-background-color-focused: ion-color($colors-ios, $button-ios-background-color, shade, ios) !default;
/// @prop - Opacity of the button when disabled
$button-ios-opacity-disabled: $alpha-ios-medium !default;
// iOS Large Button
// --------------------------------------------------

View File

@ -46,6 +46,11 @@
background-color: $button-md-text-color;
}
a[disabled],
button[disabled],
.button[disabled] {
opacity: $button-md-opacity-disabled;
}
// Material Design Default Button Color Mixin
// --------------------------------------------------

View File

@ -78,6 +78,9 @@ $button-md-ripple-background-color: $text-md-color-step-200 !default;
/// @prop - Background color of the focused button
$button-md-background-color-focused: ion-color($colors-md, $button-md-background-color, shade, md) !default;
/// @prop - Opacity of the button when disabled
$button-md-opacity-disabled: $alpha-md-medium !default;
// Material Design Large Button
// --------------------------------------------------

View File

@ -57,7 +57,6 @@ a[disabled],
button[disabled],
.button[disabled] {
cursor: default;
opacity: .4;
pointer-events: none;
}

View File

@ -1,119 +1,84 @@
/** default theme **/
:root {
--ion-alpha-lowest: .06;
--ion-alpha-low: .1;
--ion-alpha-medium: .4;
--ion-alpha-high: .75;
--ion-alpha-highest: .9;
--ion-alpha-lowest: 0.06;
--ion-alpha-low: 0.1;
--ion-alpha-medium: 0.4;
--ion-alpha-high: 0.75;
--ion-alpha-highest: 0.9;
--ion-color-primary: #488aff;
--ion-color-primary-contrast: #fff;
--ion-color-primary-rgb: '72,138,255';
--ion-color-primary-contrast: #ffffff;
--ion-color-primary-rgb: "72,138,255";
--ion-color-primary-shade: #3f79e0;
--ion-color-primary-tint: #427feb;
--ion-color-secondary: #32db64;
--ion-color-secondary-contrast: #fff;
--ion-color-secondary-rgb: '50,219,100';
--ion-color-secondary-contrast: #ffffff;
--ion-color-secondary-rgb: "50,219,100";
--ion-color-secondary-shade: #2cc158;
--ion-color-secondary-tint: #2ec95c;
--ion-color-tertiary: #f4a942;
--ion-color-tertiary-contrast: #fff;
--ion-color-tertiary-rgb: '244,169,66';
--ion-color-tertiary-contrast: #ffffff;
--ion-color-tertiary-rgb: "244,169,66";
--ion-color-tertiary-shade: #d6903d;
--ion-color-tertiary-tint: #ffa529;
--ion-color-success: #10dc60;
--ion-color-success-contrast: #fff;
--ion-color-success-rgb: '16,220,96';
--ion-color-success-contrast: #ffffff;
--ion-color-success-rgb: "16,220,96";
--ion-color-success-shade: #10cb60;
--ion-color-success-tint: #23df6d;
--ion-color-warning: #ffce00;
--ion-color-warning-contrast: #000;
--ion-color-warning-rgb: '255,206,0';
--ion-color-warning-contrast: #000000;
--ion-color-warning-rgb: "255,206,0";
--ion-color-warning-shade: #f1c100;
--ion-color-warning-tint: #ffd214;
--ion-color-danger: #f53d3d;
--ion-color-danger-contrast: #fff;
--ion-color-danger-rgb: '245,61,61';
--ion-color-danger-contrast: #ffffff;
--ion-color-danger-rgb: "245,61,61";
--ion-color-danger-shade: #d83636;
--ion-color-danger-tint: #e13838;
--ion-color-light: #f4f4f4;
--ion-color-light-contrast: #000;
--ion-color-light-rgb: '244,244,244';
--ion-color-light-contrast: #000000;
--ion-color-light-rgb: "244,244,244";
--ion-color-light-shade: #d7d7d7;
--ion-color-light-tint: #e0e0e0;
--ion-color-medium: #989aa2;
--ion-color-medium-contrast: #000;
--ion-color-medium-rgb: '152,154,162';
--ion-color-medium-contrast: #000000;
--ion-color-medium-rgb: "152,154,162";
--ion-color-medium-shade: #8c8e95;
--ion-color-medium-tint: #86888f;
--ion-color-dark: #222;
--ion-color-dark-contrast: #fff;
--ion-color-dark-rgb: '34,34,34';
--ion-color-dark: #222222;
--ion-color-dark-contrast: #ffffff;
--ion-color-dark-rgb: "34,34,34";
--ion-color-dark-shade: #343434;
--ion-color-dark-tint: #3d3d3d;
--ion-backdrop-color: #000;
--ion-background-color: #fff;
--ion-background-color-rgb: '255,255,255';
--ion-backdrop-color: #000000;
--ion-background-color: #ffffff;
--ion-background-color-rgb: "255,255,255";
--ion-background-color-step-100: #f2f2f2;
--ion-background-color-step-200: #dcdcdc;
--ion-background-color-step-300: #bdbdbd;
--ion-background-color-step-400: #444;
--ion-background-color-step-400: #444444;
--ion-border-color: #b2b2b2;
--ion-box-shadow-color: #000;
--ion-text-color: #000;
--ion-text-color-rgb: '0,0,0';
--ion-text-color-step-100: #222;
--ion-text-color-step-200: #666;
--ion-text-color-step-300: #999;
--ion-box-shadow-color: #000000;
--ion-text-color: #000000;
--ion-text-color-rgb: "0,0,0";
--ion-text-color-step-100: #222222;
--ion-text-color-step-200: #666666;
--ion-text-color-step-300: #999999;
--ion-text-color-step-400: #c5c5c5;
--ion-tabbar-background-color: #f8f8f8;
--ion-tabbar-border-color: var(--ion-border-color);
--ion-tabbar-border-color: #b2b2b2;
--ion-tabbar-text-color: #8c8c8c;
--ion-tabbar-text-color-active: #488aff;
--ion-toolbar-background-color: #f8f8f8;
--ion-toolbar-border-color: var(--ion-border-color);
--ion-toolbar-border-color: #b2b2b2;
--ion-toolbar-color-active: #488aff;
--ion-toolbar-color-inactive: #8c8c8c;
--ion-toolbar-text-color: var(--ion-text-color);
--ion-item-background-color: var(--ion-background-color);
--ion-item-background-color-active: var(--ion-background-color);
--ion-toolbar-text-color: #000000;
--ion-item-background-color: #ffffff;
--ion-item-background-color-active: #ffffff;
--ion-item-border-color: #c8c7cc;
--ion-item-text-color: var(--ion-text-color);
--ion-placeholder-text-color: #999;
/* Material Design */
--ion-color-md-light: #e3e3e3;
--ion-color-md-light-contrast: #000;
--ion-color-md-light-rgb: '227,227,227';
--ion-color-md-light-shade: #d0d0d0;
--ion-color-md-light-tint: #f0f0f0;
--ion-border-md-color: #c1c4cd;
--ion-text-md-color-step-200: var(--ion-text-color-step-200);
--ion-tabbar-md-border-color: rgba(0, 0, 0, .07);
--ion-tabbar-md-text-color: var(--ion-text-md-color-step-200);
--ion-toolbar-md-border-color: var(--ion-border-md-color);
--ion-toolbar-md-text-color: #424242;
--ion-item-md-background-color-active: #f1f1f1;
--ion-item-md-border-color: #dedede;
/* iOS */
--ion-color-ios-primary: var(--ion-color-primary);
--ion-background-ios-color: var(--ion-background-color);
--ion-background-ios-color-step-100: #f7f7f7;
--ion-background-ios-color-step-200: #bdbdbd;
--ion-background-ios-color-step-300: #999;
--ion-background-ios-color-step-400: #222;
--ion-text-ios-color-step-100: #222;
--ion-text-ios-color-step-200: #666;
--ion-text-ios-color-step-300: #8f8f8f;
--ion-text-ios-color-step-400: #aeaeae;
--ion-border-ios-color: #c1c4cd;
--ion-tabbar-ios-translucent-background-color: rgba(248, 248, 248, 0.8);
--ion-tabbar-ios-border-color: rgba(0, 0, 0, .2);
--ion-tabbar-ios-text-color-active: var(--ion-color-ios-primary);
--ion-toolbar-ios-translucent-background-color: rgba(248, 248, 248, 0.8);
--ion-toolbar-ios-border-color: rgba(0, 0, 0, .2);
--ion-toolbar-ios-color-active: var(--ion-color-ios-primary);
--ion-item-ios-background-color: var(--ion-background-ios-color);
--ion-item-ios-background-color-active: #d9d9d9;
--ion-item-text-color: #000000;
--ion-placeholder-text-color: #999999;
}