mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-18 19:21:34 +08:00
chore(packages): move the packages to root
This commit is contained in:
186
core/scripts/theme-builder/src/components/Color.ts
Normal file
186
core/scripts/theme-builder/src/components/Color.ts
Normal 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}`;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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};
|
@ -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>
|
||||
];
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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};
|
@ -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);
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
|
||||
select {
|
||||
margin: 10px 0 0px 10px;
|
||||
}
|
@ -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};
|
@ -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>
|
||||
];
|
||||
}
|
||||
}
|
23
core/scripts/theme-builder/src/components/helpers.ts
Normal file
23
core/scripts/theme-builder/src/components/helpers.ts
Normal 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';
|
@ -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;
|
||||
}
|
@ -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};
|
@ -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>
|
||||
];
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
@ -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`;
|
@ -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());
|
||||
}
|
||||
}
|
||||
};
|
@ -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);
|
||||
}
|
@ -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>
|
||||
];
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user