diff --git a/.gitignore b/.gitignore index c41284fa..84d35142 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ dash_bootstrap_components/metadata.json dist changelog.tmp dash_bootstrap_components/_components/ +.sass-cache diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 00000000..82e1a2b3 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,168 @@ +module.exports = function(grunt) { + grunt.loadNpmTasks("grunt-contrib-clean"); + grunt.loadNpmTasks("grunt-contrib-concat"); + grunt.loadNpmTasks("grunt-contrib-copy"); + grunt.loadNpmTasks("grunt-sass"); + + grunt.initConfig({ + pkg: grunt.file.readJSON("package.json"), + builddir: "css", + buildtheme: "bootstrap", + banner: + "/*!\n" + + " * Dash Bootstrap CSS v<%= pkg.version %>\n" + + ' * Copyright 2019-<%= grunt.template.today("yyyy") %> <%= pkg.author %>\n' + + " * Licensed under <%= pkg.license %>\n" + + " * Based on Bootstrap and Bootswatch\n" + + "*/\n", + swatch: { + bootstrap: {}, + cerulean: {}, + cosmo: {}, + cyborg: {}, + darkly: {}, + flatly: {}, + journal: {}, + litera: {}, + lumen: {}, + lux: {}, + materia: {}, + minty: {}, + pulse: {}, + sandstone: {}, + simplex: {}, + sketchy: {}, + slate: {}, + solar: {}, + spacelab: {}, + superhero: {}, + united: {}, + yeti: {} + }, + clean: { + build: { + src: ["css/*/*.scss"] + } + }, + concat: { + options: { + banner: "<%= banner %>", + stripBanners: false + }, + dist: { + src: [], + dest: "" + } + }, + copy: { + main: { + expand: true, + cwd: "scss", + src: "components/*", + dest: "css/" + }, + swatch: { + expand: true, + cwd: "node_modules/bootswatch/dist/<%=buildtheme%>/", + src: "*.scss", + dest: "css/<%=buildtheme%>" + } + }, + sass: { + options: { + implementation: require("node-sass"), + sourceMap: false + } + } + }); + + grunt.registerTask("default", function() {}); + + grunt.registerTask("build", "build themes from scss", function(theme) { + var t = theme; + if (!t) { + for (t in grunt.config("swatch")) { + grunt.task.run(["copy", "build-theme:" + t]); + } + grunt.task.run(["clean:build"]); + } else { + grunt.task.run(["copy", "build-theme:" + t, "clean:build"]); + } + }); + + grunt.registerTask("build-theme", "build a regular theme from scss", function( + theme, + compress + ) { + theme = theme === undefined ? grunt.config("buildtheme") : theme; + grunt.config("buildtheme", theme); + compress = compress === undefined ? true : compress; + + var isValidTheme = + theme === "bootstrap" || + (grunt.file.exists( + "node_modules/bootswatch/dist/" + theme, + "_variables.scss" + ) && + grunt.file.exists( + "node_modules/bootswatch/dist/" + theme, + "_bootswatch.scss" + )); + + // cancel the build (without failing) if this directory is not a valid theme + if (!isValidTheme) { + return; + } + + if (theme !== "bootstrap") { + grunt.task.run(["copy:swatch"]) + } + + var concatSrc; + var concatDest; + var scssDest; + var scssSrc; + var files = {}; + var dist = {}; + concatSrc = + "scss/build/" + + (theme === "bootstrap" ? "bootstrap" : "bootswatch") + + ".scss"; + concatDest = "<%=builddir%>/" + theme + "/build.scss"; + scssSrc = "<%=builddir%>/" + theme + "/build.scss"; + scssDest = "<%=builddir%>/" + theme + "/bootstrap.css"; + + dist = { src: concatSrc, dest: concatDest }; + grunt.config("concat.dist", dist); + files = {}; + files[scssDest] = scssSrc; + grunt.config("sass.dist.files", files); + grunt.config("sass.dist.options.outputStyle", "expanded"); + + grunt.task.run([ + "concat", + "sass:dist", + compress + ? "compress:" + + scssDest + + ":" + + "<%=builddir%>/" + + theme + + "/bootstrap.min.css" + : "none" + ]); + }); + + grunt.registerTask("compress", "compress a generic css with sass", function( + fileSrc, + fileDst + ) { + var files = {}; + files[fileDst] = fileSrc; + grunt.log.writeln("compressing file " + fileSrc); + + grunt.config("sass.dist.files", files); + grunt.config("sass.dist.options.outputStyle", "compressed"); + grunt.task.run(["sass:dist"]); + }); +}; diff --git a/package.json b/package.json index aa4cb78e..73597552 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "demo": "webpack-dev-server --hot --inline --port=8888 --config=webpack/config.demo.js", "build:lib": "webpack --config=webpack/config.lib.js", "build:py": "mkdirp dash_bootstrap_components/_components && dash-generate-components ./src/components dash_bootstrap_components/_components && move-cli dash_bootstrap_components/_components/_imports_.py dash_bootstrap_components/_components/__init__.py", + "build:css": "grunt build", "format": "prettier src/**/*.js --write", "lint": "prettier src/**/*.js --list-different", "prepublish": "NODE_ENV=production npm run build-dist && NODE_ENV=production npm run build:lib", @@ -33,15 +34,23 @@ "babel-plugin-transform-object-rest-spread": "^6.26.0", "babel-preset-es2015": "^6.24.1", "babel-preset-react": "^6.24.1", + "bootstrap": "^4.3.1", + "bootswatch": "^4.3.1", "copyfiles": "^2.1.0", "css-loader": "^1.0.1", "enzyme": "^3.6.0", "enzyme-adapter-react-16": "^1.5.0", + "grunt": "^1.0.3", + "grunt-contrib-clean": "^2.0.0", + "grunt-contrib-concat": "^1.0.1", + "grunt-contrib-copy": "^1.0.0", + "grunt-sass": "^3.0.2", "jest": "^23.6.0", "jest-environment-jsdom-global": "^1.1.0", "jsdom": "^12.0.0", "mkdirp": "^0.5.1", "move-cli": "^1.2.0", + "node-sass": "^4.11.0", "prettier": "^1.14.3", "react-docgen": "^2.21.0", "rimraf": "^2.6.2", diff --git a/scss/build/bootstrap.scss b/scss/build/bootstrap.scss new file mode 100644 index 00000000..972682ed --- /dev/null +++ b/scss/build/bootstrap.scss @@ -0,0 +1,5 @@ +@import '../../node_modules/bootstrap/scss/bootstrap'; + +@import '../components/_utilities'; +@import '../components/datepicker'; +@import '../components/dropdown'; diff --git a/scss/build/bootswatch.scss b/scss/build/bootswatch.scss new file mode 100644 index 00000000..5a46f355 --- /dev/null +++ b/scss/build/bootswatch.scss @@ -0,0 +1,7 @@ +@import 'variables'; +@import '../../node_modules/bootstrap/scss/bootstrap'; +@import 'bootswatch'; + +@import '../components/_utilities'; +@import '../components/datepicker'; +@import '../components/dropdown'; diff --git a/scss/components/_utilities.scss b/scss/components/_utilities.scss new file mode 100644 index 00000000..bb174134 --- /dev/null +++ b/scss/components/_utilities.scss @@ -0,0 +1,14 @@ +// Color contrast with threshold +@function color-yiq($color, $dark: $yiq-text-dark, $light: $yiq-text-light, $threshold: $yiq-contrasted-threshold) { + $r: red($color); + $g: green($color); + $b: blue($color); + + $yiq: (($r * 299) + ($g * 587) + ($b * 114)) / 1000; + + @if ($yiq >= $threshold) { + @return $dark; + } @else { + @return $light; + } +} diff --git a/scss/components/datepicker.scss b/scss/components/datepicker.scss new file mode 100644 index 00000000..e30b5224 --- /dev/null +++ b/scss/components/datepicker.scss @@ -0,0 +1,194 @@ +.dash-bootstrap { + .DateRangePickerInput, + .SingleDatePickerInput { + height: $input-height; + @include font-size($input-font-size); + font-family: $input-font-family; + font-weight: $input-font-weight; + color: $input-color; + background-color: $input-bg; + background-clip: padding-box; + border: $input-border-width solid $input-border-color; + @include border-radius($input-border-radius, 0); + @include transition($input-transition); + + &:hover { + box-shadow: none; + } + } + + .DateInput { + @include font-size($input-font-size); + font-weight: $input-font-weight; + color: $input-color; + background-color: transparent; + background-clip: padding-box; + padding: 0; + height: calc(#{$input-line-height * 1em} + #{$input-padding-y * 2}); + } + + .DateInput__input { + padding: $input-padding-y $input-padding-x; + height: calc(#{$input-line-height * 1em} + #{$input-padding-y * 2}); + } + + .DateInput__display-text { + padding: $input-padding-y $input-padding-x; + height: calc(#{$input-line-height * 1em} + #{$input-padding-y * 2}); + line-height: $input-line-height; + color: $input-color; + font-family: $input-font-family; + font-weight: $input-font-weight; + } + + .DateInput__display-text--focused { + background: rgba(theme-color("primary"), 0.1); + } + + .DateRangePicker__picker, + .SingleDatePicker__picker { + top: calc(#{$input-height} + 15px); + background-color: transparent; + } + + .DayPicker { + @include reset-text(); + @include font-size($popover-font-size); + background-color: $popover-bg; + } + + .DayPicker--horizontal { + @include border-radius($popover-border-radius, 0); + @if $enable-shadows { + @include box-shadow($popover-box-shadow); + } @else { + box-shadow: none; + } + border: $popover-border-width solid $popover-border-color; + } + + .transition-container { + @include border-radius($popover-border-radius, 0); + width: calc(318px - 2 * #{$popover-border-width})!important; + } + + .DateInput--with-caret::after { + top: calc(#{$input-height} + 6px); + border-bottom-color: $popover-bg; + } + + .DateInput--with-caret::before { + top: calc(#{$input-height} + 5px); + border-bottom-color: $popover-border-color; + } + + .CalendarMonth__caption { + color: $popover-body-color; + } + + .CalendarMonthGrid { + background-color: $popover-bg; + } + + .CalendarDay { + color: $popover-body-color; + border-color: color-yiq($popover-bg, gray("200"), gray("600")); + } + + .CalendarDay--outside { + border: 0; + cursor: default; + } + + .CalendarDay--hovered { + background: color-yiq($popover-bg, gray("100"), gray("600")); + border: 1px solid color-yiq($popover-bg, gray("300"), gray("500")); + color: color-yiq(gray("100"), $yiq-text-dark, $yiq-text-light, 200); + } + + .CalendarDay--selected-span { + $span-color: color-yiq($popover-bg, lighten(theme-color("primary"), 10), darken(theme-color("primary"), 10)); + background: $span-color; + border: 1px double $span-color; + color: color-yiq($span-color); + } + + .CalendarDay--hovered-span { + $span-color: color-yiq($popover-bg, lighten(theme-color("primary"), 5), darken(theme-color("primary"), 5)); + background: $span-color; + color: color-yiq($span-color); + } + + .CalendarDay--after-hovered-start { + $span-color: color-yiq($popover-bg, lighten(theme-color("info"), 15), darken(theme-color("info"), 15)); + background: $span-color; + color: color-yiq($span-color); + } + + .CalendarDay--selected, + .CalendarDay--selected-end, + .CalendarDay--selected-start { + background: theme-color("primary"); + border-color: theme-color("primary"); + color: color-yiq(theme-color("primary")); + } + + .CalendarDay--blocked-out-of-range { + color: color-yiq($popover-bg, gray("200"), gray("700")); + border-color: color-yiq($popover-bg, gray("200"), gray("700")); + background: $popover-bg; + cursor: default; + } + + .DayPickerNavigation--horizontal .DayPickerNavigation__next { + right: 20px; + } + + .DayPickerNavigation--horizontal .DayPickerNavigation__next, + .DayPickerNavigation--horizontal .DayPickerNavigation__prev { + @include border-radius($input-border-radius, 0); + border: $input-border-width solid $popover-border-color; + background: $popover-bg; + + svg { + fill: $popover-body-color; + } + + &:hover { + background: color-yiq($popover-bg, gray("200"), gray("800")); + + svg { + fill: color-yiq(gray("100"), $yiq-text-dark, $yiq-text-light, 200); + } + } + } + + .DayPickerKeyboardShortcuts__show--bottom-right { + border-top: 12px solid transparent; + border-bottom: 12px solid theme-color("primary"); + border-left: 16px solid transparent; + border-right: 16px solid theme-color("primary"); + @if $enable-rounded { + border-bottom-right-radius: calc(#{$popover-border-radius} - #{$popover-border-width}); + } @else { + border-bottom-right-radius: 0; + } + + &:hover { + border-top: 12px solid transparent; + border-bottom: 12px solid darken(theme-color("primary"), 7.5%); + border-left: 16px solid transparent; + border-right: 16px solid darken(theme-color("primary"), 7.5%); + } + + .DayPickerKeyboardShortcuts__show_span { + bottom: -11px; + right: -12px; + color: color-yiq(theme-color("primary")); + } + } + + .CalendarMonth__caption { + color: $popover-header-color; + } +} diff --git a/scss/components/dropdown.scss b/scss/components/dropdown.scss new file mode 100644 index 00000000..36ab1c56 --- /dev/null +++ b/scss/components/dropdown.scss @@ -0,0 +1,95 @@ +.dash-bootstrap { + .Select-control { + height: $input-height; + @include font-size($input-font-size); + font-family: $input-font-family; + font-weight: $input-font-weight; + line-height: $input-line-height; + color: $input-color; + background-color: $input-bg; + background-clip: padding-box; + border: $input-border-width solid $input-border-color; + @include border-radius($input-border-radius, 0); + @include transition($input-transition); + + &:hover { + box-shadow: none; + } + } + + .is-open > .Select-control { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + } + + .is-focused:not(.is-open) > .Select-control { + border-color: $input-focus-border-color; + outline: 0; + box-shadow: $input-focus-box-shadow; + } + + .Select.is-disabled > .Select-control { + background-color: $input-disabled-bg; + } + + .Select--multi { + .Select-value { + @include border-radius($input-border-radius / 2, 0); + color: theme-color("primary"); + border-color: rgba(theme-color("primary"), 0.25); + background-color: rgba(theme-color("primary"), 0.1); + } + + .Select-value-icon { + border-right-color: rgba(theme-color("primary"), 0.25); + } + } + + .has-value.Select--single > .Select-control .Select-value .Select-value-label, + .has-value.is-pseudo-focused.Select--single > .Select-control .Select-value .Select-value-label { + color: $input-color; + } + + .Select-multi-value-wrapper { + height: calc(#{$input-line-height * 1em} + #{$input-padding-y * 2}); + } + + .Select-placeholder { + color: $input-placeholder-color; + padding-left: $input-padding-x; + padding-right: $input-padding-x; + } + + .Select--single > .Select-control .Select-value, + .Select-placeholder { + line-height: calc(#{$input-line-height * 1em} + #{$input-padding-y * 2}); + } + + .Select-input { + height: calc(#{$input-line-height * 1em} + #{$input-padding-y * 2}); + } + + .Select-menu-outer { + @if $enable-rounded { + border-bottom-right-radius: $input-border-radius; + border-bottom-left-radius: $input-border-radius; + } @else { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + } + background-color: $input-bg; + background-clip: padding-box; + border: $input-border-width solid $input-border-color; + border-top-color: $input-border-color; + box-shadow: none; + color: $input-color; + } + // !important is necessary to override inline styles + .VirtualizedSelectOption { + height: calc(1.5em + 0.75rem)!important; + } + + .VirtualizedSelectFocusedOption { + background-color: rgba(theme-color("primary"), 0.1)!important; + } +} diff --git a/scss/components/slider.scss b/scss/components/slider.scss new file mode 100644 index 00000000..e69de29b