diff --git a/.gitignore b/.gitignore
index fe188929e2..6ef0abc5fe 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,6 +18,7 @@ dist/
node_modules/
tmp/
temp/
+packages/core/theme-builder/
$RECYCLE.BIN/
.DS_Store
diff --git a/packages/core/package.json b/packages/core/package.json
index 69adaa68b1..3075c90e58 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -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",
diff --git a/packages/core/scripts/theme-builder/readme.md b/packages/core/scripts/theme-builder/readme.md
new file mode 100644
index 0000000000..810e8531e4
--- /dev/null
+++ b/packages/core/scripts/theme-builder/readme.md
@@ -0,0 +1,3 @@
+# Theme Builder
+
+ npm run theme-builder
diff --git a/packages/core/scripts/theme-builder/server.js b/packages/core/scripts/theme-builder/server.js
new file mode 100644
index 0000000000..200cb3c820
--- /dev/null
+++ b/packages/core/scripts/theme-builder/server.js
@@ -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}/`);
+});
\ No newline at end of file
diff --git a/packages/core/scripts/theme-builder/src/components.d.ts b/packages/core/scripts/theme-builder/src/components.d.ts
new file mode 100644
index 0000000000..d1a6d3ffc2
--- /dev/null
+++ b/packages/core/scripts/theme-builder/src/components.d.ts
@@ -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 }[];
+ }
+ }
+}
+
diff --git a/packages/core/scripts/theme-builder/src/components/app-preview/app-preview.css b/packages/core/scripts/theme-builder/src/components/app-preview/app-preview.css
new file mode 100644
index 0000000000..5c1f851746
--- /dev/null
+++ b/packages/core/scripts/theme-builder/src/components/app-preview/app-preview.css
@@ -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;
+}
\ No newline at end of file
diff --git a/packages/core/scripts/theme-builder/src/components/app-preview/app-preview.tsx b/packages/core/scripts/theme-builder/src/components/app-preview/app-preview.tsx
new file mode 100644
index 0000000000..4e863fa564
--- /dev/null
+++ b/packages/core/scripts/theme-builder/src/components/app-preview/app-preview.tsx
@@ -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 [
+
+
+
+ ];
+ }
+}
diff --git a/packages/core/scripts/theme-builder/src/components/color-selector/color-selector.css b/packages/core/scripts/theme-builder/src/components/color-selector/color-selector.css
new file mode 100644
index 0000000000..c53bc8e29f
--- /dev/null
+++ b/packages/core/scripts/theme-builder/src/components/color-selector/color-selector.css
@@ -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;
+}
\ No newline at end of file
diff --git a/packages/core/scripts/theme-builder/src/components/color-selector/color-selector.tsx b/packages/core/scripts/theme-builder/src/components/color-selector/color-selector.tsx
new file mode 100644
index 0000000000..e760013075
--- /dev/null
+++ b/packages/core/scripts/theme-builder/src/components/color-selector/color-selector.tsx
@@ -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 [
+
+ ];
+ }
+}
+
+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(/,$/, '')})`;
+}
diff --git a/packages/core/scripts/theme-builder/src/components/css-text/css-text.css b/packages/core/scripts/theme-builder/src/components/css-text/css-text.css
new file mode 100644
index 0000000000..cbb1cf9f4e
--- /dev/null
+++ b/packages/core/scripts/theme-builder/src/components/css-text/css-text.css
@@ -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;
+}
diff --git a/packages/core/scripts/theme-builder/src/components/css-text/css-text.tsx b/packages/core/scripts/theme-builder/src/components/css-text/css-text.tsx
new file mode 100644
index 0000000000..290341ae0c
--- /dev/null
+++ b/packages/core/scripts/theme-builder/src/components/css-text/css-text.tsx
@@ -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 [
+
+ {getThemeUrl(this.themeName)}
+
,
+
+
+
,
+
+
+
+
+
+ ];
+ }
+}
diff --git a/packages/core/scripts/theme-builder/src/components/demo-selection/demo-selection.css b/packages/core/scripts/theme-builder/src/components/demo-selection/demo-selection.css
new file mode 100644
index 0000000000..7640765281
--- /dev/null
+++ b/packages/core/scripts/theme-builder/src/components/demo-selection/demo-selection.css
@@ -0,0 +1,4 @@
+
+select {
+ margin: 10px 0 0px 10px;
+}
diff --git a/packages/core/scripts/theme-builder/src/components/demo-selection/demo-selection.tsx b/packages/core/scripts/theme-builder/src/components/demo-selection/demo-selection.tsx
new file mode 100644
index 0000000000..5383f5297c
--- /dev/null
+++ b/packages/core/scripts/theme-builder/src/components/demo-selection/demo-selection.tsx
@@ -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 [
+
+
+
+
+
+
+
+ ];
+ }
+}
diff --git a/packages/core/scripts/theme-builder/src/components/theme-builder/theme-builder.css b/packages/core/scripts/theme-builder/src/components/theme-builder/theme-builder.css
new file mode 100644
index 0000000000..e9c7061024
--- /dev/null
+++ b/packages/core/scripts/theme-builder/src/components/theme-builder/theme-builder.css
@@ -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;
+}
\ No newline at end of file
diff --git a/packages/core/scripts/theme-builder/src/components/theme-builder/theme-builder.tsx b/packages/core/scripts/theme-builder/src/components/theme-builder/theme-builder.tsx
new file mode 100644
index 0000000000..64b77d3096
--- /dev/null
+++ b/packages/core/scripts/theme-builder/src/components/theme-builder/theme-builder.tsx
@@ -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 [
+
+
+
+
+
+
+
+
+
+ ];
+ }
+
+}
diff --git a/packages/core/scripts/theme-builder/src/components/theme-selector/theme-selector.css b/packages/core/scripts/theme-builder/src/components/theme-selector/theme-selector.css
new file mode 100644
index 0000000000..2cd1eff771
--- /dev/null
+++ b/packages/core/scripts/theme-builder/src/components/theme-selector/theme-selector.css
@@ -0,0 +1,8 @@
+
+select {
+ margin: 10px 0 0 10px;
+}
+
+section {
+ margin: 10px;
+}
diff --git a/packages/core/scripts/theme-builder/src/components/theme-selector/theme-selector.tsx b/packages/core/scripts/theme-builder/src/components/theme-selector/theme-selector.tsx
new file mode 100644
index 0000000000..d0f907eedf
--- /dev/null
+++ b/packages/core/scripts/theme-builder/src/components/theme-selector/theme-selector.tsx
@@ -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 [
+
+
+
+
+ {this.themeVariables.map(d => )}
+
+
+ ];
+ }
+}
diff --git a/packages/core/scripts/theme-builder/src/global/app.css b/packages/core/scripts/theme-builder/src/global/app.css
new file mode 100644
index 0000000000..1db3d86969
--- /dev/null
+++ b/packages/core/scripts/theme-builder/src/global/app.css
@@ -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;
+}
diff --git a/packages/core/scripts/theme-builder/src/index.html b/packages/core/scripts/theme-builder/src/index.html
new file mode 100644
index 0000000000..0174f11381
--- /dev/null
+++ b/packages/core/scripts/theme-builder/src/index.html
@@ -0,0 +1,19 @@
+
+
+
+
+ Ionic Theme Builder
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/core/scripts/theme-builder/src/theme-variables.ts b/packages/core/scripts/theme-builder/src/theme-variables.ts
new file mode 100644
index 0000000000..af160c31ba
--- /dev/null
+++ b/packages/core/scripts/theme-builder/src/theme-variables.ts
@@ -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';
diff --git a/packages/core/scripts/theme-builder/stencil.config.js b/packages/core/scripts/theme-builder/stencil.config.js
new file mode 100644
index 0000000000..f490bbfa14
--- /dev/null
+++ b/packages/core/scripts/theme-builder/stencil.config.js
@@ -0,0 +1,11 @@
+exports.config = {
+ globalStyle: 'src/global/app.css',
+ wwwDir: '../../theme-builder',
+ serviceWorker: false
+};
+
+exports.devServer = {
+ root: '../../',
+ watchGlob: 'src/**',
+ openUrl: '/theme-builder'
+}
diff --git a/packages/core/scripts/theme-builder/tsconfig.json b/packages/core/scripts/theme-builder/tsconfig.json
new file mode 100644
index 0000000000..6da84ddecc
--- /dev/null
+++ b/packages/core/scripts/theme-builder/tsconfig.json
@@ -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"
+ ]
+}
diff --git a/packages/core/src/themes/css/default.css b/packages/core/src/themes/css/default.css
new file mode 100644
index 0000000000..63c88a05f8
--- /dev/null
+++ b/packages/core/src/themes/css/default.css
@@ -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;
+}
\ No newline at end of file
diff --git a/packages/core/src/themes/css/oceanic.css b/packages/core/src/themes/css/oceanic.css
new file mode 100644
index 0000000000..a50bf026b5
--- /dev/null
+++ b/packages/core/src/themes/css/oceanic.css
@@ -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;
+}
\ No newline at end of file