├── .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 |
154 |
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 |
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 |
--------------------------------------------------------------------------------