chore(packages): move the packages to root

This commit is contained in:
Brandy Carney
2018-03-12 16:02:25 -04:00
parent 097f1a2cd3
commit d37623a2ca
1255 changed files with 38 additions and 38 deletions

View File

@ -0,0 +1,186 @@
interface RGB {
b: number
g: number,
r: number,
}
interface HSL {
h: number,
l: number
s: 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) {
l = Math.round(l * 255);
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 = Math.min(1, Math.max(0, (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 hsl: HSL;
readonly rgb: RGB;
readonly yiq: number;
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);
}
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());
}
contrast (threshold: number = 128): Color {
return new Color((this.yiq >= threshold ? '#000' : '#fff'));
}
mix (from: string | RGB | HSL | Color, amount: number = .5): Color {
const base: Color = from instanceof Color ? from : new Color(from);
return new Color(mixColors(this, base, amount));
}
shade (weight: number = .12): Color {
return this.mix({r: 0, g: 0, b: 0}, weight);
}
tint (weight: number = .1): Color {
return this.mix({r: 255, g: 255, b: 255}, weight);
}
toList (): string {
const {r, g, b}: RGB = this.rgb;
return `${r},${g},${b}`;
}
}

View File

@ -0,0 +1,15 @@
div {
box-sizing: border-box;
width: 100%;
height: calc(100% - 20px);
padding: 10px;
}
iframe {
box-sizing: border-box;
display: block;
width: 100%;
height: 100%;
border: 1px solid gray;
}

View File

@ -0,0 +1,61 @@
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

@ -0,0 +1,123 @@
import { Component, Event, EventEmitter, Listen, Prop, Watch } from '@stencil/core';
import { Color } from '../Color';
@Component({
tag: 'app-preview',
styleUrl: 'app-preview.css',
shadow: true
})
export class AppPreview {
@Prop() cssText: string;
@Prop() demoMode: string;
@Prop() demoUrl: string;
hasIframeListener: boolean = false;
@Prop() hoverProperty: string;
iframe: HTMLIFrameElement;
@Event() propertiesUsed: EventEmitter;
applyStyles () {
if (this.iframe && this.iframe.contentDocument && this.iframe.contentDocument.documentElement) {
const iframeDoc = this.iframe.contentDocument;
const themerStyleId = 'themer-style';
let themerStyle: HTMLStyleElement = iframeDoc.getElementById(themerStyleId) as any;
if (!themerStyle) {
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;
}
}
@Watch('cssText')
onCssTextChange () {
console.log('AppPreview onCssTextChange');
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');
}
}
}
onIframeLoad () {
this.applyStyles();
this.iframe.contentDocument.documentElement.addEventListener('mousemove', this.onIframeMouseMove.bind(this));
}
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.keyCode === 17) {
const el: HTMLElement = this.iframe.contentDocument.documentElement;
el.classList.remove('theme-property-searching');
this.propertiesUsed.emit({
properties: []
});
}
}
render () {
const url = `${this.demoUrl}?ionicplatform=${this.demoMode}`;
return [
<div>
<iframe src={url} ref={el => this.iframe = el as any} onLoad={this.onIframeLoad.bind(this)}></iframe>
</div>
];
}
}

View File

@ -0,0 +1,29 @@
h1 {
margin: 10px 0 0 0;
padding: 0;
font-size: 14px;
}
textarea {
margin: 12px 0 0 0;
min-width: 360px;
height: 680px;
font-family: Courier New, Courier, monospace;
}
button {
margin: 10px 10px 0 0;
padding: 10px 15px;
font-size: 14px;
cursor: pointer;
}
.instructions {
font-size: 12px;
width: 300px;
}
.instructions p {
padding-left: 8px;
}

View File

@ -0,0 +1,85 @@
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 {deleteCssUrl, getThemeUrl, saveCssUrl, STORED_THEME_KEY} 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

@ -0,0 +1,96 @@
import { Component, Element, Prop } from '@stencil/core';
import { deleteCssUrl, getThemeUrl, saveCssUrl, STORED_THEME_KEY } from '../helpers';
@Component({
tag: 'css-text',
styleUrl: 'css-text.css'
})
export class CssText {
@Prop() cssText: string;
@Element() el: HTMLElement;
@Prop() themeName: string;
createNew (ev: UIEvent) {
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: UIEvent) {
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 [
<h1>
{getThemeUrl(this.themeName)}
</h1>,
<div>
<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>
</div>,
<div class="instructions">
<h2>Instructions</h2>
<p>Primary CSS Properties will highlight on hover.</p>
<p><b>CTRL + Hover: Property</b><br/> Will visibly toggle color in preview.</p>
<p><b>CTRL + Hover: Preview</b><br/>Will visibly highlight properties used under the mouse.</p>
<p><b>ALT + Double Click: Primary Property</b><br/>Auto generate steps or shade/tint/contrast
variations.</p>
</div>
];
}
saveCss (themeName: string, cssText: string) {
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);
});
}
submitUpdate (ev: UIEvent) {
ev.stopPropagation();
ev.preventDefault();
this.saveCss(this.themeName, this.cssText);
}
}

View File

@ -0,0 +1,4 @@
select {
margin: 10px 0 0px 10px;
}

View File

@ -0,0 +1,54 @@
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

@ -0,0 +1,41 @@
import { Component, Event, EventEmitter, Prop } from '@stencil/core';
@Component({
tag: 'demo-selection',
styleUrl: 'demo-selection.css',
shadow: true
})
export class DemoSelection {
@Prop() demoData: { name: string, url: string }[];
@Prop() demoMode: string;
@Event() demoModeChange: EventEmitter;
@Prop() demoUrl: string;
@Event() demoUrlChange: EventEmitter;
onChangeMode (ev) {
this.demoModeChange.emit(ev.currentTarget.value);
}
onChangeUrl (ev) {
this.demoUrlChange.emit(ev.currentTarget.value);
}
render () {
return [
<div>
<select onChange={this.onChangeUrl.bind(this)}>
{this.demoData.map(d => <option value={d.url} selected={d.url === this.demoUrl}>{d.name}</option>)}
</select>
<select onChange={this.onChangeMode.bind(this)}>
<option value="md" selected={'md' === this.demoMode}>md</option>
<option value="ios" selected={'ios' === this.demoMode}>ios</option>
</select>
</div>
];
}
}

View File

@ -0,0 +1,23 @@
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`;
export function saveCssUrl (themeName: string, cssText: string) {
cssText = encodeURIComponent(cssText);
return `${SAVE_CSS_URL}?theme=${themeName}&css=${cssText}`;
}
export function deleteCssUrl (themeName: string) {
return `${DELETE_CSS_URL}?theme=${themeName}`;
}
export function getThemeUrl (themeName: string) {
return `${CSS_THEME_FILE_PATH}/${themeName}.css`;
}
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';

View File

@ -0,0 +1,24 @@
main {
display: flex;
height: calc(100% - 10px);
}
main > section {
flex: 1 auto;
}
.preview-column {
max-width: 480px;
min-height: 700px;
}
.selector-column {
max-width: 600px;
height: 100%;
overflow-y: scroll;
}
#css-proxy {
display: none;
}

View File

@ -0,0 +1,96 @@
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

@ -0,0 +1,100 @@
import { Component, Listen, State } from '@stencil/core';
import { DATA_URL, STORED_DEMO_MODE_KEY, STORED_DEMO_URL_KEY } from '../helpers';
@Component({
tag: 'theme-builder',
styleUrl: 'theme-builder.css',
shadow: true
})
export class ThemeBuilder {
@State() cssText: string = '';
demoData: { name: string, url: string }[];
@State() demoMode: string;
@State() demoUrl: string;
@State() hoverProperty: string;
@State() propertiesUsed: string[];
themeData: { name: string }[];
@State() themeName: string = '';
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;
}
@Listen('demoModeChange')
onDemoModeChange (ev) {
this.demoMode = ev.detail;
localStorage.setItem(STORED_DEMO_MODE_KEY, this.demoMode);
}
@Listen('demoUrlChange')
onDemoUrlChange (ev) {
this.demoUrl = ev.detail;
localStorage.setItem(STORED_DEMO_URL_KEY, this.demoUrl);
}
@Listen('propertiesUsed')
onPropertiesUsed (ev) {
this.propertiesUsed = ev.detail.properties;
}
@Listen('propertyHoverStart')
onPropertyHoverStart (ev) {
this.hoverProperty = ev.detail.property;
}
@Listen('propertyHoverStop')
onPropertyHoverStop () {
this.hoverProperty = undefined;
}
@Listen('themeCssChange')
onThemeCssChange (ev) {
this.cssText = ev.detail.cssText;
this.themeName = ev.detail.themeName;
console.log('ThemeBuilder themeCssChange', this.themeName);
}
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}
hoverProperty={this.hoverProperty}></app-preview>
</section>
<section class="selector-column">
<theme-selector themeData={this.themeData} propertiesUsed={this.propertiesUsed}></theme-selector>
</section>
<section>
<css-text themeName={this.themeName} cssText={this.cssText}></css-text>
</section>
</main>
];
}
}

View File

@ -0,0 +1,133 @@
select {
margin: 10px 0 0 10px;
}
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;
box-shadow: rgba(0, 0, 0, .5) 1px 1px;
}
.color-buttons button:hover {
opacity: .8;
}
.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;
}
.checkbox {
padding: 8px 8px 0 0;
font-size: 10px;
color: #333333;
width: 100px;
}
.settings {
display: flex;
flex-direction: column;
}
.settings .row {
display: flex;
flex-direction: row;
justify-content: flex-start;
}
.right {
display: flex;
flex-direction: column;
justify-content: flex-end;
}

View File

@ -0,0 +1,142 @@
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

@ -0,0 +1,386 @@
import { Component, Element, Event, EventEmitter, Listen, Prop, State } from '@stencil/core';
import { ComputedType, THEME_VARIABLES, ThemeVariable } from '../../theme-variables';
import { Color } from '../Color';
import { COLOR_URL, getThemeUrl, STORED_THEME_KEY } from '../helpers';
const PLACEHOLDER_COLOR = '#ff00ff';
@Component({
tag: 'theme-selector',
styleUrl: 'theme-selector.css'
})
export class ThemeSelector {
@Element() el: HTMLThemeSelectorElement;
@State() generateContrast: boolean = false;
@State() generateSteps: boolean = true;
@State() generateVariations: boolean = true;
@State() palettes: any[];
@Prop() propertiesUsed: string[] = [];
@Event() propertyHoverStart: EventEmitter;
@Event() propertyHoverStop: EventEmitter;
@State() searchMode: boolean;
@State() showSteps: boolean = true;
@Event() themeCssChange: EventEmitter;
@Prop() themeData: { name: string }[];
@State() themeName: string;
@State() themeVariables: ThemeVariable[] = [];
private currentHoveredProperty: string;
private cssHolder: HTMLStyleElement;
private proxyElement: HTMLElement;
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.updateComputed(property);
this.generateCss();
}
async componentDidLoad () {
const storedThemeName = localStorage.getItem(STORED_THEME_KEY);
const defaultThemeName = this.themeData[0].name;
this.cssHolder = document.createElement('style');
this.el.insertBefore(this.cssHolder, this.el.firstChild);
this.themeName = storedThemeName || defaultThemeName;
await this.loadThemeCss();
}
generateCss () {
console.log('ThemeSelector generateCss', this.themeName);
const c: string[] = [];
c.push(`/** ${this.themeName} theme **/`);
c.push(`\n`);
c.push(':root {');
this.themeVariables.forEach(themeVariable => {
const variableValue = themeVariable.value,
value = variableValue instanceof Color ? variableValue.hex : variableValue;
c.push(` ${themeVariable.property}: ${value};`);
this.el.style.setProperty(themeVariable.property, value.toString());
});
c.push('}');
const cssText = c.join('\n');
this.themeCssChange.emit({
cssText: cssText,
themeName: this.themeName
});
}
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
});
}
}
}
isVariableDependentOn (property: string, variable: ThemeVariable) {
const params = (variable.computed && variable.computed && variable.computed.params) || {};
if (!property) return true;
return (variable.property === property || params.from === property || params.property === property);
}
async loadThemeCss () {
console.log('ThemeSelector loadThemeCss');
const themeUrl = getThemeUrl(this.themeName);
const css = await fetch(themeUrl).then(r => r.text());
this.parseCss(css);
this.generateCss();
}
async onChangeUrl (ev) {
this.themeName = ev.currentTarget.value;
localStorage.setItem(STORED_THEME_KEY, this.themeName);
await this.loadThemeCss();
}
@Listen('colorChange')
onColorChange (ev) {
console.log('ThemeSelector colorChange');
this.changeColor(ev.detail.property, ev.detail.value);
}
onColorClick (ev: MouseEvent) {
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.toLowerCase());
}
@Listen('generateColors')
onGenerateColors (ev) {
const color: Color = ev.detail.color,
steps: Boolean = ev.detail.steps,
property = ev.detail.property;
if (color && property) {
if (steps) {
this.themeVariables.filter((variable: ThemeVariable) => {
const type: ComputedType = variable.computed && variable.computed.type,
params = (variable.computed && variable.computed.params) || {};
if (type === ComputedType.step && params.property === property) {
return variable;
}
}).forEach((variable: ThemeVariable) => {
const params: any = variable.computed.params || {},
stepFromVariable: ThemeVariable = this.themeVariables.find(themeVariable => themeVariable.property === params.from),
stepFromValue = stepFromVariable && stepFromVariable.value;
if (stepFromValue instanceof Color) {
variable.value = color.mix(stepFromValue, params.amount);
}
});
} else {
this.updateVariations(property, color);
}
this.generateCss();
(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();
}
}
parseCss (css: string) {
console.log('ThemeSelector parseCss');
this.cssHolder.innerHTML = css.replace(':root', `#${this.proxyElement.id}`);
const computed = window.getComputedStyle(this.proxyElement);
this.themeVariables = THEME_VARIABLES.map(themeVariable => {
const value = (computed.getPropertyValue(themeVariable.property) || PLACEHOLDER_COLOR),
type: string = (themeVariable.computed && themeVariable.computed.type) || (!Color.isColor(value) ? 'percent' : 'color');
return Object.assign({}, themeVariable, {
property: themeVariable.property.trim(),
value: type === 'color' || type === ComputedType.step ? new Color(value) : type === 'percent' ? parseFloat(value) : value
});
});
}
render () {
const
onColorClick = this.onColorClick.bind(this),
variables = <section>
{
this.themeVariables
.filter(d => !(d.computed && (d.computed.type === ComputedType.rgblist || (d.computed.type === ComputedType.step && !this.showSteps))))
.map(d => {
const computedReferences: ComputedType[] = this.themeVariables
.filter(variable => variable.computed && variable.computed.params && variable.computed.params.property === d.property)
.map(variable => variable.computed.type);
return <variable-selector
class={{'is-primary': !!computedReferences.length, used: this.propertiesUsed.indexOf(d.property) >= 0}}
property={d.property} value={d.value}
usedWith={Array.from(new Set(computedReferences))}></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">
{this.themeVariables
.filter(variable => !!variable.quickPick)
.map(variable => <button onClick={onColorClick}
data-property={variable.property}
style={{backgroundColor: `var(${variable.property})`}}>{variable.quickPick.text}</button>)
}
</div>
</div>)}
</div>)
}
</div>
</section>;
return [
<div id="css-proxy" ref={el => this.proxyElement = el}></div>,
<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>
<div class="right">
<button type="button" class="search-toggle"
onClick={this.toggleSearchMode.bind(this)}>{this.searchMode ? 'Close' : 'Open'} Search
</button>
<div class="settings">
<div class="row">
<div class="checkbox">
<input type="checkbox" id="generateContrast" checked={this.generateContrast}
onChange={this.toggleCheck.bind(this, 'generateContrast')}></input>
<label>Auto Contrast</label>
</div>
<div class="checkbox">
<input type="checkbox" id="generateVariations" checked={this.generateVariations}
onChange={this.toggleCheck.bind(this, 'generateVariations')}></input>
<label>Auto Shade/Tint</label>
</div>
</div>
<div class="row">
<div class="checkbox">
<input type="checkbox" id="generateSteps" checked={this.generateSteps}
onChange={this.toggleCheck.bind(this, 'generateSteps')}></input>
<label>Auto Steps</label>
</div>
<div class="checkbox">
<input type="checkbox" id="showSteps" checked={this.showSteps}
onChange={this.toggleCheck.bind(this, 'showSteps')}></input>
<label>Show Steps</label>
</div>
</div>
</div>
</div>
</div>
{this.searchMode ? search : variables}
</div>
];
}
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}`).then(r => r.json()) || [];
} catch (e) {
this.palettes = [];
}
}
toggleCheck (param: string, ev: Event) {
this[param] = (ev.target as HTMLInputElement).checked;
}
toggleSearchMode () {
this.searchMode = !this.searchMode;
}
updateComputed (property?: string) {
this.themeVariables
.filter(themeVariable => !!themeVariable.computed && this.isVariableDependentOn(property, themeVariable))
.forEach(themeVariable => {
const computed = themeVariable.computed,
type: ComputedType = computed.type,
params = computed.params || {};
if (type === ComputedType.rgblist) {
const referenceVariable: ThemeVariable = this.themeVariables.find(themeVariable => themeVariable.property === params.property),
value = referenceVariable && referenceVariable.value;
if (value instanceof Color) {
themeVariable.value = value.toList();
}
} else if (this.generateSteps && type === ComputedType.step) {
const referenceVariable: ThemeVariable = this.themeVariables.find(themeVariable => themeVariable.property === params.property),
stepFromVariable: ThemeVariable = this.themeVariables.find(themeVariable => themeVariable.property === params.from),
referenceValue = referenceVariable && referenceVariable.value,
fromValue = stepFromVariable && stepFromVariable.value;
if (referenceValue instanceof Color && (typeof(fromValue) === 'string' || fromValue instanceof Color)) {
themeVariable.value = referenceValue.mix(fromValue, params.amount);
}
}
});
if (this.generateVariations) {
this.themeVariables
.filter(themeVariable => !themeVariable.computed && this.isVariableDependentOn(property, themeVariable))
.forEach(themeVariable => {
const property: string = themeVariable.property,
color: Color = themeVariable.value as Color;
this.updateVariations(property, color);
});
}
}
updateVariations (property: string, color: Color) {
const tint: ThemeVariable = this.themeVariables.find((variable: ThemeVariable) => variable.property === `${property}-tint`),
shade: ThemeVariable = this.themeVariables.find((variable: ThemeVariable) => variable.property === `${property}-shade`);
tint && (tint.value = color.tint());
shade && (shade.value = color.shade());
if (this.generateContrast) {
const contrast: ThemeVariable = this.themeVariables.find((variable: ThemeVariable) => variable.property === `${property}-contrast`);
contrast && (contrast.value = color.contrast());
}
}
};

View File

@ -0,0 +1,52 @@
section {
display: flex;
}
.invalid {
background: #f0b2b1;
}
.property-label {
font-family: Courier New, Courier, monospace;
white-space: nowrap;
cursor: pointer;
flex: 1;
padding-left: 10px;
}
input[type="text"] {
font-family: Courier New, Courier, monospace;
font-size: 14px;
width: 150px;
}
input[type="color"] {
-webkit-appearance: none;
border: none;
width: 64px;
height: 20px;
outline: none;
background: transparent;
}
input[type="color"]::-webkit-color-swatch-wrapper {
padding: 0;
}
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));
}
:host(.used) section .property-label {
outline: 1px solid rgba(0, 0, 0, .5);
}
:host(.is-primary) .property-label:hover {
background-color: rgba(255, 75, 24, 0.16);
}

View File

@ -0,0 +1,95 @@
import { Component, Element, Event, EventEmitter, Listen, Method, Prop } from '@stencil/core';
import { ComputedType } from '../../theme-variables';
import { Color } from '../Color';
@Component({
tag: 'variable-selector',
styleUrl: 'variable-selector.css',
shadow: true
})
export class VariableSelector {
@Event() colorChange: EventEmitter;
@Element() el: HTMLElement;
@Event() generateColors: EventEmitter;
@Prop() isRgb: boolean;
@Prop() property: string;
@Prop() type: 'color' | 'percent';
@Prop() usedWith: string[];
@Prop({ mutable: true }) value: Color | string | number;
@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 (this.usedWith) {
this.generateColors.emit({
color,
steps: this.usedWith.indexOf(ComputedType.step) >= 0,
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>
];
}
}