diff --git a/.babelrc b/.babelrc
new file mode 100644
index 00000000..d9bc3196
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1,23 @@
+{
+ "env": {
+ "test": {
+ "presets": [
+ ["@babel/env", {
+ "targets": {
+ "node": 8
+ }
+ }]
+ ]
+ },
+ "development": {
+ "presets": [
+ ["@babel/env", {
+ "modules": false
+ }]
+ ]
+ }
+ },
+ "plugins": [
+ "@babel/plugin-proposal-export-default-from"
+ ]
+}
diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 00000000..1ec68eeb
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,16 @@
+{
+ "env": {
+ "browser": true,
+ "es6": true
+ },
+ "parserOptions": {
+ "ecmaVersion": 6,
+ "sourceType": "module",
+ "ecmaFeatures": {
+ "jsx": true
+ }
+ },
+ "rules": {
+ "semi": 2
+ }
+}
diff --git a/.jsdoc/.bin/contributors.js b/.jsdoc/.bin/contributors.js
new file mode 100755
index 00000000..5f6ae085
--- /dev/null
+++ b/.jsdoc/.bin/contributors.js
@@ -0,0 +1,55 @@
+#!/usr/bin/env node
+
+const fs = require('fs');
+const path = require('path');
+const https = require('https');
+const { exec } = require('child_process');
+const readline = require('readline').createInterface({
+ input: process.stdin,
+ output: process.stdout
+});
+
+const options = {
+ port: 443,
+ method: 'GET',
+ hostname: 'api.github.com',
+ path: '/repos/objectivehtml/FlipClock/contributors',
+ headers: {
+ 'User-Agent': 'node'
+ }
+};
+
+const req = https.request(options, (res) => {
+ let body = '';
+
+ res.on('data', chunk => body += chunk);
+ res.on('end', () => {
+ let html = '
Contributors
';
+
+ const contributors = JSON.parse(body);
+
+ contributors.forEach(contributor => {
+ html += [
+ `\n`,
+ ``,
+ `
`,
+ ` `,
+ `
${contributor.login}
`,
+ `
(${contributor.contributions} contribution${contributor.contributions !== 1 ? 's' : ''})
`,
+ `
`,
+ ``
+ ].join('\n');
+ });
+
+ fs.writeFileSync(path.join(__dirname, '../../docs/contributors.html'), html);
+
+ process.exit(0);
+ });
+
+});
+
+req.on('error', (error) => {
+ console.error(error)
+});
+
+req.end();
\ No newline at end of file
diff --git a/.jsdoc/flipclock/README.md b/.jsdoc/flipclock/README.md
new file mode 100755
index 00000000..1946bef5
--- /dev/null
+++ b/.jsdoc/flipclock/README.md
@@ -0,0 +1,12 @@
+The default template for JSDoc 3 uses: [the Taffy Database library](http://taffydb.com/) and the [Underscore Template library](http://underscorejs.org/).
+
+
+## Generating Typeface Fonts
+
+The default template uses the [OpenSans](https://www.google.com/fonts/specimen/Open+Sans) typeface. The font files can be regenerated as follows:
+
+1. Open the [OpenSans page at Font Squirrel]().
+2. Click on the 'Webfont Kit' tab.
+3. Either leave the subset drop-down as 'Western Latin (Default)', or, if we decide we need more glyphs, than change it to 'No Subsetting'.
+4. Click the 'DOWNLOAD @FONT-FACE KIT' button.
+5. For each typeface variant we plan to use, copy the 'eot', 'svg' and 'woff' files into the 'templates/default/static/fonts' directory.
diff --git a/.jsdoc/flipclock/publish.js b/.jsdoc/flipclock/publish.js
new file mode 100755
index 00000000..ce09b341
--- /dev/null
+++ b/.jsdoc/flipclock/publish.js
@@ -0,0 +1,807 @@
+const doop = require('jsdoc/util/doop');
+const env = require('jsdoc/env');
+const fs = require('jsdoc/fs');
+const helper = require('jsdoc/util/templateHelper');
+const logger = require('jsdoc/util/logger');
+const path = require('jsdoc/path');
+const stripBom = require('jsdoc/util/stripbom');
+const taffy = require('taffydb').taffy;
+const template = require('jsdoc/template');
+const util = require('util');
+const _ = require('underscore');
+
+const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
+
+const htmlsafe = helper.htmlsafe;
+const linkto = helper.linkto;
+const resolveAuthorLinks = helper.resolveAuthorLinks;
+const hasOwnProp = Object.prototype.hasOwnProperty;
+
+let data;
+let view;
+
+let outdir = path.normalize(env.opts.destination);
+
+function find(spec) {
+ return helper.find(data, spec);
+}
+
+function tutoriallink(tutorial) {
+ return helper.toTutorial(tutorial, null, {
+ tag: 'em',
+ classname: 'disabled',
+ prefix: 'Tutorial: '
+ });
+}
+
+function getAncestorLinks(doclet) {
+ return helper.getAncestorLinks(data, doclet);
+}
+
+function hashToLink(doclet, hash) {
+ let url;
+
+ if ( !/^(#.+)/.test(hash) ) {
+ return hash;
+ }
+
+ url = helper.createLink(doclet);
+ url = url.replace(/(#.+|$)/, hash);
+
+ return `${hash}`;
+}
+
+function needsSignature({kind, type, meta}) {
+ let needsSig = false;
+
+ // function and class definitions always get a signature
+ if (kind === 'function' || kind === 'class') {
+ needsSig = true;
+ }
+ // typedefs that contain functions get a signature, too
+ else if (kind === 'typedef' && type && type.names &&
+ type.names.length) {
+ for (let i = 0, l = type.names.length; i < l; i++) {
+ if (type.names[i].toLowerCase() === 'function') {
+ needsSig = true;
+ break;
+ }
+ }
+ }
+ // and namespaces that are functions get a signature (but finding them is a
+ // bit messy)
+ else if (kind === 'namespace' && meta && meta.code &&
+ meta.code.type && meta.code.type.match(/[Ff]unction/)) {
+ needsSig = true;
+ }
+
+ return needsSig;
+}
+
+function getSignatureAttributes({optional, nullable}) {
+ const attributes = [];
+
+ if (optional) {
+ attributes.push('opt');
+ }
+
+ if (nullable === true) {
+ attributes.push('nullable');
+ }
+ else if (nullable === false) {
+ attributes.push('non-null');
+ }
+
+ return attributes;
+}
+
+function updateItemName(item) {
+ const attributes = getSignatureAttributes(item);
+ let itemName = item.name || '';
+
+ if (item.variable) {
+ itemName = `…${itemName}`;
+ }
+
+ if (attributes && attributes.length) {
+ itemName = util.format( '%s%s', itemName,
+ attributes.join(', ') );
+ }
+
+ return itemName;
+}
+
+function addParamAttributes(params) {
+ return params.filter(({name}) => name && !name.includes('.')).map(updateItemName);
+}
+
+function buildItemTypeStrings(item) {
+ const types = [];
+
+ if (item && item.type && item.type.names) {
+ item.type.names.forEach(name => {
+ types.push( linkto(name, htmlsafe(name)) );
+ });
+ }
+
+ return types;
+}
+
+function buildAttribsString(attribs) {
+ let attribsString = '';
+
+ if (attribs && attribs.length) {
+ attribsString = htmlsafe( util.format('(%s) ', attribs.join(', ')) );
+ }
+
+ return attribsString;
+}
+
+function addNonParamAttributes(items) {
+ let types = [];
+
+ items.forEach(item => {
+ types = types.concat( buildItemTypeStrings(item) );
+ });
+
+ return types;
+}
+
+function addSignatureParams(f) {
+ const params = f.params ? addParamAttributes(f.params) : [];
+
+ f.signature = util.format( '%s(%s)', (f.signature || ''), params.join(', ') );
+}
+
+function addSignatureReturns(f) {
+ const attribs = [];
+ let attribsString = '';
+ let returnTypes = [];
+ let returnTypesString = '';
+ const source = f.yields || f.returns;
+
+ // jam all the return-type attributes into an array. this could create odd results (for example,
+ // if there are both nullable and non-nullable return types), but let's assume that most people
+ // who use multiple @return tags aren't using Closure Compiler type annotations, and vice-versa.
+ if (source) {
+ source.forEach(item => {
+ helper.getAttribs(item).forEach(attrib => {
+ if (!attribs.includes(attrib)) {
+ attribs.push(attrib);
+ }
+ });
+ });
+
+ attribsString = buildAttribsString(attribs);
+ }
+
+ if (source) {
+ returnTypes = addNonParamAttributes(source);
+ }
+ if (returnTypes.length) {
+ returnTypesString = util.format( ' → %s{%s}', attribsString, returnTypes.join('|') );
+ }
+
+ f.signature = `${f.signature || ''}${returnTypesString}`;
+}
+
+function addSignatureTypes(f) {
+ const types = f.type ? buildItemTypeStrings(f) : [];
+
+ f.signature = `${f.signature || ''}${types.length ? ` :${types.join('|')}` : ''}`;
+}
+
+function addAttribs(f) {
+ const attribs = helper.getAttribs(f);
+ const attribsString = buildAttribsString(attribs);
+
+ f.attribs = util.format('%s', attribsString);
+}
+
+function shortenPaths(files, commonPrefix) {
+ Object.keys(files).forEach(file => {
+ files[file].shortened = files[file].resolved.replace(commonPrefix, '')
+ // always use forward slashes
+ .replace(/\\/g, '/');
+ });
+
+ return files;
+}
+
+function getPathFromDoclet({meta}) {
+ if (!meta) {
+ return null;
+ }
+
+ return meta.path && meta.path !== 'null' ?
+ path.join(meta.path, meta.filename) :
+ meta.filename;
+}
+
+function generate(title, docs, filename, resolveLinks) {
+ let docData;
+ let html;
+ let outpath;
+
+ resolveLinks = resolveLinks !== false;
+
+ docData = {
+ env: env,
+ pkg: pkg,
+ title: title,
+ docs: docs
+ };
+
+ outpath = path.join(outdir, filename);
+ html = view.render('container.tmpl', docData);
+
+ if (resolveLinks) {
+ html = helper.resolveLinks(html); // turn {@link foo} into foo
+ }
+
+ fs.writeFileSync(outpath, html, 'utf8');
+}
+
+function generateSourceFiles(sourceFiles, encoding = 'utf8') {
+ Object.keys(sourceFiles).forEach(file => {
+ let source;
+ // links are keyed to the shortened path in each doclet's `meta.shortpath` property
+ const sourceOutfile = helper.getUniqueFilename(sourceFiles[file].shortened);
+
+ helper.registerLink(sourceFiles[file].shortened, sourceOutfile);
+
+ try {
+ source = {
+ kind: 'source',
+ code: helper.htmlsafe( fs.readFileSync(sourceFiles[file].resolved, encoding) )
+ };
+ }
+ catch (e) {
+ logger.error('Error while generating source file %s: %s', file, e.message);
+ }
+
+ generate(`Source: ${sourceFiles[file].shortened}`, [source], sourceOutfile, false);
+ });
+}
+
+/**
+ * Look for classes or functions with the same name as modules (which indicates that the module
+ * exports only that class or function), then attach the classes or functions to the `module`
+ * property of the appropriate module doclets. The name of each class or function is also updated
+ * for display purposes. This function mutates the original arrays.
+ *
+ * @private
+ * @param {Array.} doclets - The array of classes and functions to
+ * check.
+ * @param {Array.} modules - The array of module doclets to search.
+ */
+function attachModuleSymbols(doclets, modules) {
+ const symbols = {};
+
+ // build a lookup table
+ doclets.forEach(symbol => {
+ symbols[symbol.longname] = symbols[symbol.longname] || [];
+ symbols[symbol.longname].push(symbol);
+ });
+
+ modules.forEach(module => {
+ if (symbols[module.longname]) {
+ module.modules = symbols[module.longname]
+ // Only show symbols that have a description. Make an exception for classes, because
+ // we want to show the constructor-signature heading no matter what.
+ .filter(({description, kind}) => description || kind === 'class')
+ .map(symbol => {
+ symbol = doop(symbol);
+
+ if (symbol.kind === 'class' || symbol.kind === 'function') {
+ symbol.name = `${symbol.name.replace('module:', '(require("')}"))`;
+ }
+
+ return symbol;
+ });
+ }
+ });
+}
+
+function buildMemberNav(items, itemHeading, itemsSeen, linktoFn, itemsOnly = false) {
+ let nav = '';
+
+ if (items.length) {
+ let itemsNav = '';
+
+ items.forEach(item => {
+ let displayName;
+
+ if ( !hasOwnProp.call(item, 'longname') ) {
+ itemsNav += `${linktoFn('', item.name)}`;
+ }
+ else if ( !hasOwnProp.call(itemsSeen, item.longname) ) {
+ if (env.conf.templates.default.useLongnameInNav) {
+ displayName = item.longname;
+ } else {
+ displayName = item.name;
+ }
+ itemsNav += `${linktoFn(item.longname, displayName.replace(/\b(module|event):/g, ''))}`;
+
+ itemsSeen[item.longname] = true;
+ }
+ });
+
+ if (itemsOnly) {
+ return itemsNav;
+ }
+
+ if (itemsNav !== '') {
+ if(itemHeading) {
+ nav += `${itemHeading}
`;
+ }
+
+ nav += ``;
+ }
+ }
+
+ return nav;
+}
+
+function linktoTutorial(longName, name) {
+ return tutoriallink(name);
+}
+
+function linktoExternal(longName, name) {
+ return linkto(longName, name.replace(/(^"|"$)/g, ''));
+}
+
+/**
+ * Create the navigation sidebar.
+ * @param {object} members The members that will be used to create the sidebar.
+ * @param {array