~tychi/code-mirror-shield

176bdc6261fdea8b5a773cf68f3c53116a617df2 — Tyler Childs 8 months ago d8189f6 main
feature: add todo list example
M README.md => README.md +7 -1
@@ 10,7 10,12 @@ You _can_ touch this. And click it. Or **clack** it.

Any machine that'll survive past [y2k38](https://y2k38.info) will do.

To get started, play with the following examples:
## Quick Start

1. [Manage your Todo List](/public/todo-list/index.html)
1. [Or like, manage your Todo List](/todo-list/index.html)

To have fun, play with the following examples:

* [hello-world](/hello-world.html)
* [hello-universe](/hello-universe.html)


@@ 18,6 23,7 @@ To get started, play with the following examples:
* [style](/style.css)
* [editor](/editor.js)


## Local Development
Clone [code-mirror-shield](https://git.sr.ht/~tychi/code-mirror-shield)


A public/todo-list/LICENSE => public/todo-list/LICENSE +7 -0
@@ 0,0 1,7 @@
Public Domain

Author: Tyler Childs
Email: email@tychi.me
Date: January 3rd, 2022

"Tyler Childs <email@tychi.me>"

A public/todo-list/README.md => public/todo-list/README.md +17 -0
@@ 0,0 1,17 @@
# Tag • [Demo](https://thelanding.page/apps/todo) • [TodoMVC](http://todomvc.com)

Tag is a light-weight library currently incubating at Netflix. The main goals are portability through declaration, approachability with a limited volcabulary, and adhereing to web-first principles.

## Support

- [Email](mailto:email@tychi.me)

*Let us [know](https://github.com/tastejs/todomvc/issues) if you discover anything worth sharing.*

## Implementation

Uses [Tag](https://thelanding.page/tag)

## Credit

Created by [~tychi](http://tychi.me)

A public/todo-list/data/flags.js => public/todo-list/data/flags.js +27 -0
@@ 0,0 1,27 @@
const filters = {
  all: { key: 'ALL', label: 'All' },
  active: { key: 'ACTIVE', label: 'Active' },
  completed: { key: 'COMPLETED', label: 'Completed' },
}

export default {
  actions: {
    enter: 13,
  },
  filterOrdered: [
    filters.all.key,
    filters.active.key,
    filters.completed.key,
  ],
  filters: {
    [filters.all.key]: {
      label: filters.all.label,
    },
    [filters.active.key]: {
      label: filters.active.label,
    },
    [filters.completed.key]: {
      label: filters.completed.label,
    },
  }
}

A public/todo-list/data/initialTodoList.js => public/todo-list/data/initialTodoList.js +25 -0
@@ 0,0 1,25 @@
const initialTodoList = {
  filter: 'ALL',
  items: [
    {
      id: 'dishes',
      completed: false,
      editing: false,
      task: 'Dishes'
    },
    {
      id: 'groceries',
      completed: true,
      editing: false,
      task: 'Groceries'
    },
    {
      id: 'bills',
      completed: false,
      editing: false,
      task: 'Bills'
    }
  ]
}

export default initialTodoList

A public/todo-list/features/createItem.js => public/todo-list/features/createItem.js +24 -0
@@ 0,0 1,24 @@
// a helper function to add an item to the items state
export default function createItem($, task) {
  // build a new item, with a random id for collision-free duplicates
  const item = {
    task,
    completed: false,
    editing: false,
    id: task +  Math.floor((Math.random() * 100) + 1)
  }

  // a helper function for appending an item into the item state
  const handler = (state, payload) => {
    return {
      ...state,
      items: [
        ...state.items,
        payload
      ]
    }
  }

  // add the new item to the items state
  $.set(item, handler)
}

A public/todo-list/features/deleteItem.js => public/todo-list/features/deleteItem.js +19 -0
@@ 0,0 1,19 @@
// a helper function to remove an item from the items state
export default function deleteItem($, item) {
  // a helper function for filtering the current item out of the item state
  const handler = (state, payload) => {
    return {
      ...state,
      items: [
        ...state.items.filter((item) => {
          if(item.id !== payload.id) {
            return item
          }
        })
      ]
    }
  }

  // remove the item from the item state
  $.set(item, handler)
}

A public/todo-list/features/findItemById.js => public/todo-list/features/findItemById.js +5 -0
@@ 0,0 1,5 @@
// a helper function to locate an item by an id
export default function findItemById($, id) {
  const { items } = $.get()
  return items.find(x => x.id === id)
}

A public/todo-list/features/onClearCompletedAction.js => public/todo-list/features/onClearCompletedAction.js +15 -0
@@ 0,0 1,15 @@
// adds a click event listener for when the clear completed action is performed
// the items state is then updated to contain only incomplete items
export default function onClearCompletedAction($) {
  $.on(
    'click',
    '[data-clear-completed]',
    function clearCompleted() {
      const { items } = $.get()
      const onlyIncompleteItems = items.filter(x =>
        !x.completed
      )
      $.set({ items: onlyIncompleteItems })
    }
  )
}

A public/todo-list/features/onCompletenessToggle.js => public/todo-list/features/onCompletenessToggle.js +20 -0
@@ 0,0 1,20 @@
// adds a click event listener for when the complete all action is performed
// all items will be marked as completed in the items state
export default function onCompletenessToggle($) {
  $.on(
    'change',
    '[data-toggle-all]',
    (event) => handler($, event)
  )
}

function handler($, event) {
  const { checked } = event.target
  const { items } = $.get()
  const markedItems = items.map(x => ({
    ...x,
    completed: checked
  }))

  $.set({ items: markedItems })
}

A public/todo-list/features/onFilterChange.js => public/todo-list/features/onFilterChange.js +12 -0
@@ 0,0 1,12 @@
export default function onFilterChange($) {
  // adds a click event listener for when a filter is chosen
  // the filter state is the updated to contain the new filter
  $.on(
    'click',
    '[data-filter]',
    function chooseFilter(event) {
      const { filter } = event.target.dataset
      $.set({ filter })
    }
  )
}

A public/todo-list/features/onItemChange.js => public/todo-list/features/onItemChange.js +35 -0
@@ 0,0 1,35 @@
import findItemById from './findItemById.js'
import updateItem from './updateItem.js'

// add a keypress event listener for updating an item
export default function onItemChange($, flags) {
  if(!flags.actions) return

  function changeItemHandler($, event) {
    const id = event.target.dataset.changeId
    const { value } = event.target
    const item = findItemById($, id)

    updateItem($, {
      ...item,
      editing: false,
      task: value
    })
  }

  $.on(
    'keypress',
    '[data-change-id]',
    (event) => {
      if (event.which === flags.actions.enter) {
        changeItemHandler($, event)
      }
    }
  )

  $.on(
    'blur',
    '[data-change-id]',
    (event) => changeItemHandler($, event)
  )
}

A public/todo-list/features/onItemDelete.js => public/todo-list/features/onItemDelete.js +19 -0
@@ 0,0 1,19 @@
import findItemById from './findItemById.js'
import deleteItem from './deleteItem.js'

// add a click event listener for removing an item from items state
export default function onItemDelete($) {
  function deleteItemHandler($, event) {
    const id = event.target.dataset.deleteId
    const item = findItemById($, id)

    // calls a helper function to delete this item from the items state
    deleteItem($, item)
  }

  $.on(
    'click',
    '[data-delete-id]',
    (event) => deleteItemHandler($, event)
  )
}

A public/todo-list/features/onItemEdit.js => public/todo-list/features/onItemEdit.js +21 -0
@@ 0,0 1,21 @@
import findItemById from './findItemById.js'
import updateItem from './updateItem.js'

// add a keypress event listener for updating an item
export default function onItemEdit($) {
  function editItemHandler($, event) {
    const id = event.target.dataset.editId
    const item = findItemById($, id)

    updateItem($, {
      ...item,
      editing: true,
    })
  }

  $.on(
    'dblclick',
    '[data-edit-id]',
    (event) => editItemHandler($, event)
  )
}

A public/todo-list/features/onItemToggle.js => public/todo-list/features/onItemToggle.js +23 -0
@@ 0,0 1,23 @@
import findItemById from './findItemById.js'
import updateItem from './updateItem.js'

// add a click event listener for toggling a task's completeness
export default function onItemToggle($) {
  function toggleItemHandler($, event) {
    // get the id of the toggled task and locate the corresponding item in state
    const id = event.target.dataset.toggleId
    const item = findItemById($, id)

    // calls a helper function to update the items state for this updated item
    updateItem($, {
      ...item,
      completed: !item.completed
    })
  }

  $.on(
    'click',
    '[data-toggle-id]',
    (event) => toggleItemHandler($, event)
  )
}

A public/todo-list/features/onNewItemInput.js => public/todo-list/features/onNewItemInput.js +27 -0
@@ 0,0 1,27 @@
import createItem from './createItem.js'

// adds a submit event listener for the form
export default function onNewItemInput($, flags = {}) {
  if(!flags.actions) return

  function newItemHandler($, event) {
    const input = event.target

    if (event.which === flags.actions.enter) {
      // if the input is valid, create a new item and clear the field
      if(validate(input)) {
        createItem($, input.value.trim())
        input.value = ''
      }
    }
  }

  $.on(
    'keypress',
    '[data-new-item-input]',
    function (event) { newItemHandler($, event) }
  )
}

// a quick check to see if the value is not empty
function validate({ value }) { return !!value }

A public/todo-list/features/showClearCompletedAction.js => public/todo-list/features/showClearCompletedAction.js +14 -0
@@ 0,0 1,14 @@
// a helper function for rendering the clear and complete actions
export default function showClearCompletedAction($) {
  // grab the items state
  const { items } = $.get()
  const button = `
    <button data-clear-completed class="clear-completed">
      Clear completed
    </button>
  `
  // true, if there are are COMPLETED items
  const hasCompleteItems = items.some(x => x.completed)

  return hasCompleteItems ? button : ''
}

A public/todo-list/features/showCompletenessToggle.js => public/todo-list/features/showCompletenessToggle.js +16 -0
@@ 0,0 1,16 @@
export default function showCompletenessToggle($) {
  // grab the items state
  const { items } = $.get()

  // true, if there are are COMPLETED items
  const hasIncompleteItems = items.some(x => !x.completed)

  const checked = hasIncompleteItems
    ? ''
    : 'checked="true"'

  return `
    <input data-toggle-all ${checked} id="toggle-all" class="toggle-all" type="checkbox">
    <label for="toggle-all">Mark all as complete</label>
  `
}

A public/todo-list/features/showFilters.js => public/todo-list/features/showFilters.js +22 -0
@@ 0,0 1,22 @@
export default function showFilters($, flags = {}) {
  if(!flags.filterOrdered) return

  // get the filter state
  const { filter } = $.get()

  // a render helper to generate a button from a key
  const render = (key) => `
    <li>
      <a
        href="#/${key}"
        data-filter="${key}"
        ${filter === key ? 'class="selected"' : ''}
      >
        ${flags.filters[key].label}
      </a>
    </li>
  `

  // loop over filter options and return a string of buttons
  return flags.filterOrdered.map(render).join('')
}

A public/todo-list/features/showIncompleteCount.js => public/todo-list/features/showIncompleteCount.js +10 -0
@@ 0,0 1,10 @@
// a render helper for returning the number of tasks remaining
export default function showIncompleteCount($) {
  const { items } = $.get()
  const incomplete = items.filter(x => !x.completed)
  return `
    <span class="todo-count">
      ${incomplete.length} remaining
    </span>
  `
}

A public/todo-list/features/showListItems.js => public/todo-list/features/showListItems.js +48 -0
@@ 0,0 1,48 @@
// returns the list of items corresponding to the current filter
export default function showListItems($, flags = {}) {
  if(!flags.filterOrdered) return

  // grab the filter and items state
  const { filter, items } = $.get()
  const [ALL, ACTIVE, COMPLETED] = flags.filterOrdered

  // a callback function that determines if an item matches the current filter
  const filterItems = (item) => {
    // create a dictionary of true/false from the item based on filter
    const lookup = {
      [ALL]: true,
      [ACTIVE]: !item.completed,
      [COMPLETED]: item.completed
    }

    // use the current filter to know if the item is shown/hidden
    return lookup[filter]
  }

  return items
    .filter(filterItems)
    .map(item => {
      // if the item is completed, mark it as done for styling
      const classList = `class="${
        item.completed ? 'completed' : ''
      } ${
        item.editing ? 'editing' : ''
      }"`

      const checked = item.completed
        ? 'checked="true"'
        : ''

      // for all visible items return a toggle button and a delete button
      return `
        <li ${classList}>
          <div class="view">
            <input ${checked} data-toggle-id="${item.id}" class="toggle" type="checkbox">
            <label data-edit-id="${item.id}">${item.task}</label>
            <button data-delete-id="${item.id}" class="destroy"></button>
          </div>
          <input data-change-id="${item.id}" class="edit" type="text" value="${item.task}" />
        </li>
      `
    }).join('') // convert this array to a string
}

A public/todo-list/features/showNewItemForm.js => public/todo-list/features/showNewItemForm.js +6 -0
@@ 0,0 1,6 @@
// a render helper for returning a create task form
export default function showNewItemForm(_$) {
  return `
    <input data-new-item-input class="new-todo" placeholder="What needs to be done?" autofocus>
  `
}

A public/todo-list/features/updateItem.js => public/todo-list/features/updateItem.js +25 -0
@@ 0,0 1,25 @@
// a helper function to update the items state for an item
export default function updateItem($, item) {
  // a helper function for merging an item with updates in the items state
  const handler = (state, payload) => {
    return {
      ...state,
      items: [
        ...state.items.map((item) => {
          if(item.id !== payload.id) {
            return item
          }

          return {
            ...item,
            ...payload
          }
        })
      ]
    }
  }

  // update the item in the item state
  $.set(item, handler)
}


A public/todo-list/index.html => public/todo-list/index.html +105 -0
@@ 0,0 1,105 @@
<!doctype html>
<html lang="en" data-framework="tag">
	<head>
		<meta charset="utf-8">
		<title>Tag • TodoMVC</title>
		<link rel="stylesheet" href="vendor/todomvc-app.css">
		<link rel="stylesheet" href="vendor/todomvc-common-base.css">
	</head>
	<body>
    <noscript>JavaScript is required to manage your todo list</noscript>
    <section class="todoapp">
      <todo-list>
        <!--
        <header class="header">
          <h1>todos</h1>
          <input class="new-todo" placeholder="What needs to be done?" autofocus>
        </header>
        <section class="main">
          <input id="toggle-all" class="toggle-all" type="checkbox">
          <label for="toggle-all">Mark all as complete</label>
          <ul class="todo-list"></ul>
          <footer class="footer">
            <span class="todo-count">X remaining</span>
            <ul class="filters">
              <li>
                <a href="#/" class="selected">All</a>
              </li>
              <li>
              <a href="#/active">Active</a>
              </li>
              <li>
              <a href="#/completed">Completed</a>
              </li>
            </ul>
            <button class="clear-completed">Clear completed</button>
          </footer>
        </section>
        -->
      </todo-list>
    </section>
		<footer class="info">
			<p>Double-click to edit a todo</p>
			<p>Written by <a href="https://tychi.me">~tychi</a></p>
			<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
		</footer>
    <script type="module">
      // tag is a tiny library for binding HTML fragments to JavaScript closures
      import tag from './vendor/tag.bundle.js'

      import initialTodoList from './data/initialTodoList.js'
      import flags from './data/flags.js'

      import showListItems from './features/showListItems.js'
      import showNewItemForm from './features/showNewItemForm.js'
      import showFilters from './features/showFilters.js'
      import showIncompleteCount from './features/showIncompleteCount.js'
      import showClearCompletedAction from './features/showClearCompletedAction.js'
      import showCompletenessToggle from './features/showCompletenessToggle.js'

      const interactives = [
        './features/onClearCompletedAction.js',
        './features/onFilterChange.js',

        './features/onNewItemInput.js',
        './features/onItemEdit.js',
        './features/onItemChange.js',
        './features/onItemToggle.js',
        './features/onItemDelete.js',

        './features/onCompletenessToggle.js'
      ];

      // create a new tag: <todo-list>
      // define the initial state and shape of the data
      const $ = tag('todo-list', initialTodoList)

      // html is a render function; if a string is returned, it is rendered
      // whenever state changes, the render function will be called on each target
      $.html((target) => `
        <header class="header">
          <h1>todos</h1>
          ${showNewItemForm($)}
        </header>
        <section class="main">
          ${showCompletenessToggle($)}
          <ul class="todo-list">
            ${showListItems($, flags)}
          </ul>
          <footer class="footer">
            ${showIncompleteCount($)}
            <ul class="filters">
              ${showFilters($, flags)}
            </ul>
            ${showClearCompletedAction($)}
          </footer>
        </section>
      `)

      interactives.forEach(async (url) => {
        const { default: start } = await import(url)  
        start($, flags)
      })
    </script>
	</body>
</html>

A public/todo-list/vendor/tag.bundle.js => public/todo-list/vendor/tag.bundle.js +287 -0
@@ 0,0 1,287 @@
// snapshot of MIT licensed https://thelanding.page/tag.bundle.js
function uuidv4() {
	return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
		var r = Math.random() * 16 | 0, v = c == 'x' ? r : r & 3 | 8;
		return v.toString(16);
	});
}
const CACHE = "CACHE";
function createStore(initialState = {
}, notify = ()=>null
	, save = ()=>null
) {
	let state = {
		[CACHE]: {
		},
		...initialState
	};
	const context = {
		set: function(schema, payload, handler = defaultHandler) {
			if (typeof handler === 'function') {
				const newCache = touchCache(state[CACHE], schema);
				const newResource = handler(state[schema] || {
				}, payload);
				state = {
					...state,
					[CACHE]: newCache,
					[schema]: newResource
				};
				save(schema, state[schema]);
				notify(state);
			} else {
				console.error('No Resource Handler provided: ', schema, payload);
			}
		},
		get: function(schema) {
			return state[schema];
		}
	};
	return context;
}
function touchCache(state, schema) {
	return {
		...state,
		[schema]: uuidv4()
	};
}
function defaultHandler(state, payload) {
	return {
		...state,
		...payload
	};
}
const databaseName = 'ion';
const storeName = 'cache';
const database = new Promise(function initialize(resolve, reject) {
	const request = indexedDB.open(databaseName, 1);
	request.onupgradeneeded = function(event) {
		const database = event.target.result;
		database.createObjectStore(storeName, {
			keyPath: 'schema',
			autoIncrement: false
		});
	};
	request.onsuccess = function(event) {
		resolve(event.target.result);
	};
});
async function load(keys) {
	const db = await database;
	const transaction = db.transaction(storeName);
	const objectStore = transaction.objectStore(storeName);
	const rows = await new Promise(function loadFromDatabase(resolve, reject) {
		const rows = [];
		const read = objectStore.openCursor();
		read.onsuccess = function(event) {
			let cursor = event.target.result;
			if (cursor) {
				if (keys.includes(cursor.key)) {
					rows.push(cursor.value);
				}
				cursor.continue();
			} else {
				resolve(rows);
			}
		};
		read.onerror = reject;
	});
	return rows;
}
async function save(schema, data) {
	const db = await database;
	const record = {
		schema,
		data
	};
	const transaction = db.transaction(storeName, 'readwrite');
	const objectStore = transaction.objectStore(storeName);
	let request;
	return new Promise(function saveToDatabase(resolve, reject) {
		try {
			request = objectStore.get(schema);
			request.onsuccess = function(event) {
				const request = objectStore.put(record);
				request.onsuccess = resolve;
			};
		} catch (e) {
			const request = objectStore.add(record);
			request.onsuccess = resolve;
			request.onerror = reject;
		}
	});
}
const __default = {
	save,
	load
};
const renderEvent = new Event('render');
let selectors = [];
function observe(selector) {
	selectors = [
		...selectors,
		selector
	];
	render();
}
function disregard(selector) {
	const index = selectors.indexOf(selector);
	if (index >= 0) {
		selectors = [
			...selectors.slice(0, index),
			...selectors.slice(index + 1)
		];
	}
}
function render(_state) {
	const subscribers = getSubscribers(document);
	dispatchRender(subscribers);
}
function getSubscribers(node) {
	if (selectors.length > 0) return [
		...node.querySelectorAll(selectors.join(', '))
	];
	else return [];
}
function dispatchRender(subscribers) {
	subscribers.map((s)=>s.dispatchEvent(renderEvent)
	);
}
const config = {
	childList: true,
	subtree: true
};
function mutationObserverCallback(mutationsList, observer) {
	const subscriberCollections = [
		...mutationsList
	].map((m)=>getSubscribers(m.target)
	);
	subscriberCollections.forEach(dispatchRender);
}
const observer = new MutationObserver(mutationObserverCallback);
observer.observe(document.body, config);
function listen(type, selector, handler, scope) {
	const callback = (event)=>{
		if (event.target && event.target.matches && event.target.matches(selector)) {
			handler.call(this, event, scope);
		}
	};
	document.addEventListener(type, callback, true);
	return function unlisten() {
		document.removeEventListener(type, callback, true);
	};
}
function on(type, selector, handler) {
	const unbind = listen(type, selector, handler, this);
	if (type === 'render') {
		observe(selector);
	}
	return function unlisten() {
		if (type === 'render') {
			disregard(selector);
		}
		unbind();
	};
}
let lastState = {
};
let subscribers = [
	render
];
const notify = (state)=>{
	lastState = state;
	subscribers.map(function notifySubscriber(notify) {
		notify(state);
	});
};
const store = createStore({
}, notify, __default.save);
const ion = {
	set: store.set,
	get: store.get,
	load: function load(schema) {
		__default.load(schema).then(function restoreFromCache(rows) {
			rows.map(({ schema , data  })=>store.set(schema, data)
			);
		});
	},
	restore: function restore(schema) {
		return __default.load(schema).then(function restoreFromCache(rows) {
			const row = rows.find((x)=>x.schema === schema
			) || {
				data: {
				}
			};
			return row.data;
		});
	},
	relay: function relay(subscriber) {
		subscribers = [
			...subscribers,
			subscriber
		];
		subscriber(lastState);
	}
};
ion.on = on.bind(ion);
let virtualDOM;
const render1 = (target, html)=>{
	if (virtualDOM) {
		virtualDOM(target, html);
	} else {
		target.innerHTML = html;
	}
};
async function html(slug, callback) {
	ion.on('render', slug, (event)=>{
		const { loaded  } = get(slug);
		if (!loaded) return;
		const html = callback(event.target);
		if (html) render1(event.target, html);
	});
	const { innerHTML  } = await import('https://esm.sh/diffhtml');
	virtualDOM = innerHTML;
}
function css(slug, stylesheet) {
	const styles = `
		<style type="text/css" data-tag=${slug}>
			${stylesheet.replace(/&/gi, slug)}
		</style>
	`;
	document.body.insertAdjacentHTML("beforeend", styles);
}
function get(slug) {
	return ion.get(slug) || {
	};
}
function set(slug, payload, middleware) {
	ion.set(slug, payload, middleware);
}
function on1(slug, eventName, selector, callback) {
	ion.on(eventName, `${slug} ${selector}`, callback);
}
function restore(slug, initialState) {
	const promise = ion.restore(slug);
	promise.then((state)=>{
		set(slug, {
			...initialState,
			...state,
			loaded: true
		});
	});
	set(slug, initialState);
	return promise;
}
function tag(slug, initialState = {
}) {
	restore(slug, initialState);
	return {
		css: css.bind(null, slug),
		get: get.bind(null, slug),
		on: on1.bind(null, slug),
		html: html.bind(null, slug),
		restore: restore.bind(null, slug),
		set: set.bind(null, slug),
		slug
	};
}
export { tag as default };

A public/todo-list/vendor/todomvc-app.css => public/todo-list/vendor/todomvc-app.css +379 -0
@@ 0,0 1,379 @@
html,
body {
	margin: 0;
	padding: 0;
}

button {
	margin: 0;
	padding: 0;
	border: 0;
	background: none;
	font-size: 100%;
	vertical-align: baseline;
	font-family: inherit;
	font-weight: inherit;
	color: inherit;
	-webkit-appearance: none;
	appearance: none;
	-webkit-font-smoothing: antialiased;
	-moz-osx-font-smoothing: grayscale;
}

body {
	font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
	line-height: 1.4em;
	background: #f5f5f5;
	color: #4d4d4d;
	min-width: 230px;
	max-width: 550px;
	margin: 0 auto;
	-webkit-font-smoothing: antialiased;
	-moz-osx-font-smoothing: grayscale;
	font-weight: 300;
}

:focus {
	outline: 0;
}

.hidden {
	display: none;
}

.todoapp {
	background: #fff;
	margin: 130px 0 40px 0;
	position: relative;
	box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
	            0 25px 50px 0 rgba(0, 0, 0, 0.1);
}

.todoapp input::-webkit-input-placeholder {
	font-style: italic;
	font-weight: 300;
	color: #e6e6e6;
}

.todoapp input::-moz-placeholder {
	font-style: italic;
	font-weight: 300;
	color: #e6e6e6;
}

.todoapp input::input-placeholder {
	font-style: italic;
	font-weight: 300;
	color: #e6e6e6;
}

.todoapp h1 {
	position: absolute;
	top: -155px;
	width: 100%;
	font-size: 100px;
	font-weight: 100;
	text-align: center;
	color: rgba(175, 47, 47, 0.15);
	-webkit-text-rendering: optimizeLegibility;
	-moz-text-rendering: optimizeLegibility;
	text-rendering: optimizeLegibility;
}

.new-todo,
.edit {
	position: relative;
	margin: 0;
	width: 100%;
	font-size: 24px;
	font-family: inherit;
	font-weight: inherit;
	line-height: 1.4em;
	border: 0;
	color: inherit;
	padding: 6px;
	border: 1px solid #999;
	box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
	box-sizing: border-box;
	-webkit-font-smoothing: antialiased;
	-moz-osx-font-smoothing: grayscale;
}

.new-todo {
	padding: 16px 16px 16px 60px;
	border: none;
	background: rgba(0, 0, 0, 0.003);
	box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
}

.main {
	position: relative;
	z-index: 2;
	border-top: 1px solid #e6e6e6;
}

.toggle-all {
	width: 1px;
	height: 1px;
	border: none; /* Mobile Safari */
	opacity: 0;
	position: absolute;
	right: 100%;
	bottom: 100%;
}

.toggle-all + label {
	width: 60px;
	height: 34px;
	font-size: 0;
	position: absolute;
	top: -52px;
	left: -13px;
	-webkit-transform: rotate(90deg);
	transform: rotate(90deg);
}

.toggle-all + label:before {
	content: '❯';
	font-size: 22px;
	color: #e6e6e6;
	padding: 10px 27px 10px 27px;
}

.toggle-all:checked + label:before {
	color: #737373;
}

.todo-list {
	margin: 0;
	padding: 0;
	list-style: none;
}

.todo-list li {
	position: relative;
	font-size: 24px;
	border-bottom: 1px solid #ededed;
}

.todo-list li:last-child {
	border-bottom: none;
}

.todo-list li.editing {
	border-bottom: none;
	padding: 0;
}

.todo-list li.editing .edit {
	display: block;
	width: 506px;
	padding: 12px 16px;
	margin: 0 0 0 43px;
}

.todo-list li.editing .view {
	display: none;
}

.todo-list li .toggle {
	text-align: center;
	width: 40px;
	/* auto, since non-WebKit browsers doesn't support input styling */
	height: auto;
	position: absolute;
	top: 0;
	bottom: 0;
	margin: auto 0;
	border: none; /* Mobile Safari */
	-webkit-appearance: none;
	appearance: none;
}

.todo-list li .toggle {
	opacity: 0;
}

.todo-list li .toggle + label {
	/*
		Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
		IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
	*/
	background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
	background-repeat: no-repeat;
	background-position: center left;
}

.todo-list li .toggle:checked + label {
	background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
}

.todo-list li label {
	word-break: break-all;
	padding: 15px 15px 15px 60px;
	display: block;
	line-height: 1.2;
	transition: color 0.4s;
}

.todo-list li.completed label {
	color: #d9d9d9;
	text-decoration: line-through;
}

.todo-list li .destroy {
	display: none;
	position: absolute;
	top: 0;
	right: 10px;
	bottom: 0;
	width: 40px;
	height: 40px;
	margin: auto 0;
	font-size: 30px;
	color: #cc9a9a;
	margin-bottom: 11px;
	transition: color 0.2s ease-out;
}

.todo-list li .destroy:hover {
	color: #af5b5e;
}

.todo-list li .destroy:after {
	content: '×';
}

.todo-list li:hover .destroy {
	display: block;
}

.todo-list li .edit {
	display: none;
}

.todo-list li.editing:last-child {
	margin-bottom: -1px;
}

.footer {
	color: #777;
	padding: 10px 15px;
	height: 20px;
	text-align: center;
	border-top: 1px solid #e6e6e6;
}

.footer:before {
	content: '';
	position: absolute;
	right: 0;
	bottom: 0;
	left: 0;
	height: 50px;
	overflow: hidden;
	box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
	            0 8px 0 -3px #f6f6f6,
	            0 9px 1px -3px rgba(0, 0, 0, 0.2),
	            0 16px 0 -6px #f6f6f6,
	            0 17px 2px -6px rgba(0, 0, 0, 0.2);
}

.todo-count {
	float: left;
	text-align: left;
}

.todo-count strong {
	font-weight: 300;
}

.filters {
	margin: 0;
	padding: 0;
	list-style: none;
	position: absolute;
	right: 0;
	left: 0;
}

.filters li {
	display: inline;
}

.filters li a {
	color: inherit;
	margin: 3px;
	padding: 3px 7px;
	text-decoration: none;
	border: 1px solid transparent;
	border-radius: 3px;
}

.filters li a:hover {
	border-color: rgba(175, 47, 47, 0.1);
}

.filters li a.selected {
	border-color: rgba(175, 47, 47, 0.2);
}

.clear-completed,
html .clear-completed:active {
	float: right;
	position: relative;
	line-height: 20px;
	text-decoration: none;
	cursor: pointer;
}

.clear-completed:hover {
	text-decoration: underline;
}

.info {
	margin: 65px auto 0;
	color: #bfbfbf;
	font-size: 10px;
	text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
	text-align: center;
}

.info p {
	line-height: 1;
}

.info a {
	color: inherit;
	text-decoration: none;
	font-weight: 400;
}

.info a:hover {
	text-decoration: underline;
}

/*
	Hack to remove background from Mobile Safari.
	Can't use it globally since it destroys checkboxes in Firefox
*/
@media screen and (-webkit-min-device-pixel-ratio:0) {
	.toggle-all,
	.todo-list li .toggle {
		background: none;
	}

	.todo-list li .toggle {
		height: 40px;
	}
}

@media (max-width: 430px) {
	.footer {
		height: 50px;
	}

	.filters {
		bottom: 10px;
	}
}

A public/todo-list/vendor/todomvc-common-base.css => public/todo-list/vendor/todomvc-common-base.css +141 -0
@@ 0,0 1,141 @@
hr {
	margin: 20px 0;
	border: 0;
	border-top: 1px dashed #c5c5c5;
	border-bottom: 1px dashed #f7f7f7;
}

.learn a {
	font-weight: normal;
	text-decoration: none;
	color: #b83f45;
}

.learn a:hover {
	text-decoration: underline;
	color: #787e7e;
}

.learn h3,
.learn h4,
.learn h5 {
	margin: 10px 0;
	font-weight: 500;
	line-height: 1.2;
	color: #000;
}

.learn h3 {
	font-size: 24px;
}

.learn h4 {
	font-size: 18px;
}

.learn h5 {
	margin-bottom: 0;
	font-size: 14px;
}

.learn ul {
	padding: 0;
	margin: 0 0 30px 25px;
}

.learn li {
	line-height: 20px;
}

.learn p {
	font-size: 15px;
	font-weight: 300;
	line-height: 1.3;
	margin-top: 0;
	margin-bottom: 0;
}

#issue-count {
	display: none;
}

.quote {
	border: none;
	margin: 20px 0 60px 0;
}

.quote p {
	font-style: italic;
}

.quote p:before {
	content: '“';
	font-size: 50px;
	opacity: .15;
	position: absolute;
	top: -20px;
	left: 3px;
}

.quote p:after {
	content: '”';
	font-size: 50px;
	opacity: .15;
	position: absolute;
	bottom: -42px;
	right: 3px;
}

.quote footer {
	position: absolute;
	bottom: -40px;
	right: 0;
}

.quote footer img {
	border-radius: 3px;
}

.quote footer a {
	margin-left: 5px;
	vertical-align: middle;
}

.speech-bubble {
	position: relative;
	padding: 10px;
	background: rgba(0, 0, 0, .04);
	border-radius: 5px;
}

.speech-bubble:after {
	content: '';
	position: absolute;
	top: 100%;
	right: 30px;
	border: 13px solid transparent;
	border-top-color: rgba(0, 0, 0, .04);
}

.learn-bar > .learn {
	position: absolute;
	width: 272px;
	top: 8px;
	left: -300px;
	padding: 10px;
	border-radius: 5px;
	background-color: rgba(255, 255, 255, .6);
	transition-property: left;
	transition-duration: 500ms;
}

@media (min-width: 899px) {
	.learn-bar {
		width: auto;
		padding-left: 300px;
	}

	.learn-bar > .learn {
		left: 8px;
	}
}