├── .gitignore ├── src ├── components │ ├── todo-form │ │ ├── todo-form.html │ │ └── index.js │ ├── todo-footer │ │ ├── index.js │ │ └── todo-footer.html │ ├── index.js │ ├── todo-item │ │ ├── todo-item.html │ │ └── index.js │ ├── todo-list │ │ ├── todo-list.html │ │ └── index.js │ └── containers │ │ └── todo │ │ ├── todo.html │ │ └── index.js ├── index.html ├── index.js ├── stores │ └── todoStore.js └── index.less ├── .editorconfig ├── README.md ├── package.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | typings/ 3 | dist/ 4 | -------------------------------------------------------------------------------- /src/components/todo-form/todo-form.html: -------------------------------------------------------------------------------- 1 |
2 | 6 |
7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A small ES5 demo app built with: 2 | - Angular 1.5 _for component architecture_ 3 | - UI-router 1.0.0-beta.1 _for routing to components_ 4 | - MobX _for state management_ 5 | - Webpack _for module bundling_ 6 | 7 | Demo: http://gaui.is/github/todoApp-angular-mobx-webpack 8 | 9 | Implementation details: http://gaui.is/angular-1-5-todo-app 10 | 11 | Regards to [TodoMVC](http://todomvc.com) 12 | -------------------------------------------------------------------------------- /src/components/todo-footer/index.js: -------------------------------------------------------------------------------- 1 | var todoFooter = { 2 | controller: todoFooterController, 3 | template: require('./todo-footer.html'), 4 | bindings: { 5 | itemsLeft: '<', 6 | onClearCompleted: '&' 7 | } 8 | }; 9 | 10 | function todoFooterController() { 11 | var self = this; 12 | 13 | self.clearCompleted = function clearCompleted() { 14 | // Call parent 15 | self.onClearCompleted(); 16 | }; 17 | } 18 | 19 | module.exports = todoFooter; 20 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | var todoContainer = require('./containers/todo'); 2 | var todoForm = require('./todo-form'); 3 | var todoList = require('./todo-list'); 4 | var todoItem = require('./todo-item'); 5 | var todoFooter = require('./todo-footer'); 6 | 7 | angular.module('todoApp') 8 | .component('todo', todoContainer) 9 | .component('todoForm', todoForm) 10 | .component('todoList', todoList) 11 | .component('todoItem', todoItem) 12 | .component('todoFooter', todoFooter); 13 | -------------------------------------------------------------------------------- /src/components/todo-item/todo-item.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | 8 | 9 | 10 |
    11 |
    12 | 13 |
    14 |
  • 15 | -------------------------------------------------------------------------------- /src/components/todo-list/todo-list.html: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /src/components/todo-form/index.js: -------------------------------------------------------------------------------- 1 | var todoForm = { 2 | controller: todoFormController, 3 | template: require('./todo-form.html'), 4 | bindings: { 5 | onSubmit: '&' 6 | } 7 | }; 8 | 9 | function todoFormController() { 10 | var self = this; 11 | 12 | self.$onInit = function $onInit() { 13 | self.newTodo = {}; 14 | resetTodo(); 15 | }; 16 | 17 | self.submitForm = function submitForm() { 18 | // Call parent 19 | self.onSubmit({ 20 | $event: { 21 | todo: self.newTodo 22 | } 23 | }); 24 | 25 | resetTodo(); 26 | }; 27 | 28 | function resetTodo() { 29 | self.newTodo = {}; 30 | } 31 | } 32 | 33 | module.exports = todoForm; 34 | -------------------------------------------------------------------------------- /src/components/todo-footer/todo-footer.html: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /src/components/containers/todo/todo.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |

    todos

    4 | 5 |
    6 | 7 |
    8 | 9 | 10 | 15 | 16 |
    17 | 18 | 21 | 22 |
    23 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | TodoMVC 8 | 9 | 10 | 11 | 12 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/components/todo-list/index.js: -------------------------------------------------------------------------------- 1 | var todoList = { 2 | controller: todoListController, 3 | template: require('./todo-list.html'), 4 | bindings: { 5 | list: '<', 6 | onDelete: '&', 7 | onUpdate: '&', 8 | onToggle: '&' 9 | } 10 | }; 11 | 12 | function todoListController() { 13 | var self = this; 14 | 15 | self.deleteTodo = function deleteTodo(event) { 16 | // Call parent 17 | self.onDelete({ 18 | $event: { 19 | index: event.index 20 | } 21 | }); 22 | }; 23 | 24 | self.updateTodo = function updateTodo(event) { 25 | // Call parent 26 | self.onUpdate({ 27 | $event: { 28 | index: event.index, 29 | todo: event.todo 30 | } 31 | }); 32 | }; 33 | 34 | self.toggleTodo = function toggleTodo(event) { 35 | // Call parent 36 | self.onToggle({ 37 | $event: { 38 | index: event.index 39 | } 40 | }); 41 | }; 42 | } 43 | 44 | module.exports = todoList; 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todoApp", 3 | "version": "1.0.2", 4 | "description": "", 5 | "main": "webpack.config.js", 6 | "scripts": { 7 | "dev": "SET NODE_ENV=test && webpack-dev-server --hot --inline --progress --colors --content-base src", 8 | "build:test": "SET NODE_ENV=test && webpack", 9 | "build:live": "SET NODE_ENV=live && webpack --devtool source-map" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "clean-webpack-plugin": "^0.1.10", 15 | "copy-webpack-plugin": "^3.0.1", 16 | "css-loader": "^0.23.1", 17 | "file-loader": "^0.9.0", 18 | "html-loader": "^0.4.3", 19 | "json-loader": "^0.5.4", 20 | "less": "^2.7.1", 21 | "less-loader": "^2.2.3", 22 | "path": "^0.12.7", 23 | "style-loader": "^0.13.1", 24 | "ts-loader": "^0.8.2", 25 | "url-loader": "^0.5.7", 26 | "webpack": "^1.13.1" 27 | }, 28 | "dependencies": { 29 | "angular": "^1.5.8", 30 | "angular-ui-router": "^1.0.0-beta.1", 31 | "mobx": "^2.4.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | var todoStore = require('./stores/todoStore'); 2 | 3 | // Styles 4 | require('./index.less'); 5 | 6 | angular.module('todoApp', ['ui.router']) 7 | .service('todoStore', todoStore) 8 | .config(config); 9 | 10 | function config($stateProvider, $httpProvider, 11 | $urlRouterProvider, $locationProvider) { 12 | 13 | $urlRouterProvider.otherwise('/'); 14 | 15 | $locationProvider.html5Mode(false); 16 | 17 | $stateProvider 18 | .state('todo', { 19 | abstract: true, 20 | url: '/', 21 | template: '' 22 | }) 23 | .state('todo.all', { 24 | url: '', 25 | component: 'todo', 26 | resolve: { 27 | filter: function () { 28 | return 'all' 29 | } 30 | } 31 | }) 32 | .state('todo.active', { 33 | url: 'active', 34 | component: 'todo', 35 | resolve: { 36 | filter: function () { 37 | return 'active' 38 | } 39 | } 40 | }) 41 | .state('todo.completed', { 42 | url: 'completed', 43 | component: 'todo', 44 | resolve: { 45 | filter: function () { 46 | return 'completed' 47 | } 48 | } 49 | }); 50 | } 51 | 52 | require('./components'); 53 | -------------------------------------------------------------------------------- /src/stores/todoStore.js: -------------------------------------------------------------------------------- 1 | var mobx = require('mobx'); 2 | 3 | function todoStore() { 4 | var store = this; 5 | var todoList = mobx.observable([]); 6 | 7 | store.getAllTodos = function getAllTodos() { 8 | return todoList; 9 | }; 10 | 11 | store.addTodo = function addTodo(todo) { 12 | var newTodo = Object.assign({}, todo, { 13 | completed: false 14 | }); 15 | 16 | todoList.push(newTodo); 17 | }; 18 | 19 | store.deleteTodo = function deleteTodo(index) { 20 | todoList.splice(index, 1); 21 | }; 22 | 23 | store.updateTodo = function updateTodo(index, todo) { 24 | todoList[index].description = todo.description; 25 | }; 26 | 27 | store.toggleTodo = function toggleTodo(index) { 28 | todoList[index].completed = !todoList[index].completed; 29 | }; 30 | 31 | store.toggleAllTodos = function toggleAllTodos() { 32 | todoList.forEach(function(item) { 33 | item.completed = !item.completed; 34 | }); 35 | }; 36 | 37 | store.clearCompleted = function clearCompleted() { 38 | var filteredArray = todoList.filter(function(item) { 39 | return !item.completed; 40 | }); 41 | 42 | todoList.replace(filteredArray); 43 | }; 44 | } 45 | 46 | module.exports = todoStore; 47 | -------------------------------------------------------------------------------- /src/components/todo-item/index.js: -------------------------------------------------------------------------------- 1 | var todoItem = { 2 | controller: todoItemController, 3 | template: require('./todo-item.html'), 4 | bindings: { 5 | index: '<', 6 | description: '<', 7 | completed: '<', 8 | onDelete: '&', 9 | onUpdate: '&', 10 | onToggle: '&' 11 | } 12 | }; 13 | 14 | function todoItemController() { 15 | var self = this; 16 | 17 | self.$onInit = function $onInit() { 18 | self.editing = false; 19 | }; 20 | 21 | self.enableEditing = function enableEditing() { 22 | self.editing = true; 23 | }; 24 | 25 | self.deleteTodo = function deleteTodo() { 26 | // Call parent 27 | self.onDelete({ 28 | $event: { 29 | index: self.index 30 | } 31 | }); 32 | }; 33 | 34 | self.updateTodo = function updateTodo() { 35 | // Call parent 36 | self.onUpdate({ 37 | $event: { 38 | index: self.index, 39 | todo: { 40 | description: self.description 41 | } 42 | } 43 | }); 44 | 45 | self.editing = false; 46 | }; 47 | 48 | self.toggleTodo = function toggleTodo() { 49 | // Call parent 50 | self.onToggle({ 51 | $event: { 52 | index: self.index 53 | } 54 | }); 55 | }; 56 | } 57 | 58 | module.exports = todoItem; 59 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var path = require('path'); 3 | var CopyWebpackPlugin = require('copy-webpack-plugin'); 4 | var CleanWebpackPlugin = require('clean-webpack-plugin'); 5 | 6 | module.exports = { 7 | context: path.join(__dirname, 'src'), 8 | entry: { 9 | app: './index.js', 10 | vendors: [ 11 | 'angular', 12 | 'angular-ui-router', 13 | 'lodash', 14 | 'mobx' 15 | ] 16 | }, 17 | output: { 18 | path: path.join(__dirname, 'dist'), 19 | filename: 'app.js' 20 | }, 21 | resolve: { 22 | extensions: ['', '.js'] 23 | }, 24 | module: { 25 | loaders: [ 26 | { test: /\.json$/, loader: 'json', exclude: /node_modules/ }, 27 | { test: /\.html$/, loader: 'html', exclude: /node_modules/ }, 28 | { test: /\.css$/, loader: 'style!css', exclude: /node_modules/ }, 29 | { test: /\.less$/, loader: 'style!css!less', exclude: /node_modules/ }, 30 | { test: /\.(png|jpg)(\?]?.*)?$/, loader : 'file', exclude: /node_modules/ }, 31 | { test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: "url?limit=10000&mimetype=application/font-woff" }, 32 | { test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: "file" }, 33 | ] 34 | }, 35 | plugins: [ 36 | new webpack.optimize.CommonsChunkPlugin('vendors', 'vendors.js'), 37 | new webpack.DefinePlugin({ 38 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'test') 39 | }), 40 | new CopyWebpackPlugin([ 41 | { from: 'index.html' } 42 | ]), 43 | new CleanWebpackPlugin(['dist']) 44 | ] 45 | }; 46 | -------------------------------------------------------------------------------- /src/components/containers/todo/index.js: -------------------------------------------------------------------------------- 1 | var mobx = require('mobx'); 2 | 3 | var todoContainer = { 4 | controller: todoContainerController, 5 | template: require('./todo.html'), 6 | bindings: { 7 | filter: '<' 8 | } 9 | }; 10 | 11 | function todoContainerController(todoStore, $state) { 12 | var self = this; 13 | 14 | var dispose = mobx.autorun(function () { 15 | var todoList = todoStore.getAllTodos(); 16 | self.todoList = getListBasedOnFilter(todoList, self.filter); 17 | self.incompletedItems = getListBasedOnFilter(todoList, 'active').length; 18 | console.log('%cNEW STATE:', 'font-weight: bold'); 19 | console.log(JSON.stringify(mobx.toJS(todoList), null, 2)); 20 | }); 21 | 22 | self.$onDestroy = function $onDestroy() { 23 | dispose(); 24 | }; 25 | 26 | self.addTodo = function addTodo(event) { 27 | todoStore.addTodo(event.todo); 28 | }; 29 | 30 | self.deleteTodo = function deleteTodo(event) { 31 | todoStore.deleteTodo(event.index); 32 | }; 33 | 34 | self.updateTodo = function updateTodo(event) { 35 | todoStore.updateTodo(event.index, event.todo); 36 | }; 37 | 38 | self.toggleTodo = function toggleTodo(event) { 39 | todoStore.toggleTodo(event.index); 40 | }; 41 | 42 | self.toggleAllTodos = function toggleAllTodos() { 43 | todoStore.toggleAllTodos(); 44 | }; 45 | 46 | self.clearCompleted = function clearCompleted() { 47 | todoStore.clearCompleted(); 48 | }; 49 | 50 | function getListBasedOnFilter(list, filter) { 51 | if (!filter) return list; 52 | 53 | var filterMap = { 54 | all: function (item) { return true; }, 55 | active: function (item) { return !item.completed }, 56 | completed: function (item) { return item.completed } 57 | }; 58 | 59 | return list.filter(filterMap[filter]); 60 | } 61 | } 62 | 63 | module.exports = todoContainer; 64 | -------------------------------------------------------------------------------- /src/index.less: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | button { 8 | margin: 0; 9 | padding: 0; 10 | border: 0; 11 | background: none; 12 | font-size: 100%; 13 | vertical-align: baseline; 14 | font-family: inherit; 15 | font-weight: inherit; 16 | color: inherit; 17 | -webkit-appearance: none; 18 | appearance: none; 19 | -webkit-font-smoothing: antialiased; 20 | -moz-osx-font-smoothing: grayscale; 21 | } 22 | 23 | body { 24 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 25 | line-height: 1.4em; 26 | background: #f5f5f5; 27 | color: #4d4d4d; 28 | min-width: 230px; 29 | max-width: 550px; 30 | margin: 0 auto; 31 | -webkit-font-smoothing: antialiased; 32 | -moz-osx-font-smoothing: grayscale; 33 | font-weight: 300; 34 | } 35 | 36 | :focus { 37 | outline: 0; 38 | } 39 | 40 | .hidden { 41 | display: none; 42 | } 43 | 44 | .todoapp { 45 | background: #fff; 46 | margin: 130px 0 40px 0; 47 | position: relative; 48 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 49 | 0 25px 50px 0 rgba(0, 0, 0, 0.1); 50 | } 51 | 52 | .todoapp input::-webkit-input-placeholder { 53 | font-style: italic; 54 | font-weight: 300; 55 | color: #e6e6e6; 56 | } 57 | 58 | .todoapp input::-moz-placeholder { 59 | font-style: italic; 60 | font-weight: 300; 61 | color: #e6e6e6; 62 | } 63 | 64 | .todoapp input::input-placeholder { 65 | font-style: italic; 66 | font-weight: 300; 67 | color: #e6e6e6; 68 | } 69 | 70 | .todoapp h1 { 71 | position: absolute; 72 | top: -155px; 73 | width: 100%; 74 | font-size: 100px; 75 | font-weight: 100; 76 | text-align: center; 77 | color: rgba(175, 47, 47, 0.15); 78 | -webkit-text-rendering: optimizeLegibility; 79 | -moz-text-rendering: optimizeLegibility; 80 | text-rendering: optimizeLegibility; 81 | } 82 | 83 | .new-todo, 84 | .edit { 85 | position: relative; 86 | margin: 0; 87 | width: 100%; 88 | font-size: 24px; 89 | font-family: inherit; 90 | font-weight: inherit; 91 | line-height: 1.4em; 92 | border: 0; 93 | color: inherit; 94 | padding: 6px; 95 | border: 1px solid #999; 96 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 97 | box-sizing: border-box; 98 | -webkit-font-smoothing: antialiased; 99 | -moz-osx-font-smoothing: grayscale; 100 | } 101 | 102 | .new-todo { 103 | padding: 16px 16px 16px 60px; 104 | border: none; 105 | background: rgba(0, 0, 0, 0.003); 106 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); 107 | } 108 | 109 | .main { 110 | position: relative; 111 | z-index: 2; 112 | border-top: 1px solid #e6e6e6; 113 | } 114 | 115 | label[for='toggle-all'] { 116 | display: none; 117 | } 118 | 119 | .toggle-all { 120 | position: absolute; 121 | top: -55px; 122 | left: -12px; 123 | width: 60px; 124 | height: 34px; 125 | text-align: center; 126 | border: none; /* Mobile Safari */ 127 | } 128 | 129 | .toggle-all:before { 130 | content: '❯'; 131 | font-size: 22px; 132 | color: #e6e6e6; 133 | padding: 10px 27px 10px 27px; 134 | } 135 | 136 | .toggle-all:checked:before { 137 | color: #737373; 138 | } 139 | 140 | .todo-list { 141 | margin: 0; 142 | padding: 0; 143 | list-style: none; 144 | } 145 | 146 | .todo-list li { 147 | position: relative; 148 | font-size: 24px; 149 | border-bottom: 1px solid #ededed; 150 | } 151 | 152 | .todo-list li:last-child { 153 | border-bottom: none; 154 | } 155 | 156 | .todo-list li.editing { 157 | border-bottom: none; 158 | padding: 0; 159 | margin-left: 43px; 160 | } 161 | 162 | .todo-list li.editing .edit { 163 | display: block; 164 | width: 100%; 165 | padding: 12px 16px; 166 | } 167 | 168 | .todo-list li.editing .view { 169 | display: none; 170 | } 171 | 172 | .todo-list li .toggle { 173 | text-align: center; 174 | width: 40px; 175 | /* auto, since non-WebKit browsers doesn't support input styling */ 176 | height: auto; 177 | position: absolute; 178 | top: 0; 179 | bottom: 0; 180 | margin: auto 0; 181 | border: none; /* Mobile Safari */ 182 | -webkit-appearance: none; 183 | appearance: none; 184 | } 185 | 186 | .todo-list li .toggle:after { 187 | content: url('data:image/svg+xml;utf8,'); 188 | } 189 | 190 | .todo-list li .toggle:checked:after { 191 | content: url('data:image/svg+xml;utf8,'); 192 | } 193 | 194 | .todo-list li label { 195 | word-break: break-all; 196 | padding: 15px 60px 15px 15px; 197 | margin-left: 45px; 198 | display: block; 199 | line-height: 1.2; 200 | transition: color 0.4s; 201 | } 202 | 203 | .todo-list li.completed label { 204 | color: #d9d9d9; 205 | text-decoration: line-through; 206 | } 207 | 208 | .todo-list li .destroy { 209 | display: none; 210 | position: absolute; 211 | top: 0; 212 | right: 10px; 213 | bottom: 0; 214 | width: 40px; 215 | height: 40px; 216 | margin: auto 0; 217 | font-size: 30px; 218 | color: #cc9a9a; 219 | margin-bottom: 11px; 220 | transition: color 0.2s ease-out; 221 | } 222 | 223 | .todo-list li .destroy:hover { 224 | color: #af5b5e; 225 | } 226 | 227 | .todo-list li .destroy:after { 228 | content: '×'; 229 | } 230 | 231 | .todo-list li:hover .destroy { 232 | display: block; 233 | } 234 | 235 | .todo-list li .edit { 236 | display: none; 237 | } 238 | 239 | .todo-list li.editing:last-child { 240 | margin-bottom: -1px; 241 | } 242 | 243 | .footer { 244 | color: #777; 245 | padding: 10px 15px; 246 | height: 20px; 247 | text-align: center; 248 | border-top: 1px solid #e6e6e6; 249 | } 250 | 251 | .footer:before { 252 | content: ''; 253 | position: absolute; 254 | right: 0; 255 | bottom: 0; 256 | left: 0; 257 | height: 50px; 258 | overflow: hidden; 259 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 260 | 0 8px 0 -3px #f6f6f6, 261 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 262 | 0 16px 0 -6px #f6f6f6, 263 | 0 17px 2px -6px rgba(0, 0, 0, 0.2); 264 | } 265 | 266 | .todo-count { 267 | float: left; 268 | text-align: left; 269 | } 270 | 271 | .todo-count strong { 272 | font-weight: 300; 273 | } 274 | 275 | .filters { 276 | margin: 0; 277 | padding: 0; 278 | list-style: none; 279 | position: absolute; 280 | right: 0; 281 | left: 0; 282 | } 283 | 284 | .filters li { 285 | display: inline; 286 | } 287 | 288 | .filters li a { 289 | color: inherit; 290 | margin: 3px; 291 | padding: 3px 7px; 292 | text-decoration: none; 293 | border: 1px solid transparent; 294 | border-radius: 3px; 295 | } 296 | 297 | .filters li a:hover { 298 | border-color: rgba(175, 47, 47, 0.1); 299 | } 300 | 301 | .filters li a.selected { 302 | border-color: rgba(175, 47, 47, 0.2); 303 | } 304 | 305 | .clear-completed, 306 | html .clear-completed:active { 307 | float: right; 308 | position: relative; 309 | line-height: 20px; 310 | text-decoration: none; 311 | cursor: pointer; 312 | } 313 | 314 | .clear-completed:hover { 315 | text-decoration: underline; 316 | } 317 | 318 | .info { 319 | margin: 65px auto 0; 320 | color: #bfbfbf; 321 | font-size: 10px; 322 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 323 | text-align: center; 324 | } 325 | 326 | .info p { 327 | line-height: 1; 328 | } 329 | 330 | .info a { 331 | color: inherit; 332 | text-decoration: none; 333 | font-weight: 400; 334 | } 335 | 336 | .info a:hover { 337 | text-decoration: underline; 338 | } 339 | 340 | /* 341 | Hack to remove background from Mobile Safari. 342 | Can't use it globally since it destroys checkboxes in Firefox 343 | */ 344 | @media screen and (-webkit-min-device-pixel-ratio:0) { 345 | .toggle-all, 346 | .todo-list li .toggle { 347 | background: none; 348 | } 349 | 350 | .todo-list li .toggle { 351 | height: 40px; 352 | } 353 | 354 | .toggle-all { 355 | -webkit-transform: rotate(90deg); 356 | transform: rotate(90deg); 357 | -webkit-appearance: none; 358 | appearance: none; 359 | } 360 | } 361 | 362 | @media (max-width: 430px) { 363 | .footer { 364 | height: 50px; 365 | } 366 | 367 | .filters { 368 | bottom: 10px; 369 | } 370 | } 371 | --------------------------------------------------------------------------------