From 76afd9d77f5e3ace51b790a940bd32018d08c168 Mon Sep 17 00:00:00 2001 From: Adam Bradley Date: Thu, 21 Dec 2017 12:04:33 -0600 Subject: [PATCH] feat(themes): theme builder app --- .gitignore | 1 + packages/core/package.json | 4 + packages/core/scripts/theme-builder/readme.md | 3 + packages/core/scripts/theme-builder/server.js | 141 +++++++++++++ .../scripts/theme-builder/src/components.d.ts | 193 ++++++++++++++++++ .../components/app-preview/app-preview.css | 15 ++ .../components/app-preview/app-preview.tsx | 52 +++++ .../color-selector/color-selector.css | 35 ++++ .../color-selector/color-selector.tsx | 77 +++++++ .../src/components/css-text/css-text.css | 20 ++ .../src/components/css-text/css-text.tsx | 87 ++++++++ .../demo-selection/demo-selection.css | 4 + .../demo-selection/demo-selection.tsx | 41 ++++ .../theme-builder/theme-builder.css | 18 ++ .../theme-builder/theme-builder.tsx | 84 ++++++++ .../theme-selector/theme-selector.css | 8 + .../theme-selector/theme-selector.tsx | 121 +++++++++++ .../scripts/theme-builder/src/global/app.css | 19 ++ .../core/scripts/theme-builder/src/index.html | 19 ++ .../theme-builder/src/theme-variables.ts | 109 ++++++++++ .../scripts/theme-builder/stencil.config.js | 11 + .../core/scripts/theme-builder/tsconfig.json | 22 ++ packages/core/src/themes/css/default.css | 32 +++ packages/core/src/themes/css/oceanic.css | 32 +++ 24 files changed, 1148 insertions(+) create mode 100644 packages/core/scripts/theme-builder/readme.md create mode 100644 packages/core/scripts/theme-builder/server.js create mode 100644 packages/core/scripts/theme-builder/src/components.d.ts create mode 100644 packages/core/scripts/theme-builder/src/components/app-preview/app-preview.css create mode 100644 packages/core/scripts/theme-builder/src/components/app-preview/app-preview.tsx create mode 100644 packages/core/scripts/theme-builder/src/components/color-selector/color-selector.css create mode 100644 packages/core/scripts/theme-builder/src/components/color-selector/color-selector.tsx create mode 100644 packages/core/scripts/theme-builder/src/components/css-text/css-text.css create mode 100644 packages/core/scripts/theme-builder/src/components/css-text/css-text.tsx create mode 100644 packages/core/scripts/theme-builder/src/components/demo-selection/demo-selection.css create mode 100644 packages/core/scripts/theme-builder/src/components/demo-selection/demo-selection.tsx create mode 100644 packages/core/scripts/theme-builder/src/components/theme-builder/theme-builder.css create mode 100644 packages/core/scripts/theme-builder/src/components/theme-builder/theme-builder.tsx create mode 100644 packages/core/scripts/theme-builder/src/components/theme-selector/theme-selector.css create mode 100644 packages/core/scripts/theme-builder/src/components/theme-selector/theme-selector.tsx create mode 100644 packages/core/scripts/theme-builder/src/global/app.css create mode 100644 packages/core/scripts/theme-builder/src/index.html create mode 100644 packages/core/scripts/theme-builder/src/theme-variables.ts create mode 100644 packages/core/scripts/theme-builder/stencil.config.js create mode 100644 packages/core/scripts/theme-builder/tsconfig.json create mode 100644 packages/core/src/themes/css/default.css create mode 100644 packages/core/src/themes/css/oceanic.css 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 [ +
+
+ +
+
+ +
+
+ {this.property} +
+
+ ]; + } +} + +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