diff --git a/src/Button/index.tsx b/src/Button/index.tsx new file mode 100644 index 00000000..57c77e0d --- /dev/null +++ b/src/Button/index.tsx @@ -0,0 +1,153 @@ +import * as React from "react"; +import * as PropTypes from "prop-types"; + +import ElementState from "../ElementState"; +import Icon from "../Icon"; +import Tooltip from "../Tooltip"; +import { setStylesToManager, setStyleToManager } from "../styles/setStylesToManager"; + +export interface DataProps { + /** + * Control `Button` border size. + */ + borderSize?: string; + /** + * Is onMouseEnter Inline Style will assign to default `hoverStyle`. + */ + hoverStyle?: React.CSSProperties; + /** + * Is onMouseDown Inline Style will assign to default `hoverStyle`. + */ + activeStyle?: React.CSSProperties; + /** + * icon use the Iconfont like `\uE00A` or iconName `HeartLegacy`. + */ + icon?: string; + /** + * This will assign to default `iconStyle`. + */ + iconStyle?: React.CSSProperties; + /** + * will change to icon position, default is `left`. + */ + iconPosition?: "left" | "right"; + /** + * if `true`, will become `Disabled Button`. + */ + disabled?: boolean; + /** + * `tooltip` is any type, you can passe a `React.Element` or `string`. + */ + tooltip?: React.ReactElement | string; + /** + * Set custom Button `background`. + */ + background?: string; +} + +export interface ButtonProps extends DataProps, React.HTMLAttributes {} + +export class Button extends React.Component { + static defaultProps: ButtonProps = { + borderSize: "2px", + iconPosition: "left" + }; + + static contextTypes = { theme: PropTypes.object }; + context: { theme: ReactUWP.ThemeType }; + + refs: { container: HTMLButtonElement }; + + render() { + const { + borderSize, + style, + hoverStyle, + children, + icon, + iconStyle, + iconPosition, + disabled, + tooltip, + background, + activeStyle, + ...attributes + } = this.props; + const { theme } = this.context; + + const currIconStyle: React.CSSProperties = { + padding: "0 4px", + display: "inline", + ...theme.prepareStyles(iconStyle) + }; + + const styles = setStylesToManager({ + className: "button", + theme, + styles: { + root: { + display: "inline-block", + verticalAlign: "middle", + cursor: "pointer", + color: theme.baseHigh, + outline: "none", + padding: "4px 16px", + transition: "all .25s", + border: `${borderSize} solid transparent`, + background: background || theme.baseLow, + ...theme.prepareStyles(style), + "&:hover": disabled ? void 0 : { + border: `2px solid ${theme.baseMediumLow}` + }, + "&:active": disabled ? void 0 : { + background: theme.baseMediumLow + }, + "&:disabled": { + background: theme.baseMedium, + cursor: "not-allowed", + color: theme.baseMedium + } + }, + icon: { + padding: "0 4px", + display: "inline-block", + ...theme.prepareStyles(iconStyle) + } + } + }); + + const normalRender = ( + icon ? (iconPosition === "right" ? ( + + ) : ( + + )) : ( + + ) + ); + + return tooltip ? ( + + {normalRender} + + ) : normalRender; + } +} + +export default Button; diff --git a/src/Theme/index.tsx b/src/Theme/index.tsx index ee97abe3..44e2f394 100644 --- a/src/Theme/index.tsx +++ b/src/Theme/index.tsx @@ -1,6 +1,7 @@ import * as React from "react"; import * as PropTypes from "prop-types"; +import StyleManager from "../styles/StyleManager"; import darkTheme from "../styles/darkTheme"; import getTheme from "../styles/getTheme"; import RenderToBody from "../RenderToBody"; @@ -193,10 +194,10 @@ export class Theme extends React.Component { generateAcrylicTextures: this.generateAcrylicTextures, forceUpdateTheme: this.forceUpdateTheme } as ReactUWP.ThemeType); + theme.styleManager = new StyleManager(theme); } handleNewTheme = (theme: ReactUWP.ThemeType) => { - this.bindNewThemeMethods(theme); this.props.themeWillUpdate(theme); } diff --git a/src/styles/StyleManager.ts b/src/styles/StyleManager.ts new file mode 100644 index 00000000..f9efd4cf --- /dev/null +++ b/src/styles/StyleManager.ts @@ -0,0 +1,101 @@ +import * as createHash from "murmurhash-js/murmurhash3_gc"; +import isUnitlessNumber from "../common/react/isUnitlessNumber"; + +const replace2Dashes = (key: string) => key.replace(/[A-Z]/g, $1 => `-${$1.toLowerCase()}`); +const getStyleValue = (key: string, value: string) => ((typeof value === "number" && !(isUnitlessNumber as any)[key]) ? `${value}px` : value); + +export interface CustomCSSProperties extends React.CSSProperties { + "&:hover"?: React.CSSProperties; + "&:active"?: React.CSSProperties; + "&:focus"?: React.CSSProperties; + "&:disabled"?: React.CSSProperties; +} + +const extendsStyleKeys: any = { + "&:hover": true, + "&:active": true, + "&:focus": true, + "&:disabled": true +}; + +export class StyleManager { + globalClassName: string; + theme: ReactUWP.ThemeType; + themeId = 0; + styleElement: HTMLStyleElement = null; + sheets: any = {}; + + constructor(theme: ReactUWP.ThemeType, globalClassName?: string) { + this.globalClassName = globalClassName ? `${globalClassName}-` : ""; + this.setupTheme(theme); + } + + setupTheme = (theme: ReactUWP.ThemeType) => { + this.theme = theme; + this.themeId = createHash([theme.accent, theme.themeName, theme.useFluentDesign].join(", ")); + } + + style2CSSText = (style: React.CSSProperties) => style ? Object.keys(style).map(key => ( + ` ${replace2Dashes(key)}: ${getStyleValue(key, style[key])};` + )).join("\n") : void 0 + + renderSheets = () => {}; + + sheetsToString = () => `\n${Object.keys(this.sheets).map(id => this.sheets[id].CSSText).join("")}`; + + updateTheme = () => {}; + + addSheet = (style: CustomCSSProperties, className = "", callback = () => {}) => { + const id = createHash(`${this.themeId}: ${JSON.stringify(style)}`); + const classNameWithHash = `${this.globalClassName}${className}-${id}`; + const styleKeys = Object.keys(style); + let CSSText = ""; + let contentCSSText = ""; + let extendsCSSText = ""; + + for (const styleKey of styleKeys) { + if (extendsStyleKeys[styleKey]) { + const extendsStyle = style[styleKey]; + if (extendsStyle) { + extendsCSSText += `.${classNameWithHash}${styleKey.slice(1)} {\n${this.style2CSSText(extendsStyle)}\n}\n`; + } + } else { + contentCSSText += ` ${replace2Dashes(styleKey)}: ${getStyleValue(styleKey, style[styleKey])};\n`; + } + } + + CSSText += `.${classNameWithHash} {\n${contentCSSText}\n}\n`; + CSSText += extendsCSSText; + + this.sheets[id] = { CSSText, classNameWithHash, id, className }; + callback(); + return this.sheets[id]; + } + + addSheetWithUpdate = (style: CustomCSSProperties, className = "") => { + return this.addSheet(style, className, this.updateSheetsToDOM); + } + + updateSheetByID = () => {}; + + updateAllSheets = () => {}; + + removeSheetByID = () => {}; + + updateSheetsToDOM = () => { + const name = `data-uwp-jss-${this.themeId}`; + this.styleElement = document.querySelector(`[${name}]`) as HTMLStyleElement; + const textContent = this.sheetsToString(); + + if (!this.styleElement) { + this.styleElement = document.createElement("style"); + this.styleElement.setAttribute(name, ""); + this.styleElement.textContent = textContent; + document.head.appendChild(this.styleElement); + } else { + this.styleElement.textContent = textContent; + } + } +} + +export default StyleManager; diff --git a/src/styles/getTheme.ts b/src/styles/getTheme.ts new file mode 100644 index 00000000..15328f4a --- /dev/null +++ b/src/styles/getTheme.ts @@ -0,0 +1,197 @@ +import * as tinycolor from "tinycolor2"; +import setSegoeMDL2AssetsFonts from "./fonts/segoe-mdl2-assets"; +import IS_NODE_ENV from "../common/nodeJS/IS_NODE_ENV"; +import prefixAll from "../common/prefixAll"; + +if (!IS_NODE_ENV) { + setSegoeMDL2AssetsFonts(); +} + +export function darken(color: string, coefficient: number) { + const hsl = tinycolor(color).toHsl(); + hsl.l = hsl.l * (1 - coefficient); + return tinycolor(hsl).toRgbString(); +} + +export function lighten(color: string, coefficient: number) { + const hsl = tinycolor(color).toHsl(); + hsl.l = hsl.l + (100 - hsl.l) * coefficient; + return tinycolor(hsl).toRgbString(); +} + +export interface ThemeConfig { + themeName?: "dark" | "light"; + accent?: string; + + useFluentDesign?: boolean; + desktopBackgroundImage?: string; + userAgent?: string; +} + +export default function getTheme(themeConfig?: ThemeConfig): ReactUWP.ThemeType { + themeConfig = themeConfig || {}; + let { + themeName, + accent, + + useFluentDesign, + desktopBackgroundImage, + userAgent + } = themeConfig; + + themeName = themeName || "dark"; + accent = accent || "#0078D7"; + useFluentDesign = useFluentDesign === void 0 ? false : useFluentDesign; + + const isDark = themeName === "dark"; + const baseHigh = isDark ? "#fff" : "#000"; + const altHigh = isDark ? "#000" : "#fff"; + const baseHighColor = tinycolor(baseHigh); + const altHighColor = tinycolor(altHigh); + const accentColor = tinycolor(accent); + const accentColorHsl = accentColor.toHsl(); + + const altMediumLow = altHighColor.setAlpha(0.4).toRgbString(); + const altMedium = altHighColor.setAlpha(0.6).toRgbString(); + const altMediumHigh = altHighColor.setAlpha(0.8).toRgbString(); + + const theme: ReactUWP.ThemeType = { + themeName, + fonts: { + sansSerifFonts: "Segoe UI, Microsoft YaHei, Open Sans, sans-serif, Hiragino Sans GB, Arial, Lantinghei SC, STHeiti, WenQuanYi Micro Hei, SimSun", + segoeMDL2Assets: "Segoe MDL2 Assets" + }, + + styleManager: {}, + + useFluentDesign, + desktopBackground: void 0, + desktopBackgroundImage, + + haveAcrylicTextures: false, + acrylicTexture40: { + background: altMediumLow + }, + acrylicTexture60: { + background: altMedium + }, + acrylicTexture80: { + background: altMediumHigh + }, + + accent, + accentLighter1: lighten(accentColor.toHexString(), 0.5), + accentLighter2: lighten(accentColor.toHexString(), 0.7), + accentLighter3: lighten(accentColor.toHexString(), 0.9), + accentDarker1: darken(accentColor.toHexString(), 0.5), + accentDarker2: darken(accentColor.toHexString(), 0.7), + accentDarker3: darken(accentColor.toHexString(), 0.9), + + baseLow: baseHighColor.setAlpha(0.2).toRgbString(), + baseMediumLow: baseHighColor.setAlpha(0.4).toRgbString(), + baseMedium: baseHighColor.setAlpha(0.6).toRgbString(), + baseMediumHigh: baseHighColor.setAlpha(0.8).toRgbString(), + baseHigh, + + altLow: altHighColor.setAlpha(0.2).toRgbString(), + altMediumLow, + altMedium, + altMediumHigh, + altHigh, + + listLow: baseHighColor.setAlpha(0.1).toRgbString(), + listMedium: baseHighColor.setAlpha(0.2).toRgbString(), + listAccentLow: accentColor.setAlpha(0.6).toRgbString(), + listAccentMedium: accentColor.setAlpha(0.8).toRgbString(), + listAccentHigh: accentColor.setAlpha(0.9).toRgbString(), + + chromeLow: isDark ? "#171717" : "#f2f2f2", + chromeMediumLow: isDark ? "#2b2b2b" : "#f2f2f2", + chromeMedium: isDark ? "#1f1f1f" : "#e6e6e6", + chromeHigh: isDark ? "#767676" : "#ccc", + + chromeAltLow: isDark ? "#f2f2f2" : "#171717", + chromeDisabledLow: isDark ? "#858585" : "#7a7a7a", + chromeDisabledHigh: isDark ? "#333" : "#ccc", + + chromeBlackLow: tinycolor("#000").setAlpha(0.2).toRgbString(), + chromeBlackMediumLow: tinycolor("#000").setAlpha(0.4).toRgbString(), + chromeBlackMedium: tinycolor("#000").setAlpha(0.8).toRgbString(), + chromeBlackHigh: "#000", + chromeWhite: "#fff", + + isDarkTheme: isDark, + prepareStyles: prefixAll(userAgent), + + toasts: [], + + typographyStyles: { + header: { + fontWeight: "lighter", + fontSize: 46, + lineHeight: "56px" + }, + subHeader: { + fontWeight: "lighter", + fontSize: 34, + lineHeight: "40px" + }, + + title: { + fontWeight: "lighter", + fontSize: 24, + lineHeight: "28px" + }, + subTitle: { + fontWeight: "normal", + fontSize: 20, + lineHeight: "24px" + }, + subTitleAlt: { + fontWeight: "normal", + fontSize: 18, + lineHeight: "20px" + }, + + base: { + fontWeight: 300, + fontSize: 15, + lineHeight: "20px" + }, + baseAlt: { + fontWeight: "bold", + fontSize: 15, + lineHeight: "20px" + }, + body: { + fontWeight: 200, + fontSize: 15, + lineHeight: "20px" + }, + + captionAlt: { + fontWeight: "lighter", + fontSize: 13, + lineHeight: "16px" + }, + caption: { + fontWeight: "lighter", + fontSize: 12, + lineHeight: "14px" + } + }, + zIndex: { + listView: 10, + calendarView: 20, + dropDownMenu: 102, + commandBar: 200, + tooltip: 201, + flyout: 202, + contentDialog: 300, + header: 301, + mediaPlayer: 2147483647, + toast: 310 + } + } as ReactUWP.ThemeType; + return theme; +}