Browse Source

First commit of a mostly working blog. This most likely has way too much.

master
Zed A. Shaw 2 weeks ago
parent
commit
7eee558995
100 changed files with 16478 additions and 0 deletions
  1. +4
    -0
      cypress.json
  2. +5
    -0
      cypress/fixtures/example.json
  3. +19
    -0
      cypress/integration/spec.js
  4. +17
    -0
      cypress/plugins/index.js
  5. +25
    -0
      cypress/support/commands.js
  6. +20
    -0
      cypress/support/index.js
  7. +35
    -0
      gulpfile.js
  8. +176
    -0
      lib/builderator.js
  9. +52
    -0
      lib/buildrules.js
  10. +78
    -0
      lib/docs.js
  11. +11
    -0
      lib/logging.js
  12. +39
    -0
      lib/mail.js
  13. +48
    -0
      lib/rr.js
  14. +9827
    -0
      package-lock.json
  15. +63
    -0
      package.json
  16. BIN
      public/android-chrome-192x192.png
  17. BIN
      public/android-chrome-256x256.png
  18. BIN
      public/apple-touch-icon.png
  19. +9
    -0
      public/browserconfig.xml
  20. BIN
      public/favicon-16x16.png
  21. BIN
      public/favicon-32x32.png
  22. BIN
      public/favicon.ico
  23. BIN
      public/mstile-150x150.png
  24. +19
    -0
      public/safari-pinned-tab.svg
  25. +0
    -0
      public/test.txt
  26. +106
    -0
      rollup.config.js
  27. +38
    -0
      sass/_accordions.scss
  28. +20
    -0
      sass/_animations.scss
  29. +47
    -0
      sass/_autocomplete.scss
  30. +77
    -0
      sass/_avatars.scss
  31. +60
    -0
      sass/_badges.scss
  32. +71
    -0
      sass/_bars.scss
  33. +40
    -0
      sass/_base.scss
  34. +29
    -0
      sass/_breadcrumbs.scss
  35. +198
    -0
      sass/_buttons.scss
  36. +222
    -0
      sass/_calendars.scss
  37. +43
    -0
      sass/_cards.scss
  38. +136
    -0
      sass/_carousels.scss
  39. +33
    -0
      sass/_chips.scss
  40. +31
    -0
      sass/_codes.scss
  41. +115
    -0
      sass/_comparison-sliders.scss
  42. +36
    -0
      sass/_dropdowns.scss
  43. +21
    -0
      sass/_empty.scss
  44. +37
    -0
      sass/_filters.scss
  45. +557
    -0
      sass/_forms.scss
  46. +22
    -0
      sass/_hero.scss
  47. +5
    -0
      sass/_icons.scss
  48. +34
    -0
      sass/_labels.scss
  49. +503
    -0
      sass/_layout.scss
  50. +75
    -0
      sass/_media.scss
  51. +66
    -0
      sass/_menus.scss
  52. +57
    -0
      sass/_meters.scss
  53. +10
    -0
      sass/_mixins.scss
  54. +87
    -0
      sass/_modals.scss
  55. +36
    -0
      sass/_navbar.scss
  56. +48
    -0
      sass/_navs.scss
  57. +446
    -0
      sass/_normalize.scss
  58. +104
    -0
      sass/_off-canvas.scss
  59. +60
    -0
      sass/_pagination.scss
  60. +23
    -0
      sass/_panels.scss
  61. +135
    -0
      sass/_parallax.scss
  62. +65
    -0
      sass/_popovers.scss
  63. +45
    -0
      sass/_progress.scss
  64. +99
    -0
      sass/_sliders.scss
  65. +71
    -0
      sass/_steps.scss
  66. +57
    -0
      sass/_tables.scss
  67. +66
    -0
      sass/_tabs.scss
  68. +38
    -0
      sass/_tiles.scss
  69. +56
    -0
      sass/_timelines.scss
  70. +48
    -0
      sass/_toasts.scss
  71. +79
    -0
      sass/_tooltips.scss
  72. +171
    -0
      sass/_typography.scss
  73. +8
    -0
      sass/_utilities.scss
  74. +123
    -0
      sass/_variables.scss
  75. +34
    -0
      sass/_viewer-360.scss
  76. +315
    -0
      sass/icons/_icons-action.scss
  77. +54
    -0
      sass/icons/_icons-core.scss
  78. +127
    -0
      sass/icons/_icons-navigation.scss
  79. +161
    -0
      sass/icons/_icons-object.scss
  80. +6
    -0
      sass/mixins/_avatar.scss
  81. +54
    -0
      sass/mixins/_button.scss
  82. +8
    -0
      sass/mixins/_clearfix.scss
  83. +27
    -0
      sass/mixins/_color.scss
  84. +11
    -0
      sass/mixins/_label.scss
  85. +65
    -0
      sass/mixins/_position.scss
  86. +9
    -0
      sass/mixins/_shadow.scss
  87. +6
    -0
      sass/mixins/_text.scss
  88. +5
    -0
      sass/mixins/_toast.scss
  89. +206
    -0
      sass/pattern.scss
  90. +18
    -0
      sass/spectre-exp.scss
  91. +10
    -0
      sass/spectre-icons.scss
  92. +48
    -0
      sass/spectre.scss
  93. +35
    -0
      sass/utilities/_colors.scss
  94. +24
    -0
      sass/utilities/_cursors.scss
  95. +44
    -0
      sass/utilities/_display.scss
  96. +50
    -0
      sass/utilities/_divider.scss
  97. +34
    -0
      sass/utilities/_loading.scss
  98. +54
    -0
      sass/utilities/_position.scss
  99. +8
    -0
      sass/utilities/_shapes.scss
  100. +64
    -0
      sass/utilities/_text.scss

+ 4
- 0
cypress.json View File

@@ -0,0 +1,4 @@
{
"baseUrl": "http://localhost:3000",
"video": false
}

+ 5
- 0
cypress/fixtures/example.json View File

@@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

+ 19
- 0
cypress/integration/spec.js View File

@@ -0,0 +1,19 @@
describe('Sapper template app', () => {
beforeEach(() => {
cy.visit('/')
});

it('has the correct <h1>', () => {
cy.contains('h1', 'Great success!')
});

it('navigates to /about', () => {
cy.get('nav a').contains('about').click();
cy.url().should('include', '/about');
});

it('navigates to /blog', () => {
cy.get('nav a').contains('blog').click();
cy.url().should('include', '/blog');
});
});

+ 17
- 0
cypress/plugins/index.js View File

@@ -0,0 +1,17 @@
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************

// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)

module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
}

+ 25
- 0
cypress/support/commands.js View File

@@ -0,0 +1,25 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This is will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })

+ 20
- 0
cypress/support/index.js View File

@@ -0,0 +1,20 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************

// Import commands.js using ES2015 syntax:
import './commands'

// Alternatively you can use CommonJS syntax:
// require('./commands')

+ 35
- 0
gulpfile.js View File

@@ -0,0 +1,35 @@
const gulp = require('gulp');
const autoprefixer = require('gulp-autoprefixer');
const cleancss = require('gulp-clean-css');
const csscomb = require('gulp-csscomb');
const rename = require('gulp-rename');
const sass = require('gulp-sass');

let paths = {
source: './sass/*.scss',
};

const styles = () => {
return gulp.src(paths.source)
.pipe(sass({outputStyle: 'compact', precision: 10})
.on('error', sass.logError)
)
.pipe(autoprefixer())
.pipe(csscomb())
.pipe(cleancss())
.pipe(rename({
suffix: '.min'
}))
.pipe(gulp.dest('./static/css/'));
};


const auto = () => {
return gulp.watch(['./**/*.scss'], styles);
}

module.exports = {
styles,
default: auto
};


+ 176
- 0
lib/builderator.js View File

@@ -0,0 +1,176 @@
const glob = require('fast-glob').sync;
const { Rools, Rule } = require('rools');
const _ = require('lodash');
const fs = require('fs');
const assert = require('assert');
const { log } = require('./logging');
const Path = require('path');
const execSync = require('child_process').execSync;


const source = (base, pattern) => {
let found = glob(`${base}${pattern}`).sort();

return found.map(f => {
let fdata = Path.parse(f);
fdata.base = base;
fdata.pattern = pattern;
fdata.stat = fs.statSync(f);
fdata.path = f;
fdata.tail = f.substring(base.length);
fdata.stem = Path.basename(fdata.tail, fdata.ext);
fdata.path_no_ext = `${fdata.dir}/${fdata.stem}`;
fdata.tail_no_ext = fdata.tail.substring(0, fdata.tail.length - fdata.ext.length);
return fdata;
});
}

const with_extension = (a,b) => {
if(a.tail === b.tail) {
a.other = b;
b.other = a;
}
return a.tail === b.tail;
}

const without_extension = (a, b) => {
if(a.tail_no_ext === b.tail_no_ext) {
a.other = b;
b.other = a;
// if used to set the above, might want to do it different?
return true;
} else {
return false;
}
}

const delta = (src, dest, cmp=with_extension) => {
assert(cmp != undefined, "cmp function cannot be undefined");
let result = {
params: {src, dest, cmp},
files: dest,
common: _.intersectionWith(src, dest, cmp),
missing: _.differenceWith(src, dest, cmp),
}

// we only care about common files that have old mtimes (and maybe mtime isn't right?)
result.changed = result.common.filter(f => {
assert(f.tail_no_ext == f.other.tail_no_ext, `Tails don't match but should ${f.tail_no_ext} !== ${f.other.tail_no_ext}`);

return f.stat.mtime > f.other.stat.mtime
});

return result;
}

const from_to = (src_base, src_pattern, dest_base, dest_pattern, cmp=without_extension) =>
{
assert(cmp != undefined, "cmp function cannot be undefined");
assert(src_base.endsWith('/'), `src_base must end in / you have ${src_base}`);
assert(dest_base.endsWith('/'), `dest_base must end in / you have ${dest_base}`);

let src = source(src_base, src_pattern);
let dest = source(dest_base, dest_pattern);

return delta(src, dest, cmp);
}

const changed = (delta_files) => {
return delta_files.missing.length > 0 || delta_files.changed.length > 0;
}


const rule = (name, actions) => {
assert(name, "Name is required.");
assert(actions.when, `${name} does not have a when`);
assert(actions.then, `${name} does not have a then`);

actions.name = name;

let r = new Rule(actions);
return r;
}

const run = async (facts, target_rules) => {
const rools = new Rools({ logging: { error: true, debug: false}});
await rools.register(target_rules);
let result = await rools.evaluate(facts);
}

const mkdir = (dir) => {
/* Makes a directory recursively. */
if(!fs.existsSync(dir)) {
log.debug("making dir", dir);
fs.mkdirSync(dir, { recursive: true });
}
}

const mkdir_to = (target) => {
/* Given any path to a file, makes sure all its directories are in place. */
let dirs = Path.parse(target);
if(!fs.existsSync(dirs.dir)) {
log.debug("making dir", dirs.dir);
fs.mkdirSync(dirs.dir, { recursive: true });
}
}

const write = (target, data) => {
/* Convenience function that makes sure the dir is there then writes the data. */
mkdir_to(target);
fs.writeFileSync(target, data);
}

const copy = (src, dest, filter=null) => {
/** Like write but does an OS copy after making the target dir. When given a
* filter it will give the filter function the arguments, let it return contents,
* then write those instead of a copy.
*/
mkdir_to(src);

if(filter) {
try {
let raw_data = fs.readFileSync(src);
let data = filter(src, dest, raw_data);
write(dest, data);
} catch (error) {
log.error(error, "Problem with filter write in copy");
}
} else {
fs.copyFileSync(src, dest);
}
}

const rm = (file_name, ignore=false) => {
log.warn("Deleting file", file_name);
try {
fs.unlinkSync(file_name);
} catch(error) {
if(ignore) {
log.debug(`Requested delete file ${file_name} doesn't exist, but ignored.`);
} else {
log.error(error, filename);
throw error;
}
}
}

const index_rollup = (src_index, target_index, contents, key) => {
let index = JSON.parse(fs.readFileSync(src_index));

index[key] = glob(contents).map(f => JSON.parse(fs.readFileSync(f)) );

write(target_index, JSON.stringify(index));

return index;
}

const exec = (cmd, opts) => {
log.info("EXEC", cmd, opts || '');
return execSync(cmd, opts);
}


module.exports = {
rule, run, delta, without_extension, index_rollup, rm, exec,
with_extension, source, changed, from_to, mkdir, mkdir_to, write, copy
}

+ 52
- 0
lib/buildrules.js View File

@@ -0,0 +1,52 @@
const build = require('./builderator');
const { log } = require('./logging');
const _ = require('lodash');
const docs = require('./docs');

const markdown = (key, base_url_filter) => {
return {
when: f => build.changed(f[key]),
then: f => {
let targets = _.concat(f[key].changed, f[key].missing);
log.debug("Running markdown to html for", key, "with", targets.length, "target count");

for(let t of targets) {
log.debug(`Convert ${t.path} to ${t.other ? t.other.path : "COPY"} with tail ${t.tail_no_ext}`);
let base_url = base_url_filter(t);
let { toc, content, metadata } = docs.load(t.path, base_url);
metadata.slug = t.stem;
build.write(`${f.TARGET}/media/${key}/${t.tail_no_ext}.html`, content);
build.write(
`${f.TARGET}/api/${key}/${t.tail_no_ext}.json`,
JSON.stringify( { toc, metadata} ));
}

f[key].updated = true;
}
}
}

const copy = (key, filter) => {
return {
when: f => build.changed(f[key]),
then: f => {
let targets = _.concat(f[key].changed, f[key].missing).filter(f => !f.path.includes('proto'));
for(let t of targets) {
log.debug(`Convert ${t.path} to ${t.other ? t.other.path : "COPY"} with tail ${t.tail_no_ext}`);
let dest = `${f.TARGET}/api/${t.tail}`;
if(filter) {
build.copy(t.path, dest, (src, dest, data) => filter(t, data));
} else {
build.copy(t.path, dest);
}
}

f[key].updated = true;
}
}
}


module.exports = {
markdown, copy
}

+ 78
- 0
lib/docs.js View File

@@ -0,0 +1,78 @@
const { Remarkable } = require('remarkable');
const { linkify } = require('remarkable/linkify');
const Prism = require('prismjs');
const urlSlug = require('url-slug');
const { log } = require('./logging');
const fs = require('fs');

const highlight = (str, lang) => {
// TODO: Prism mentions if you try to include languages here then all of them get included
// TODO: Prism recommends loading languages in the rollup stage
if(lang in Prism.languages) {
return Prism.highlight(str, Prism.languages[lang], lang);
} else {
console.log(`!!!!!!!!!!!!!!! Language ${lang} not found in Prism.`);
return '';
}
}

/* A separate renderer just for the titles that doesn't need anything else. */
const title_render = new Remarkable('full').use(rem => {
let pass = (tokens, idx) => '';
rem.renderer.rules.paragraph_open = pass;
rem.renderer.rules.paragraph_close = pass;
});

const split = (raw_md) => {
let [metadata, ...body] = raw_md.split('------');
metadata = JSON.parse(metadata);
body = body.join('------');
return [metadata, body];
}

const render = (raw_md, base_url) => {
let toc = [];
let [metadata, body] = split(raw_md);

const renderer = new Remarkable('full', {
highlight
}).use(linkify).use(rem => {
rem.renderer.rules.heading_open = (tokens, idx) => {
let level = tokens[idx].hLevel;
let content = tokens[idx + 1].content;
let slug = urlSlug(content);
toc.push({level, content, slug});
return `<h${level} id="${slug}"><a href="${base_url}#${slug}">`;
}

rem.renderer.rules.heading_close = (tokens, idx) => {
return `</a></h${tokens[idx].hLevel}>\n`;
}
});

let content = renderer.render(body);

// now we can use the TOC to figure out a title
metadata.title = toc[0].content;

try {
// finally, run the renderer on all of the TOC
toc.forEach(t => t.html = title_render.render(t.content));
} catch(error) {
log.error(error);
}

return {toc, content, metadata};
}

const load = (path, base_url) => {
log.debug("Rendering ", path);
let raw_md = fs.readFileSync(path);
return render(raw_md.toString(), base_url);
}

module.exports = {
render,
load,
split
}

+ 11
- 0
lib/logging.js View File

@@ -0,0 +1,11 @@
const pino = require('pino');


exports.log = pino({
level: 'debug',
prettyPrint: {
levelFirst: true,
colorize: true
},
prettifier: require('pino-pretty')
});

+ 39
- 0
lib/mail.js View File

@@ -0,0 +1,39 @@
const nodemailer = require('nodemailer');
const secrets = require('../lib/secrets');
const assert = require('assert');
const fs = require('fs');
const _ = require('lodash');

exports.dev_transporter = nodemailer.createTransport({
streamTransport: true
});

exports.prod_transporter = nodemailer.createTransport({
secure: false,
ignoreTLS: true,
port: 25,
host: '10.0.2.2',
name: 'learnjsthehardway.com',
});

exports.simple_send = async (transporter, email) => {
assert(transporter, "Must give a valid transporter.");
assert(email.to && email.from, "Email must have a to and from.");
await transporter.sendMail(email, (err, info) => {
if(err) {
log.error(err, "ERROR Sending email");
} else {
log.debug(info.envelope);
log.debug(info.messageId);
}
});
}

exports.template = (src) => {
let content = fs.readFileSync(src);
return _.template(content);
}

exports.transporter = secrets.env == 'development' ? exports.dev_transporter : exports.prod_transporter;



+ 48
- 0
lib/rr.js View File

@@ -0,0 +1,48 @@
const build = require('./builderator');
const path = require('path');
const {log} = require('./logging');
const assert = require('assert');

exports.load_rules = (rules_path, excludes) => {
const rules_module = require(rules_path);
const target_rules = Object.keys(rules_module)
.filter(k => k.startsWith('needs') && excludes.indexOf(k) == -1)
.map(k => rules_module[k]);

return [target_rules, rules_module];
}

/*
* Takes a comma separated list of rule names without the needs_ prefix and returns them with
* the needs_ prefix added.
*/
const parse_excludes = (not_needed) => {
assert(not_needed !== undefined, "Must give at least an empty string.");
return not_needed.split(',').map(n => 'needs_' + n);
}

exports.run = async (rules_name, config) => {
const rules_path = path.resolve(rules_name);
log.debug(`Rules named ${rules_name} resolves to ${rules_path}.`, config);

const excludes = parse_excludes(config.not_needed || '');
log.debug("Excluding rules", excludes);

const [target_rules, rules_module] = exports.load_rules(rules_path, excludes);
let facts;

try {
facts = await rules_module.facts(config);
await build.run(facts, target_rules);

if(rules_module.finished) {
await rules_module.finished(facts);
}
} catch(error) {
log.error(error, `Failure in running ${rules_name}`);
if(rules_module.failure) {
await rules_module.failure(error, facts);
}
}
}


+ 9827
- 0
package-lock.json
File diff suppressed because it is too large
View File


+ 63
- 0
package.json View File

@@ -0,0 +1,63 @@
{
"name": "TODO",
"description": "TODO",
"version": "0.0.1",
"scripts": {
"dev": "sapper dev",
"build": "sapper build --legacy",
"export": "sapper export --legacy",
"start": "node __sapper__/build",
"cy:run": "cypress run",
"cy:open": "cypress open",
"test": "run-p --race dev cy:run",
"staging": "./scripts/rr ./scripts/services/ci.js --debug --force --production --not_needed=restart --build_target=deployment/staging"
},
"dependencies": {
"body-parser": "^1.19.0",
"compression": "^1.7.1",
"cookie-session": "^1.4.0",
"deep-cleaner": "^1.2.1",
"fast-exif": "^1.0.1",
"feed": "^4.2.0",
"morgan": "^1.10.0",
"pino": "^6.3.2",
"pino-pretty": "^4.0.0",
"polka": "next",
"prismjs": "^1.20.0",
"sharp": "^0.25.4",
"sirv": "^0.4.0",
"svelte-preprocess": "^3.9.10"
},
"devDependencies": {
"@babel/core": "^7.0.0",
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/plugin-transform-runtime": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"@babel/runtime": "^7.0.0",
"@rollup/plugin-babel": "^5.0.0",
"@rollup/plugin-commonjs": "^12.0.0",
"@rollup/plugin-node-resolve": "^8.0.0",
"@rollup/plugin-replace": "^2.2.0",
"fast-glob": "^3.2.4",
"gulp": "^4.0.2",
"gulp-autoprefixer": "^7.0.1",
"gulp-clean-css": "^4.3.0",
"gulp-csscomb": "^3.1.0",
"gulp-rename": "^2.0.0",
"gulp-sass": "^4.1.0",
"nodemailer": "^6.4.10",
"npm-run-all": "^4.1.5",
"pm2": "^4.4.0",
"remarkable": "^2.0.1",
"rollup": "^2.3.4",
"rollup-plugin-svelte": "^5.0.1",
"rollup-plugin-terser": "^5.3.0",
"rools": "^2.2.7",
"sapper": "^0.27.0",
"simple-git": "^2.10.0",
"svelte": "^3.0.0",
"url-slug": "^2.3.1",
"wait-on": "^5.0.1",
"watch": "^1.0.2"
}
}

BIN
public/android-chrome-192x192.png View File

Before After
Width: 192  |  Height: 192  |  Size: 4.9KB

BIN
public/android-chrome-256x256.png View File

Before After
Width: 256  |  Height: 256  |  Size: 7.8KB

BIN
public/apple-touch-icon.png View File

Before After
Width: 180  |  Height: 180  |  Size: 3.8KB

+ 9
- 0
public/browserconfig.xml View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#ffc40d</TileColor>
</tile>
</msapplication>
</browserconfig>

BIN
public/favicon-16x16.png View File

Before After
Width: 16  |  Height: 16  |  Size: 722B

BIN
public/favicon-32x32.png View File

Before After
Width: 32  |  Height: 32  |  Size: 1.0KB

BIN
public/favicon.ico View File

Before After

BIN
public/mstile-150x150.png View File

Before After
Width: 270  |  Height: 270  |  Size: 2.5KB

+ 19
- 0
public/safari-pinned-tab.svg View File

@@ -0,0 +1,19 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="256.000000pt" height="256.000000pt" viewBox="0 0 256.000000 256.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.11, written by Peter Selinger 2001-2013
</metadata>
<g transform="translate(0.000000,256.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M0 1280 l0 -1280 1280 0 1280 0 0 1280 0 1280 -1280 0 -1280 0 0
-1280z m1364 401 c26 -27 31 -39 31 -81 0 -43 -5 -54 -33 -82 -28 -28 -39 -33
-82 -33 -43 0 -54 5 -82 33 -28 28 -33 39 -33 81 0 52 25 92 70 112 37 16 96
3 129 -30z m-28 -386 c53 -22 78 -85 72 -185 -7 -124 -65 -207 -183 -261 -22
-10 -41 -17 -42 -15 -1 1 -9 18 -17 38 l-16 36 45 26 c55 33 94 83 102 130 5
34 4 36 -21 36 -67 0 -114 57 -102 123 7 37 27 60 66 76 35 14 56 13 96 -4z"/>
</g>
</svg>

+ 0
- 0
public/test.txt View File


+ 106
- 0
rollup.config.js View File

@@ -0,0 +1,106 @@
import resolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import commonjs from '@rollup/plugin-commonjs';
import svelte from 'rollup-plugin-svelte';
import autoPreprocess from 'svelte-preprocess';
import babel from '@rollup/plugin-babel';
import { terser } from 'rollup-plugin-terser';
import config from 'sapper/config/rollup.js';
import pkg from './package.json';

const mode = process.env.NODE_ENV;
const dev = mode === 'development';
const legacy = !!process.env.SAPPER_LEGACY_BUILD;

const onwarn = (warning, onwarn) => (warning.code === 'CIRCULAR_DEPENDENCY' && /[/\\]@sapper[/\\]/.test(warning.message)) || onwarn(warning);

export default {
client: {
input: config.client.input(),
output: config.client.output(),
plugins: [
replace({
'process.browser': true,
'process.env.NODE_ENV': JSON.stringify(mode)
}),
svelte({
dev,
hydratable: true,
emitCss: true,
preprocess: autoPreprocess({}),
}),
resolve({
browser: true,
dedupe: ['svelte']
}),
commonjs(),

legacy && babel({
extensions: ['.js', '.mjs', '.html', '.svelte'],
babelHelpers: 'runtime',
exclude: ['node_modules/@babel/**'],
presets: [
['@babel/preset-env', {
targets: '> 0.25%, not dead'
}]
],
plugins: [
'@babel/plugin-syntax-dynamic-import',
['@babel/plugin-transform-runtime', {
useESModules: true
}]
]
}),

!dev && terser({
module: true
})
],

preserveEntrySignatures: false,
onwarn,
},

server: {
input: config.server.input(),
output: config.server.output(),
plugins: [
replace({
'process.browser': false,
'process.env.NODE_ENV': JSON.stringify(mode)
}),
svelte({
generate: 'ssr',
dev,
preprocess: autoPreprocess({}),
}),
resolve({
dedupe: ['svelte']
}),
commonjs()
],
external: Object.keys(pkg.dependencies).concat(
require('module').builtinModules || Object.keys(process.binding('natives'))
),

preserveEntrySignatures: 'strict',
onwarn,
},

serviceworker: {
input: config.serviceworker.input(),
output: config.serviceworker.output(),
plugins: [
resolve(),
replace({
'process.browser': true,
'process.env.NODE_ENV': JSON.stringify(mode)
}),
commonjs(),
!dev && terser()
],

preserveEntrySignatures: false,
onwarn,
}
};

+ 38
- 0
sass/_accordions.scss View File

@@ -0,0 +1,38 @@
// Accordions
.accordion {
input:checked ~,
&[open] {
& .accordion-header {
.icon {
transform: rotate(90deg);
}
}

& .accordion-body {
max-height: 50rem;
}
}

.accordion-header {
display: block;
padding: $unit-1 $unit-2;

.icon {
transition: transform .25s;
}
}

.accordion-body {
margin-bottom: $layout-spacing;
max-height: 0;
overflow: hidden;
transition: max-height .25s;
}
}

// Remove default details marker in Webkit
summary.accordion-header {
&::-webkit-details-marker {
display: none;
}
}

+ 20
- 0
sass/_animations.scss View File

@@ -0,0 +1,20 @@
// Animations
@keyframes loading {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

@keyframes slide-down {
0% {
opacity: 0;
transform: translateY(-$unit-8);
}
100% {
opacity: 1;
transform: translateY(0);
}
}

+ 47
- 0
sass/_autocomplete.scss View File

@@ -0,0 +1,47 @@
// Autocomplete
.form-autocomplete {
position: relative;

.form-autocomplete-input {
align-content: flex-start;
display: flex;
flex-wrap: wrap;
height: auto;
min-height: $unit-8;
padding: $unit-h;

&.is-focused {
@include control-shadow();
border-color: $primary-color;
}

.form-input {
border-color: transparent;
box-shadow: none;
display: inline-block;
flex: 1 0 auto;
height: $unit-6;
line-height: $unit-4;
margin: $unit-h;
width: auto;
}
}

.menu {
left: 0;
position: absolute;
top: 100%;
width: 100%;
}

&.autocomplete-oneline {
.form-autocomplete-input {
flex-wrap: nowrap;
overflow-x: auto;
}

.chip {
flex: 1 0 auto;
}
}
}

+ 77
- 0
sass/_avatars.scss View File

@@ -0,0 +1,77 @@
// Avatars
.avatar {
@include avatar-base();
background: $primary-color;
border-radius: 50%;
color: rgba($light-color, .85);
display: inline-block;
font-weight: 300;
line-height: 1.25;
margin: 0;
position: relative;
vertical-align: middle;

&.avatar-xs {
@include avatar-base($unit-4);
}
&.avatar-sm {
@include avatar-base($unit-6);
}
&.avatar-lg {
@include avatar-base($unit-12);
}
&.avatar-xl {
@include avatar-base($unit-16);
}

img {
border-radius: 50%;
height: 100%;
position: relative;
width: 100%;
z-index: $zindex-0;
}

.avatar-icon,
.avatar-presence {
background: $bg-color-light;
bottom: 14.64%;
height: 50%;
padding: $border-width-lg;
position: absolute;
right: 14.64%;
transform: translate(50%, 50%);
width: 50%;
z-index: $zindex-0 + 1;
}

.avatar-presence {
background: $gray-color;
box-shadow: 0 0 0 $border-width-lg $light-color;
border-radius: 50%;
height: .5em;
width: .5em;

&.online {
background: $success-color;
}

&.busy {
background: $error-color;
}

&.away {
background: $warning-color;
}
}

&[data-initial]::before {
color: currentColor;
content: attr(data-initial);
left: 50%;
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
z-index: $zindex-0;
}
}

+ 60
- 0
sass/_badges.scss View File

@@ -0,0 +1,60 @@
// Badges
.badge {
position: relative;
white-space: nowrap;

&[data-badge],
&:not([data-badge]) {
&::after {
background: $primary-color;
background-clip: padding-box;
border-radius: .5rem;
box-shadow: 0 0 0 .1rem $bg-color-light;
color: $light-color;
content: attr(data-badge);
display: inline-block;
transform: translate(-.05rem, -.5rem);
}
}
&[data-badge] {
&::after {
font-size: $font-size-sm;
height: .9rem;
line-height: 1;
min-width: .9rem;
padding: .1rem .2rem;
text-align: center;
white-space: nowrap;
}
}
&:not([data-badge]),
&[data-badge=""] {
&::after {
height: 6px;
min-width: 6px;
padding: 0;
width: 6px;
}
}

// Badges for Buttons
&.btn {
&::after {
position: absolute;
top: 0;
right: 0;
transform: translate(50%, -50%);
}
}

// Badges for Avatars
&.avatar {
&::after {
position: absolute;
top: 14.64%;
right: 14.64%;
transform: translate(50%, -50%);
z-index: $zindex-1;
}
}
}

+ 71
- 0
sass/_bars.scss View File

@@ -0,0 +1,71 @@
// Bars
.bar {
background: $bg-color-dark;
border-radius: $border-radius;
display: flex;
flex-wrap: nowrap;
height: $unit-4;
width: 100%;

&.bar-sm {
height: $unit-1;
}

// TODO: attr() support
.bar-item {
background: $primary-color;
color: $light-color;
display: block;
font-size: $font-size-sm;
flex-shrink: 0;
line-height: $unit-4;
height: 100%;
position: relative;
text-align: center;
width: 0;

&:first-child {
border-bottom-left-radius: $border-radius;
border-top-left-radius: $border-radius;
}
&:last-child {
border-bottom-right-radius: $border-radius;
border-top-right-radius: $border-radius;
flex-shrink: 1;
}
}
}

// Slider bar
.bar-slider {
height: $border-width-lg;
margin: $layout-spacing 0;
position: relative;

.bar-item {
left: 0;
padding: 0;
position: absolute;
&:not(:last-child):first-child {
background: $bg-color-dark;
z-index: $zindex-0;
}
}

.bar-slider-btn {
background: $primary-color;
border: 0;
border-radius: 50%;
height: $unit-3;
padding: 0;
position: absolute;
right: 0;
top: 50%;
transform: translate(50%, -50%);
width: $unit-3;

&:active {
box-shadow: 0 0 0 .1rem $primary-color;
}
}
}

+ 40
- 0
sass/_base.scss View File

@@ -0,0 +1,40 @@
// Base
*,
*::before,
*::after {
box-sizing: inherit;
}

html {
box-sizing: border-box;
font-size: $html-font-size;
line-height: $html-line-height;
-webkit-tap-highlight-color: transparent;
}

body {
background: $body-bg;
color: $body-font-color;
font-family: $body-font-family;
font-size: $font-size;
overflow-x: hidden;
text-rendering: optimizeLegibility;
}

a {
color: $link-color;
outline: none;
text-decoration: underline;

&:focus {
@include control-shadow();
}

&:focus,
&:hover,
&:active,
&.active {
color: $link-color-dark;
text-decoration: underline;
}
}

+ 29
- 0
sass/_breadcrumbs.scss View File

@@ -0,0 +1,29 @@
// Breadcrumbs
.breadcrumb {
list-style: none;
margin: $unit-1 0;
padding: $unit-1 0;

.breadcrumb-item {
color: $gray-color-dark;
display: inline-block;
margin: 0;
padding: $unit-1 0;

&:not(:last-child) {
margin-right: $unit-1;

a {
color: $gray-color-dark;
}
}

&:not(:first-child) {
&::before {
color: $gray-color-dark;
content: "/";
padding-right: $unit-2;
}
}
}
}

+ 198
- 0
sass/_buttons.scss View File

@@ -0,0 +1,198 @@
// Buttons
.btn {
appearance: none;
background: transparent;
border: $border-width solid $primary-color;
border-radius: $border-radius;
color: $primary-color;
cursor: pointer;
display: inline-block;
font-size: $font-size;
height: $control-size;
line-height: $line-height;
outline: none;
padding: $control-padding-y $control-padding-x;
text-align: center;
text-decoration: none;
transition: background .2s, border .2s, box-shadow .2s, color .2s;
user-select: none;
vertical-align: middle;
white-space: nowrap;
&:focus {
@include control-shadow();
}
&:focus,
&:hover {
background: $secondary-color;
border-color: $primary-color-dark;
color: $light-color;
text-decoration: none;
}
&:active,
&.active {
background: $primary-color-dark;
border-color: darken($primary-color-dark, 5%);
color: $light-color;
text-decoration: none;
&.loading {
&::after {
border-bottom-color: $light-color;
border-left-color: $light-color;
}
}
}
&[disabled],
&:disabled,
&.disabled {
cursor: default;
opacity: .5;
pointer-events: none;
}

// Button Primary
&.btn-primary {
background: $primary-color;
border-color: $primary-color-dark;
color: $light-color;
&:focus,
&:hover {
background: darken($primary-color-dark, 2%);
border-color: darken($primary-color-dark, 5%);
color: $light-color;
}
&:active,
&.active {
background: darken($primary-color-dark, 4%);
border-color: darken($primary-color-dark, 7%);
color: $light-color;
}
&.loading {
&::after {
border-bottom-color: $light-color;
border-left-color: $light-color;
}
}
}

// Button Colors
&.btn-success {
@include button-variant($success-color);
}

&.btn-warning {
@include button-variant($warning-color);
}

&.btn-error {
@include button-variant($error-color);
}

// Button Link
&.btn-link {
background: transparent;
border-color: transparent;
color: $link-color;
&:focus,
&:hover,
&:active,
&.active {
color: $link-color-dark;
}
}

// Button Sizes
&.btn-sm {
font-size: $font-size-sm;
height: $control-size-sm;
padding: $control-padding-y-sm $control-padding-x-sm;
}

&.btn-lg {
font-size: $font-size-lg;
height: $control-size-lg;
padding: $control-padding-y-lg $control-padding-x-lg;
}

// Button Block
&.btn-block {
display: block;
width: 100%;
}

// Button Action
&.btn-action {
width: $control-size;
padding-left: 0;
padding-right: 0;

&.btn-sm {
width: $control-size-sm;
}

&.btn-lg {
width: $control-size-lg;
}
}

// Button Clear
&.btn-clear {
background: transparent;
border: 0;
color: currentColor;
height: $unit-5;
line-height: $unit-4;
margin-left: $unit-1;
margin-right: -2px;
opacity: 1;
padding: $unit-h;
text-decoration: none;
width: $unit-5;

&:focus,
&:hover {
background: rgba($bg-color, .5);
opacity: .95;
}

&::before {
content: "\2715";
}
}
}

// Button groups
.btn-group {
display: inline-flex;
flex-wrap: wrap;

.btn {
flex: 1 0 auto;
&:first-child:not(:last-child) {
border-bottom-right-radius: 0;
border-top-right-radius: 0;
}
&:not(:first-child):not(:last-child) {
border-radius: 0;
margin-left: -$border-width;
}
&:last-child:not(:first-child) {
border-bottom-left-radius: 0;
border-top-left-radius: 0;
margin-left: -$border-width;
}
&:focus,
&:hover,
&:active,
&.active {
z-index: $zindex-0;
}
}

&.btn-group-block {
display: flex;

.btn {
flex: 1 0 0;
}
}
}

+ 222
- 0
sass/_calendars.scss View File

@@ -0,0 +1,222 @@
// Calendars
.calendar {
border: $border-width solid $border-color;
border-radius: $border-radius;
display: block;
min-width: 280px;

.calendar-nav {
align-items: center;
background: $bg-color;
border-top-left-radius: $border-radius;
border-top-right-radius: $border-radius;
display: flex;
font-size: $font-size-lg;
padding: $layout-spacing;
}

.calendar-header,
.calendar-body {
display: flex;
flex-wrap: wrap;
justify-content: center;
padding: $layout-spacing 0;

.calendar-date {
flex: 0 0 14.28%; // 7 calendar-items each row
max-width: 14.28%;
}
}

.calendar-header {
background: $bg-color;
border-bottom: $border-width solid $border-color;
color: $gray-color;
font-size: $font-size-sm;
text-align: center;
}

.calendar-body {
color: $gray-color-dark;
}

.calendar-date {
border: 0;
padding: $unit-1;

.date-item {
appearance: none;
background: transparent;
border: $border-width solid transparent;
border-radius: 50%;
color: $gray-color-dark;
cursor: pointer;
font-size: $font-size-sm;
height: $unit-7;
line-height: $unit-5;
outline: none;
padding: $unit-h;
position: relative;
text-align: center;
text-decoration: none;
transition: background .2s, border .2s, box-shadow .2s, color .2s;
vertical-align: middle;
white-space: nowrap;
width: $unit-7;

&.date-today {
border-color: $secondary-color-dark;
color: $primary-color;
}

&:focus {
@include control-shadow();
}

&:focus,
&:hover {
background: $secondary-color-light;
border-color: $secondary-color-dark;
color: $primary-color;
text-decoration: none;
}
&:active,
&.active {
background: $primary-color-dark;
border-color: darken($primary-color-dark, 5%);
color: $light-color;
}

// Calendar badge support
&.badge {
&::after {
position: absolute;
top: 3px;
right: 3px;
transform: translate(50%, -50%);
}
}
}

.date-item,
.calendar-event {
&:disabled,
&.disabled {
cursor: default;
opacity: .25;
pointer-events: none;
}
}

&.prev-month,
&.next-month {
.date-item,
.calendar-event {
opacity: .25;
}
}
}

.calendar-range {
position: relative;

&::before {
background: $secondary-color;
content: "";
height: $unit-7;
left: 0;
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
}
&.range-start {
&::before {
left: 50%;
}
}
&.range-end {
&::before {
right: 50%;
}
}

&.range-start,
&.range-end {
.date-item {
background: $primary-color-dark;
border-color: darken($primary-color-dark, 5%);
color: $light-color;
}
}

.date-item {
color: $primary-color;
}
}

// Calendars size
&.calendar-lg {
.calendar-body {
padding: 0;

.calendar-date {
border-bottom: $border-width solid $border-color;
border-right: $border-width solid $border-color;
display: flex;
flex-direction: column;
height: 5.5rem;
padding: 0;

&:nth-child(7n) {
border-right: 0;
}
&:nth-last-child(-n+7) {
border-bottom: 0;
}
}
}

.date-item {
align-self: flex-end;
height: $unit-7;
margin-right: $layout-spacing-sm;
margin-top: $layout-spacing-sm;
}

.calendar-range {
&::before {
top: 19px;
}
&.range-start {
&::before {
left: auto;
width: 19px;
}
}
&.range-end {
&::before {
right: 19px;
}
}
}

.calendar-events {
flex-grow: 1;
line-height: 1;
overflow-y: auto;
padding: $layout-spacing-sm;
}

.calendar-event {
border-radius: $border-radius;
font-size: $font-size-sm;
display: block;
margin: $unit-h auto;
overflow: hidden;
padding: 3px 4px;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}

+ 43
- 0
sass/_cards.scss View File

@@ -0,0 +1,43 @@
// Cards
.card {
background: $bg-color-light;
border: $border-width solid $border-color;
border-radius: $border-radius;
display: flex;
flex-direction: column;

.card-header,
.card-body,
.card-footer {
padding: $layout-spacing-lg;
padding-bottom: 0;

&:last-child {
padding-bottom: $layout-spacing-lg;
}
}

.card-body {
flex: 1 1 auto;
}

.card-image {
padding-top: $layout-spacing-lg;

&:first-child {
padding-top: 0;

img {
border-top-left-radius: $border-radius;
border-top-right-radius: $border-radius;
}
}

&:last-child {
img {
border-bottom-left-radius: $border-radius;
border-bottom-right-radius: $border-radius;
}
}
}
}

+ 136
- 0
sass/_carousels.scss View File

@@ -0,0 +1,136 @@
// Carousels
// The number of carousel images
$carousel-number: 8;

%carousel-image-checked {
animation: carousel-slidein .75s ease-in-out 1;
opacity: 1;
z-index: $zindex-1;
}

%carousel-nav-checked {
color: $gray-color-light;
}

.carousel {
background: $bg-color;
display: block;
overflow: hidden;
position: relative;
width: 100%;
-webkit-overflow-scrolling: touch;
z-index: $zindex-0;

.carousel-container {
height: 100%;
left: 0;
position: relative;
&::before {
content: "";
display: block;
padding-bottom: 56.25%;
}

.carousel-item {
animation: carousel-slideout 1s ease-in-out 1;
height: 100%;
left: 0;
margin: 0;
opacity: 0;
position: absolute;
top: 0;
width: 100%;

&:hover {
.item-prev,
.item-next {
opacity: 1;
}
}
}

.item-prev,
.item-next {
background: rgba($gray-color-light, .25);
border-color: rgba($gray-color-light, .5);
color: $gray-color-light;
opacity: 0;
position: absolute;
top: 50%;
transition: all .4s;
transform: translateY(-50%);
z-index: $zindex-1;
}
.item-prev {
left: 1rem;
}
.item-next {
right: 1rem;
}
}

.carousel-locator {
@for $i from 1 through ($carousel-number) {
&:nth-of-type(#{$i}):checked ~ .carousel-container .carousel-item:nth-of-type(#{$i}) {
@extend %carousel-image-checked;
}
}

@for $i from 1 through ($carousel-number) {
&:nth-of-type(#{$i}):checked ~ .carousel-nav .nav-item:nth-of-type(#{$i}) {
@extend %carousel-nav-checked;
}
}
}

.carousel-nav {
bottom: $layout-spacing;
display: flex;
justify-content: center;
left: 50%;
position: absolute;
transform: translateX(-50%);
width: 10rem;
z-index: $zindex-1;

.nav-item {
color: rgba($gray-color-light, .5);
display: block;
flex: 1 0 auto;
height: $unit-8;
margin: $unit-1;
max-width: 2.5rem;
position: relative;

&::before {
background: currentColor;
content: "";
display: block;
height: $unit-h;
position: absolute;
top: .5rem;
width: 100%;
}
}
}
}

@keyframes carousel-slidein {
0% {
transform: translateX(100%);
}
100% {
transform: translateX(0);
}
}

@keyframes carousel-slideout {
0% {
opacity: 1;
transform: translateX(0);
}
100% {
opacity: 1;
transform: translateX(-50%);
}
}

+ 33
- 0
sass/_chips.scss View File

@@ -0,0 +1,33 @@
// Chips
.chip {
align-items: center;
background: $bg-color-dark;
border-radius: 5rem;
display: inline-flex;
font-size: 90%;
height: $unit-6;
line-height: $unit-4;
margin: $unit-h;
max-width: $control-width-sm;
overflow: hidden;
padding: $unit-1 $unit-2;
text-decoration: none;
text-overflow: ellipsis;
vertical-align: middle;
white-space: nowrap;

&.active {
background: $primary-color;
color: $light-color;
}

.avatar {
margin-left: -$unit-2;
margin-right: $unit-1;
}

.btn-clear {
border-radius: 50%;
transform: scale(.75);
}
}

+ 31
- 0
sass/_codes.scss View File

@@ -0,0 +1,31 @@
// Codes
code {
@include label-base();
@include label-variant($code-color, $black);
font-size: 85%;
}

pre {
border-radius: $border-radius;
color: $white;
position: relative;
background-color: $black;

&::before {
color: $gray-color;
content: attr(data-lang);
font-size: $font-size-sm;
position: absolute;
right: $layout-spacing;
top: $unit-h;
}

code {
color: inherit;
display: block;
line-height: 1.5;
overflow-x: auto;
padding: 1rem;
width: 100%;
}
}

+ 115
- 0
sass/_comparison-sliders.scss View File

@@ -0,0 +1,115 @@
// Image comparison slider
// Credit: http://codepen.io/solipsistacp/pen/Gpmaq
.comparison-slider {
height: 50vh;
overflow: hidden;
position: relative;
width: 100%;
-webkit-overflow-scrolling: touch;

.comparison-before,
.comparison-after {
height: 100%;
left: 0;
margin: 0;
overflow: hidden;
position: absolute;
top: 0;

img {
height: 100%;
object-fit: cover;
object-position: left center;
position: absolute;
width: 100%;
}
}

.comparison-before {
width: 100%;
z-index: 1;

.comparison-label {
right: $unit-4;
}
}

.comparison-after {
max-width: 100%;
min-width: 0;
z-index: 2;

&::before {
background: transparent;
content: "";
cursor: default;
height: 100%;
left: 0;
position: absolute;
right: $unit-4;
top: 0;
z-index: $zindex-0;
}

&::after {
background: currentColor;
border-radius: 50%;
box-shadow: 0 -5px, 0 5px;
color: $light-color;
content: "";
height: 3px;
position: absolute;
right: $unit-2;
top: 50%;
transform: translate(50%, -50%);
width: 3px;
}

.comparison-label {
left: $unit-4;
}
}

.comparison-resizer {
animation: first-run 1.5s 1 ease-in-out;
cursor: ew-resize;
height: $unit-4;
left: 0;
max-width: 100%;
min-width: $unit-4;
opacity: 0;
outline: none;
position: relative;
resize: horizontal;
top: 50%;
transform: translateY(-50%) scaleY(30);
width: 0;
}

.comparison-label {
background: rgba($dark-color, .5);
bottom: $unit-4;
color: $light-color;
padding: $unit-1 $unit-2;
position: absolute;
user-select: none;
}
}

@keyframes first-run {
0% {
width: 0;
}
25% {
width: $unit-12;
}
50% {
width: $unit-4;
}
75% {
width: $unit-6;
}
100% {
width: 0;
}
}

+ 36
- 0
sass/_dropdowns.scss View File

@@ -0,0 +1,36 @@
// Dropdown
.dropdown {
display: inline-block;
position: relative;

.menu {
animation: slide-down .15s ease 1;
display: none;
left: 0;
max-height: 50vh;
overflow-y: auto;
position: absolute;
top: 100%;
}

&.dropdown-right {
.menu {
left: auto;
right: 0;
}
}

&.active .menu,
.dropdown-toggle:focus + .menu,
.menu:hover {
display: block;
}

// Fix dropdown-toggle border radius in button groups
.btn-group {
.dropdown-toggle:nth-last-child(2) {
border-bottom-right-radius: $border-radius;
border-top-right-radius: $border-radius;
}
}
}

+ 21
- 0
sass/_empty.scss View File

@@ -0,0 +1,21 @@
// Empty states (or Blank slates)
.empty {
background: $bg-color;
border-radius: $border-radius;
color: $gray-color-dark;
text-align: center;
padding: $unit-16 $unit-8;

.empty-icon {
margin-bottom: $layout-spacing-lg;
}

.empty-title,
.empty-subtitle {
margin: $layout-spacing auto;
}

.empty-action {
margin-top: $layout-spacing-lg;
}
}

+ 37
- 0
sass/_filters.scss View File

@@ -0,0 +1,37 @@
// Filters
// The number of filter options
$filter-number: 8 !default;

%filter-checked-nav {
background: $primary-color;
color: $light-color;
}

%filter-checked-body {
display: none;
}

.filter {
.filter-nav {
margin: $layout-spacing 0;
}

.filter-body {
display: flex;
flex-wrap: wrap;
}

.filter-tag {
@for $i from 0 through ($filter-number) {
&#tag-#{$i}:checked ~ .filter-nav .chip[for="tag-#{$i}"] {
@extend %filter-checked-nav;
}
}

@for $i from 1 through ($filter-number) {
&#tag-#{$i}:checked ~ .filter-body .filter-item:not([data-tag~="tag-#{$i}"]) {
@extend %filter-checked-body;
}
}
}
}

+ 557
- 0
sass/_forms.scss View File

@@ -0,0 +1,557 @@
// Forms
.form-group {
&:not(:last-child) {
margin-bottom: $layout-spacing;
}
}

fieldset {
margin-bottom: $layout-spacing-lg;
}

legend {
font-size: $font-size-lg;
font-weight: 500;
margin-bottom: $layout-spacing-lg;
}

// Form element: Label
.form-label {
display: block;