mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
feat(themes): theme builder app
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -18,6 +18,7 @@ dist/
|
||||
node_modules/
|
||||
tmp/
|
||||
temp/
|
||||
packages/core/theme-builder/
|
||||
$RECYCLE.BIN/
|
||||
|
||||
.DS_Store
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"@types/jest": "^21.1.6",
|
||||
"chai": "^4.1.2",
|
||||
"chromedriver": "^2.33.2",
|
||||
"glob": "^7.1.2",
|
||||
"ionicons": "4.0.0-11",
|
||||
"jest": "^21.2.1",
|
||||
"mocha": "^4.0.1",
|
||||
@@ -36,6 +37,9 @@
|
||||
"clean": "rm -rf dist",
|
||||
"lint": "npm run tslint & npm run sass-lint",
|
||||
"sass-lint": "sass-lint -v -q",
|
||||
"theme-app-build": "stencil build --dev --config scripts/theme-builder/stencil.config.js",
|
||||
"theme-server": "node scripts/theme-builder/server.js",
|
||||
"theme-builder": "npm run theme-app-build && sd concurrent \"stencil build --dev --watch\" \"stencil-dev-server\" \"npm run theme-server\" ",
|
||||
"tslint": "tslint --project .",
|
||||
"tslint-fix": "tslint --project . --fix",
|
||||
"validate": "npm run clean && npm run lint && npm run test && npm run build",
|
||||
|
||||
3
packages/core/scripts/theme-builder/readme.md
Normal file
3
packages/core/scripts/theme-builder/readme.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Theme Builder
|
||||
|
||||
npm run theme-builder
|
||||
141
packages/core/scripts/theme-builder/server.js
Normal file
141
packages/core/scripts/theme-builder/server.js
Normal file
@@ -0,0 +1,141 @@
|
||||
const http = require('http');
|
||||
const url = require('url');
|
||||
const glob = require('glob');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
|
||||
const port = 5454;
|
||||
const componentsPath = '/src/components/';
|
||||
const cssPath = '/src/themes/css/';
|
||||
const srcComponentsDir = path.join(__dirname, '../../', componentsPath);
|
||||
const srcCssDir = path.join(__dirname, '../../', cssPath);
|
||||
|
||||
|
||||
function requestHandler(request, response) {
|
||||
const parsedUrl = url.parse(request.url, true);
|
||||
|
||||
response.setHeader('Access-Control-Allow-Origin', '*');
|
||||
response.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
|
||||
|
||||
if (parsedUrl.pathname === '/data') {
|
||||
requestDataHandler(response);
|
||||
|
||||
} else if (parsedUrl.pathname === '/save-css') {
|
||||
requestSaveCssHandler(parsedUrl, response);
|
||||
|
||||
} else if (parsedUrl.pathname === '/delete-css') {
|
||||
requestDeleteCssHandler(parsedUrl, response);
|
||||
|
||||
} else {
|
||||
response.end();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function requestDataHandler(response) {
|
||||
try {
|
||||
const demoPaths = glob.sync('**/index.html', {
|
||||
cwd: srcComponentsDir
|
||||
});
|
||||
|
||||
const demos = demoPaths.map(demo => {
|
||||
return {
|
||||
name: demo.toLowerCase()
|
||||
.replace(/\\/g, ' ')
|
||||
.replace(/\//g, ' ')
|
||||
.replace(/ test/g, '')
|
||||
.replace(/ index.html/g, ''),
|
||||
url: componentsPath + demo.replace(/\\/g, '/')
|
||||
};
|
||||
}).sort((a, b) => {
|
||||
if (a.name.toLowerCase() < b.name.toLowerCase()) return -1;
|
||||
if (a.name.toLowerCase() > b.name.toLowerCase()) return 1;
|
||||
return 0
|
||||
});
|
||||
|
||||
const themePaths = glob.sync('**/*.css', {
|
||||
cwd: srcCssDir
|
||||
});
|
||||
|
||||
|
||||
const themes = themePaths.map(theme => {
|
||||
return {
|
||||
name: theme.replace(/.css/g, '')
|
||||
};
|
||||
}).sort((a, b) => {
|
||||
if (a.name.toLowerCase() < b.name.toLowerCase()) return -1;
|
||||
if (a.name.toLowerCase() > b.name.toLowerCase()) return 1;
|
||||
return 0
|
||||
});
|
||||
|
||||
const data = JSON.stringify({
|
||||
demos: demos,
|
||||
themes: themes
|
||||
}, null, 2);
|
||||
|
||||
response.end(data, 'utf8');
|
||||
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
response.end('err: ' + e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function requestSaveCssHandler(parsedUrl, response) {
|
||||
try {
|
||||
const theme = (parsedUrl.query.theme || '').toLowerCase().trim();
|
||||
if (!theme) {
|
||||
response.end('missing theme querystring');
|
||||
return;
|
||||
}
|
||||
|
||||
const filePath = path.join(srcCssDir, theme + '.css');
|
||||
const css = parsedUrl.query.css || '';
|
||||
|
||||
fs.writeFileSync(filePath, css, { encoding: 'utf8' });
|
||||
|
||||
console.log('css saved!', filePath);
|
||||
|
||||
response.end('css saved! ' + filePath, 'utf8');
|
||||
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
response.end('err: ' + e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function requestDeleteCssHandler(parsedUrl, response) {
|
||||
try {
|
||||
const theme = (parsedUrl.query.theme || '').toLowerCase().trim();
|
||||
if (!theme) {
|
||||
response.end('missing theme querystring');
|
||||
return;
|
||||
}
|
||||
|
||||
const filePath = path.join(srcCssDir, theme + '.css');
|
||||
|
||||
fs.unlinkSync(filePath);
|
||||
|
||||
console.log('css deleted!', filePath);
|
||||
|
||||
response.end('css deleted! ' + filePath, 'utf8');
|
||||
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
response.end('err: ' + e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const server = http.createServer(requestHandler);
|
||||
|
||||
server.listen(port, (err) => {
|
||||
if (err) {
|
||||
return console.log(__filename, err);
|
||||
}
|
||||
|
||||
console.log(`theme server: http://localhost:${port}/`);
|
||||
});
|
||||
193
packages/core/scripts/theme-builder/src/components.d.ts
vendored
Normal file
193
packages/core/scripts/theme-builder/src/components.d.ts
vendored
Normal file
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* This is an autogenerated file created by the Stencil build process.
|
||||
* It contains typing information for all components that exist in this project
|
||||
* and imports for stencil collections that might be configured in your stencil.config.js file
|
||||
*/
|
||||
|
||||
|
||||
import {
|
||||
AppPreview as AppPreview
|
||||
} from './components/app-preview/app-preview';
|
||||
|
||||
declare global {
|
||||
interface HTMLAppPreviewElement extends AppPreview, HTMLElement {
|
||||
}
|
||||
var HTMLAppPreviewElement: {
|
||||
prototype: HTMLAppPreviewElement;
|
||||
new (): HTMLAppPreviewElement;
|
||||
};
|
||||
interface HTMLElementTagNameMap {
|
||||
"app-preview": HTMLAppPreviewElement;
|
||||
}
|
||||
interface ElementTagNameMap {
|
||||
"app-preview": HTMLAppPreviewElement;
|
||||
}
|
||||
namespace JSX {
|
||||
interface IntrinsicElements {
|
||||
"app-preview": JSXElements.AppPreviewAttributes;
|
||||
}
|
||||
}
|
||||
namespace JSXElements {
|
||||
export interface AppPreviewAttributes extends HTMLAttributes {
|
||||
cssText?: string;
|
||||
demoMode?: string;
|
||||
demoUrl?: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
import {
|
||||
ColorSelector as ColorSelector
|
||||
} from './components/color-selector/color-selector';
|
||||
|
||||
declare global {
|
||||
interface HTMLColorSelectorElement extends ColorSelector, HTMLElement {
|
||||
}
|
||||
var HTMLColorSelectorElement: {
|
||||
prototype: HTMLColorSelectorElement;
|
||||
new (): HTMLColorSelectorElement;
|
||||
};
|
||||
interface HTMLElementTagNameMap {
|
||||
"color-selector": HTMLColorSelectorElement;
|
||||
}
|
||||
interface ElementTagNameMap {
|
||||
"color-selector": HTMLColorSelectorElement;
|
||||
}
|
||||
namespace JSX {
|
||||
interface IntrinsicElements {
|
||||
"color-selector": JSXElements.ColorSelectorAttributes;
|
||||
}
|
||||
}
|
||||
namespace JSXElements {
|
||||
export interface ColorSelectorAttributes extends HTMLAttributes {
|
||||
isRgb?: boolean;
|
||||
property?: string;
|
||||
value?: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
import {
|
||||
CssText as CssText
|
||||
} from './components/css-text/css-text';
|
||||
|
||||
declare global {
|
||||
interface HTMLCssTextElement extends CssText, HTMLElement {
|
||||
}
|
||||
var HTMLCssTextElement: {
|
||||
prototype: HTMLCssTextElement;
|
||||
new (): HTMLCssTextElement;
|
||||
};
|
||||
interface HTMLElementTagNameMap {
|
||||
"css-text": HTMLCssTextElement;
|
||||
}
|
||||
interface ElementTagNameMap {
|
||||
"css-text": HTMLCssTextElement;
|
||||
}
|
||||
namespace JSX {
|
||||
interface IntrinsicElements {
|
||||
"css-text": JSXElements.CssTextAttributes;
|
||||
}
|
||||
}
|
||||
namespace JSXElements {
|
||||
export interface CssTextAttributes extends HTMLAttributes {
|
||||
cssText?: string;
|
||||
themeName?: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
import {
|
||||
DemoSelection as DemoSelection
|
||||
} from './components/demo-selection/demo-selection';
|
||||
|
||||
declare global {
|
||||
interface HTMLDemoSelectionElement extends DemoSelection, HTMLElement {
|
||||
}
|
||||
var HTMLDemoSelectionElement: {
|
||||
prototype: HTMLDemoSelectionElement;
|
||||
new (): HTMLDemoSelectionElement;
|
||||
};
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-selection": HTMLDemoSelectionElement;
|
||||
}
|
||||
interface ElementTagNameMap {
|
||||
"demo-selection": HTMLDemoSelectionElement;
|
||||
}
|
||||
namespace JSX {
|
||||
interface IntrinsicElements {
|
||||
"demo-selection": JSXElements.DemoSelectionAttributes;
|
||||
}
|
||||
}
|
||||
namespace JSXElements {
|
||||
export interface DemoSelectionAttributes extends HTMLAttributes {
|
||||
demoData?: { name: string, url: string }[];
|
||||
demoMode?: string;
|
||||
demoUrl?: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
import {
|
||||
ThemeBuilder as ThemeBuilder
|
||||
} from './components/theme-builder/theme-builder';
|
||||
|
||||
declare global {
|
||||
interface HTMLThemeBuilderElement extends ThemeBuilder, HTMLElement {
|
||||
}
|
||||
var HTMLThemeBuilderElement: {
|
||||
prototype: HTMLThemeBuilderElement;
|
||||
new (): HTMLThemeBuilderElement;
|
||||
};
|
||||
interface HTMLElementTagNameMap {
|
||||
"theme-builder": HTMLThemeBuilderElement;
|
||||
}
|
||||
interface ElementTagNameMap {
|
||||
"theme-builder": HTMLThemeBuilderElement;
|
||||
}
|
||||
namespace JSX {
|
||||
interface IntrinsicElements {
|
||||
"theme-builder": JSXElements.ThemeBuilderAttributes;
|
||||
}
|
||||
}
|
||||
namespace JSXElements {
|
||||
export interface ThemeBuilderAttributes extends HTMLAttributes {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
import {
|
||||
ThemeSelector as ThemeSelector
|
||||
} from './components/theme-selector/theme-selector';
|
||||
|
||||
declare global {
|
||||
interface HTMLThemeSelectorElement extends ThemeSelector, HTMLElement {
|
||||
}
|
||||
var HTMLThemeSelectorElement: {
|
||||
prototype: HTMLThemeSelectorElement;
|
||||
new (): HTMLThemeSelectorElement;
|
||||
};
|
||||
interface HTMLElementTagNameMap {
|
||||
"theme-selector": HTMLThemeSelectorElement;
|
||||
}
|
||||
interface ElementTagNameMap {
|
||||
"theme-selector": HTMLThemeSelectorElement;
|
||||
}
|
||||
namespace JSX {
|
||||
interface IntrinsicElements {
|
||||
"theme-selector": JSXElements.ThemeSelectorAttributes;
|
||||
}
|
||||
}
|
||||
namespace JSXElements {
|
||||
export interface ThemeSelectorAttributes extends HTMLAttributes {
|
||||
themeData?: { name: string }[];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,52 @@
|
||||
import { Component, Prop, PropDidChange } from '@stencil/core';
|
||||
|
||||
|
||||
@Component({
|
||||
tag: 'app-preview',
|
||||
styleUrl: 'app-preview.css',
|
||||
shadow: true
|
||||
})
|
||||
export class AppPreview {
|
||||
|
||||
@Prop() demoUrl: string;
|
||||
@Prop() demoMode: string;
|
||||
@Prop() cssText: string;
|
||||
iframe: HTMLIFrameElement;
|
||||
|
||||
@PropDidChange('cssText')
|
||||
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: HTMLStyleElement = iframeDoc.getElementById(themerStyleId) as any;
|
||||
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 [
|
||||
<div>
|
||||
<iframe src={url} ref={elm => this.iframe = elm as any} onLoad={this.onIframeLoad.bind(this)}></iframe>
|
||||
</div>
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
|
||||
section {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.property-label {
|
||||
font-family: Courier New, Courier, monospace;
|
||||
white-space: nowrap;
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Component, Event, EventEmitter, Prop } from '@stencil/core';
|
||||
|
||||
|
||||
@Component({
|
||||
tag: 'color-selector',
|
||||
styleUrl: 'color-selector.css',
|
||||
shadow: true
|
||||
})
|
||||
export class ColorSelector {
|
||||
|
||||
@Prop() property: string;
|
||||
@Prop({ mutable: true }) value: string;
|
||||
@Prop() isRgb: boolean;
|
||||
|
||||
|
||||
onChange(ev) {
|
||||
if (this.isRgb) {
|
||||
this.value = hexToRgb(ev.currentTarget.value);
|
||||
} else {
|
||||
this.value = ev.currentTarget.value;
|
||||
}
|
||||
|
||||
this.colorChange.emit({
|
||||
property: this.property,
|
||||
value: this.value
|
||||
});
|
||||
}
|
||||
|
||||
@Event() colorChange: EventEmitter;
|
||||
|
||||
render() {
|
||||
const value = this.value.trim().toLowerCase();
|
||||
const hex = rgbToHex(value);
|
||||
|
||||
return [
|
||||
<section>
|
||||
<div class='color-square'>
|
||||
<input type='color' value={hex} onInput={this.onChange.bind(this)} tabindex='-1' />
|
||||
</div>
|
||||
<div class='color-value'>
|
||||
<input type='text' value={value} onInput={this.onChange.bind(this)} />
|
||||
</div>
|
||||
<div class='property-label'>
|
||||
{this.property}
|
||||
</div>
|
||||
</section>
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
function rgbToHex(value: string) {
|
||||
if (value.indexOf('rgb') === -1) {
|
||||
return value;
|
||||
}
|
||||
|
||||
var c = value.replace(/[\sa-z\(\);]+/gi, '').split(',');
|
||||
c = c.map(s => parseInt(s, 10).toString(16).replace(/^([a-z\d])$/i, '0$1'));
|
||||
|
||||
return '#' + c[0] + c[1] + c[2];
|
||||
}
|
||||
|
||||
function hexToRgb(c: any) {
|
||||
if (c.indexOf('#') === -1) {
|
||||
return c;
|
||||
}
|
||||
c = c.replace(/#/, '');
|
||||
c = c.length % 6 ? c.replace(/(.)(.)(.)/, '$1$1$2$2$3$3') : c;
|
||||
c = parseInt(c, 16);
|
||||
|
||||
var a = parseFloat(a) || null;
|
||||
|
||||
const r = (c >> 16) & 255;
|
||||
const g = (c >> 8) & 255;
|
||||
const b = (c >> 0) & 255;
|
||||
|
||||
return `rgb${a ? 'a' : ''}(${[r, g, b, a].join().replace(/,$/, '')})`;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
|
||||
h1 {
|
||||
margin: 10px 0 0 0;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
margin: 12px 0 0 0;
|
||||
min-width: 280px;
|
||||
height: 500px;
|
||||
font-family: Courier New, Courier, monospace;
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 10px 10px 0 0;
|
||||
padding: 10px 15px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { Component, Prop } from '@stencil/core';
|
||||
import { STORED_THEME_KEY, deleteCssUrl, getThemeUrl, saveCssUrl } from '../../theme-variables';
|
||||
|
||||
|
||||
@Component({
|
||||
tag: 'css-text',
|
||||
styleUrl: 'css-text.css',
|
||||
shadow: true
|
||||
})
|
||||
export class CssText {
|
||||
|
||||
@Prop() themeName: string;
|
||||
@Prop() cssText: string;
|
||||
|
||||
submitUpdate(ev: UIEvent) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
|
||||
this.saveCss(this.themeName, this.cssText);
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
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>
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
select {
|
||||
margin: 10px 0 0px 10px;
|
||||
}
|
||||
@@ -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() demoUrl: string;
|
||||
@Prop() demoMode: string;
|
||||
@Event() demoUrlChange: EventEmitter;
|
||||
@Event() demoModeChange: EventEmitter;
|
||||
|
||||
onChangeUrl(ev) {
|
||||
this.demoUrlChange.emit(ev.currentTarget.value);
|
||||
}
|
||||
|
||||
onChangeMode(ev) {
|
||||
this.demoModeChange.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>
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Component, Listen, State } from '@stencil/core';
|
||||
import { DATA_URL, STORED_DEMO_MODE_KEY, STORED_DEMO_URL_KEY } from '../../theme-variables';
|
||||
|
||||
|
||||
@Component({
|
||||
tag: 'theme-builder',
|
||||
styleUrl: 'theme-builder.css',
|
||||
shadow: true
|
||||
})
|
||||
export class ThemeBuilder {
|
||||
|
||||
demoData: { name: string, url: string }[];
|
||||
themeData: { name: string }[];
|
||||
|
||||
@State() demoUrl: string;
|
||||
@State() demoMode: string;
|
||||
@State() cssText: 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('demoUrlChange')
|
||||
onDemoUrlChange(ev) {
|
||||
this.demoUrl = ev.detail;
|
||||
localStorage.setItem(STORED_DEMO_URL_KEY, this.demoUrl);
|
||||
}
|
||||
|
||||
@Listen('demoModeChange')
|
||||
onDemoModeChange(ev) {
|
||||
this.demoMode = ev.detail;
|
||||
localStorage.setItem(STORED_DEMO_MODE_KEY, this.demoMode);
|
||||
}
|
||||
|
||||
@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}></app-preview>
|
||||
</section>
|
||||
|
||||
<section class='selector-column'>
|
||||
<theme-selector themeData={this.themeData}></theme-selector>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<css-text themeName={this.themeName} cssText={this.cssText}></css-text>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
select {
|
||||
margin: 10px 0 0 10px;
|
||||
}
|
||||
|
||||
section {
|
||||
margin: 10px;
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import { Component, Event, EventEmitter, Listen, Prop, State } from '@stencil/core';
|
||||
import { STORED_THEME_KEY, THEME_VARIABLES, getThemeUrl } from '../../theme-variables';
|
||||
|
||||
|
||||
@Component({
|
||||
tag: 'theme-selector',
|
||||
styleUrl: 'theme-selector.css',
|
||||
shadow: true
|
||||
})
|
||||
export class ThemeSelector {
|
||||
|
||||
@State() themeName: string;
|
||||
@State() themeVariables: { property: string; value?: string; isRgb?: boolean; }[] = [];
|
||||
@Prop() themeData: { name: string }[];
|
||||
@Event() themeCssChange: EventEmitter;
|
||||
|
||||
|
||||
onChangeUrl(ev) {
|
||||
this.themeName = ev.currentTarget.value;
|
||||
localStorage.setItem(STORED_THEME_KEY, this.themeName);
|
||||
|
||||
this.loadThemeCss();
|
||||
}
|
||||
|
||||
componentWillLoad() {
|
||||
const storedThemeName = localStorage.getItem(STORED_THEME_KEY);
|
||||
const defaultThemeName = this.themeData[0].name;
|
||||
|
||||
this.themeName = storedThemeName || defaultThemeName;
|
||||
|
||||
this.loadThemeCss();
|
||||
}
|
||||
|
||||
loadThemeCss() {
|
||||
console.log('ThemeSelector loadThemeCss');
|
||||
|
||||
const themeUrl = getThemeUrl(this.themeName);
|
||||
|
||||
return fetch(themeUrl).then(rsp => {
|
||||
return rsp.text().then(css => {
|
||||
this.parseCss(css);
|
||||
this.generateCss();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
parseCss(css: string) {
|
||||
console.log('ThemeSelector parseCss');
|
||||
|
||||
const themer = document.getElementById('themer') as HTMLStyleElement;
|
||||
themer.innerHTML = css;
|
||||
|
||||
const computed = window.getComputedStyle(document.body);
|
||||
|
||||
this.themeVariables = THEME_VARIABLES.map(themeVariable => {
|
||||
const value = (computed.getPropertyValue(themeVariable.property) || '#eeeeee').trim().toLowerCase();
|
||||
return {
|
||||
property: themeVariable.property.trim(),
|
||||
value: value,
|
||||
isRgb: value.indexOf('rgb') > -1
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
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 => {
|
||||
themeVariable.value = (themeVariable.value || '').trim();
|
||||
c.push(` ${themeVariable.property}: ${themeVariable.value};`);
|
||||
});
|
||||
|
||||
c.push('}');
|
||||
|
||||
const cssText = c.join('\n');
|
||||
this.themeCssChange.emit({
|
||||
cssText: cssText,
|
||||
themeName: this.themeName
|
||||
});
|
||||
}
|
||||
|
||||
@Listen('colorChange')
|
||||
onColorChange(ev) {
|
||||
console.log('ThemeSelector colorChange');
|
||||
|
||||
this.themeVariables = this.themeVariables.map(themeVariable => {
|
||||
let value = themeVariable.value;
|
||||
|
||||
if (ev.detail.property === themeVariable.property) {
|
||||
value = ev.detail.value;
|
||||
}
|
||||
|
||||
return {
|
||||
property: themeVariable.property,
|
||||
value: value,
|
||||
isRgb: themeVariable.isRgb
|
||||
};
|
||||
});
|
||||
|
||||
this.generateCss();
|
||||
}
|
||||
|
||||
render() {
|
||||
return [
|
||||
<div>
|
||||
<select onChange={this.onChangeUrl.bind(this)}>
|
||||
{this.themeData.map(d => <option value={d.name} selected={this.themeName === d.name}>{d.name}</option>)}
|
||||
</select>
|
||||
|
||||
<section>
|
||||
{this.themeVariables.map(d => <color-selector property={d.property} value={d.value} isRgb={d.isRgb}></color-selector>)}
|
||||
</section>
|
||||
</div>
|
||||
];
|
||||
}
|
||||
}
|
||||
19
packages/core/scripts/theme-builder/src/global/app.css
Normal file
19
packages/core/scripts/theme-builder/src/global/app.css
Normal file
@@ -0,0 +1,19 @@
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
background: #eee;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
theme-builder {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
19
packages/core/scripts/theme-builder/src/index.html
Normal file
19
packages/core/scripts/theme-builder/src/index.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html dir="ltr" lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Ionic Theme Builder</title>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=5.0">
|
||||
<meta name="theme-color" content="#16161d">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<link href="/theme-builder/build/app.css" rel="stylesheet">
|
||||
<script src="/theme-builder/build/app.js"></script>
|
||||
<style id="themer"></style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<theme-builder></theme-builder>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
109
packages/core/scripts/theme-builder/src/theme-variables.ts
Normal file
109
packages/core/scripts/theme-builder/src/theme-variables.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
|
||||
export const THEME_VARIABLES = [
|
||||
|
||||
{
|
||||
property: '--primary'
|
||||
},
|
||||
{
|
||||
property: '--primary-contrast'
|
||||
},
|
||||
{
|
||||
property: '--secondary'
|
||||
},
|
||||
{
|
||||
property: '--secondary-contrast'
|
||||
},
|
||||
{
|
||||
property: '--tertiary'
|
||||
},
|
||||
{
|
||||
property: '--tertiary-contrast'
|
||||
},
|
||||
{
|
||||
property: '--success'
|
||||
},
|
||||
{
|
||||
property: '--success-contrast'
|
||||
},
|
||||
{
|
||||
property: '--warning'
|
||||
},
|
||||
{
|
||||
property: '--warning-contrast'
|
||||
},
|
||||
{
|
||||
property: '--danger'
|
||||
},
|
||||
{
|
||||
property: '--danger-contrast'
|
||||
},
|
||||
{
|
||||
property: '--light'
|
||||
},
|
||||
{
|
||||
property: '--light-contrast'
|
||||
},
|
||||
{
|
||||
property: '--medium'
|
||||
},
|
||||
{
|
||||
property: '--medium-contrast'
|
||||
},
|
||||
{
|
||||
property: '--dark'
|
||||
},
|
||||
{
|
||||
property: '--dark-contrast'
|
||||
},
|
||||
{
|
||||
property: '--content-color'
|
||||
},
|
||||
{
|
||||
property: '--content-sub-color'
|
||||
},
|
||||
{
|
||||
property: '--content-background'
|
||||
},
|
||||
{
|
||||
property: '--content-sub-background'
|
||||
},
|
||||
{
|
||||
property: '--toolbar-background'
|
||||
},
|
||||
{
|
||||
property: '--tabbar-background'
|
||||
},
|
||||
{
|
||||
property: '--item-background'
|
||||
},
|
||||
{
|
||||
property: '--item-sub-background'
|
||||
},
|
||||
{
|
||||
property: '--border-color'
|
||||
},
|
||||
|
||||
];
|
||||
|
||||
export const SERVER_DOMAIN = `http://localhost:5454`;
|
||||
export const DATA_URL = `${SERVER_DOMAIN}/data`;
|
||||
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';
|
||||
11
packages/core/scripts/theme-builder/stencil.config.js
Normal file
11
packages/core/scripts/theme-builder/stencil.config.js
Normal file
@@ -0,0 +1,11 @@
|
||||
exports.config = {
|
||||
globalStyle: 'src/global/app.css',
|
||||
wwwDir: '../../theme-builder',
|
||||
serviceWorker: false
|
||||
};
|
||||
|
||||
exports.devServer = {
|
||||
root: '../../',
|
||||
watchGlob: 'src/**',
|
||||
openUrl: '/theme-builder'
|
||||
}
|
||||
22
packages/core/scripts/theme-builder/tsconfig.json
Normal file
22
packages/core/scripts/theme-builder/tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"allowUnreachableCode": false,
|
||||
"declaration": false,
|
||||
"experimentalDecorators": true,
|
||||
"lib": [
|
||||
"dom",
|
||||
"es2015"
|
||||
],
|
||||
"moduleResolution": "node",
|
||||
"module": "es2015",
|
||||
"target": "es2015",
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"jsx": "react",
|
||||
"jsxFactory": "h"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
32
packages/core/src/themes/css/default.css
Normal file
32
packages/core/src/themes/css/default.css
Normal file
@@ -0,0 +1,32 @@
|
||||
/** default theme **/
|
||||
|
||||
|
||||
:root {
|
||||
--primary: #3880ff;
|
||||
--primary-contrast: #ffffff;
|
||||
--secondary: #0cd1e8;
|
||||
--secondary-contrast: #ffffff;
|
||||
--tertiary: #7044ff;
|
||||
--tertiary-contrast: #ffffff;
|
||||
--success: #10dc60;
|
||||
--success-contrast: #ffffff;
|
||||
--warning: #ffce00;
|
||||
--warning-contrast: #ffffff;
|
||||
--danger: #f04141;
|
||||
--danger-contrast: #ffffff;
|
||||
--light: #f4f5f8;
|
||||
--light-contrast: #000000;
|
||||
--medium: #898b93;
|
||||
--medium-contrast: #000000;
|
||||
--dark: #222428;
|
||||
--dark-contrast: #f4f5f8;
|
||||
--content-color: #222428;
|
||||
--content-sub-color: #222428;
|
||||
--content-background: #eeeeee;
|
||||
--content-sub-background: #eeeeee;
|
||||
--toolbar-background: #eeeeee;
|
||||
--tabbar-background: #eeeeee;
|
||||
--item-background: #eeeeee;
|
||||
--item-sub-background: #eeeeee;
|
||||
--border-color: #222428;
|
||||
}
|
||||
32
packages/core/src/themes/css/oceanic.css
Normal file
32
packages/core/src/themes/css/oceanic.css
Normal file
@@ -0,0 +1,32 @@
|
||||
/** oceanic theme **/
|
||||
|
||||
|
||||
:root {
|
||||
--primary: #3880ff;
|
||||
--primary-contrast: #ffffff;
|
||||
--secondary: #0cd1e8;
|
||||
--secondary-contrast: #ffffff;
|
||||
--tertiary: #7044ff;
|
||||
--tertiary-contrast: #ffffff;
|
||||
--success: #10dc60;
|
||||
--success-contrast: #ffffff;
|
||||
--warning: #ffce00;
|
||||
--warning-contrast: #ffffff;
|
||||
--danger: #f04141;
|
||||
--danger-contrast: #ffffff;
|
||||
--light: #f4f5f8;
|
||||
--light-contrast: #000000;
|
||||
--medium: #898b93;
|
||||
--medium-contrast: #000000;
|
||||
--dark: #222428;
|
||||
--dark-contrast: #f4f5f8;
|
||||
--content-color: #222428;
|
||||
--content-sub-color: #222428;
|
||||
--content-background: #eeeeee;
|
||||
--content-sub-background: #eeeeee;
|
||||
--toolbar-background: #eeeeee;
|
||||
--tabbar-background: #eeeeee;
|
||||
--item-background: #eeeeee;
|
||||
--item-sub-background: #eeeeee;
|
||||
--border-color: #222428;
|
||||
}
|
||||
Reference in New Issue
Block a user