├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs └── Slides.pdf ├── exerciseLICENSE ├── exercises ├── bem │ ├── exercise.json │ ├── public │ │ ├── index.html │ │ └── tests.js │ └── src │ │ └── sass │ │ └── app.scss ├── extend │ ├── exercise.json │ ├── public │ │ ├── index.html │ │ └── tests.js │ └── src │ │ └── sass │ │ └── app.scss ├── functions │ ├── exercise.json │ ├── public │ │ ├── index.html │ │ └── tests.js │ └── src │ │ └── sass │ │ ├── _mixins.scss │ │ ├── _variables.scss │ │ └── app.scss ├── if │ ├── exercise.json │ ├── public │ │ ├── index.html │ │ └── tests.js │ └── src │ │ └── sass │ │ ├── _mixins.scss │ │ ├── _variables.scss │ │ └── app.scss ├── luminance │ ├── exercise.json │ ├── public │ │ ├── index.html │ │ └── tests.js │ └── src │ │ └── sass │ │ ├── _functions.scss │ │ ├── _mixins.scss │ │ ├── _variables.scss │ │ └── app.scss ├── mixins │ ├── exercise.json │ ├── public │ │ ├── index.html │ │ └── tests.js │ └── src │ │ └── sass │ │ ├── _mixins.scss │ │ ├── _variables.scss │ │ └── app.scss ├── nesting │ ├── exercise.json │ ├── public │ │ ├── index.html │ │ └── tests.js │ └── src │ │ └── sass │ │ └── app.scss ├── parent │ ├── exercise.json │ ├── public │ │ ├── index.html │ │ └── tests.js │ └── src │ │ └── sass │ │ └── app.scss ├── range │ ├── exercise.json │ ├── public │ │ ├── index.html │ │ └── tests.js │ └── src │ │ └── sass │ │ ├── _mixins.scss │ │ ├── _variables.scss │ │ └── app.scss ├── tiny │ ├── exercise.json │ ├── public │ │ ├── index.html │ │ └── tests.js │ └── src │ │ └── sass │ │ └── app.scss └── variables │ ├── exercise.json │ ├── public │ ├── index.html │ └── tests.js │ └── src │ └── sass │ ├── _variables.scss │ └── app.scss ├── package.json ├── public ├── css │ └── normalize.css ├── index.html └── js │ ├── tester.js │ └── utils.js ├── renovate.json ├── run ├── run.bat ├── src ├── exercise.js ├── program.js └── server.js ├── tools └── rebase-exercise-branches.js └── yarn.lock /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Something broke while you were progressing through the workshop 4 | 5 | --- 6 | 7 | 8 | 11 | 12 | - [ ] **System Information** 13 | - [ ] Browser type and version 14 | - [ ] OS type and version 15 | - [ ] WINDOWS: be sure to indicate which terminal you're using -- (i.e., cmd.exe, powershell, git- bash, cygwin, Ubuntu via windows subsystem for linux, etc...) 16 | - [ ] Node version 17 | - [ ] Any error messages that may be in the console where you ran npm start 18 | - [ ] Any error messages in the JS console 19 | 20 | - [ ] **Describe the bug** 21 | 22 | 23 | - [ ] **To Reproduce** 24 | Steps to reproduce the behavior: 25 | 1. Go to '...' 26 | 2. Click on '....' 27 | 3. Scroll down to '....' 28 | 4. See error 29 | 30 | - [ ] **Expected behavior** 31 | A clear and concise description of what you expected to happen. 32 | 33 | - [ ] **Screenshots (optional)** 34 | If applicable, add screenshots to help explain your problem. 35 | 36 | - [ ] **Additional context (optional)** 37 | Add any other context about the problem here. 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | exercises/**/public/*.css -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | - "8.17.0" 5 | 6 | sudo: required 7 | dist: trusty 8 | 9 | addons: 10 | chrome: stable 11 | 12 | cache: 13 | directories: 14 | - $HOME/.npm 15 | before_install: 16 | - npm config set spin false 17 | 18 | before_script: 19 | - sudo chmod 4755 /opt/google/chrome/chrome-sandbox 20 | - sudo chown root /opt/google/chrome/chrome-sandbox 21 | 22 | install: 23 | - yarn install --non-interactive 24 | 25 | script: npm run test:ci 26 | 27 | after_success: 28 | - npm run travis-deploy-once "npm run semantic-release" 29 | branches: 30 | except: 31 | - /^v\d+\.\d+\.\d+$/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.0.13](https://github.com/mike-works/sass-fundamentals.git/compare/v1.0.12...v1.0.13) (2020-02-05) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * **deps:** update dependency livereload to v0.9.1 ([07d22e8](https://github.com/mike-works/sass-fundamentals.git/commit/07d22e8)) 7 | 8 | ## [1.0.12](https://github.com/mike-works/sass-fundamentals.git/compare/v1.0.11...v1.0.12) (2020-01-30) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * **deps:** update dependency livereload to v0.9.0 ([8325072](https://github.com/mike-works/sass-fundamentals.git/commit/8325072)) 14 | 15 | ## [1.0.11](https://github.com/mike-works/sass-fundamentals.git/compare/v1.0.10...v1.0.11) (2019-10-11) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * **deps:** update dependency commander to v2.20.3 ([42a0487](https://github.com/mike-works/sass-fundamentals.git/commit/42a0487)) 21 | 22 | ## [1.0.10](https://github.com/mike-works/sass-fundamentals.git/compare/v1.0.9...v1.0.10) (2019-10-03) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * **deps:** update dependency livereload to v0.8.2 ([5d0cd67](https://github.com/mike-works/sass-fundamentals.git/commit/5d0cd67)) 28 | 29 | ## [1.0.9](https://github.com/mike-works/sass-fundamentals.git/compare/v1.0.8...v1.0.9) (2019-09-28) 30 | 31 | 32 | ### Bug Fixes 33 | 34 | * **deps:** update dependency commander to v2.20.1 ([eda81bc](https://github.com/mike-works/sass-fundamentals.git/commit/eda81bc)) 35 | 36 | ## [1.0.8](https://github.com/mike-works/sass-fundamentals.git/compare/v1.0.7...v1.0.8) (2019-09-17) 37 | 38 | 39 | ### Bug Fixes 40 | 41 | * **deps:** update dependency livereload to v0.8.1 ([836ccc0](https://github.com/mike-works/sass-fundamentals.git/commit/836ccc0)) 42 | 43 | ## [1.0.7](https://github.com/mike-works/sass-fundamentals.git/compare/v1.0.6...v1.0.7) (2019-05-26) 44 | 45 | 46 | ### Bug Fixes 47 | 48 | * **deps:** update dependency express to v4.17.1 ([65624c5](https://github.com/mike-works/sass-fundamentals.git/commit/65624c5)) 49 | 50 | ## [1.0.6](https://github.com/mike-works/sass-fundamentals.git/compare/v1.0.5...v1.0.6) (2019-05-25) 51 | 52 | 53 | ### Bug Fixes 54 | 55 | * **deps:** update dependency livereload to v0.8.0 ([d508aa1](https://github.com/mike-works/sass-fundamentals.git/commit/d508aa1)) 56 | 57 | ## [1.0.5](https://github.com/mike-works/sass-fundamentals.git/compare/v1.0.4...v1.0.5) (2019-05-17) 58 | 59 | 60 | ### Bug Fixes 61 | 62 | * **deps:** update dependency express to v4.17.0 ([01a8f78](https://github.com/mike-works/sass-fundamentals.git/commit/01a8f78)) 63 | 64 | ## [1.0.4](https://github.com/mike-works/sass-fundamentals.git/compare/v1.0.3...v1.0.4) (2019-04-03) 65 | 66 | 67 | ### Bug Fixes 68 | 69 | * **deps:** update dependency commander to v2.20.0 ([b19c882](https://github.com/mike-works/sass-fundamentals.git/commit/b19c882)) 70 | 71 | ## [1.0.3](https://github.com/mike-works/sass-fundamentals.git/compare/v1.0.2...v1.0.3) (2019-01-05) 72 | 73 | 74 | ### Bug Fixes 75 | 76 | * **deps:** update dependency chalk to v2.4.2 ([2816bc0](https://github.com/mike-works/sass-fundamentals.git/commit/2816bc0)) 77 | 78 | ## [1.0.2](https://github.com/mike-works/sass-fundamentals.git/compare/v1.0.1...v1.0.2) (2018-11-13) 79 | 80 | 81 | ### Bug Fixes 82 | 83 | * shared renovate and semantic-release config ([d2713f2](https://github.com/mike-works/sass-fundamentals.git/commit/d2713f2)) 84 | 85 | ## [1.0.1](https://github.com/mike-works/sass-fundamentals/compare/v1.0.0...v1.0.1) (2018-08-21) 86 | 87 | 88 | ### Bug Fixes 89 | 90 | * ci-badge ([b0d2ec6](https://github.com/mike-works/sass-fundamentals/commit/b0d2ec6)) 91 | * enable custom semantic-release config ([3c8ac3b](https://github.com/mike-works/sass-fundamentals/commit/3c8ac3b)) 92 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018, Mike Works, Inc. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

6 |

7 | 8 | 9 | 10 |

11 | 12 |

13 | 14 | 15 | 16 | 17 | 18 | 19 |

20 |

21 | This is the project used for the Mike.Works Sass Fundamentals course. 22 |

23 | 24 | # Course outline and slides 25 | * [View course outline here](https://mike.works/course/sass-fundamentals-5438fec/stage/sass-fundamentals-ca61dca) 26 | * [View slides here](https://docs.mike.works/sass-fundamentals) 27 | 28 | # What are the pieces? 29 | 30 | * [node-sass-middleware](https://github.com/sass/node-sass-middleware) for Sass compilation 31 | * [express](http://expressjs.com/) for serving HTML and CSS (compiled from Sass) 32 | * [commander](https://github.com/tj/commander.js) as a foundation for a CLI to run exerises 33 | * [A little CSS testing framework](https://github.com/mike-works/sass-fundamentals/blob/master/public/js/tester.js) for asserting that exercise goals have been reached! 34 | 35 | # Getting Set Up 36 | 37 | There are a few things you need to ensure you have installed, in order to be ready for this course. 38 | 39 | ### Node.js 40 | 41 | You’ll need a relatively recent version (v4.5 or newer, v7 ideally) of node.js installed. On OS X, a great way of doing this without disturbing your existing dev environment is to install NVM. [Installation instructions are here](https://github.com/creationix/nvm#installation). 42 | 43 | You’ll know everything is set up properly when you can run 44 | 45 | ``` 46 | nvm --version # might look like "0.31.4" 47 | node --version # might look like "v7.7.3" 48 | ``` 49 | 50 | ### Visual Studio Code 51 | 52 | Particularly if you’ve never tried it before, you should install [Microsoft Visual Studio Code](https://code.visualstudio.com/). Some fantastic extensions that I use regularly include 53 | * [css-triggers](https://marketplace.visualstudio.com/items?itemName=kisstkondoros.csstriggers) 54 | * [vscode-icons](https://marketplace.visualstudio.com/items?itemName=robertohuertasm.vscode-icons) 55 | * [Sublime Text Keymap](https://marketplace.visualstudio.com/items?itemName=ms-vscode.sublime-keybindings) - Install if you’re used to sublime text keyboard shortcuts 56 | 57 | ### Check out and setup the project for this workshop 58 | 59 | ``` 60 | git clone git@github.com:mike-works/sass-fundamentals.git 61 | cd sass-fundamentals 62 | npm install 63 | ``` 64 | 65 | 66 | # How to use it 67 | Use the `run` command to launch an exercise 68 | 69 | ```sh 70 | ./run --exercise 71 | ``` 72 | 73 | # License 74 | While the general license for this project is the BSD 3-clause, the exercises 75 | themselves are proprietary and are licensed on a per-individual basis, usually 76 | as a result of purchasing a ticket to a public workshop, being a participant 77 | in a private training, or having a Front End Masters membership. 78 | 79 | Here are some guidelines for things that are **OK** and **NOT OK**, based on our 80 | understanding of how these licenses work: 81 | 82 | ### OK 83 | * Using everything in this project other than the exercises (or accompanying tests) 84 | to build a project used for your own free or commercial training material 85 | * Copying code from build scripts, configuration files, tests and development 86 | harnesses that are not part of the exercises specifically, for your own projects 87 | * As an owner of an individual license, using code from tests, exercises, or 88 | exercise solutions for your own non-training-related project. 89 | 90 | ### NOT OK (without express written consent) 91 | * Using this project, or any subset of 92 | exercises contained within this project to run your own workshops 93 | * Writing a book that uses the code for these exercises 94 | * Recording a screencast that contains one or more of this project's exercises 95 | 96 | 97 | # Copyright 98 | 99 | © 2018 [Mike.Works](https://mike.works), All Rights Reserved 100 | 101 | ###### This material may not be used for workshops, training, or any other form of instructing or teaching developers, without express written consent 102 | -------------------------------------------------------------------------------- /docs/Slides.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-works/sass-fundamentals/c1b12cd7ec582ea761284596918f2022e80ec717/docs/Slides.pdf -------------------------------------------------------------------------------- /exerciseLICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018, Mike Works, Inc. All rights reserved. 2 | 3 | This training material (all exercises and accompanying tests) is licensed to 4 | the individual who purchased it. We don't copy-protect it because that would 5 | limit your ability to use it for your own purposes. Please don't break this 6 | trust - don't allow others to use these exercises without purchasing their own 7 | license. Thanks. 8 | 9 | Redistribution and use in source and binary forms, with or without 10 | modification, are not permitted provided without an individual license. It may 11 | not be used to create training material, courses, books, articles, and the like. 12 | Contact us if you are in doubt. We make no guarantees that this code is 13 | fit for any purpose. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 19 | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /exercises/bem/exercise.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "BEM CSS Architecture", 3 | "instructions": "Design a 'one click checkout' button using BEM styles" 4 | } -------------------------------------------------------------------------------- /exercises/bem/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

BEM Buttons

7 | 8 |

9 | 12 | 13 |

14 | 15 |

16 | 19 | 20 |

21 | 22 |

23 | 26 | 27 |

28 | 29 |

30 | 33 | 34 |

35 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /exercises/bem/public/tests.js: -------------------------------------------------------------------------------- 1 | test('Primary button', function(assert) { 2 | assert.hasStyle('.btn.btn--mode-primary', { 3 | 'padding-left': '10px', 4 | 'padding-right': '10px', 5 | 'padding-top': '2px', 6 | 'padding-bottom': '2px', 7 | 'line-height': '20px', 8 | 'border-radius': assert.compare.eq('2px'), 9 | 'border-style': 'solid', 10 | 'border-color': assert.compare.truthy(), 11 | 'border-width': assert.compare.gt(0) 12 | }, 'styles are incorrect'); 13 | 14 | assert.hasStyle('.btn.btn--mode-primary', { 15 | 'background-color': 'rgb(204, 68, 102)', 16 | 'color': 'rgb(255, 255, 255)', 17 | 'opacity': '1' 18 | }, 'colors for enabled state are incorrect'); 19 | assert.hasStyle('.btn.btn--mode-primary:disabled', { 20 | 'opacity': '0.5' 21 | }, 'colors for enabled state are incorrect'); 22 | 23 | }); 24 | 25 | test('Secondary button', function(assert) { 26 | assert.hasStyle('.btn.btn--mode-secondary', { 27 | 'padding-left': '10px', 28 | 'padding-right': '10px', 29 | 'padding-top': '2px', 30 | 'padding-bottom': '2px', 31 | }, 'padding is incorrect'); 32 | 33 | assert.hasStyle('.btn.btn--mode-secondary', { 34 | 'background-color': 'rgb(237, 188, 200)', 35 | 'color': 'rgb(0, 0, 0)', 36 | 'opacity': '1' 37 | }, 'colors for enabled state are incorrect'); 38 | assert.hasStyle('.btn.btn--mode-secondary:disabled', { 39 | 'opacity': '0.5' 40 | }, 'colors for enabled state are incorrect'); 41 | }); 42 | 43 | test('Price', function(assert) { 44 | assert.hasStyle('.btn .btn__price', { 45 | 'padding-left': '3px', 46 | 'padding-right': '3px', 47 | 'padding-top': '1px', 48 | 'padding-bottom': '1px', 49 | }, 'padding is incorrect'); 50 | 51 | assert.hasStyle('.btn .btn__price', { 52 | 'background-color': '#008000', 53 | 'color': '#ffffff' 54 | }, 'colors for enabled state are incorrect'); 55 | assert.hasStyle('.btn:disabled .btn__price', { 56 | 'background-color': '#aaaaaa', 57 | 'color': '#ffffff' 58 | }, 'colors for enabled state are incorrect'); 59 | }); 60 | -------------------------------------------------------------------------------- /exercises/bem/src/sass/app.scss: -------------------------------------------------------------------------------- 1 | body { 2 | h1 { 3 | color: #c69; 4 | } 5 | } 6 | 7 | .btn { 8 | padding: 2px 10px; 9 | border-style: solid; 10 | border-width: 1px; 11 | line-height: 20px; 12 | border-radius: 2px; 13 | } -------------------------------------------------------------------------------- /exercises/extend/exercise.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Extend", 3 | "instructions": "Use the @extend directive to build buttons" 4 | } -------------------------------------------------------------------------------- /exercises/extend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

@extend Buttons

7 | 8 |

9 | 10 | 11 |

12 | 13 |

14 | 15 | 16 |

17 | 18 |

19 | 20 | 21 |

22 | 23 |

24 | 25 | 26 |

27 |

28 | 29 | 30 | 31 |

32 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /exercises/extend/public/tests.js: -------------------------------------------------------------------------------- 1 | test('Primary button', function(assert) { 2 | assert.hasStyle('.btn.btn-primary', { 3 | 'padding-left': '10px', 4 | 'padding-right': '10px', 5 | 'padding-top': '2px', 6 | 'padding-bottom': '2px', 7 | 'border-radius': assert.compare.eq('2px'), 8 | 'border-style': 'solid', 9 | 'border-color': assert.compare.truthy(), 10 | 'border-width': assert.compare.gt(0) 11 | }, 'styles are incorrect'); 12 | 13 | assert.hasStyle('.btn.btn-primary', { 14 | 'background-color': 'rgb(204, 68, 102)', 15 | 'color': 'rgb(255, 255, 255)', 16 | 'opacity': '1' 17 | }, 'colors for enabled state are incorrect'); 18 | assert.hasStyle('.btn.btn-primary:disabled', { 19 | 'opacity': '0.5' 20 | }, 'colors for enabled state are incorrect'); 21 | 22 | }); 23 | 24 | test('Secondary button', function(assert) { 25 | assert.hasStyle('.btn.btn-secondary', { 26 | 'padding-left': '10px', 27 | 'padding-right': '10px', 28 | 'padding-top': '2px', 29 | 'padding-bottom': '2px', 30 | }, 'padding is incorrect'); 31 | 32 | assert.hasStyle('.btn.btn-secondary', { 33 | 'background-color': 'rgb(237, 188, 200)', 34 | 'color': 'rgb(0, 0, 0)', 35 | 'opacity': '1' 36 | }, 'colors for enabled state are incorrect'); 37 | assert.hasStyle('.btn.btn-secondary:disabled', { 38 | 'opacity': '0.5' 39 | }, 'colors for enabled state are incorrect'); 40 | }); 41 | 42 | test('Unstyled button', function(assert) { 43 | $unstyled = document.querySelector('button.no-style'); 44 | $unstyledCss = window.getComputedStyle($unstyled); 45 | 46 | assert.hasStyle('.unstyled .btn', { 47 | 'padding-left': $unstyledCss['padding-left'], 48 | 'padding-right': $unstyledCss['padding-right'], 49 | 'padding-top': $unstyledCss['padding-top'], 50 | 'padding-bottom': $unstyledCss['padding-bottom'], 51 | }, 'padding is incorrect'); 52 | 53 | assert.hasStyle('.unstyled .btn', { 54 | 'background-color': $unstyledCss['background-color'], 55 | 'color': $unstyledCss['color'], 56 | 'opacity': $unstyledCss['opacity'] 57 | }, 'colors for enabled state are incorrect'); 58 | assert.hasStyle('.unstyled .btn:disabled', { 59 | 'opacity': $unstyledCss['opacity'] 60 | }, 'colors for enabled state are incorrect'); 61 | }); 62 | -------------------------------------------------------------------------------- /exercises/extend/src/sass/app.scss: -------------------------------------------------------------------------------- 1 | body { 2 | h1 { 3 | color: #c69; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /exercises/functions/exercise.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Functions - Color", 3 | "instructions": "Use color functions with a mixin to generate multiple themes for our set of buttons" 4 | } -------------------------------------------------------------------------------- /exercises/functions/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

Functions - Color

7 | 8 | 9 |

10 | Theme 1 11 | 12 | 13 | 14 | 15 | 16 | 17 |

18 | 19 |

20 | Theme 2 21 | 22 | 23 | 24 | 25 | 26 | 27 |

28 | 29 |

30 | Theme 3 31 | 32 | 33 | 34 | 35 | 36 | 37 |

38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /exercises/functions/public/tests.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | function testTheme(themename, c1, c2, c3, c4) { 3 | test(themename + ' button colors', function(assert) { 4 | assert.hasStyle('.' + themename + ' .btn.btn-primary', { 5 | 'background-color': c1 6 | }, 'primary button bg color is incorrect'); 7 | assert.hasStyle('.' + themename + ' .btn.btn-secondary', { 8 | 'background-color': c2 9 | }, 'secondary button bg color is incorrect'); 10 | assert.hasStyle('.' + themename + ' .btn.btn-other', { 11 | 'background-color': c3 12 | }, 'other button bg color is incorrect'); 13 | assert.hasStyle('.' + themename + ' .btn.btn-other2', { 14 | 'background-color': c4 15 | }, 'other2 button bg color is incorrect'); 16 | }); 17 | } 18 | test('First button', function(assert) { 19 | assert.hasStyle('.btn', { 20 | 'border-radius': assert.compare.eq('2px'), 21 | 'border-style': 'solid', 22 | 'border-color': assert.compare.truthy(), 23 | 'border-width': assert.compare.gt(0), 24 | 'display': 'inline-block' 25 | }, 'styles are incorrect'); 26 | }) 27 | 28 | testTheme('theme-1', '#339999', '#3c4d1a', '#3c1a4d', '#4d1a1a'); 29 | testTheme('theme-2', '#660099', '#006644', '#664400', '#226600'); 30 | testTheme('theme-3', '#cc6699', '#336699', '#669933', '#339966'); 31 | }()); -------------------------------------------------------------------------------- /exercises/functions/src/sass/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin theme($primary-color, $rotate: 120deg, $darkenpct: 20%) { 2 | 3 | } -------------------------------------------------------------------------------- /exercises/functions/src/sass/_variables.scss: -------------------------------------------------------------------------------- 1 | $hopbush: #c69 !default; 2 | $patina: #699 !default; 3 | $venus: #998099 !default; 4 | $nebula: #d2e1dd !default; 5 | 6 | $black: #000 !default; 7 | $white: #fff !default; 8 | 9 | $btn-disabled-opacity: 0.5 !default; 10 | 11 | -------------------------------------------------------------------------------- /exercises/functions/src/sass/app.scss: -------------------------------------------------------------------------------- 1 | @import '_variables'; 2 | @import '_mixins'; 3 | 4 | .btn { 5 | padding: 2px 10px; 6 | border: 1px solid transparent; 7 | border-radius: 2px; 8 | color: #fff; 9 | &:hover:not(:disabled) { 10 | box-shadow: 0 5px 15px rgba(0, 0, 0, 0.8); 11 | } 12 | &:disabled { 13 | color: #666; 14 | opacity: 0.5; 15 | } 16 | } 17 | 18 | .theme-1 { 19 | @include theme(#399, 100deg); 20 | } 21 | 22 | .theme-2 { 23 | @include theme(#609, $darkenpct: 10%); 24 | } 25 | 26 | .theme-3 { 27 | @include theme($hopbush); 28 | } -------------------------------------------------------------------------------- /exercises/if/exercise.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Control Flow - @if", 3 | "instructions": "Use @if to set button text color to either black or white, on a per-button basis" 4 | } -------------------------------------------------------------------------------- /exercises/if/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

Control Flow - @if

7 | 8 | 9 |

10 | Theme 1 11 | 12 | 13 | 14 | 15 | 16 | 17 |

18 | 19 |

20 | Theme 2 21 | 22 | 23 | 24 | 25 | 26 | 27 |

28 | 29 |

30 | Theme 3 31 | 32 | 33 | 34 | 35 | 36 | 37 |

38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /exercises/if/public/tests.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | function rgbToHsl(r, g, b) { 3 | r /= 255, g /= 255, b /= 255; 4 | 5 | var max = Math.max(r, g, b), min = Math.min(r, g, b); 6 | var h, s, l = (max + min) / 2; 7 | 8 | if (max == min) { 9 | h = s = 0; // achromatic 10 | } else { 11 | var d = max - min; 12 | s = l > 0.5 ? d / (2 - max - min) : d / (max + min); 13 | 14 | switch (max) { 15 | case r: h = (g - b) / d + (g < b ? 6 : 0); break; 16 | case g: h = (b - r) / d + 2; break; 17 | case b: h = (r - g) / d + 4; break; 18 | } 19 | 20 | h /= 6; 21 | } 22 | 23 | return [h, s, l]; 24 | } 25 | 26 | function whiteIfAbove70Brightness(h) { 27 | var hex = h.replace('#', ''); 28 | var r = parseInt(hex.substring(0, 2), 16); 29 | var g = parseInt(hex.substring(2, 4), 16); 30 | var b = parseInt(hex.substring(4, 6), 16); 31 | var parts = rgbToHsl(r, g, b); 32 | var brightness = parts[2] 33 | return brightness >= 0.7 ? '#000000' : '#ffffff'; 34 | } 35 | 36 | function testTheme(themename, c1, c2, c3, c4) { 37 | test(themename + ' button colors', function (assert) { 38 | assert.hasStyle('.' + themename + ' .btn.btn-primary', { 39 | 'background-color': c1, 40 | color: whiteIfAbove70Brightness(c1) 41 | }, 'primary button styles are incorrect'); 42 | assert.hasStyle('.' + themename + ' .btn.btn-secondary', { 43 | 'background-color': c2, 44 | color: whiteIfAbove70Brightness(c2) 45 | }, 'secondary button styles are incorrect'); 46 | assert.hasStyle('.' + themename + ' .btn.btn-other', { 47 | 'background-color': c3, 48 | color: whiteIfAbove70Brightness(c3) 49 | }, 'other button styles are incorrect'); 50 | assert.hasStyle('.' + themename + ' .btn.btn-other2', { 51 | 'background-color': c4, 52 | color: whiteIfAbove70Brightness(c4) 53 | }, 'other2 button styles are incorrect'); 54 | }); 55 | } 56 | test('First button', function (assert) { 57 | assert.hasStyle('.btn', { 58 | 'border-radius': assert.compare.eq('2px'), 59 | 'border-style': 'solid', 60 | 'border-color': assert.compare.truthy(), 61 | 'border-width': assert.compare.gt(0), 62 | 'display': 'inline-block' 63 | }, 'styles are incorrect'); 64 | }) 65 | 66 | testTheme('theme-1', '#99ff99', '#3377ff', '#ff7733', '#ff33ff'); 67 | testTheme('theme-2', '#9999ff', '#ff6666', '#66ff66', '#ffff66'); 68 | testTheme('theme-3', '#aa66aa', '#6d6d3d', '#3d6d6d', '#3d6d3d'); 69 | }()); -------------------------------------------------------------------------------- /exercises/if/src/sass/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin theme($primary-color, $rotate: 120deg, $darkenpct: 20%) { 2 | .btn-primary { 3 | @include button-base($primary-color); 4 | } 5 | .btn-secondary { 6 | @include button-base( 7 | darken( 8 | adjust-hue($primary-color, $rotate), 9 | $darkenpct) 10 | ); 11 | } 12 | .btn-other { 13 | @include button-base( 14 | darken( 15 | adjust-hue($primary-color, 360deg - $rotate), 16 | $darkenpct) 17 | ); 18 | } 19 | .btn-other2 { 20 | @include button-base( 21 | darken( 22 | adjust-hue($primary-color, 180deg), 23 | $darkenpct) 24 | ); 25 | } 26 | } 27 | 28 | @mixin button-base($color) { 29 | background-color: $color; 30 | border-color: darken($color, 20%); 31 | &:hover:not(:disabled) { 32 | background-color: lighten(saturate($color, 20%), 10%); 33 | } 34 | } -------------------------------------------------------------------------------- /exercises/if/src/sass/_variables.scss: -------------------------------------------------------------------------------- 1 | $hopbush: #c69 !default; 2 | $patina: #699 !default; 3 | $venus: #998099 !default; 4 | $nebula: #d2e1dd !default; 5 | 6 | $black: #000 !default; 7 | $white: #fff !default; 8 | 9 | $btn-disabled-opacity: 0.5 !default; 10 | 11 | -------------------------------------------------------------------------------- /exercises/if/src/sass/app.scss: -------------------------------------------------------------------------------- 1 | @import '_variables'; 2 | @import '_mixins'; 3 | 4 | .btn { 5 | padding: 2px 10px; 6 | border: 1px solid transparent; 7 | border-radius: 2px; 8 | &:hover:not(:disabled) { 9 | box-shadow: 0 5px 15px rgba(0, 0, 0, 0.8); 10 | } 11 | &:disabled { 12 | color: #666; 13 | opacity: 0.5; 14 | } 15 | } 16 | 17 | .theme-1 { 18 | @include theme(#9f9, 100deg); 19 | } 20 | 21 | .theme-2 { 22 | @include theme(#99f, $darkenpct: 10%); 23 | } 24 | 25 | .theme-3 { 26 | @include theme(#a6a); 27 | } 28 | -------------------------------------------------------------------------------- /exercises/luminance/exercise.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Writing custom functions", 3 | "instructions": "Take this solution to a previous exercise, and update it to base the logic for black/white button text to use contrast instead of brightness" 4 | } -------------------------------------------------------------------------------- /exercises/luminance/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

Control Flow - @if w/ Luminance

7 | 8 | 9 |

10 | Theme 1 11 | 12 | 13 | 14 | 15 | 16 | 17 |

18 | 19 |

20 | Theme 2 21 | 22 | 23 | 24 | 25 | 26 | 27 |

28 | 29 |

30 | Theme 3 31 | 32 | 33 | 34 | 35 | 36 | 37 |

38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /exercises/luminance/public/tests.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | function rgbToHsl(r, g, b) { 3 | r /= 255, g /= 255, b /= 255; 4 | 5 | var max = Math.max(r, g, b), min = Math.min(r, g, b); 6 | var h, s, l = (max + min) / 2; 7 | 8 | if (max == min) { 9 | h = s = 0; // achromatic 10 | } else { 11 | var d = max - min; 12 | s = l > 0.5 ? d / (2 - max - min) : d / (max + min); 13 | 14 | switch (max) { 15 | case r: h = (g - b) / d + (g < b ? 6 : 0); break; 16 | case g: h = (b - r) / d + 2; break; 17 | case b: h = (r - g) / d + 4; break; 18 | } 19 | 20 | h /= 6; 21 | } 22 | 23 | return [h, s, l]; 24 | } 25 | 26 | function testTheme(themename, c1, c2, c3, c4, c5, c6, c7, c8) { 27 | test(themename + ' button colors', function (assert) { 28 | assert.hasStyle('.' + themename + ' .btn.btn-primary', { 29 | 'background-color': c1, 30 | color: c5 31 | }, 'primary button styles are incorrect'); 32 | assert.hasStyle('.' + themename + ' .btn.btn-secondary', { 33 | 'background-color': c2, 34 | color: c6 35 | }, 'secondary button styles are incorrect'); 36 | assert.hasStyle('.' + themename + ' .btn.btn-other', { 37 | 'background-color': c3, 38 | color: c7 39 | }, 'other button styles are incorrect'); 40 | assert.hasStyle('.' + themename + ' .btn.btn-other2', { 41 | 'background-color': c4, 42 | color: c8 43 | }, 'other2 button styles are incorrect'); 44 | }); 45 | } 46 | test('First button', function (assert) { 47 | assert.hasStyle('.btn', { 48 | 'border-radius': assert.compare.eq('2px'), 49 | 'border-style': 'solid', 50 | 'border-color': assert.compare.truthy(), 51 | 'border-width': assert.compare.gt(0), 52 | 'display': 'inline-block' 53 | }, 'styles are incorrect'); 54 | }) 55 | 56 | testTheme('theme-1', '#99ff99', '#3377ff', '#ff7733', '#ff33ff', '#000000', '#ffffff', '#000000', '#000000'); 57 | testTheme('theme-2', '#9999ff', '#ff6666', '#66ff66', '#ffff66', '#000000', '#000000', '#000000', '#000000'); 58 | testTheme('theme-3', '#6666ff', '#ff0000', '#00ff00', '#ffff00', '#ffffff', '#ffffff', '#000000', '#000000'); 59 | }()); -------------------------------------------------------------------------------- /exercises/luminance/src/sass/_functions.scss: -------------------------------------------------------------------------------- 1 | /* Source: https://css-tricks.com/snippets/sass/power-function/ */ 2 | @function pow($number, $exponent) { 3 | @if (round($exponent) != $exponent) { 4 | @return exp($exponent * ln($number)); 5 | } 6 | 7 | $value: 1; 8 | 9 | @if $exponent > 0 { 10 | @for $i from 1 through $exponent { 11 | $value: $value * $number; 12 | } 13 | } @else if $exponent < 0 { 14 | @for $i from 1 through -$exponent { 15 | $value: $value / $number; 16 | } 17 | } 18 | 19 | @return $value; 20 | } 21 | 22 | @function factorial($value) { 23 | $result: 1; 24 | 25 | @if $value == 0 { 26 | @return $result; 27 | } 28 | 29 | @for $index from 1 through $value { 30 | $result: $result * $index; 31 | } 32 | 33 | @return $result; 34 | } 35 | 36 | @function summation($iteratee, $input, $initial: 0, $limit: 100) { 37 | $sum: 0; 38 | 39 | @for $index from $initial to $limit { 40 | $sum: $sum + call($iteratee, $input, $index); 41 | } 42 | 43 | @return $sum; 44 | } 45 | 46 | @function exp-maclaurin($x, $n) { 47 | @return (pow($x, $n) / factorial($n)); 48 | } 49 | 50 | @function exp($value) { 51 | @return summation('exp-maclaurin', $value, 0, 100); 52 | } 53 | 54 | @function ln-maclaurin($x, $n) { 55 | @return (pow(-1, $n + 1) / $n) * (pow($x - 1, $n)); 56 | } 57 | 58 | @function ln($value) { 59 | $ten-exp: 1; 60 | $ln-ten: 2.30258509; 61 | 62 | @while ($value > pow(10, $ten-exp)) { 63 | $ten-exp: $ten-exp + 1; 64 | } 65 | 66 | @return summation(ln-maclaurin, $value / pow(10, $ten-exp), 1, 100) + $ten-exp * $ln-ten; 67 | } -------------------------------------------------------------------------------- /exercises/luminance/src/sass/_mixins.scss: -------------------------------------------------------------------------------- 1 | @import "_functions"; 2 | 3 | @mixin theme($primary-color, $rotate: 120deg, $darkenpct: 20%) { 4 | .btn-primary { 5 | @include button-base($primary-color); 6 | } 7 | .btn-secondary { 8 | @include button-base( 9 | darken( 10 | adjust-hue($primary-color, $rotate), 11 | $darkenpct) 12 | ); 13 | } 14 | .btn-other { 15 | @include button-base( 16 | darken( 17 | adjust-hue($primary-color, 360deg - $rotate), 18 | $darkenpct) 19 | ); 20 | } 21 | .btn-other2 { 22 | @include button-base( 23 | darken( 24 | adjust-hue($primary-color, 180deg), 25 | $darkenpct) 26 | ); 27 | } 28 | } 29 | 30 | @function luminance($color) { 31 | /* REPLACE THIS WITH YOUR REAL FUNCTION */ 32 | @return 0.5; 33 | } 34 | 35 | $white_luminance: luminance(white); 36 | 37 | @mixin button-base($color) { 38 | background-color: $color; 39 | border-color: darken($color, 20%); 40 | /* luminance(#{$color}): #{luminance($color)} 41 | * luminance(white): #{luminance(white)} 42 | */ 43 | @if abs(luminance($color) - $white_luminance) > 0.7 { 44 | // @if lightness($color) < 70 { 45 | color: white; 46 | } 47 | &:hover:not(:disabled) { 48 | background-color: lighten(saturate($color, 20%), 10%); 49 | } 50 | } -------------------------------------------------------------------------------- /exercises/luminance/src/sass/_variables.scss: -------------------------------------------------------------------------------- 1 | $hopbush: #c69 !default; 2 | $patina: #699 !default; 3 | $venus: #998099 !default; 4 | $nebula: #d2e1dd !default; 5 | 6 | $black: #000 !default; 7 | $white: #fff !default; 8 | 9 | $btn-disabled-opacity: 0.5 !default; 10 | 11 | -------------------------------------------------------------------------------- /exercises/luminance/src/sass/app.scss: -------------------------------------------------------------------------------- 1 | @import '_variables'; 2 | @import '_mixins'; 3 | 4 | .btn { 5 | padding: 2px 10px; 6 | border: 1px solid transparent; 7 | border-radius: 2px; 8 | &:hover:not(:disabled) { 9 | box-shadow: 0 5px 15px rgba(0, 0, 0, 0.8); 10 | } 11 | &:disabled { 12 | color: #666; 13 | opacity: 0.5; 14 | } 15 | } 16 | 17 | .theme-1 { 18 | @include theme(#9f9, 100deg); 19 | } 20 | 21 | .theme-2 { 22 | @include theme(#99f, $darkenpct: 10%); 23 | } 24 | 25 | .theme-3 { 26 | @include theme(#66f); 27 | } 28 | -------------------------------------------------------------------------------- /exercises/mixins/exercise.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Mixins", 3 | "instructions": "Use mixins to create a bunch of colorful buttons" 4 | } -------------------------------------------------------------------------------- /exercises/mixins/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

Mixins

7 | 8 | 9 |

10 | 11 | 12 |

13 | 14 |

15 | 16 | 17 |

18 |

19 | 20 | 21 |

22 |

23 | 24 | 25 |

26 |

27 | 28 | 29 |

30 |

31 | 32 | 33 |

34 |

35 | 36 | 37 |

38 | 39 | 40 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /exercises/mixins/public/tests.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | function testButtonColor(colorName, fontColor, bgColor) { 3 | test(colorName + ' button', function(assert) { 4 | assert.hasStyle('.btn.btn-' + colorName, { 5 | color: fontColor, 6 | 'background-color': bgColor 7 | }, 'Colors are correct') 8 | }); 9 | } 10 | test('First button', function(assert) { 11 | assert.hasStyle('.btn', { 12 | 'border-radius': assert.compare.eq('2px'), 13 | 'border-style': 'solid', 14 | 'border-color': assert.compare.truthy(), 15 | 'border-width': assert.compare.gt(0), 16 | 'display': 'inline-block' 17 | }, 'styles are incorrect'); 18 | }) 19 | 20 | testButtonColor('yellow', '#ffffff', '#999900'); 21 | testButtonColor('blue', '#ffffff', '#3333aa'); 22 | testButtonColor('green', '#ffffff', '#33cc33'); 23 | testButtonColor('purple', '#ffffff', '#993399'); 24 | testButtonColor('pink', '#ffffff', '#cc6699'); 25 | testButtonColor('orange', '#ffffff', '#ff9933'); 26 | testButtonColor('brown', '#ffffff', '#995522'); 27 | 28 | }()); -------------------------------------------------------------------------------- /exercises/mixins/src/sass/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin btn-color($color) { 2 | 3 | } -------------------------------------------------------------------------------- /exercises/mixins/src/sass/_variables.scss: -------------------------------------------------------------------------------- 1 | $hopbush: #c69 !default; 2 | $patina: #699 !default; 3 | $venus: #998099 !default; 4 | $nebula: #d2e1dd !default; 5 | 6 | $black: #000 !default; 7 | $white: #fff !default; 8 | 9 | $btn-disabled-opacity: 0.5 !default; 10 | 11 | -------------------------------------------------------------------------------- /exercises/mixins/src/sass/app.scss: -------------------------------------------------------------------------------- 1 | @import '_variables'; 2 | @import '_mixins'; 3 | 4 | .btn { 5 | display: inline-block; 6 | border: 1px solid transparent; 7 | white-space: nowrap; 8 | vertical-align: middle; 9 | border-radius: 2px; 10 | user-select: none; 11 | transition: box-shadow 0.3s; 12 | &:hover:not(:disabled) { 13 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.8); 14 | } 15 | &:disabled { 16 | opacity: 0.65; 17 | box-shadow: none; 18 | } 19 | &.btn-yellow { @include btn-color(#990); } 20 | &.btn-blue { @include btn-color(#33a); } 21 | &.btn-green { @include btn-color(#3c3); } 22 | &.btn-purple { @include btn-color(#939); } 23 | &.btn-pink { @include btn-color($hopbush);} 24 | &.btn-orange { @include btn-color(#f93); } 25 | &.btn-brown { @include btn-color(#952); } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /exercises/nesting/exercise.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Nesting & Parent Selectors I", 3 | "instructions": "Design a button that has styling for primary and secondary modes, including disabled state" 4 | } -------------------------------------------------------------------------------- /exercises/nesting/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

Beautiful Buttons

7 | 8 |

9 | 10 | 11 |

12 | 13 |

14 | 15 | 16 |

17 | 18 |

19 | 20 | 21 |

22 | 23 |

24 | 25 | 26 |

27 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /exercises/nesting/public/tests.js: -------------------------------------------------------------------------------- 1 | test('Primary button', function(assert) { 2 | assert.hasStyle('.btn.btn-primary', { 3 | 'padding-left': '10px', 4 | 'padding-right': '10px', 5 | 'padding-top': '2px', 6 | 'padding-bottom': '2px', 7 | 'border-radius': assert.compare.eq('2px'), 8 | 'border-style': 'solid', 9 | 'border-color': assert.compare.truthy(), 10 | 'border-width': assert.compare.gt(0) 11 | }, 'styles are incorrect'); 12 | 13 | assert.hasStyle('.btn.btn-primary', { 14 | 'background-color': 'rgb(204, 68, 102)', 15 | 'color': 'rgb(255, 255, 255)', 16 | 'opacity': '1' 17 | }, 'colors for enabled state are incorrect'); 18 | assert.hasStyle('.btn.btn-primary:disabled', { 19 | 'opacity': '0.5' 20 | }, 'colors for enabled state are incorrect'); 21 | 22 | }); 23 | 24 | test('Secondary button', function(assert) { 25 | assert.hasStyle('.btn.btn-secondary', { 26 | 'padding-left': '10px', 27 | 'padding-right': '10px', 28 | 'padding-top': '2px', 29 | 'padding-bottom': '2px', 30 | }, 'padding is incorrect'); 31 | 32 | assert.hasStyle('.btn.btn-secondary', { 33 | 'background-color': 'rgb(237, 188, 200)', 34 | 'color': 'rgb(0, 0, 0)', 35 | 'opacity': '1' 36 | }, 'colors for enabled state are incorrect'); 37 | assert.hasStyle('.btn.btn-secondary:disabled', { 38 | 'opacity': '0.5' 39 | }, 'colors for enabled state are incorrect'); 40 | }); 41 | -------------------------------------------------------------------------------- /exercises/nesting/src/sass/app.scss: -------------------------------------------------------------------------------- 1 | body { 2 | h1 { 3 | color: #c46; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /exercises/parent/exercise.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Nesting & Parent Selectors II", 3 | "instructions": "Modify this layout so that adding the .right-nav class to the container results in the sidebar aligning to the right instead of the left" 4 | } -------------------------------------------------------------------------------- /exercises/parent/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 12 |
13 | Main Content 14 |
15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /exercises/parent/public/tests.js: -------------------------------------------------------------------------------- 1 | test('Normal container', function(assert) { 2 | var $container = document.querySelector('.container'); 3 | var hadRight = $container.classList.contains('right-nav'); 4 | $container.classList.remove('right-nav'); 5 | 6 | assert.hasStyle('.container', { 7 | 'max-width': '600px', 8 | 'width': assert.compare.lte(600) 9 | }, 'the container doesn\'t look right'); 10 | 11 | assert.hasStyle('.container > .main', { 12 | 'max-width': 'none', 13 | 'width': assert.compare.gte(200), 14 | 'margin-left': '220px', 15 | 'margin-right': '0px' 16 | }, 'main content area doesn\'t look right'); 17 | 18 | assert.hasStyle('.container > .sidebar', { 19 | 'width': '200px', 20 | 'float': 'left', 21 | }, 'the sidenav doesn\'t look right'); 22 | 23 | $container.classList.add('right-nav'); 24 | 25 | assert.hasStyle('.container.right-nav > .main', { 26 | 'max-width': 'none', 27 | 'margin-right': '220px', 28 | 'margin-left': '0px', 29 | 'width': assert.compare.gte(200) 30 | }, 'main content area doesn\'t look right'); 31 | 32 | assert.hasStyle('.container.right-nav > .sidebar', { 33 | 'width': '200px', 34 | 'float': 'right', 35 | }, 'the sidenav doesn\'t look right'); 36 | 37 | if (!hadRight) { 38 | $container.classList.remove('right-nav'); 39 | } 40 | }); -------------------------------------------------------------------------------- /exercises/parent/src/sass/app.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | max-width: 600px; 3 | width: 100%; 4 | margin: auto; 5 | background: #eee; 6 | 7 | .sidebar, 8 | .main { padding: 10px; } 9 | 10 | .main { 11 | margin-left: 220px; 12 | min-height: 100vh; 13 | border-left: 2px solid #333; 14 | } 15 | 16 | .sidebar { 17 | width: 200px; 18 | float: left; 19 | } 20 | } -------------------------------------------------------------------------------- /exercises/range/exercise.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Mixins - Range Input", 3 | "instructions": "Use the @content directive to style a range input thumb" 4 | } -------------------------------------------------------------------------------- /exercises/range/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

Mixins - Range Input

7 | 8 | 9 |

10 | 11 | 12 |

13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /exercises/range/public/tests.js: -------------------------------------------------------------------------------- 1 | // (function() { 2 | // function testButtonColor(colorName, fontColor, bgColor) { 3 | // test(colorName + ' button', function(assert) { 4 | // assert.hasStyle('.btn.btn-' + colorName, { 5 | // color: fontColor, 6 | // 'background-color': bgColor 7 | // }, 'Colors are correct') 8 | // }); 9 | // } 10 | // test('First button', function(assert) { 11 | // assert.hasStyle('.btn', { 12 | // 'border-radius': assert.compare.eq('2px'), 13 | // 'border-style': 'solid', 14 | // 'border-color': assert.compare.truthy(), 15 | // 'border-width': assert.compare.gt(0), 16 | // 'display': 'inline-block' 17 | // }, 'styles are incorrect'); 18 | // }) 19 | 20 | // testButtonColor('yellow', '#ffffff', '#999900'); 21 | // testButtonColor('blue', '#ffffff', '#3333aa'); 22 | // testButtonColor('green', '#ffffff', '#33cc33'); 23 | // testButtonColor('purple', '#ffffff', '#993399'); 24 | // testButtonColor('pink', '#ffffff', '#cc6699'); 25 | // testButtonColor('orange', '#ffffff', '#ff9933'); 26 | // testButtonColor('brown', '#ffffff', '#995522'); 27 | 28 | // }()); -------------------------------------------------------------------------------- /exercises/range/src/sass/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin range-thumb() { 2 | 3 | } -------------------------------------------------------------------------------- /exercises/range/src/sass/_variables.scss: -------------------------------------------------------------------------------- 1 | $hopbush: #c69 !default; 2 | $patina: #699 !default; 3 | $venus: #998099 !default; 4 | $nebula: #d2e1dd !default; 5 | 6 | $black: #000 !default; 7 | $white: #fff !default; 8 | 9 | $btn-disabled-opacity: 0.5 !default; 10 | 11 | -------------------------------------------------------------------------------- /exercises/range/src/sass/app.scss: -------------------------------------------------------------------------------- 1 | @import '_variables'; 2 | @import '_mixins'; 3 | 4 | input[type="range"] { 5 | // Add your styles here 6 | } -------------------------------------------------------------------------------- /exercises/tiny/exercise.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Control Flow - @for, @each", 3 | "instructions": "Build your own tiny CSS utility classes" 4 | } -------------------------------------------------------------------------------- /exercises/tiny/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

Atomic CSS - @each, @for, lists and maps

7 | 8 | 9 | m-t-5 10 | m-t-10 11 | m-t-15 12 | m-t-20 13 | m-t-25 14 | 15 | m-b-5 16 | m-b-10 17 | m-b-15 18 | m-b-20 19 | m-b-25 20 | 21 | m-l-5 22 | m-l-10 23 | m-l-15 24 | m-l-20 25 | m-l-25 26 | 27 | m-r-5 28 | m-r-10 29 | m-r-15 30 | m-r-20 31 | m-r-25 32 | 33 | p-t-5 34 | p-t-10 35 | p-t-15 36 | p-t-20 37 | p-t-25 38 | 39 | p-b-5 40 | p-b-10 41 | p-b-15 42 | p-b-20 43 | p-b-25 44 | 45 | p-l-5 46 | p-l-10 47 | p-l-15 48 | p-l-20 49 | p-l-25 50 | 51 | p-r-5 52 | p-r-10 53 | p-r-15 54 | p-r-20 55 | p-r-25 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /exercises/tiny/public/tests.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | var properties = ['margin', 'padding']; 4 | var subProperties = ['top', 'bottom', 'left', 'right']; 5 | var steps = [5, 10, 15, 20, 25]; 6 | 7 | test('Atomic classes are expressed properly', function(assert) { 8 | 9 | for(var p = 0; p < properties.length; p++) { 10 | var prop = properties[p]; 11 | for (var sp = 0; sp < subProperties.length; sp++) { 12 | var subProp = subProperties[sp]; 13 | for (var s = 0; s < steps.length; s++) { 14 | var step = steps[s]; 15 | var styleProp = prop + '-' + subProp; 16 | var styleVal = step + 'px'; 17 | var className = '.' + prop.substr(0, 1) + '-' + subProp.substr(0, 1) + '-' + step; 18 | var styl = {}; 19 | styl[styleProp] = styleVal; 20 | assert.hasStyle(className, styl, 21 | className 22 | ); 23 | } 24 | } 25 | } 26 | 27 | }); 28 | 29 | }()); -------------------------------------------------------------------------------- /exercises/tiny/src/sass/app.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike-works/sass-fundamentals/c1b12cd7ec582ea761284596918f2022e80ec717/exercises/tiny/src/sass/app.scss -------------------------------------------------------------------------------- /exercises/variables/exercise.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Variables", 3 | "instructions": "Refactor your Sass to take advantage of variables" 4 | } -------------------------------------------------------------------------------- /exercises/variables/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

Variables & Buttons

7 | 8 |

9 | 10 | 11 |

12 | 13 |

14 | 15 | 16 |

17 | 18 |

19 | 20 | 21 |

22 | 23 |

24 | 25 | 26 |

27 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /exercises/variables/public/tests.js: -------------------------------------------------------------------------------- 1 | test('Primary button', function(assert) { 2 | assert.hasStyle('.btn.btn-primary', { 3 | 'padding-left': '10px', 4 | 'padding-right': '10px', 5 | 'padding-top': '2px', 6 | 'padding-bottom': '2px', 7 | 'border-radius': assert.compare.eq('2px'), 8 | 'border-style': 'solid', 9 | 'border-color': assert.compare.truthy(), 10 | 'border-width': assert.compare.gt(0) 11 | }, 'styles are incorrect'); 12 | 13 | assert.hasStyle('.btn.btn-primary', { 14 | 'background-color': 'rgb(204, 102, 153)', 15 | 'color': 'rgb(255, 255, 255)', 16 | 'opacity': '1' 17 | }, 'colors for enabled state are incorrect'); 18 | assert.hasStyle('.btn.btn-primary:disabled', { 19 | 'opacity': '0.5' 20 | }, 'colors for enabled state are incorrect'); 21 | 22 | }); 23 | 24 | test('Secondary button', function(assert) { 25 | assert.hasStyle('.btn.btn-secondary', { 26 | 'padding-left': '10px', 27 | 'padding-right': '10px', 28 | 'padding-top': '2px', 29 | 'padding-bottom': '2px', 30 | }, 'padding is incorrect'); 31 | 32 | assert.hasStyle('.btn.btn-secondary', { 33 | 'background-color': '#d2e1dd', 34 | 'color': 'rgb(0, 0, 0)', 35 | 'opacity': '1' 36 | }, 'colors for enabled state are incorrect'); 37 | assert.hasStyle('.btn.btn-secondary:disabled', { 38 | 'opacity': '0.5' 39 | }, 'colors for enabled state are incorrect'); 40 | }); 41 | -------------------------------------------------------------------------------- /exercises/variables/src/sass/_variables.scss: -------------------------------------------------------------------------------- 1 | $hopbush: #c69 !default; 2 | $patina: #699 !default; 3 | $venus: #998099 !default; 4 | $nebula: #d2e1dd !default; 5 | 6 | $black: #000 !default; 7 | $white: #fff !default; 8 | 9 | $btn-disabled-opacity: 0.5 !default; 10 | 11 | -------------------------------------------------------------------------------- /exercises/variables/src/sass/app.scss: -------------------------------------------------------------------------------- 1 | body { 2 | h1 { 3 | color: #c69; 4 | } 5 | } 6 | .btn { 7 | padding: 2px 10px; 8 | border-radius: 2px; 9 | &.btn-primary { 10 | border: 1px solid rebeccapurple; 11 | background-color: lawngreen; 12 | color: #fff; 13 | } 14 | &.btn-secondary { 15 | border: 1px solid cadetblue; 16 | background-color: peru; 17 | color: #000; 18 | } 19 | &:disabled { 20 | opacity: 0.5; 21 | } 22 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mike-works/sass-fundamentals", 3 | "version": "0.0.0-development", 4 | "description": "A sass workshop project", 5 | "main": "index.js", 6 | "scripts": { 7 | "test:ci": "echo ok", 8 | "travis-deploy-once": "travis-deploy-once", 9 | "semantic-release": "semantic-release" 10 | }, 11 | "repository": "https://github.com/mike-works/sass-fundamentals.git", 12 | "keywords": [ 13 | "sass", 14 | "css" 15 | ], 16 | "author": "Mike North (https://mike.works)", 17 | "license": "UNLICENSED", 18 | "bugs": { 19 | "url": "https://github.com/mike-works/sass-fundamentals/issues" 20 | }, 21 | "homepage": "https://github.com/mike-works/sass-fundamentals#readme", 22 | "dependencies": { 23 | "chalk": "2.4.2", 24 | "commander": "2.20.3", 25 | "express": "4.21.2", 26 | "livereload": "0.9.3" 27 | }, 28 | "devDependencies": { 29 | "@mike-works/js-lib-renovate-config": "2.0.0", 30 | "@mike-works/workshop-semantic-release-config": "1.0.0", 31 | "node-sass-middleware": "0.11.0", 32 | "semantic-release": "15.14.0", 33 | "travis-deploy-once": "5.0.11" 34 | }, 35 | "release": { 36 | "extends": "@mike-works/workshop-semantic-release-config" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /public/css/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v6.0.0 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in 9 | * IE on Windows Phone and in iOS. 10 | */ 11 | 12 | html { 13 | line-height: 1.15; /* 1 */ 14 | -ms-text-size-adjust: 100%; /* 2 */ 15 | -webkit-text-size-adjust: 100%; /* 2 */ 16 | } 17 | 18 | /* Sections 19 | ========================================================================== */ 20 | 21 | /** 22 | * Add the correct display in IE 9-. 23 | */ 24 | 25 | article, 26 | aside, 27 | footer, 28 | header, 29 | nav, 30 | section { 31 | display: block; 32 | } 33 | 34 | /** 35 | * Correct the font size and margin on `h1` elements within `section` and 36 | * `article` contexts in Chrome, Firefox, and Safari. 37 | */ 38 | 39 | h1 { 40 | font-size: 2em; 41 | margin: 0.67em 0; 42 | } 43 | 44 | /* Grouping content 45 | ========================================================================== */ 46 | 47 | /** 48 | * Add the correct display in IE 9-. 49 | * 1. Add the correct display in IE. 50 | */ 51 | 52 | figcaption, 53 | figure, 54 | main { /* 1 */ 55 | display: block; 56 | } 57 | 58 | /** 59 | * Add the correct margin in IE 8. 60 | */ 61 | 62 | figure { 63 | margin: 1em 40px; 64 | } 65 | 66 | /** 67 | * 1. Add the correct box sizing in Firefox. 68 | * 2. Show the overflow in Edge and IE. 69 | */ 70 | 71 | hr { 72 | box-sizing: content-box; /* 1 */ 73 | height: 0; /* 1 */ 74 | overflow: visible; /* 2 */ 75 | } 76 | 77 | /** 78 | * 1. Correct the inheritance and scaling of font size in all browsers. 79 | * 2. Correct the odd `em` font sizing in all browsers. 80 | */ 81 | 82 | pre { 83 | font-family: monospace, monospace; /* 1 */ 84 | font-size: 1em; /* 2 */ 85 | } 86 | 87 | /* Text-level semantics 88 | ========================================================================== */ 89 | 90 | /** 91 | * 1. Remove the gray background on active links in IE 10. 92 | * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. 93 | */ 94 | 95 | a { 96 | background-color: transparent; /* 1 */ 97 | -webkit-text-decoration-skip: objects; /* 2 */ 98 | } 99 | 100 | /** 101 | * 1. Remove the bottom border in Chrome 57- and Firefox 39-. 102 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 103 | */ 104 | 105 | abbr[title] { 106 | border-bottom: none; /* 1 */ 107 | text-decoration: underline; /* 2 */ 108 | text-decoration: underline dotted; /* 2 */ 109 | } 110 | 111 | /** 112 | * Prevent the duplicate application of `bolder` by the next rule in Safari 6. 113 | */ 114 | 115 | b, 116 | strong { 117 | font-weight: inherit; 118 | } 119 | 120 | /** 121 | * Add the correct font weight in Chrome, Edge, and Safari. 122 | */ 123 | 124 | b, 125 | strong { 126 | font-weight: bolder; 127 | } 128 | 129 | /** 130 | * 1. Correct the inheritance and scaling of font size in all browsers. 131 | * 2. Correct the odd `em` font sizing in all browsers. 132 | */ 133 | 134 | code, 135 | kbd, 136 | samp { 137 | font-family: monospace, monospace; /* 1 */ 138 | font-size: 1em; /* 2 */ 139 | } 140 | 141 | /** 142 | * Add the correct font style in Android 4.3-. 143 | */ 144 | 145 | dfn { 146 | font-style: italic; 147 | } 148 | 149 | /** 150 | * Add the correct background and color in IE 9-. 151 | */ 152 | 153 | mark { 154 | background-color: #ff0; 155 | color: #000; 156 | } 157 | 158 | /** 159 | * Add the correct font size in all browsers. 160 | */ 161 | 162 | small { 163 | font-size: 80%; 164 | } 165 | 166 | /** 167 | * Prevent `sub` and `sup` elements from affecting the line height in 168 | * all browsers. 169 | */ 170 | 171 | sub, 172 | sup { 173 | font-size: 75%; 174 | line-height: 0; 175 | position: relative; 176 | vertical-align: baseline; 177 | } 178 | 179 | sub { 180 | bottom: -0.25em; 181 | } 182 | 183 | sup { 184 | top: -0.5em; 185 | } 186 | 187 | /* Embedded content 188 | ========================================================================== */ 189 | 190 | /** 191 | * Add the correct display in IE 9-. 192 | */ 193 | 194 | audio, 195 | video { 196 | display: inline-block; 197 | } 198 | 199 | /** 200 | * Add the correct display in iOS 4-7. 201 | */ 202 | 203 | audio:not([controls]) { 204 | display: none; 205 | height: 0; 206 | } 207 | 208 | /** 209 | * Remove the border on images inside links in IE 10-. 210 | */ 211 | 212 | img { 213 | border-style: none; 214 | } 215 | 216 | /** 217 | * Hide the overflow in IE. 218 | */ 219 | 220 | svg:not(:root) { 221 | overflow: hidden; 222 | } 223 | 224 | /* Forms 225 | ========================================================================== */ 226 | 227 | /** 228 | * Remove the margin in Firefox and Safari. 229 | */ 230 | 231 | button, 232 | input, 233 | optgroup, 234 | select, 235 | textarea { 236 | margin: 0; 237 | } 238 | 239 | /** 240 | * Show the overflow in IE. 241 | * 1. Show the overflow in Edge. 242 | */ 243 | 244 | button, 245 | input { /* 1 */ 246 | overflow: visible; 247 | } 248 | 249 | /** 250 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 251 | * 1. Remove the inheritance of text transform in Firefox. 252 | */ 253 | 254 | button, 255 | select { /* 1 */ 256 | text-transform: none; 257 | } 258 | 259 | /** 260 | * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` 261 | * controls in Android 4. 262 | * 2. Correct the inability to style clickable types in iOS and Safari. 263 | */ 264 | 265 | button, 266 | html [type="button"], /* 1 */ 267 | [type="reset"], 268 | [type="submit"] { 269 | -webkit-appearance: button; /* 2 */ 270 | } 271 | 272 | /** 273 | * Remove the inner border and padding in Firefox. 274 | */ 275 | 276 | button::-moz-focus-inner, 277 | [type="button"]::-moz-focus-inner, 278 | [type="reset"]::-moz-focus-inner, 279 | [type="submit"]::-moz-focus-inner { 280 | border-style: none; 281 | padding: 0; 282 | } 283 | 284 | /** 285 | * Restore the focus styles unset by the previous rule. 286 | */ 287 | 288 | button:-moz-focusring, 289 | [type="button"]:-moz-focusring, 290 | [type="reset"]:-moz-focusring, 291 | [type="submit"]:-moz-focusring { 292 | outline: 1px dotted ButtonText; 293 | } 294 | 295 | /** 296 | * 1. Correct the text wrapping in Edge and IE. 297 | * 2. Correct the color inheritance from `fieldset` elements in IE. 298 | * 3. Remove the padding so developers are not caught out when they zero out 299 | * `fieldset` elements in all browsers. 300 | */ 301 | 302 | legend { 303 | box-sizing: border-box; /* 1 */ 304 | color: inherit; /* 2 */ 305 | display: table; /* 1 */ 306 | max-width: 100%; /* 1 */ 307 | padding: 0; /* 3 */ 308 | white-space: normal; /* 1 */ 309 | } 310 | 311 | /** 312 | * 1. Add the correct display in IE 9-. 313 | * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera. 314 | */ 315 | 316 | progress { 317 | display: inline-block; /* 1 */ 318 | vertical-align: baseline; /* 2 */ 319 | } 320 | 321 | /** 322 | * Remove the default vertical scrollbar in IE. 323 | */ 324 | 325 | textarea { 326 | overflow: auto; 327 | } 328 | 329 | /** 330 | * 1. Add the correct box sizing in IE 10-. 331 | * 2. Remove the padding in IE 10-. 332 | */ 333 | 334 | [type="checkbox"], 335 | [type="radio"] { 336 | box-sizing: border-box; /* 1 */ 337 | padding: 0; /* 2 */ 338 | } 339 | 340 | /** 341 | * Correct the cursor style of increment and decrement buttons in Chrome. 342 | */ 343 | 344 | [type="number"]::-webkit-inner-spin-button, 345 | [type="number"]::-webkit-outer-spin-button { 346 | height: auto; 347 | } 348 | 349 | /** 350 | * 1. Correct the odd appearance in Chrome and Safari. 351 | * 2. Correct the outline style in Safari. 352 | */ 353 | 354 | [type="search"] { 355 | -webkit-appearance: textfield; /* 1 */ 356 | outline-offset: -2px; /* 2 */ 357 | } 358 | 359 | /** 360 | * Remove the inner padding and cancel buttons in Chrome and Safari on macOS. 361 | */ 362 | 363 | [type="search"]::-webkit-search-cancel-button, 364 | [type="search"]::-webkit-search-decoration { 365 | -webkit-appearance: none; 366 | } 367 | 368 | /** 369 | * 1. Correct the inability to style clickable types in iOS and Safari. 370 | * 2. Change font properties to `inherit` in Safari. 371 | */ 372 | 373 | ::-webkit-file-upload-button { 374 | -webkit-appearance: button; /* 1 */ 375 | font: inherit; /* 2 */ 376 | } 377 | 378 | /* Interactive 379 | ========================================================================== */ 380 | 381 | /* 382 | * Add the correct display in IE 9-. 383 | * 1. Add the correct display in Edge, IE, and Firefox. 384 | */ 385 | 386 | details, /* 1 */ 387 | menu { 388 | display: block; 389 | } 390 | 391 | /* 392 | * Add the correct display in all browsers. 393 | */ 394 | 395 | summary { 396 | display: list-item; 397 | } 398 | 399 | /* Scripting 400 | ========================================================================== */ 401 | 402 | /** 403 | * Add the correct display in IE 9-. 404 | */ 405 | 406 | canvas { 407 | display: inline-block; 408 | } 409 | 410 | /** 411 | * Add the correct display in IE. 412 | */ 413 | 414 | template { 415 | display: none; 416 | } 417 | 418 | /* Hidden 419 | ========================================================================== */ 420 | 421 | /** 422 | * Add the correct display in IE 10-. 423 | */ 424 | 425 | [hidden] { 426 | display: none; 427 | } 428 | 429 | body { 430 | margin: 0; 431 | } -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

root public

6 | 7 | 8 | -------------------------------------------------------------------------------- /public/js/tester.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | function printValue(txt) { 3 | if (txt.indexOf('rgb(') === 0) { 4 | let hex = txt 5 | .replace('rgb(', '') 6 | .replace(')', '').trim() 7 | .split(',') 8 | .map(function(c) { 9 | let x = parseInt(c.trim(), 10).toString(16); 10 | return x.length < 2 ? '0' + x : x; 11 | }) 12 | .join(''); 13 | return '#' + hex; 14 | } else { 15 | return txt; 16 | } 17 | } 18 | 19 | const assert = { 20 | compare: { 21 | truthy() { 22 | return { 23 | check: function(x) { 24 | return !!x 25 | }, 26 | description: 'Should be non-empty' 27 | }; 28 | }, 29 | gt(num) { 30 | return { 31 | check: function(x) { 32 | return parseFloat(x, 10) > num; 33 | }, 34 | description: 'Greater than ' + num 35 | }; 36 | }, 37 | lt(num) { 38 | return { 39 | check: function(x) { 40 | return parseFloat(x, 10) < num; 41 | }, 42 | description: 'Less than ' + num 43 | }; 44 | }, 45 | gte(num) { 46 | return { 47 | check: function(x) { 48 | return parseFloat(x, 10) >= num; 49 | }, 50 | description: 'Greater than or equal to' + num 51 | }; 52 | }, 53 | lte(num) { 54 | return { 55 | check: function(x) { 56 | return parseFloat(x, 10) <= num; 57 | }, 58 | description: 'Less than or equal to' + num 59 | }; 60 | }, 61 | eq(y) { 62 | return { 63 | check: function(x) { 64 | return x == y; 65 | }, 66 | description: 'Equal to ' + y 67 | }; 68 | } 69 | }, 70 | ok: function(check, description) { 71 | if (!check) { 72 | throw { description }; 73 | } 74 | }, 75 | equal: function(a, b, description) { 76 | if (a !== b) { 77 | throw { description }; 78 | } 79 | }, 80 | hasStyle: function(selector, styles, description) { 81 | var elem = document.querySelector(selector); 82 | if (!elem) { 83 | throw { 84 | description: 'No element found: ' + selector 85 | }; 86 | } 87 | var elemStyles = window.getComputedStyle(elem); 88 | for (let s in styles) { 89 | if (styles.hasOwnProperty(s)) { 90 | switch (typeof styles[s]) { 91 | case 'object': 92 | if (!styles[s].check(elemStyles[s])) { 93 | throw { description, 94 | detail: 'Expected $("' + selector + '") to meet the following condition for ' + s + ':\n' + styles[s].description + '\nFound: ' + elemStyles[s] 95 | }; 96 | } 97 | break; 98 | default: 99 | if (elemStyles[s] !== styles[s] && styles[s] !== printValue(elemStyles[s])) { 100 | throw { description, 101 | detail: 'Expected $("' + selector + '") to have value for ' + s + ':\n' + styles[s] + '.\n' + printValue(elemStyles[s]) + ' was found instead' 102 | } 103 | } 104 | break; 105 | } 106 | } 107 | } 108 | } 109 | }; 110 | var _errorContainer = null; 111 | 112 | function setupErrorList() { 113 | _errorContainer = document.createElement('ul'); 114 | _errorContainer.className = 'test-failures'; 115 | _errorContainer.style = 'position: fixed;bottom: 0; max-height: 100vh; overflow-y: scroll; right: 0; width: 200px; padding-left:0; background: red; list-style: none; color: white; font-family: Sans-Serif'; 116 | document.body.appendChild(_errorContainer); 117 | return _errorContainer; 118 | } 119 | 120 | function addErrorToList(info) { 121 | var li = document.createElement('li'); 122 | li.className = 'test-failure'; 123 | li.style = 'margin: 10px; background: #f55; padding: 10px' 124 | var s = '

' + info.name + '

' + info.description + ''; 125 | if (info.detail) { 126 | s += '
' + info.detail + '
'; 127 | } 128 | li.innerHTML = s; 129 | _errorContainer.appendChild(li); 130 | } 131 | 132 | function onFail(info) { 133 | if (!_errorContainer) { 134 | setupErrorList(); 135 | } 136 | addErrorToList(info); 137 | } 138 | 139 | function test(name, cb) { 140 | try { 141 | cb(assert); 142 | } catch(e) { 143 | console.error(e); 144 | onFail(Object.assign(e, { name })); 145 | } 146 | }; 147 | 148 | function start() { 149 | window.test = test; 150 | }; 151 | window.onload = start(); 152 | }()); -------------------------------------------------------------------------------- /public/js/utils.js: -------------------------------------------------------------------------------- 1 | window.setTheme = function (th) { 2 | debugger; 3 | document.body.classList; 4 | } -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@mike-works/js-lib-renovate-config"] 3 | } 4 | -------------------------------------------------------------------------------- /run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const program = require('./src/program'); 4 | const server = require('./src/server'); 5 | 6 | server(program); -------------------------------------------------------------------------------- /run.bat: -------------------------------------------------------------------------------- 1 | node run -e %2 2 | -------------------------------------------------------------------------------- /src/exercise.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | 5 | function Exercise(name, opts) { 6 | this.name = name; 7 | }; 8 | 9 | function spaces(num) { 10 | let s = ''; 11 | for (let i = 0; i < num; i++) { 12 | s += ' '; 13 | } 14 | return s; 15 | } 16 | 17 | Exercise.prototype = { 18 | path: function() { 19 | return path.join(__dirname, '..', 'exercises', this.name); 20 | }, 21 | load: function () { 22 | this.info = JSON.parse(fs.readFileSync(path.join(this.path(), 'exercise.json')).toString()); 23 | return this; 24 | }, 25 | begin: function() { 26 | let w = 60; 27 | let spaceStr = spaces(w); 28 | console.log(''); 29 | console.log( 30 | chalk.bgBlue(spaceStr + '\n') + 31 | chalk.bgBlue.white(' STARTING EXERCISE: ') + 32 | chalk.bgBlue.yellow.bold(this.info.title) + 33 | chalk.bgBlue(spaces(w - 24 - this.info.title.length) + '\n') + 34 | chalk.bgBlue(spaceStr + '\n') 35 | ); 36 | console.log(this.info.instructions); 37 | console.log('\n'); 38 | return this; 39 | } 40 | } 41 | 42 | module.exports = Exercise; -------------------------------------------------------------------------------- /src/program.js: -------------------------------------------------------------------------------- 1 | const program = require('commander'); 2 | const fs = require('fs'); 3 | const chalk = require('chalk'); 4 | 5 | let opts = {}; 6 | 7 | let exercises = fs 8 | .readdirSync('./exercises') 9 | .map((x) => x.toLowerCase()); 10 | 11 | program 12 | .option('-e, --exercise ', 'Which exercise to run') 13 | .description('start serving up an exercise') 14 | .option('-p, --port ', 'Port to serve the app on') 15 | .parse(process.argv); 16 | 17 | opts.port = program.port || 3000; 18 | opts.exercise = ('' + program.exercise).trim(); 19 | 20 | if (!opts.exercise) { 21 | console.error(chalk.red('ERROR: must specify an --exercise argument')); 22 | program.help(); 23 | } 24 | if (exercises.indexOf(opts.exercise.toLowerCase()) < 0) { 25 | console.error(chalk.red('ERROR: not a valid exercise')); 26 | console.error(chalk.yellow('Valid exercises are:...')); 27 | console.error(chalk.yellow(exercises.map((e) => '\t - ' + e).join('\n'))); 28 | program.help(); 29 | } 30 | 31 | module.exports = opts; -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const sassMiddleware = require('node-sass-middleware'); 3 | const path = require('path'); 4 | const chalk = require('chalk'); 5 | const livereload = require('livereload'); 6 | const Exercise = require('./exercise'); 7 | 8 | module.exports = function (opts) { 9 | var server = livereload.createServer({ 10 | exts: ['scss', 'html'] 11 | }); 12 | let pth = path.join(__dirname, '..', 'exercises', opts.exercise, '**/*'); 13 | server.watch(pth); 14 | 15 | let app = express(); 16 | 17 | let sassMw = sassMiddleware({ 18 | /* Options */ 19 | src: path.join(__dirname, '..', 'exercises', opts.exercise, 'src', 'sass'), 20 | dest: path.join(__dirname, '..', 'exercises', opts.exercise, 'public'), 21 | outputStyle: 'expanded' 22 | }); 23 | 24 | // Note: you must place sass-middleware *before* `express.static` or else it will 25 | // not work. 26 | let publicPath = path.join(__dirname, '..', 'exercises', opts.exercise, 'public'); 27 | 28 | let staticMw = express.static(publicPath); 29 | 30 | app.use(sassMw); 31 | app.use('/', staticMw); 32 | app.use('/js', express.static(path.join(__dirname, '..', 'public', 'js'))); 33 | app.use('/css', express.static(path.join(__dirname, '..', 'public', 'css'))); 34 | 35 | new Exercise(opts.exercise).load().begin(); 36 | 37 | app.listen(opts.port); 38 | console.log(chalk.yellow('Running on http://localhost:' + opts.port + '. Press Ctrl + c to stop')); 39 | return app; 40 | } -------------------------------------------------------------------------------- /tools/rebase-exercise-branches.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /* eslint-env node */ 3 | const { exec } = require('child_process'); 4 | 5 | const EXERCISE_SETS = { 6 | femasters: [ 7 | 'femasters/begin', 8 | 'femasters/1-begin', 'femasters/1-complete', 9 | 'femasters/2-begin', 'femasters/2-complete', 10 | 'femasters/3-begin', 'femasters/3-complete', 11 | 'femasters/4-begin', 'femasters/4-complete', 12 | 'femasters/5-begin', 'femasters/5-complete', 13 | 'femasters/6-begin', 'femasters/6-complete', 14 | 'femasters/7-begin', 'femasters/7-complete', 15 | 'femasters/8-begin', 'femasters/8-complete', 16 | 'femasters/9-begin', 'femasters/9-complete', 17 | 'femasters/10-begin', 'femasters/10-complete', 18 | 'femasters/11-begin', 'femasters/11-complete', 19 | 'femasters/final' 20 | ], 21 | linkedin: [ 22 | 'linkedin/begin', 23 | 'linkedin/1-begin', 'linkedin/1-complete', 24 | 'linkedin/2-begin', 'linkedin/2-complete', 25 | 'linkedin/3-begin', 'linkedin/3-complete', 26 | 'linkedin/4-begin', 'linkedin/4-complete', 27 | 'linkedin/5-begin', 'linkedin/5-complete', 28 | 'linkedin/6-begin', 'linkedin/6-complete', 29 | 'linkedin/7-begin', 'linkedin/7-complete', 30 | 'linkedin/8-begin', 'linkedin/8-complete', 31 | 'linkedin/9-begin', 'linkedin/9-complete', 32 | 'linkedin/10-begin', 'linkedin/10-complete', 33 | 'linkedin/11-begin', 'linkedin/11-complete', 34 | 'linkedin/final' 35 | ] 36 | }; 37 | 38 | function checkoutBranch(branchName) { 39 | console.log('schedule CHECKOUT ', branchName); 40 | return new Promise((resolve, reject) => { 41 | console.log('begin CHECKOUT ', branchName); 42 | let gco = exec(`git checkout ${branchName}`, (err, stdout, stderr) => { 43 | console.log('complete CHECKOUT ', branchName); 44 | resolve(); 45 | }); 46 | }) 47 | } 48 | 49 | function rebaseOnto(baseBranchName) { 50 | console.log('schedule REBASE ', baseBranchName); 51 | return new Promise((resolve, reject) => { 52 | console.log('begin REBASE ', baseBranchName); 53 | exec(`git rebase ${baseBranchName}`, (err, stdout, stderr) => { 54 | console.log('complete REBASE ', baseBranchName); 55 | resolve(); 56 | }); 57 | }); 58 | } 59 | 60 | function forcePush() { 61 | console.log('schedule FORCEPUSH '); 62 | return new Promise((resolve, reject) => { 63 | console.log('begin FORCEPUSH '); 64 | exec(`git push --force`, (err, stdout, stderr) => { 65 | console.log('complete FORCEPUSH'); 66 | resolve(); 67 | }); 68 | }); 69 | } 70 | 71 | function rebaseBranch(branchName, baseBranchName) { 72 | return checkoutBranch(branchName) 73 | .then(() => rebaseOnto(baseBranchName)) 74 | .then(() => forcePush()); 75 | } 76 | 77 | let p = Promise.resolve(); 78 | for (let e in EXERCISE_SETS) { 79 | let branches = EXERCISE_SETS[e]; 80 | for (let b = 1; b < branches.length; b++) { 81 | p = p.then(() => rebaseBranch(branches[b], branches[b - 1])); 82 | } 83 | } --------------------------------------------------------------------------------