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;
+ }
+}