├── .gitignore ├── index.html ├── js └── bundle.js ├── package.json ├── readme.md ├── src ├── actions │ └── TodoActions.js ├── app.js ├── routes.js ├── store │ └── todoStore.js ├── utils │ ├── assign.js │ ├── eventHandler.js │ ├── pluralize.js │ ├── rxLifecycleMixin.js │ ├── store.js │ └── uuid.js └── views │ ├── footer.jsx │ ├── header.jsx │ ├── mainView.jsx │ ├── todoItem.jsx │ └── todoList.jsx └── styles ├── bg.png └── styles.css /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React • TodoMVC 6 | 7 | 8 | 9 |
10 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-rxjs-todomvc", 3 | "version": "0.1.0", 4 | "description": "TodoMVC implementations with React and RxJS", 5 | "main": "index.js", 6 | "dependencies": { 7 | "reactify": "~0.10.0", 8 | "envify": "~1.2.1", 9 | "react": "~0.10.0", 10 | "director": "~1.2.3", 11 | "rx": "~2.2.17" 12 | }, 13 | "devDependencies": {}, 14 | "scripts": { 15 | "test": "echo \"Error: no test specified\" && exit 1", 16 | "install": "browserify -t reactify src/app.js -o js/bundle.js " 17 | }, 18 | "author": "François de Campredon (https://github.com/fdecampredon)", 19 | "license": "Apache Version 2" 20 | } 21 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # React Rx TodoMVC Example 2 | 3 | > [TodoMVC](http://todomvc.com/) implementation built on top of [React](http://facebook.github.io/react/) and [RxJS](https://github.com/Reactive-Extensions/RxJS) 4 | 5 | # Running 6 | 7 | Simply start a static server on this project folder. 8 | 9 | # Building 10 | 11 | You have to install [Browserify](http://browserify.org/) then simply run these command : 12 | ``` 13 | npm install 14 | ``` 15 | this will install all the dependencies and bundle the project sources files. 16 | 17 | # Implementation 18 | 19 | This implementation has been inspired by the [React Flux architecture](https://github.com/facebook/react/tree/master/examples/todomvc-flux) 20 | 21 | In this implementation has 3 main parts, a `TodoStore`, a list of actions contained in `TodoActions`, and a list of views in the form of React Components. 22 | 23 | ## The TodoStore: 24 | 25 | This store exposes 2 streams: 26 | * `todos`: An RxJS observable that will contain an up-to-date list of todos. 27 | * `updates`: an RxJS `Observer` that will receive operations to apply on the list of todos, those operations take the form of functions that create a new version of our todos list. 28 | 29 | ## The TodoActions: 30 | 31 | A list of Rx Observer that will be exposed to our components, this actions are registered against the `updates` stream of the TodoStore, and will push new operations into this stream when they receive values. 32 | 33 | ## The Views: 34 | 35 | A set of React components that will react to changes in our TodoStores `todos` stream. 36 | In this implementation state and events handlers are managed in a 'reactive' way through the use of a special RxMixin. 37 | 38 | 39 | ``` 40 | TodoStore.todos --------------> React Components ---- (push value) ---> TodoActions-- + 41 | Ʌ | 42 | | | 43 | | | 44 | | | 45 | +--(apply operation on the todos list) ---TodoStore.updates <--- (push operations) --+ 46 | ``` 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/actions/TodoActions.js: -------------------------------------------------------------------------------- 1 | /*jshint node : true */ 2 | 3 | var Rx = require('rx'), 4 | assign = require('../utils/assign'), 5 | uuid = require('../utils/uuid'); 6 | 7 | /** 8 | * A set of actions that will be exposed into views 9 | * Thoses actions will trigger model update 10 | */ 11 | var TodoActions = { 12 | create: new Rx.Subject(), 13 | updateTitle: new Rx.Subject(), 14 | toggle: new Rx.Subject(), 15 | toggleAll: new Rx.Subject(), 16 | destroy: new Rx.Subject(), 17 | clearCompleted: new Rx.Subject(), 18 | }; 19 | 20 | /** 21 | * Register our actions against an updates stream 22 | * each one of our actions will push operation to apply on the model 23 | * into the stream. 24 | */ 25 | TodoActions.register = function (updates) { 26 | this.create 27 | .map(function (title) { 28 | return function (todos) { 29 | return todos.concat({ 30 | id: uuid(), 31 | title: title, 32 | completed: false 33 | }); 34 | }; 35 | }) 36 | .subscribe(updates); 37 | 38 | this.updateTitle 39 | .map(function(update) { 40 | var todoToSave = update.todo, 41 | text = update.text; 42 | return function (todos) { 43 | return todos.map(function (todo) { 44 | return todo !== todoToSave ? 45 | todo : 46 | assign({}, todo, {title: text}) 47 | ; 48 | }); 49 | }; 50 | }) 51 | .subscribe(updates); 52 | 53 | this.toggle 54 | .map(function(todoToToggle) { 55 | return function (todos) { 56 | return todos.map(function (todo) { 57 | return todo !== todoToToggle ? 58 | todo : 59 | assign({}, todo, {completed: !todo.completed}); 60 | }); 61 | }; 62 | }) 63 | .subscribe(updates); 64 | 65 | 66 | this.toggleAll 67 | .map(function(checked) { 68 | return function (todos) { 69 | return todos.map(function (todo) { 70 | return { 71 | id: todo.id, 72 | title: todo.title, 73 | completed: checked 74 | }; 75 | }); 76 | }; 77 | }) 78 | .subscribe(updates); 79 | 80 | 81 | this.destroy 82 | .map(function(deletedTodo) { 83 | return function (todos) { 84 | return todos.filter(function (todo) { 85 | return todo !== deletedTodo; 86 | }); 87 | }; 88 | }) 89 | .subscribe(updates); 90 | 91 | 92 | this.clearCompleted 93 | .map(function () { 94 | return function (todos) { 95 | return todos.filter(function (todo) { 96 | return !todo.completed; 97 | }); 98 | }; 99 | }) 100 | .subscribe(updates); 101 | }; 102 | 103 | 104 | module.exports = TodoActions; -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true, browser: true*/ 2 | 3 | var Rx = require('rx'), 4 | React = require('react/addons'), 5 | TodoStore = require('./store/todoStore'), 6 | TodoActions = require('./actions/TodoActions'), 7 | MainView = require('./views/mainView.jsx'); 8 | 9 | 10 | var todoStore = new TodoStore('react-todos'); 11 | 12 | //register our actions against our store updates stream 13 | TodoActions.register(todoStore.updates); 14 | 15 | React.renderComponent( 16 | MainView({ todoStore: todoStore }), 17 | document.getElementById('todoapp') 18 | ); 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | 3 | module.exports = { 4 | ALL_TODOS: '', 5 | ACTIVE_TODOS: 'active', 6 | COMPLETED_TODOS: 'completed' 7 | }; 8 | -------------------------------------------------------------------------------- /src/store/todoStore.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | 3 | var Rx = require('rx'), 4 | assign = require('../utils/assign'), 5 | store = require('../utils/store'); 6 | 7 | // our store expose 2 streams : 8 | // `updates`: that should receive operations to be applied on our list of todo 9 | // `todos`: an observable that will contains our up to date list of todo 10 | function TodoStore(key) { 11 | this.updates = new Rx.BehaviorSubject(store(key)); 12 | 13 | this.todos = this.updates 14 | .scan(function (todos, operation) { 15 | return operation(todos); 16 | }); 17 | 18 | 19 | this.key = key; 20 | this.todos.forEach(function (todos) { 21 | store(key, todos); 22 | }); 23 | } 24 | 25 | 26 | 27 | module.exports = TodoStore; -------------------------------------------------------------------------------- /src/utils/assign.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | 3 | module.exports = function assign(target, items) { 4 | 5 | items = [].slice.call(arguments); 6 | 7 | return items.reduce(function (target, item) { 8 | return Object.keys(item).reduce(function (target, property) { 9 | target[property] = item[property]; 10 | return target; 11 | }, target); 12 | }, target); 13 | }; -------------------------------------------------------------------------------- /src/utils/eventHandler.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true, proto: true*/ 2 | 3 | var Rx = require('rx'); 4 | 5 | exports.create = function () { 6 | var subject = function() { 7 | subject.onNext.apply(subject, arguments); 8 | }; 9 | 10 | getEnumerablePropertyNames(Rx.Subject.prototype) 11 | .forEach(function (property) { 12 | subject[property] = Rx.Subject.prototype[property]; 13 | }); 14 | Rx.Subject.call(subject); 15 | 16 | return subject; 17 | }; 18 | 19 | 20 | function getEnumerablePropertyNames(target) { 21 | var result = []; 22 | for (var key in target) { 23 | result.push(key); 24 | } 25 | return result; 26 | } 27 | 28 | -------------------------------------------------------------------------------- /src/utils/pluralize.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | 3 | module.exports = function pluralize(count, word) { 4 | return count === 1 ? word : word + 's'; 5 | }; 6 | -------------------------------------------------------------------------------- /src/utils/rxLifecycleMixin.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | 3 | var Rx = require('rx'); 4 | 5 | var RxLifecycleMixin = { 6 | componentWillMount: function () { 7 | this.lifecycle = { 8 | componentWillMount: new Rx.Subject(), 9 | componentDidMount : new Rx.Subject(), 10 | componentWillReceiveProps : new Rx.Subject(), 11 | componentWillUpdate : new Rx.Subject(), 12 | componentDidUpdate : new Rx.Subject(), 13 | componentWillUnmount: new Rx.Subject() 14 | }; 15 | }, 16 | 17 | componentDidMount: function () { 18 | this.lifecycle.componentDidMount.onNext(); 19 | }, 20 | 21 | componentWillReceiveProps: function (nextProps) { 22 | this.lifecycle.componentWillReceiveProps.onNext(nextProps); 23 | }, 24 | 25 | componentWillUpdate: function (nextProps, nextState) { 26 | this.lifecycle.componentWillUpdate.onNext({ 27 | nextProps: nextProps, 28 | nextState: nextState 29 | }); 30 | }, 31 | 32 | componentDidUpdate: function (prevProps, prevState) { 33 | this.lifecycle.componentDidUpdate.onNext({ 34 | prevProps: prevProps, 35 | prevState: prevState 36 | }); 37 | }, 38 | 39 | componentWillUnmount: function () { 40 | this.lifecycle.componentWillUnmount.onNext(); 41 | } 42 | }; 43 | 44 | module.exports = RxLifecycleMixin; 45 | -------------------------------------------------------------------------------- /src/utils/store.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true, browser:true*/ 2 | 3 | module.exports = function store(namespace, data) { 4 | if (data) { 5 | return localStorage.setItem(namespace, JSON.stringify(data)); 6 | } 7 | 8 | var localStore = localStorage.getItem(namespace); 9 | return (localStore && JSON.parse(localStore)) || []; 10 | }; -------------------------------------------------------------------------------- /src/utils/uuid.js: -------------------------------------------------------------------------------- 1 | /*jshint bitwise:false, node:true */ 2 | 3 | module.exports = function uuid() { 4 | var i, random; 5 | var result = ''; 6 | 7 | for (i = 0; i < 32; i++) { 8 | random = Math.random() * 16 | 0; 9 | if (i === 8 || i === 12 || i === 16 || i === 20) { 10 | result += '-'; 11 | } 12 | result += (i === 12 ? 4 : (i === 16 ? (random & 3 | 8) : random)) 13 | .toString(16); 14 | } 15 | 16 | return result; 17 | }; -------------------------------------------------------------------------------- /src/views/footer.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | */ 4 | 5 | /*jshint node:true*/ 6 | 'use strict'; 7 | 8 | 9 | var React = require('react/addons'), 10 | pluralize = require('../utils/pluralize'), 11 | EventHandler = require('../utils/eventHandler'), 12 | routes = require('../routes'), 13 | TodoActions = require('../actions/TodoActions'); 14 | 15 | var TodoFooter = React.createClass({ 16 | componentWillMount: function () { 17 | var clearButtonClick = EventHandler.create(); 18 | clearButtonClick.subscribe(TodoActions.clearCompleted); 19 | this.handlers = { 20 | clearButtonClick : clearButtonClick 21 | }; 22 | }, 23 | 24 | render: function () { 25 | var activeTodoWord = pluralize(this.props.count, 'item'); 26 | var clearButton = null; 27 | 28 | if (this.props.completedCount > 0) { 29 | clearButton = ( 30 | 35 | ); 36 | } 37 | 38 | // React idiom for shortcutting to `classSet` since it'll be used often 39 | var cx = React.addons.classSet; 40 | var nowShowing = this.props.nowShowing; 41 | return ( 42 | 73 | ); 74 | } 75 | }); 76 | 77 | module.exports = TodoFooter; -------------------------------------------------------------------------------- /src/views/header.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | */ 4 | 5 | /*jshint node:true*/ 6 | 'use strict'; 7 | 8 | var React = require('react/addons'), 9 | EventHandler = require('../utils/eventHandler'), 10 | TodoActions = require('../actions/TodoActions'); 11 | 12 | var ENTER_KEY = 13; 13 | 14 | var TodoHeader = React.createClass({ 15 | componentWillMount: function () { 16 | var newFieldKeyDown = EventHandler.create(); 17 | var enterEvent = newFieldKeyDown.filter(function (event) { 18 | return event.keyCode === ENTER_KEY; 19 | }); 20 | 21 | enterEvent.forEach(function (event) { 22 | event.stopPropagation(); 23 | event.preventDefault(); 24 | }); 25 | 26 | enterEvent 27 | .map(function (event) { 28 | return event.target.value.trim(); 29 | }) 30 | .filter(function (value) { 31 | return !!value; 32 | }).subscribe(TodoActions.create); 33 | 34 | enterEvent 35 | .forEach(function (event) { 36 | event.target.value = ''; 37 | }); 38 | 39 | 40 | this.handlers = { 41 | newFieldKeyDown: newFieldKeyDown 42 | }; 43 | }, 44 | 45 | render: function () { 46 | return ( 47 | 56 | ); 57 | } 58 | }); 59 | 60 | module.exports = TodoHeader; -------------------------------------------------------------------------------- /src/views/mainView.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | */ 4 | 5 | /*jshint node: true, browser: true, newcap:false*/ 6 | 'use strict'; 7 | 8 | 9 | var React = require('react/addons'), 10 | Router = require('director').Router, 11 | Rx = require('rx'), 12 | routes = require('../routes'), 13 | TodoHeader = require('./header.jsx'), 14 | TodoFooter = require('./footer.jsx'), 15 | TodoList = require('./todoList.jsx'); 16 | 17 | 18 | var MainView = React.createClass({ 19 | getInitialState: function () { 20 | return {}; 21 | }, 22 | 23 | componentWillMount: function () { 24 | var todoStore = this.props.todoStore; 25 | 26 | var currentRoute = new Rx.BehaviorSubject(''), 27 | onNext = currentRoute.onNext; 28 | 29 | var router = Router({ 30 | '/': function () { 31 | currentRoute.onNext(routes.ALL_TODOS); 32 | }, 33 | '/active': function () { 34 | currentRoute.onNext(routes.ACTIVE_TODOS); 35 | }, 36 | '/completed':function () { 37 | currentRoute.onNext(routes.COMPLETED_TODOS); 38 | }, 39 | }); 40 | 41 | router.init('/'); 42 | 43 | var shownTodos = todoStore.todos 44 | .combineLatest( 45 | currentRoute, 46 | function (todos, currentRoute) { 47 | 48 | var activeTodoCount = todos.reduce(function (accum, todo) { 49 | return todo.completed ? accum : accum + 1; 50 | }, 0); 51 | 52 | var completedCount = todos.length - activeTodoCount; 53 | 54 | var shownTodos = todos.filter(function (todo) { 55 | switch (currentRoute) { 56 | case routes.ACTIVE_TODOS: 57 | return !todo.completed; 58 | case routes.COMPLETED_TODOS: 59 | return todo.completed; 60 | default: 61 | return true; 62 | } 63 | }, this); 64 | 65 | return { 66 | activeTodoCount: activeTodoCount, 67 | completedCount: completedCount, 68 | shownTodos: shownTodos, 69 | currentRoute: currentRoute 70 | }; 71 | } 72 | ) 73 | .subscribe(this.setState.bind(this)); 74 | }, 75 | 76 | 77 | 78 | render: function () { 79 | var footer; 80 | if (this.state.activeTodoCount || this.state.completedCount) { 81 | footer = 85 | } 86 | 87 | var list; 88 | if (this.state.shownTodos && this.state.shownTodos.length) { 89 | list = 90 | } 91 | 92 | return ( 93 |
94 | 95 | {list} 96 | {footer} 97 |
98 | ); 99 | } 100 | }); 101 | 102 | module.exports = MainView; 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /src/views/todoItem.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | */ 4 | /*jshint node:true*/ 5 | 6 | 'use strict'; 7 | 8 | var React = require('react/addons'), 9 | EventHandler = require('../utils/eventHandler'), 10 | TodoActions = require('../actions/TodoActions'), 11 | RxLifecycleMixin = require('../utils/rxLifecycleMixin'); 12 | 13 | var ESCAPE_KEY = 27; 14 | var ENTER_KEY = 13; 15 | 16 | var TodoItem = React.createClass({ 17 | mixins: [RxLifecycleMixin], 18 | getInitialState: function () { 19 | return { 20 | editing: false, 21 | editText: this.props.todo.title 22 | }; 23 | }, 24 | 25 | componentWillMount: function () { 26 | var setState = this.setState.bind(this); 27 | 28 | var toggleClick = EventHandler.create(); 29 | toggleClick 30 | .map(this.getTodo) 31 | .subscribe(TodoActions.toggle); 32 | 33 | var destroyButtonClick = EventHandler.create(); 34 | destroyButtonClick 35 | .map(this.getTodo) 36 | .subscribe(TodoActions.destroy); 37 | 38 | var labelDoubleClick = EventHandler.create(); 39 | labelDoubleClick 40 | .map(function () { 41 | return { 42 | editing: true 43 | }; 44 | }) 45 | .subscribe(setState); 46 | 47 | var editFieldKeyDown = EventHandler.create(); 48 | editFieldKeyDown 49 | .filter(function (event) { 50 | return event.keyCode === ESCAPE_KEY; 51 | }) 52 | .map(function () { 53 | return { 54 | editing: false, 55 | editText: this.props.todo.title 56 | }; 57 | }.bind(this)) 58 | .subscribe(setState); 59 | 60 | editFieldKeyDown 61 | .filter(function (event) { 62 | return event.keyCode === ENTER_KEY; 63 | }) 64 | .subscribe(this.submit); 65 | 66 | var editFieldBlur = EventHandler.create(); 67 | editFieldBlur 68 | .subscribe(this.submit); 69 | 70 | 71 | var editFieldChange = EventHandler.create(); 72 | editFieldChange 73 | .map(function (e) { 74 | return { 75 | editText: e.target.value 76 | }; 77 | }) 78 | .subscribe(setState); 79 | 80 | this.lifecycle.componentDidUpdate 81 | .filter(function (prev) { 82 | return this.state.editing && !prev.prevState.editing; 83 | }.bind(this)) 84 | .subscribe(function() { 85 | var node = this.refs.editField.getDOMNode(); 86 | node.focus(); 87 | node.value = this.props.todo.title; 88 | node.setSelectionRange(node.value.length, node.value.length); 89 | }.bind(this)); 90 | 91 | this.handlers = { 92 | toggleClick: toggleClick, 93 | destroyButtonClick: destroyButtonClick, 94 | labelDoubleClick : labelDoubleClick, 95 | editFieldKeyDown: editFieldKeyDown, 96 | editFieldBlur: editFieldBlur, 97 | editFieldChange: editFieldChange 98 | }; 99 | }, 100 | 101 | 102 | submit: function () { 103 | var val = this.state.editText.trim(); 104 | if (val) { 105 | TodoActions.updateTitle.onNext({ 106 | text: val, 107 | todo: this.getTodo() 108 | }); 109 | this.setState({ 110 | editText: val, 111 | editing: false 112 | }); 113 | } else { 114 | TodoActions.destroy.onNext(this.props.todo); 115 | } 116 | }, 117 | 118 | getTodo: function () { 119 | return this.props.todo; 120 | }, 121 | 122 | 123 | /** 124 | * This is a completely optional performance enhancement that you can implement 125 | * on any React component. If you were to delete this method the app would still 126 | * work correctly (and still be very performant!), we just use it as an example 127 | * of how little code it takes to get an order of magnitude performance improvement. 128 | */ 129 | shouldComponentUpdate: function (nextProps, nextState) { 130 | return ( 131 | nextProps.todo !== this.props.todo || 132 | nextState.editing !== this.state.editing || 133 | nextState.editText !== this.state.editText 134 | ); 135 | }, 136 | 137 | render: function () { 138 | return ( 139 |
  • 143 |
    144 | 150 | 153 |
    155 | 163 |
  • 164 | ); 165 | } 166 | }); 167 | 168 | module.exports = TodoItem; 169 | -------------------------------------------------------------------------------- /src/views/todoList.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jsx React.DOM 3 | */ 4 | 5 | /*jshint node: true, browser: true, newcap:false*/ 6 | 7 | 'use strict'; 8 | 9 | var React = require('react/addons'), 10 | EventHandler = require('../utils/eventHandler'), 11 | TodoActions = require('../actions/TodoActions'), 12 | TodoItem = require('./todoItem.jsx'); 13 | 14 | 15 | var TodoList = React.createClass({ 16 | componentWillMount: function () { 17 | var toggleAllChange = EventHandler.create(); 18 | toggleAllChange 19 | .map(function (event) { 20 | return event.target.checked; 21 | }) 22 | .subscribe(TodoActions.toggleAll); 23 | 24 | this.handlers = { 25 | toggleAllChange: toggleAllChange 26 | } 27 | }, 28 | 29 | 30 | 31 | render: function () { 32 | var todoItems = this.props.todos.map(function (todo) { 33 | return ( 34 | 38 | ); 39 | }, this); 40 | 41 | return ( 42 |
    43 | 49 |
      50 | {todoItems} 51 |
    52 |
    53 | ); 54 | } 55 | }); 56 | 57 | module.exports = TodoList; 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /styles/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fdecampredon/react-rxjs-todomvc/4ba4725ba0033bd3017e3e69fcce1de1719f20a4/styles/bg.png -------------------------------------------------------------------------------- /styles/styles.css: -------------------------------------------------------------------------------- 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 | color: inherit; 16 | -webkit-appearance: none; 17 | -ms-appearance: none; 18 | -o-appearance: none; 19 | appearance: none; 20 | } 21 | 22 | body { 23 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 24 | line-height: 1.4em; 25 | background: #eaeaea url('bg.png'); 26 | color: #4d4d4d; 27 | width: 550px; 28 | margin: 0 auto; 29 | -webkit-font-smoothing: antialiased; 30 | -moz-font-smoothing: antialiased; 31 | -ms-font-smoothing: antialiased; 32 | -o-font-smoothing: antialiased; 33 | font-smoothing: antialiased; 34 | } 35 | 36 | button, 37 | input[type="checkbox"] { 38 | outline: none; 39 | } 40 | 41 | #todoapp { 42 | background: #fff; 43 | background: rgba(255, 255, 255, 0.9); 44 | margin: 130px 0 40px 0; 45 | border: 1px solid #ccc; 46 | position: relative; 47 | border-top-left-radius: 2px; 48 | border-top-right-radius: 2px; 49 | box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.2), 50 | 0 25px 50px 0 rgba(0, 0, 0, 0.15); 51 | } 52 | 53 | #todoapp:before { 54 | content: ''; 55 | border-left: 1px solid #f5d6d6; 56 | border-right: 1px solid #f5d6d6; 57 | width: 2px; 58 | position: absolute; 59 | top: 0; 60 | left: 40px; 61 | height: 100%; 62 | } 63 | 64 | #todoapp input::-webkit-input-placeholder { 65 | font-style: italic; 66 | } 67 | 68 | #todoapp input::-moz-placeholder { 69 | font-style: italic; 70 | color: #a9a9a9; 71 | } 72 | 73 | #todoapp h1 { 74 | position: absolute; 75 | top: -120px; 76 | width: 100%; 77 | font-size: 70px; 78 | font-weight: bold; 79 | text-align: center; 80 | color: #b3b3b3; 81 | color: rgba(255, 255, 255, 0.3); 82 | text-shadow: -1px -1px rgba(0, 0, 0, 0.2); 83 | -webkit-text-rendering: optimizeLegibility; 84 | -moz-text-rendering: optimizeLegibility; 85 | -ms-text-rendering: optimizeLegibility; 86 | -o-text-rendering: optimizeLegibility; 87 | text-rendering: optimizeLegibility; 88 | } 89 | 90 | #header { 91 | padding-top: 15px; 92 | border-radius: inherit; 93 | } 94 | 95 | #header:before { 96 | content: ''; 97 | position: absolute; 98 | top: 0; 99 | right: 0; 100 | left: 0; 101 | height: 15px; 102 | z-index: 2; 103 | border-bottom: 1px solid #6c615c; 104 | background: #8d7d77; 105 | background: -webkit-gradient(linear, left top, left bottom, from(rgba(132, 110, 100, 0.8)),to(rgba(101, 84, 76, 0.8))); 106 | background: -webkit-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8)); 107 | background: linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8)); 108 | filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670'); 109 | border-top-left-radius: 1px; 110 | border-top-right-radius: 1px; 111 | } 112 | 113 | #new-todo, 114 | .edit { 115 | position: relative; 116 | margin: 0; 117 | width: 100%; 118 | font-size: 24px; 119 | font-family: inherit; 120 | line-height: 1.4em; 121 | border: 0; 122 | outline: none; 123 | color: inherit; 124 | padding: 6px; 125 | border: 1px solid #999; 126 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 127 | -moz-box-sizing: border-box; 128 | -ms-box-sizing: border-box; 129 | -o-box-sizing: border-box; 130 | box-sizing: border-box; 131 | -webkit-font-smoothing: antialiased; 132 | -moz-font-smoothing: antialiased; 133 | -ms-font-smoothing: antialiased; 134 | -o-font-smoothing: antialiased; 135 | font-smoothing: antialiased; 136 | } 137 | 138 | #new-todo { 139 | padding: 16px 16px 16px 60px; 140 | border: none; 141 | background: rgba(0, 0, 0, 0.02); 142 | z-index: 2; 143 | box-shadow: none; 144 | } 145 | 146 | #main { 147 | position: relative; 148 | z-index: 2; 149 | border-top: 1px dotted #adadad; 150 | } 151 | 152 | label[for='toggle-all'] { 153 | display: none; 154 | } 155 | 156 | #toggle-all { 157 | position: absolute; 158 | top: -42px; 159 | left: -4px; 160 | width: 40px; 161 | text-align: center; 162 | /* Mobile Safari */ 163 | border: none; 164 | } 165 | 166 | #toggle-all:before { 167 | content: '»'; 168 | font-size: 28px; 169 | color: #d9d9d9; 170 | padding: 0 25px 7px; 171 | } 172 | 173 | #toggle-all:checked:before { 174 | color: #737373; 175 | } 176 | 177 | #todo-list { 178 | margin: 0; 179 | padding: 0; 180 | list-style: none; 181 | } 182 | 183 | #todo-list li { 184 | position: relative; 185 | font-size: 24px; 186 | border-bottom: 1px dotted #ccc; 187 | } 188 | 189 | #todo-list li:last-child { 190 | border-bottom: none; 191 | } 192 | 193 | #todo-list li.editing { 194 | border-bottom: none; 195 | padding: 0; 196 | } 197 | 198 | #todo-list li.editing .edit { 199 | display: block; 200 | width: 506px; 201 | padding: 13px 17px 12px 17px; 202 | margin: 0 0 0 43px; 203 | } 204 | 205 | #todo-list li.editing .view { 206 | display: none; 207 | } 208 | 209 | #todo-list li .toggle { 210 | text-align: center; 211 | width: 40px; 212 | /* auto, since non-WebKit browsers doesn't support input styling */ 213 | height: auto; 214 | position: absolute; 215 | top: 0; 216 | bottom: 0; 217 | margin: auto 0; 218 | /* Mobile Safari */ 219 | border: none; 220 | -webkit-appearance: none; 221 | -ms-appearance: none; 222 | -o-appearance: none; 223 | appearance: none; 224 | } 225 | 226 | #todo-list li .toggle:after { 227 | content: '✔'; 228 | /* 40 + a couple of pixels visual adjustment */ 229 | line-height: 43px; 230 | font-size: 20px; 231 | color: #d9d9d9; 232 | text-shadow: 0 -1px 0 #bfbfbf; 233 | } 234 | 235 | #todo-list li .toggle:checked:after { 236 | color: #85ada7; 237 | text-shadow: 0 1px 0 #669991; 238 | bottom: 1px; 239 | position: relative; 240 | } 241 | 242 | #todo-list li label { 243 | white-space: pre; 244 | word-break: break-word; 245 | padding: 15px 60px 15px 15px; 246 | margin-left: 45px; 247 | display: block; 248 | line-height: 1.2; 249 | -webkit-transition: color 0.4s; 250 | transition: color 0.4s; 251 | } 252 | 253 | #todo-list li.completed label { 254 | color: #a9a9a9; 255 | text-decoration: line-through; 256 | } 257 | 258 | #todo-list li .destroy { 259 | display: none; 260 | position: absolute; 261 | top: 0; 262 | right: 10px; 263 | bottom: 0; 264 | width: 40px; 265 | height: 40px; 266 | margin: auto 0; 267 | font-size: 22px; 268 | color: #a88a8a; 269 | -webkit-transition: all 0.2s; 270 | transition: all 0.2s; 271 | } 272 | 273 | #todo-list li .destroy:hover { 274 | text-shadow: 0 0 1px #000, 275 | 0 0 10px rgba(199, 107, 107, 0.8); 276 | -webkit-transform: scale(1.3); 277 | -ms-transform: scale(1.3); 278 | transform: scale(1.3); 279 | } 280 | 281 | #todo-list li .destroy:after { 282 | content: '✖'; 283 | } 284 | 285 | #todo-list li:hover .destroy { 286 | display: block; 287 | } 288 | 289 | #todo-list li .edit { 290 | display: none; 291 | } 292 | 293 | #todo-list li.editing:last-child { 294 | margin-bottom: -1px; 295 | } 296 | 297 | #footer { 298 | color: #777; 299 | padding: 0 15px; 300 | position: absolute; 301 | right: 0; 302 | bottom: -31px; 303 | left: 0; 304 | height: 20px; 305 | z-index: 1; 306 | text-align: center; 307 | } 308 | 309 | #footer:before { 310 | content: ''; 311 | position: absolute; 312 | right: 0; 313 | bottom: 31px; 314 | left: 0; 315 | height: 50px; 316 | z-index: -1; 317 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3), 318 | 0 6px 0 -3px rgba(255, 255, 255, 0.8), 319 | 0 7px 1px -3px rgba(0, 0, 0, 0.3), 320 | 0 43px 0 -6px rgba(255, 255, 255, 0.8), 321 | 0 44px 2px -6px rgba(0, 0, 0, 0.2); 322 | } 323 | 324 | #todo-count { 325 | float: left; 326 | text-align: left; 327 | } 328 | 329 | #filters { 330 | margin: 0; 331 | padding: 0; 332 | list-style: none; 333 | position: absolute; 334 | right: 0; 335 | left: 0; 336 | } 337 | 338 | #filters li { 339 | display: inline; 340 | } 341 | 342 | #filters li a { 343 | color: #83756f; 344 | margin: 2px; 345 | text-decoration: none; 346 | } 347 | 348 | #filters li a.selected { 349 | font-weight: bold; 350 | } 351 | 352 | #clear-completed { 353 | float: right; 354 | position: relative; 355 | line-height: 20px; 356 | text-decoration: none; 357 | background: rgba(0, 0, 0, 0.1); 358 | font-size: 11px; 359 | padding: 0 10px; 360 | border-radius: 3px; 361 | box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.2); 362 | } 363 | 364 | #clear-completed:hover { 365 | background: rgba(0, 0, 0, 0.15); 366 | box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.3); 367 | } 368 | 369 | #info { 370 | margin: 65px auto 0; 371 | color: #a6a6a6; 372 | font-size: 12px; 373 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7); 374 | text-align: center; 375 | } 376 | 377 | #info a { 378 | color: inherit; 379 | } 380 | 381 | /* 382 | Hack to remove background from Mobile Safari. 383 | Can't use it globally since it destroys checkboxes in Firefox and Opera 384 | */ 385 | 386 | @media screen and (-webkit-min-device-pixel-ratio:0) { 387 | #toggle-all, 388 | #todo-list li .toggle { 389 | background: none; 390 | } 391 | 392 | #todo-list li .toggle { 393 | height: 40px; 394 | } 395 | 396 | #toggle-all { 397 | top: -56px; 398 | left: -15px; 399 | width: 65px; 400 | height: 41px; 401 | -webkit-transform: rotate(90deg); 402 | -ms-transform: rotate(90deg); 403 | transform: rotate(90deg); 404 | -webkit-appearance: none; 405 | appearance: none; 406 | } 407 | } 408 | 409 | .hidden { 410 | display: none; 411 | } 412 | 413 | hr { 414 | margin: 20px 0; 415 | border: 0; 416 | border-top: 1px dashed #C5C5C5; 417 | border-bottom: 1px dashed #F7F7F7; 418 | } 419 | 420 | .learn a { 421 | font-weight: normal; 422 | text-decoration: none; 423 | color: #b83f45; 424 | } 425 | 426 | .learn a:hover { 427 | text-decoration: underline; 428 | color: #787e7e; 429 | } 430 | 431 | .learn h3, 432 | .learn h4, 433 | .learn h5 { 434 | margin: 10px 0; 435 | font-weight: 500; 436 | line-height: 1.2; 437 | color: #000; 438 | } 439 | 440 | .learn h3 { 441 | font-size: 24px; 442 | } 443 | 444 | .learn h4 { 445 | font-size: 18px; 446 | } 447 | 448 | .learn h5 { 449 | margin-bottom: 0; 450 | font-size: 14px; 451 | } 452 | 453 | .learn ul { 454 | padding: 0; 455 | margin: 0 0 30px 25px; 456 | } 457 | 458 | .learn li { 459 | line-height: 20px; 460 | } 461 | 462 | .learn p { 463 | font-size: 15px; 464 | font-weight: 300; 465 | line-height: 1.3; 466 | margin-top: 0; 467 | margin-bottom: 0; 468 | } 469 | 470 | .quote { 471 | border: none; 472 | margin: 20px 0 60px 0; 473 | } 474 | 475 | .quote p { 476 | font-style: italic; 477 | } 478 | 479 | .quote p:before { 480 | content: '“'; 481 | font-size: 50px; 482 | opacity: .15; 483 | position: absolute; 484 | top: -20px; 485 | left: 3px; 486 | } 487 | 488 | .quote p:after { 489 | content: '”'; 490 | font-size: 50px; 491 | opacity: .15; 492 | position: absolute; 493 | bottom: -42px; 494 | right: 3px; 495 | } 496 | 497 | .quote footer { 498 | position: absolute; 499 | bottom: -40px; 500 | right: 0; 501 | } 502 | 503 | .quote footer img { 504 | border-radius: 3px; 505 | } 506 | 507 | .quote footer a { 508 | margin-left: 5px; 509 | vertical-align: middle; 510 | } 511 | 512 | .speech-bubble { 513 | position: relative; 514 | padding: 10px; 515 | background: rgba(0, 0, 0, .04); 516 | border-radius: 5px; 517 | } 518 | 519 | .speech-bubble:after { 520 | content: ''; 521 | position: absolute; 522 | top: 100%; 523 | right: 30px; 524 | border: 13px solid transparent; 525 | border-top-color: rgba(0, 0, 0, .04); 526 | } 527 | 528 | .learn-bar > .learn { 529 | position: absolute; 530 | width: 272px; 531 | top: 8px; 532 | left: -300px; 533 | padding: 10px; 534 | border-radius: 5px; 535 | background-color: rgba(255, 255, 255, .6); 536 | -webkit-transition-property: left; 537 | transition-property: left; 538 | -webkit-transition-duration: 500ms; 539 | transition-duration: 500ms; 540 | } 541 | 542 | @media (min-width: 899px) { 543 | .learn-bar { 544 | width: auto; 545 | margin: 0 0 0 300px; 546 | } 547 | 548 | .learn-bar > .learn { 549 | left: 8px; 550 | } 551 | 552 | .learn-bar #todoapp { 553 | width: 550px; 554 | margin: 130px auto 40px auto; 555 | } 556 | } 557 | --------------------------------------------------------------------------------