first commit

This commit is contained in:
madrilene 2022-10-10 14:41:35 +02:00
commit db1207a8a2
136 changed files with 20660 additions and 0 deletions

155
.eleventy.js Normal file
View file

@ -0,0 +1,155 @@
/**
* I strive to keep the `.eleventy.js` file clean and uncluttered. Most adjustments must be made in:
* - `src/config/collections.js`
* - `src/config/filters.js`
* - `src/config/plugins.js`
* - `src/config/shortcodes.js`
* - `src/config/transforms.js`
*/
// module import filters
const {
wordCount,
limit,
sortByKey,
toHtml,
where,
toISOString,
formatDate,
dividedBy,
newlineToBr,
toAbsoluteUrl,
stripNewlines,
stripHtml,
getLatestCollectionItemDate,
minifyCss,
mdInline
} = require('./config/filters/index.js');
// module import shortcodes
const {
asideShortcode,
insertionShortcode,
imageShortcode,
imageShortcodePlaceholder,
liteYoutube
} = require('./config/shortcodes/index.js');
// module import collections
const {getAllProjects} = require('./config/collections/index.js');
// module import transforms
// plugins
const markdownLib = require('./config/plugins/markdown.js');
const {EleventyRenderPlugin} = require('@11ty/eleventy');
const syntaxHighlight = require('@11ty/eleventy-plugin-syntaxhighlight');
const {slugifyString} = require('./config/utils');
const {escape} = require('lodash');
const pluginWebc = require('@11ty/eleventy-plugin-webc');
module.exports = eleventyConfig => {
// Tell 11ty to use the .eleventyignore and ignore our .gitignore file
eleventyConfig.setUseGitIgnore(false);
// --------------------- Custom Watch Targets -----------------------
eleventyConfig.addWatchTarget('./src/_assets');
eleventyConfig.addWatchTarget('./utils/*.js');
// --------------------- layout aliases -----------------------
eleventyConfig.addLayoutAlias('base', 'base.njk');
eleventyConfig.addLayoutAlias('page', 'page.njk');
eleventyConfig.addLayoutAlias('home', 'home.njk');
eleventyConfig.addLayoutAlias('post', 'post.njk');
// --------------------- Custom filters -----------------------
eleventyConfig.addFilter('wordCount', wordCount);
eleventyConfig.addFilter('limit', limit);
eleventyConfig.addFilter('sortByKey', sortByKey);
eleventyConfig.addFilter('where', where);
eleventyConfig.addFilter('escape', escape);
eleventyConfig.addFilter('toHtml', toHtml);
eleventyConfig.addFilter('toIsoString', toISOString);
eleventyConfig.addFilter('formatDate', formatDate);
eleventyConfig.addFilter('dividedBy', dividedBy);
eleventyConfig.addFilter('newlineToBr', newlineToBr);
eleventyConfig.addFilter('toAbsoluteUrl', toAbsoluteUrl);
eleventyConfig.addFilter('stripNewlines', stripNewlines);
eleventyConfig.addFilter('stripHtml', stripHtml);
eleventyConfig.addFilter('slugify', slugifyString);
eleventyConfig.addFilter('toJson', JSON.stringify);
eleventyConfig.addFilter('fromJson', JSON.parse);
eleventyConfig.addFilter('getLatestCollectionItemDate', getLatestCollectionItemDate);
eleventyConfig.addFilter('cssmin', minifyCss);
eleventyConfig.addFilter('md', mdInline);
eleventyConfig.addFilter('keys', Object.keys);
eleventyConfig.addFilter('values', Object.values);
eleventyConfig.addFilter('entries', Object.entries);
// --------------------- Custom shortcodes ---------------------
eleventyConfig.addPairedShortcode('aside', asideShortcode);
eleventyConfig.addPairedShortcode('insertion', insertionShortcode);
eleventyConfig.addNunjucksAsyncShortcode('image', imageShortcode);
eleventyConfig.addNunjucksAsyncShortcode('imagePlaceholder', imageShortcodePlaceholder);
eleventyConfig.addShortcode('youtube', liteYoutube);
eleventyConfig.addShortcode('year', () => `${new Date().getFullYear()}`); // current year, stephanie eckles
// --------------------- Custom transforms ---------------------
// --------------------- Custom collections -----------------------
eleventyConfig.addCollection('projects', getAllProjects);
// --------------------- Plugins ---------------------
eleventyConfig.addPlugin(EleventyRenderPlugin);
eleventyConfig.addPlugin(syntaxHighlight);
eleventyConfig.setLibrary('md', markdownLib);
eleventyConfig.addPlugin(pluginWebc, {
components: 'src/_includes/webc/*.webc'
});
// --------------------- Passthrough File Copy -----------------------
eleventyConfig.addPassthroughCopy('src/_assets/fonts/');
eleventyConfig.addPassthroughCopy('src/_assets/images/');
// social icons von images zu root
eleventyConfig.addPassthroughCopy({
'src/_assets/images/favicon/site.webmanifest': 'site.webmanifest'
});
eleventyConfig.addPassthroughCopy({
'src/_assets/images/favicon/favicon.ico': 'favicon.ico'
});
eleventyConfig.addPassthroughCopy({
'src/_assets/images/favicon/favicon.svg': 'favicon.svg'
});
eleventyConfig.addPassthroughCopy({
'src/_assets/images/favicon/apple-touch-icon.png': 'apple-touch-icon.png'
});
eleventyConfig.addPassthroughCopy({
'src/_assets/images/favicon/favicon-32x32.png': 'favicon-32x32.png'
});
eleventyConfig.addPassthroughCopy({
'src/_assets/images/favicon/favicon-16x16.png': 'favicon-16x16.png'
});
eleventyConfig.addPassthroughCopy({
'src/_assets/images/favicon/android-chrome-192x192.png': 'android-chrome-192x192.png'
});
eleventyConfig.addPassthroughCopy({
'src/_assets/images/favicon/android-chrome-512x512.png': 'android-chrome-512x512.png'
});
eleventyConfig.addPassthroughCopy({
'src/_assets/images/favicon/maskable.png': 'maskable.png'
});
return {
dir: {
input: 'src',
output: 'dist',
includes: '_includes',
layouts: '_layouts'
},
markdownTemplateEngine: 'njk',
dataTemplateEngine: 'njk',
htmlTemplateEngine: 'njk'
};
};

1
.eleventyignore Normal file
View file

@ -0,0 +1 @@
node_modules

16
.gitignore vendored Normal file
View file

@ -0,0 +1,16 @@
# Misc
*.log
npm-debug.*
*.scssc
*.log
*.swp
.DS_Store
Thumbs.db
.sass-cache
.env
.cache
# Node modules and output
node_modules
dist
src/_includes/css

9
.prettierrc Normal file
View file

@ -0,0 +1,9 @@
{
"printWidth": 90,
"tabWidth": 2,
"singleQuote": true,
"bracketSpacing": false,
"quoteProps": "consistent",
"trailingComma": "none",
"arrowParens": "avoid"
}

View file

@ -0,0 +1,9 @@
/** Returns all blog posts as a collection. */
const getAllProjects = collection => {
const projects = collection.getFilteredByGlob('./src/projects/*.md');
return projects.reverse();
};
module.exports = {
getAllProjects
};

26
config/constants/index.js Normal file
View file

@ -0,0 +1,26 @@
const path = require('path');
const dir = {
input: 'src/',
output: 'dist',
includes: '_includes',
layouts: '_layouts',
data: '_data',
assets: '_assets',
};
const imagePaths = {
source: path.join(dir.input, dir.assets, 'images'),
output: path.join(dir.output, dir.assets, 'images'),
};
const scriptDirs = {
source: path.join(dir.input, dir.assets, 'scripts'),
output: path.join(dir.output, dir.assets, 'scripts'),
};
module.exports = {
dir,
imagePaths,
scriptDirs,
};

152
config/filters/index.js Normal file
View file

@ -0,0 +1,152 @@
const lodash = require('lodash');
const dayjs = require('dayjs');
const CleanCSS = require('clean-css');
const markdownLib = require('../plugins/markdown');
const site = require('../../src/_data/meta');
const {throwIfNotType} = require('../utils');
const md = require('markdown-it')();
/** Returns the first `limit` elements of the the given array. */
const limit = (array, limit) => {
if (limit < 0) {
throw new Error(`Negative limits are not allowed: ${limit}.`);
}
return array.slice(0, limit);
};
/** Sorts the given array of objects by a string denoting chained key paths. */
const sortByKey = (arrayOfObjects, keyPath, order = 'ASC') => {
const sorted = lodash.sortBy(arrayOfObjects, object => lodash.get(object, keyPath));
if (order === 'ASC') return sorted;
if (order === 'DESC') return sorted.reverse();
throw new Error(`Invalid sort order: ${order}`);
};
/** Returns all entries from the given array that match the specified key:value pair. */
const where = (arrayOfObjects, keyPath, value) =>
arrayOfObjects.filter(object => lodash.get(object, keyPath) === value);
/** Returns the word count of the given string. */
const wordCount = str => {
throwIfNotType(str, 'string');
const matches = str.match(/[\w\d'-]+/gi);
return matches?.length ?? 0;
};
/** Converts the given markdown string to HTML, returning it as a string. */
const toHtml = markdownString => {
return markdownLib.renderInline(markdownString);
};
/** Divides the first argument by the second. */
const dividedBy = (numerator, denominator) => {
if (denominator === 0) {
throw new Error(`Cannot divide by zero: ${numerator} / ${denominator}`);
}
return numerator / denominator;
};
/** Replaces every newline with a line break. */
const newlineToBr = str => {
throwIfNotType(str, 'string');
return str.replace(/\n/g, '<br>');
};
/** Removes every newline from the given string. */
const stripNewlines = str => {
throwIfNotType(str, 'string');
return str.replace(/\n/g, '');
};
/** Removes all tags from an HTML string. */
const stripHtml = str => {
throwIfNotType(str, 'string');
return str.replace(/<[^>]+>/g, '');
};
/** Formats the given string as an absolute url. */
const toAbsoluteUrl = url => {
throwIfNotType(url, 'string');
// Replace trailing slash, e.g., site.com/ => site.com
const siteUrl = site.url.replace(/\/$/, '');
// Replace starting slash, e.g., /path/ => path/
const relativeUrl = url.replace(/^\//, '');
return `${siteUrl}/${relativeUrl}`;
};
/** Converts the given date string to ISO8610 format. */
const toISOString = dateString => dayjs(dateString).toISOString();
/** Formats a date using dayjs's conventions: https://day.js.org/docs/en/display/format */
const formatDate = (date, format) => dayjs(date).format(format);
/**
* @param {*} collection - an array of collection items that are assumed to have either data.lastUpdated or a date property
* @returns the most recent date of update or publication among the given collection items, or undefined if the array is empty.
*/
const getLatestCollectionItemDate = collection => {
const itemsSortedByLatestDate = collection
.filter(item => !!item.data?.lastUpdated || !!item.date)
.sort((item1, item2) => {
const date1 = item1.data?.lastUpdated ?? item1.date;
const date2 = item2.data?.lastUpdated ?? item2.date;
if (dayjs(date1).isAfter(date2)) {
return -1;
}
if (dayjs(date2).isAfter(date1)) {
return 1;
}
return 0;
});
const latestItem = itemsSortedByLatestDate[0];
return latestItem?.data?.lastUpdated ?? latestItem?.date;
};
const minifyCss = code => new CleanCSS({}).minify(code).styles;
/**
* Render content as inline markdown if single line, or full
* markdown if multiline. for md in yaml
* @param {string} [content]
* @param {import('markdown-it').Options} [opts]
* @return {string|undefined}
*/
const mdInline = (content, opts) => {
if (!content) {
return;
}
if (opts) {
md.set(opts);
}
let inline = !content.includes('\n');
// If there's quite a bit of content, we want to make sure
// it's marked up for readability purposes
if (inline && content.length > 200) {
inline = false;
}
return inline ? md.renderInline(content) : md.render(content);
};
module.exports = {
limit,
sortByKey,
where,
wordCount,
toHtml,
toISOString,
formatDate,
dividedBy,
newlineToBr,
stripNewlines,
stripHtml,
toAbsoluteUrl,
getLatestCollectionItemDate,
minifyCss,
mdInline
};

View file

@ -0,0 +1,47 @@
const markdownIt = require('markdown-it');
const markdownItPrism = require('markdown-it-prism');
const markdownItAnchor = require('markdown-it-anchor');
const markdownItClass = require('@toycode/markdown-it-class');
const markdownItLinkAttributes = require('markdown-it-link-attributes');
const markdownItEmoji = require('markdown-it-emoji');
const markdownItFootnote = require('markdown-it-footnote');
const markdownitMark = require('markdown-it-mark');
const markdownitAbbr = require('markdown-it-abbr');
const {slugifyString} = require('../utils');
const markdownLib = markdownIt({
html: true,
breaks: true,
linkify: true,
typographer: true
})
// https://github.com/11ty/eleventy/issues/2438
.disable('code')
.use(markdownItPrism, {
defaultLanguage: 'plaintext'
})
.use(markdownItAnchor, {
slugify: slugifyString,
tabIndex: false,
permalink: markdownItAnchor.permalink.headerLink({
class: 'heading-anchor'
})
})
.use(markdownItClass, {
ol: 'list',
ul: 'list'
})
.use(markdownItLinkAttributes, {
// Only external links (explicit protocol; internal links use relative paths)
pattern: /^https?:/,
attrs: {
target: '_blank',
rel: 'noreferrer noopener'
}
})
.use(markdownItEmoji)
.use(markdownItFootnote)
.use(markdownitMark)
.use(markdownitAbbr);
module.exports = markdownLib;

View file

@ -0,0 +1,12 @@
const markdownLib = require('../../plugins/markdown');
const outdent = require('outdent');
const aside = children => {
if (!children) {
throw new Error('You must provide a non-empty string for an aside.');
}
const content = markdownLib.renderInline(children);
return `<aside class="post-aside">${outdent`${content}`}</aside>`;
};
module.exports = aside;

View file

@ -0,0 +1,44 @@
const Image = require('@11ty/eleventy-img');
const path = require('path');
const imageShortcode = async (src, pcls, cls, alt, loading, sizes = '100vw') => {
if (!alt) {
throw new Error(`Missing \`alt\` on Image from: ${src}`);
}
let metadata = await Image(src, {
widths: [400, 700, 1280],
formats: ['avif', 'webp', 'jpeg'],
urlPath: '/_assets/images/',
outputDir: './dist/_assets/images/',
// Custom Image Filename
filenameFormat: function (id, src, width, format, options) {
const extension = path.extname(src);
const name = path.basename(src, extension);
return `${name}-${width}w.${format}`;
}
});
let lowsrc = metadata.jpeg[0];
return `<picture class="${pcls}">
${Object.values(metadata)
.map(imageFormat => {
return ` <source type="${imageFormat[0].sourceType}" srcset="${imageFormat
.map(entry => entry.srcset)
.join(', ')}" sizes="${sizes}">`;
})
.join('\n')}
<img
src="${lowsrc.url}"
class="${cls}"
width="${lowsrc.width}"
height="${lowsrc.height}"
alt="${alt}"
loading="${loading}"
decoding="async">
</picture>`;
};
module.exports = imageShortcode;

View file

@ -0,0 +1,74 @@
const Image = require('@11ty/eleventy-img');
const path = require('path');
const htmlmin = require('html-minifier');
const imageShortcodePlaceholder = async (
src,
fcls,
pcls,
cls,
alt,
caption,
sizes = '100vw'
) => {
if (!alt) {
throw new Error(`Missing \`alt\` on myImage from: ${src}`);
}
let metadata = await Image(src, {
widths: [400, 700, 1280],
formats: ['avif', 'webp', 'jpeg'],
urlPath: '/_assets/images/',
outputDir: './dist/_assets/images/',
filenameFormat: function (id, src, width, format, options) {
const extension = path.extname(src);
const name = path.basename(src, extension);
return `${name}-${width}w.${format}`;
}
});
let lowsrc = metadata.jpeg[0];
// getting the url to use
let imgSrc = src;
if (!imgSrc.startsWith('.')) {
const inputPath = this.page.inputPath;
const pathParts = inputPath.split('/');
pathParts.pop();
imgSrc = pathParts.join('/') + '/' + src;
}
return htmlmin.minify(
`<figure class="${fcls}">
<picture class="${pcls}">
${Object.values(metadata)
.map(imageFormat => {
return ` <source type="${imageFormat[0].sourceType}" srcset="${imageFormat
.map(entry => entry.srcset)
.join(', ')}" sizes="${sizes}">`;
})
.join('\n')}
<img
src="/_assets/images/image-placeholder.png"
data-src="${lowsrc.url}"
class="${cls}"
width="${lowsrc.width}"
height="${lowsrc.height}"
alt="${alt}"
loading = 'lazy'
decoding="async">
</picture>
${
caption
? `<figcaption class="cluster font-display"><p>${caption}</p> <img
src="/_assets/svg/arrow.svg" alt="Arrow icon" width="78" height="75" aria-hidden="true" />
</figcaption>`
: ``
}
</figure>`,
{collapseWhitespace: true}
);
};
module.exports = imageShortcodePlaceholder;

View file

@ -0,0 +1,12 @@
const imageShortcode = require('./image');
const imageShortcodePlaceholder = require('./imagePlaceholder');
const insertionShortcode = require('./insertion');
const asideShortcode = require('./aside');
const liteYoutube = require('./youtube-lite');
module.exports = {
imageShortcode,
imageShortcodePlaceholder,
insertionShortcode,
asideShortcode,
liteYoutube
};

View file

@ -0,0 +1,30 @@
const {outdent} = require('outdent');
const markdownLib = require('../../plugins/markdown');
// this is not working yet
const insertion = (img, figcaption, alt, children) => {
if (!img) {
throw new Error(`Must have an image-path.`);
}
if (!alt) {
throw new Error('Images must have an alt text.');
}
if (!figcaption) {
throw new Error('Must have a figcaption');
}
if (!children) {
throw new Error('add a content.');
}
const content = markdownLib.render(children.trim());
return outdent`<div class="switcher py-size-1 breakout">
{% imagePlaceholder "./src/_assets/images/${img}", "bg-transparent pt-size-0", "
h-full", "object-cover w-full h-max rounded-tr-2xl", "${figcaption}", "${alt}", "
(max-width: 463px) 400px, (max-width: 718px) 700px, (max-width: 912px) 400px, 700px" %}
<div class="font-display text-size-2 opacity-80">
${content}
</div>
</div>`;
};
module.exports = insertion;

View file

@ -0,0 +1,10 @@
const liteYoutube = (id, label) => {
return `
<div class="youtube-embed"> <lite-youtube videoid="${id}" style="background-image: url('https://i.ytimg.com/vi/${id}/hqdefault.jpg');">
<button type="button" class="lty-playbtn">
<span class="lyt-visually-hidden">${label}</span>
</button>
</lite-youtube></div>
`;
};
module.exports = liteYoutube;

View file

@ -0,0 +1,20 @@
const htmlmin = require('html-minifier');
const isProduction = process.env.ELEVENTY_ENV === 'production';
const compressHTML = (content, outputPath) => {
if (outputPath.endsWith('.html') && isProduction) {
let minified = htmlmin.minify(content, {
useShortDoctype: true,
removeComments: true,
collapseWhitespace: true,
minifyCSS: true,
minifyJS: true
});
return minified;
}
return content;
};
module.exports = {
compressHTML
};

74
config/utils/index.js Normal file
View file

@ -0,0 +1,74 @@
const sanitize = require('sanitize-html');
const slugify = require('slugify');
/**
* Returns an array of all unique values from the given collection under the specified key.
* Credit: https://www.webstoemp.com/blog/basic-custom-taxonomies-with-eleventy/.
* @param {*} collectionItems - an array of collection items to map to their unique values under a key
* @param {*} key - the key to look up in the item's data object
* @returns
*/
const getAllUniqueKeyValues = (collectionItems, key) => {
// First map each collection item (e.g., blog post) to the value it holds under key.
let values = collectionItems.map(item => item.data[key] ?? []);
// Recursively flatten it to a 1D array
values = values.flat();
// Remove duplicates
values = [...new Set(values)];
// Sort alphabetically
values = values.sort((key1, key2) =>
key1.localeCompare(key2, 'en', {sensitivity: 'base'})
);
return values;
};
/** Converts the given string to a slug form. */
const slugifyString = str => {
return slugify(str, {
replacement: '-',
remove: /[#,&,+()$~%.'":*?<>{}]/g,
lower: true
});
};
/** Helper to throw an error if the provided argument is not of the expected. */
const throwIfNotType = (arg, expectedType) => {
if (typeof arg !== expectedType) {
throw new Error(
`Expected argument of type ${expectedType} but instead got ${arg} (${typeof arg})`
);
}
};
/** Maps a config of attribute-value pairs to an HTML string representing those same attribute-value pairs.
* There's also this, but it's ESM only: https://github.com/sindresorhus/stringify-attributes
*/
const stringifyAttributes = attributeMap => {
return Object.entries(attributeMap)
.map(([attribute, value]) => `${attribute}="${value}"`)
.join(' ');
};
/** Sanitizes an HTML string. */
const sanitizeHtml = html => {
return sanitize(html, {
allowedAttributes: {
...sanitize.defaults.allowedAttributes,
// Syntax highlighting
pre: ['class'],
code: ['class'],
span: ['class'],
// Styled lists
ol: ['class'],
ul: ['class']
}
});
};
module.exports = {
getAllUniqueKeyValues,
slugifyString,
throwIfNotType,
stringifyAttributes,
sanitizeHtml
};

23
netlify.toml Normal file
View file

@ -0,0 +1,23 @@
[[plugins]]
package = "@netlify/plugin-a11y"
[plugins.inputs]
# Do not fail the build if a11y issues are found
failWithIssues = false
# Perform a11y check against WCAG 2.1 AAA
wcagLevel = 'WCAG2AAA'
[[headers]]
for = "/*"
[headers.values]
Content-Security-Policy = "upgrade-insecure-requests; block-all-mixed-content;"
X-Content-Type-Options = "nosniff"
X-Frame-Options = "DENY"
X-XSS-Protection = "1; mode=block"
Referrer-Policy = "strict-origin-when-cross-origin"
Permissions-Policy = "autoplay=(), camera=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), publickey-credentials-get=()"
[build]
command = "npm run build"
publish = "dist"

16087
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

74
package.json Normal file
View file

@ -0,0 +1,74 @@
{
"name": "eleventy-excellent",
"version": "1.0.1",
"engines": {
"node": "16.x"
},
"description": "Eleventy starter based on the workflow suggested by Andy Bell's https://buildexcellentwebsit.es/.",
"main": ".eleventy.js",
"scripts": {
"clean": "rimraf dist",
"dev:postcss": "postcss src/_assets/css/global.css --o dist/_assets/css/global.css --watch --verbose",
"dev:scripts": "esbuild src/_assets/scripts/app.js src/_assets/scripts/is-land.js --watch --outdir=dist/_assets",
"dev:11ty": "eleventy --serve --watch",
"build:postcss": "NODE_ENV=production postcss src/_assets/css/global.css -o dist/_assets/css/global.css",
"build:11ty": "cross-env ELEVENTY_ENV=production eleventy",
"build:scripts": "esbuild src/_assets/scripts/app.js src/_assets/scripts/is-land.js --bundle --minify --outdir=dist/_assets --platform=node --tree-shaking=true",
"start": "run-p dev:*",
"build": "run-s clean build:*"
},
"keywords": [],
"repository": {
"type": "git",
"url": "https://github.com/madrilene/eleventy-excellent.git"
},
"author": "Lene Saile",
"license": "ISC",
"dependencies": {
"@11ty/eleventy": "^2.0.0-canary.16",
"@11ty/eleventy-fetch": "^3.0.0",
"@11ty/eleventy-img": "^2.0.1",
"@11ty/eleventy-plugin-syntaxhighlight": "^4.1.0",
"@11ty/eleventy-plugin-webc": "^0.4.0",
"@11ty/is-land": "^2.0.3",
"install": "^0.13.0",
"markdown-it-footnote": "^3.0.3",
"npm": "^8.19.2",
"tailwindcss": "^3.0.23"
},
"devDependencies": {
"@netlify/plugin-a11y": "^1.0.0-beta.1",
"@toycode/markdown-it-class": "^1.2.4",
"clean-css": "^5.3.1",
"concurrently": "^7.4.0",
"cross-env": "^7.0.3",
"cssnano": "^5.1.7",
"dayjs": "^1.11.5",
"esbuild": "^0.15.10",
"eslint": "^8.24.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"glob": "^8.0.3",
"html-minifier": "^4.0.0",
"lodash": "^4.17.21",
"markdown-it": "^13.0.1",
"markdown-it-abbr": "^1.0.4",
"markdown-it-anchor": "^8.6.5",
"markdown-it-emoji": "^2.0.2",
"markdown-it-link-attributes": "^4.0.1",
"markdown-it-mark": "^3.0.1",
"markdown-it-prism": "^2.3.0",
"netlify-plugin-11ty": "^1.1.0",
"npm-run-all": "^4.1.5",
"outdent": "^0.8.0",
"postcss": "^8.4.8",
"postcss-cli": "^10.0.0",
"postcss-import": "^15.0.0",
"postcss-import-ext-glob": "^2.0.1",
"postcss-js": "^4.0.0",
"prettier": "^2.3.2",
"rimraf": "^3.0.2",
"sanitize-html": "^2.7.2",
"slugify": "^1.6.5"
}
}

8
postcss.config.js Normal file
View file

@ -0,0 +1,8 @@
module.exports = {
plugins: [
require('postcss-import-ext-glob'),
require('postcss-import'),
require('tailwindcss'),
require('cssnano')
]
};

93
readme.md Normal file
View file

@ -0,0 +1,93 @@
# Eleventy starter
Very opiniated Eleventy starter based on the workflow suggested by Andy Bell's <https://buildexcellentwebsit.es/>.
- [Eleventy starter](#eleventy-starter)
- [Logbook](#logbook)
- [22-10-03](#22-10-03)
- [22-10-04](#22-10-04)
- [Using this](#using-this)
- [Install dependencies](#install-dependencies)
- [Working locally](#working-locally)
- [Creating a production build](#creating-a-production-build)
- [Credits and Thank yous](#credits-and-thank-yous)
## Logbook
### 22-10-03
- first commit. Updated
### 22-10-04
- All markdown syntax set. Some tests with web components and webC.
- TODO: dark mode
## Using this
### Install dependencies
```
npm install
```
### Working locally
Starts watch tasks to compile when changes detected
```
npm start
```
### Creating a production build
Minify JS, inline and minify CSS.
```
npm run build
```
## Credits and Thank yous
**Heydon Pickering**
I strongly orientate myself on Heydon's approaches and love his books.
- https://every-layout.dev/
- https://inclusive-components.design/
**Andy Bell**
His CSS methodology makes sense to me. I'm still working on understanding it fully and using it consistently. It also goes hand in hand with the Every Layout solutions he co-authors. An ardent opponent of the utility class framework Tailwind CSS. But has recently published an approach that incorporates Tailwind CSS into his methodology. This is built into my website and I'm working on tweaking it.
I learned how to use Eleventy in 2020 with his (now free) course.
- https://cube.fyi/
- https://buildexcellentwebsit.es/
- https://learneleventyfromscratch.com/
**Zach Leatherman**
He is developing Eleventy.
- https://www.11ty.dev/
- https://www.zachleat.com/
**Stephanie Eckles**
She provides a lot of resources for Eleventy and modern CSS.
- https://smolcss.dev/
- https://moderncss.dev/
**Aleksandr Hovhannisyan**
I love order and structure. Aleksandr Hovhannisyan does this in an exemplary way, which is why I based the structure of eleventy.js on his online resume and blog. Also, he writes great tutorials in his blog.
- https://github.com/AleksandrHovhannisyan
**Josh Comeau**
He created a fantastic CSS course that I am currently working through. Also, his in-depth tutorials are a great resource.
- https://www.joshwcomeau.com/tutorials/
- https://courses.joshwcomeau.com/css-for-js

View file

@ -0,0 +1,39 @@
const viewports = require('../design-tokens/viewports.json');
/**
* Takes an array of tokens and sends back and array of name
* and clamp pairs for CSS fluid values.
*
* @param {array} tokens array of {name: string, min: number, max: number}
* @returns {array} {name: string, value: string}
*/
const clampGenerator = tokens => {
const rootSize = 16;
return tokens.map(({name, min, max}) => {
if (min === max) {
return `${min / rootSize}rem`;
}
// Convert the min and max sizes to rems
const minSize = min / rootSize;
const maxSize = max / rootSize;
// Convert the pixel viewport sizes into rems
const minViewport = viewports.min / rootSize;
const maxViewport = viewports.max / rootSize;
// Slope and intersection allow us to have a fluid value but also keep that sensible
const slope = (maxSize - minSize) / (maxViewport - minViewport);
const intersection = -1 * minViewport * slope + minSize;
return {
name,
value: `clamp(${minSize}rem, ${intersection.toFixed(2)}rem + ${(
slope * 100
).toFixed(2)}vw, ${maxSize}rem)`
};
});
};
module.exports = clampGenerator;

View file

@ -0,0 +1,20 @@
const slugify = require('slugify');
/**
* Converts human readable tokens into tailwind config friendly ones
*
* @param {array} tokens {name: string, value: any}
* @return {object} {key, value}
*/
const tokensToTailwind = tokens => {
const nameSlug = text => slugify(text, {lower: true});
let response = {};
tokens.forEach(({name, value}) => {
response[nameSlug(name)] = value;
});
return response;
};
module.exports = tokensToTailwind;

View file

@ -0,0 +1 @@
/* A blank block because there is *always* a button */

View file

@ -0,0 +1,12 @@
.card {
background: var(--color-dark);
color: var(--color-light);
padding: var(--space-m-l);
border-radius: var(--border-radius);
max-width: unset;
}
.card ::selection {
color: var(--color-dark);
background: var(--color-secondary);
}

View file

@ -0,0 +1,245 @@
code,
pre {
padding: 0.125em 0.4em;
font-family: var(--font-mono);
background: var(--color-primary);
border-radius: var(--border-radius);
color: var(--color-light);
font-size: var(--size-step-0);
}
pre[class*='language-'] {
padding: var(--space-s-m);
}
code[class*='language-'] {
padding: 0;
}
code[class*='language-'],
pre[class*='language-'] {
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
color: var(--color-light);
background: var(--color-dark);
line-height: 1.5;
border-radius: var(--border-radius);
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
code[class*='language-']::-moz-selection,
pre[class*='language-']::-moz-selection,
code[class*='language-'] ::-moz-selection,
pre[class*='language-'] ::-moz-selection {
background: var(--color-dark);
}
code[class*='language-']::selection,
pre[class*='language-']::selection,
code[class*='language-'] ::selection,
pre[class*='language-'] ::selection {
background: var(--color-dark);
}
:not(pre) > code[class*='language-'] {
white-space: normal;
border-radius: var(--border-radius);
padding: 0.1em;
}
pre[class*='language-'] {
overflow: auto;
position: relative;
}
.language-css > code,
.language-sass > code,
.language-scss > code {
color: #fd9170;
}
[class*='language-'] .namespace {
opacity: 0.7;
}
.token.atrule {
color: #c792ea;
}
.token.attr-name {
color: #ffcb6b;
}
.token.attr-value {
color: #c3e88d;
}
.token.attribute {
color: #c3e88d;
}
.token.boolean {
color: #c792ea;
}
.token.builtin {
color: #ffcb6b;
}
.token.cdata {
color: #80cbc4;
}
.token.char {
color: #80cbc4;
}
.token.class {
color: #ffcb6b;
}
.token.class-name {
color: #f2ff00;
}
.token.color {
color: #f2ff00;
}
.token.comment {
color: #779daf;
}
.token.constant {
color: #c792ea;
}
.token.deleted {
color: #f07178;
}
.token.doctype {
color: #546e7a;
}
.token.entity {
color: #f07178;
}
.token.function {
color: #c792ea;
}
.token.hexcode {
color: #f2ff00;
}
.token.id {
color: #c792ea;
font-weight: bold;
}
.token.important {
color: #c792ea;
font-weight: bold;
}
.token.inserted {
color: #80cbc4;
}
.token.keyword {
color: #c792ea;
font-style: italic;
}
.token.number {
color: #fd9170;
}
.token.operator {
color: #89ddff;
}
.token.prolog {
color: #546e7a;
}
.token.property {
color: #80cbc4;
}
.token.pseudo-class {
color: #c3e88d;
}
.token.pseudo-element {
color: #c3e88d;
}
.token.punctuation {
color: #89ddff;
}
.token.regex {
color: #f2ff00;
}
.token.selector {
color: #f07178;
}
.token.string {
color: #c3e88d;
}
.token.symbol {
color: #c792ea;
}
.token.tag {
color: #f07178;
}
.token.unit {
color: #f07178;
}
.token.url {
color: #fd9170;
}
.token.variable {
color: #f07178;
}
.codepen {
padding: var(--space-sm);
color: var(--color-text-accent);
border: 2px dashed var(--color-bg-accent);
}
.cp_embed_wrapper {
position: relative;
overflow: auto;
display: grid;
place-items: center;
grid-template-areas: 'container';
resize: horizontal;
}
.cp_embed_wrapper iframe {
grid-area: container;
width: 100%;
}

View file

@ -0,0 +1,6 @@
.curve {
display: block;
height: 3.5em;
width: 100%;
fill: var(--spot-color, var(--color-light));
}

View file

@ -0,0 +1,24 @@
.features {
--grid-placement: auto-fit;
--grid-min-item-size: clamp(16rem, 33%, 20rem);
--gutter: var(--space-l-xl);
--flow-space: var(--space-s);
text-align: center;
}
.features svg {
display: block;
margin-inline: auto;
height: 4em;
}
.features a {
text-decoration: none;
}
.features a:hover {
text-decoration: underline;
text-decoration-thickness: 0.08ex;
text-underline-offset: 0.2ex;
}

View file

@ -0,0 +1,64 @@
details {
padding: 1em;
background-color: #eee;
border-radius: 0.5em;
}
details:not(:last-child) {
margin-bottom: 0.5em;
}
p:first-child {
margin-top: 0;
}
body {
padding-bottom: 20vh;
}
/* Demo styles */
is-land,
.demo-component {
display: block;
padding: 0.5em;
margin: 0.5em 0;
outline: 2px solid lightblue;
}
is-land:last-child,
.demo-component:last-child {
margin-bottom: 0;
}
is-land:first-child,
.demo-component:first-child {
margin-top: 0;
}
is-land[on\:idle],
is-land[on\:idle][ready] {
display: inline-flex;
gap: 0.25em;
align-items: center;
}
is-land span {
display: inline-flex;
width: 2em;
margin: 0 0.25em;
}
/* Demo loading states */
is-land[ready] {
display: block;
background-color: rgba(114, 233, 110, 0.2);
outline: 2px solid rgb(85, 173, 82);
}
.test-c-finish {
background-color: rgba(112, 110, 233, 0.2);
outline: 2px solid rgb(97, 82, 173);
}
/* List logos */
.examples {
line-height: 2;
}
.examples img {
margin-right: 0.3em;
width: 1.5em;
height: 1.5em;
vertical-align: text-bottom;
}

View file

@ -0,0 +1,119 @@
.logo {
font-weight: bold;
text-transform: uppercase;
margin-block: var(--space-xs);
display: flex;
align-items: center;
gap: var(--space-xs);
}
@media (min-width: 48em) {
nav.navbar {
--nav-button-display: none;
--nav-position: static;
}
nav.navbar ul {
--nav-list-background: transparent;
--nav-list-layout: row;
--nav-list-position: static;
--nav-list-padding: 0;
--nav-list-height: auto;
--nav-list-width: 100%;
--nav-list-shadow: none;
--nav-list-transform: none;
--nav-list-visibility: visible;
}
}
/* set on parent div to get the right z-index context */
.ontop {
z-index: 1;
position: relative;
}
nav.navbar {
position: var(--nav-position, fixed);
/* inset-block-start: 0.5rem; */
inset-inline-end: 1rem;
}
/* Remove default list styling and create layout for list */
nav.navbar ul {
background: var(--nav-list-background, var(--color-light));
box-shadow: var(--nav-list-shadow, -5px 0 11px 0 rgb(0 0 0 / 0.2));
display: flex;
flex-direction: var(--nav-list-layout, column);
flex-wrap: wrap;
gap: 0.9rem;
height: var(--nav-list-height, 100vh);
list-style: none;
margin: 0;
padding: var(--nav-list-padding, 2rem);
position: var(--nav-list-position, fixed);
inset-block-start: 0; /* Logical property. Equivalent to top: 0; */
inset-inline-end: 0; /* Logical property. Equivalent to right: 0; */
width: var(--nav-list-width, min(22rem, 100vw));
visibility: var(--nav-list-visibility, visible);
}
nav.navbar [aria-expanded='false'] + ul {
transform: var(--nav-list-transform, translateX(100%));
visibility: var(--nav-list-visibility, hidden);
}
/* animates ul only when opening to avoid flash on page load, svg always */
@media (prefers-reduced-motion: no-preference) {
nav.navbar [aria-expanded='true'] + ul,
nav.navbar svg {
transition: transform 0.4s cubic-bezier(0.68, -0.55, 0.27, 1.55),
visibility 0.05s linear;
}
}
/* Basic link styling */
nav.navbar a {
--text-color: var(--color-dark);
border-block-end: 3px solid var(--border-color, transparent);
color: var(--text-color);
padding: 0.1rem;
text-decoration: none;
}
/* Change the border-color on :hover and :focus */
nav.navbar a:where(:hover, :focus) {
--border-color: var(--text-color);
}
/* Change border-color and color for the active page */
nav.navbar [aria-current='page'] {
--border-color: var(--color-primary);
--text-color: var(--color-primary);
}
/* Reset button styling */
nav.navbar button {
all: unset;
display: var(--nav-button-display, flex);
align-items: center;
position: relative;
z-index: 2;
cursor: pointer;
align-items: center;
}
nav.navbar span {
font-size: var(--size-step-0);
font-weight: bold;
text-transform: uppercase;
padding-inline-end: var(--space-xs);
}
nav.navbar svg {
height: 100%;
width: auto;
}
nav.navbar [aria-expanded='true'] svg {
transform: var(--nav-list-rotate, rotate(45deg));
}

View file

@ -0,0 +1,37 @@
.prose {
--flow-space: var(--space-l-xl);
--wrapper-max-width: 55rem;
}
.prose :is(h2, h3, h4) + * {
--flow-space: var(--space-s-m);
}
.prose blockquote {
border-inline-start: 10px solid var(--color-secondary);
padding: var(--space-m-l);
font-family: var(--font-serif);
font-style: italic;
font-size: var(--size-step-2);
}
.prose blockquote > * + * {
margin-top: var(--space-m-l);
}
.prose blockquote :last-child {
font-family: var(--font-base);
font-style: normal;
font-size: var(--size-step-1);
}
.prose .heading-anchor:is(:hover, :focus) {
text-decoration: underline;
}
.prose .heading-anchor {
text-decoration: none;
}
.prose mark {
background: var(--color-primary-glare);
}

View file

@ -0,0 +1,19 @@
.section > .curve:first-child {
transform: rotate(180deg) translateY(-1px);
}
.section__inner {
background: var(--spot-color, var(--color-dark));
color: var(--color-light);
}
.section blockquote {
font-weight: 700;
line-height: 1;
font-size: var(--size-step-4);
letter-spacing: var(--tracking-s);
}
.section :is(h1, h2, h3, blockquote) {
opacity: 95%;
}

View file

@ -0,0 +1,14 @@
.signoff h2 {
font-weight: 400;
font-size: var(--size-step-1);
max-width: 30ch;
letter-spacing: var(--tracking);
}
.signoff p {
font-size: var(--size-step-6);
font-weight: 700;
letter-spacing: var(--tracking-s);
line-height: 1;
color: var(--color-primary);
}

View file

@ -0,0 +1,16 @@
.site-foot {
padding: var(--space-s-m);
background: var(--color-dark);
color: var(--color-light);
}
.site-foot__inner {
display: flex;
gap: var(--space-s-m);
align-items: center;
}
.site-foot svg {
width: 3em;
height: 3em;
}

View file

@ -0,0 +1,34 @@
.skip-link {
clip: rect(1px, 1px, 1px, 1px);
display: block;
height: 1px;
overflow: hidden;
position: absolute;
width: 1px;
top: 1rem;
left: 1rem;
z-index: 999;
}
.skip-link:focus {
clip: auto;
height: auto;
overflow: visible;
width: auto;
background-color: var(--color-dark);
color: var(--color-light);
padding: var(--space-s-m);
line-height: 1;
}
.skip-link:not(:focus) {
border: 0;
clip: rect(0 0 0 0);
height: auto;
margin: 0;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
white-space: nowrap;
}

View file

@ -0,0 +1,11 @@
.cluster > * {
display: flex;
flex-wrap: wrap;
/* ↓ multiply by -1 to negate the halved value */
margin: calc(var(--space-s-l) / 2 * -1);
}
.cluster > * > * {
/* ↓ half the value, because of the 'doubling up' */
margin: calc(var(--space-s-l) / 2);
}

View file

@ -0,0 +1,33 @@
/* AUTO GRID
Related Every Layout: https://every-layout.dev/layouts/grid/
More info on the flexible nature: https://piccalil.li/tutorial/create-a-responsive-grid-layout-with-no-media-queries-using-css-grid/
A flexible layout that will create an auto-fill grid with
configurable grid item sizes
CUSTOM PROPERTIES AND CONFIGURATION
--gutter (var(--space-s-m)): This defines the space
between each item.
--grid-min-item-size (14rem): How large each item should be
ideally, as a minimum.
--grid-placement (auto-fill): Set either auto-fit or auto-fill
to change how empty grid tracks are handled */
.grid {
display: grid;
grid-template-columns: repeat(
var(--grid-placement, auto-fill),
minmax(var(--grid-min-item-size, 16rem), 1fr)
);
gap: var(--gutter, var(--space-s-l));
}
.grid[data-rows='masonry'] {
grid-template-rows: masonry;
align-items: start;
}
.grid[data-layout='50-50'] {
--grid-placement: auto-fit;
--grid-min-item-size: clamp(16rem, 50vw, 26rem);
}

View file

@ -0,0 +1,44 @@
/*
SIDEBAR
More info: https://every-layout.dev/layouts/sidebar/
A layout that allows you to have a flexible main content area
and a "fixed" width sidebar that sits on the left or right.
If there is not enough viewport space to fit both the sidebar
width *and* the main content minimum width, they will stack
on top of each other
CUSTOM PROPERTIES AND CONFIGURATION
--gutter (var(--space-size-1)): This defines the space
between the sidebar and main content.
--sidebar-target-width (20rem): How large the sidebar should be
--sidebar-content-min-width(50%): The minimum size of the main content area
EXCEPTIONS
.sidebar[data-direction='rtl']: flips the sidebar to be on the right
*/
.sidebar {
display: flex;
flex-wrap: wrap;
gap: var(--gutter, var(--space-s-l));
align-items: center;
}
.sidebar > :first-child {
flex-basis: var(--sidebar-target-width, 20rem);
flex-grow: 1;
}
.sidebar > :last-child {
flex-basis: 0;
flex-grow: 999;
min-width: var(--sidebar-content-min-width, 50%);
}
/*
A flipped version where the sidebar is on the right
*/
.sidebar[data-direction='rtl'] {
flex-direction: row-reverse;
}

View file

@ -0,0 +1,15 @@
@import 'tailwindcss/base';
@import 'global/reset.css';
@import 'global/fonts.css';
@import 'tailwindcss/components';
@import 'global/variables.css';
@import 'global/global-styles.css';
@import-glob 'blocks/*.css';
@import-glob 'compositions/*.css';
@import-glob 'utilities/*.css';
@import 'tailwindcss/utilities';

View file

@ -0,0 +1,47 @@
@font-face {
font-family: Outfit;
font-style: normal;
font-display: swap;
font-weight: 400;
src: local(''), url(/_assets/fonts/outfit/outfit-v5-latin-regular.woff2) format('woff2'),
url(/assets/fonts/outfit/outfit-v5-latin-regular.woff) format('woff');
}
@font-face {
font-family: Outfit;
font-style: normal;
font-display: swap;
font-weight: 700;
src: local(''),
url(/_assets/fonts/outfit/outfit-v5-latin-700-webfont.woff2) format('woff2'),
url(/assets/fonts/outfit/outfit-v5-latin-700-webfont.woff) format('woff');
}
@font-face {
font-family: Charter;
font-style: normal;
font-display: swap;
font-weight: 400;
src: local(''), url(/_assets/fonts/charter/charter_regular.woff2) format('woff2'),
url(/assets/fonts/charter/charter_regular.woff) format('woff');
}
@font-face {
font-family: Charter;
font-style: normal;
font-display: swap;
font-weight: 700;
src: local(''), url(/_assets/fonts/charter/charter_bold.woff2) format('woff2'),
url(/assets/fonts/charter/charter_bold.woff) format('woff');
}
@font-face {
font-family: 'RobotoMono';
font-weight: normal;
font-style: normal;
font-display: swap;
src: local(''),
url(/_assets/fonts/robotomono/robotomono-variablefont_wght-webfont.woff2)
format('woff2'),
url(/_assets/fonts/robotomono/robotomono-variablefont_wght-webfont.woff)
format('woff');
}

View file

@ -0,0 +1,118 @@
/*
Global styles
Low-specificity, global styles that apply to the whole
project: https://cube.fyi/css.html
*/
body {
color: var(--color-dark);
background: var(--color-light);
font-size: var(--size-step-1);
font-family: var(--font-base);
line-height: 1.4;
letter-spacing: var(--tracking);
}
/* Base typesetting */
h1,
h2,
h3 {
line-height: 1;
letter-spacing: var(--tracking-s);
}
h1 {
font-size: var(--size-step-5);
}
h2 {
font-size: var(--size-step-4);
}
h3 {
font-size: var(--size-step-3);
}
/* Set line lengths */
p,
li,
blockquote:not([class]) {
max-width: 50ch;
}
h1,
h2,
h3 {
max-width: 20ch;
}
/* More generic elements */
blockquote:not([class]) {
font-family: var(--font-serif);
font-size: var(--size-step-2);
}
/* Markdown blockquuote:not([class])s aren't ideal, so we're presuming the person quoted is the last p */
blockquote:not([class]) p:last-of-type {
font-family: var(--font-base);
font-size: var(--size-step-1);
font-weight: normal;
}
svg {
height: 2ex;
width: auto;
flex: none;
}
[role='list'] {
padding: 0;
}
/* Base interactive elements */
a {
color: currentcolor;
}
a:hover {
text-decoration: none;
}
:focus {
outline: 2px solid;
outline-offset: 0.3ch;
}
:target {
scroll-margin-top: 2ex;
}
main:focus {
outline: none;
}
article [href^='http']:not([href*='lene.dev'])::after {
display: inline-block;
width: 1em;
height: 1em;
background-image: url('/_assets/images/icn-external.svg');
background-repeat: no-repeat;
background-position: center;
background-size: 60% auto;
/* alternative text rules */
content: '(external link)';
overflow: hidden;
white-space: nowrap;
text-indent: 1em; /* the width of the icon */
}
/* Base selection styles that invert whatever colours are rendered */
::selection {
background: var(--color-dark);
color: var(--color-light);
}
.preload-transitions * {
transition: none !important;
}

View file

@ -0,0 +1,66 @@
/* Modern reset: https://piccalil.li/blog/a-modern-css-reset/ */
/* Box sizing rules */
*,
*::before,
*::after {
box-sizing: border-box;
}
/* Remove default margin */
body,
h1,
h2,
h3,
h4,
p,
figure,
blockquote,
dl,
dd {
margin: 0;
}
/* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */
ul[role='list'],
ol[role='list'] {
list-style: none;
}
/* Prevent zooming when orientation changes on some iOS devices */
html {
text-size-adjust: none;
-webkit-text-size-adjust: none;
}
/* Set core root defaults */
html:focus-within {
scroll-behavior: smooth;
}
/* Set core body defaults */
body {
min-height: 100vh;
text-rendering: optimizeSpeed;
line-height: 1.5;
}
/* A elements that don't have a class get default styles */
a:not([class]) {
text-decoration-skip-ink: auto;
}
/* Make images easier to work with */
img,
picture {
max-width: 100%;
display: block;
}
/* Inherit fonts for inputs and buttons */
input,
button,
textarea,
select {
font: inherit;
}

View file

@ -0,0 +1,11 @@
/* Global variables */
:root {
--gutter: var(--space-s-m);
--border-radius: var(--size-step-1);
--transition-base: 250ms ease;
--transition-movement: 200ms linear;
--transition-fade: 200ms ease;
--transition-bounce: 500ms cubic-bezier(0.5, 0.05, 0.2, 1.5);
--tracking: -0.05ch;
--tracking-s: -0.075ch;
}

View file

@ -0,0 +1,8 @@
/*
FLOW UTILITY
Like the Every Layout stack: https://every-layout.dev/layouts/stack/
Info about this implementation: https://piccalil.li/quick-tip/flow-utility/
*/
.flow > * + * {
margin-top: var(--flow-space, 1em);
}

View file

@ -0,0 +1,9 @@
/**
* REGION
* Add consistent vertical padding to create regions of content
* Can either be configured by setting --region-space or use a default from the space scale
*/
.region {
padding-top: var(--region-space, var(--space-l-2xl));
padding-bottom: var(--region-space, var(--space-l-2xl));
}

View file

@ -0,0 +1,14 @@
/**
* WRAPPER
* Sets a max width, adds a consistent gutter and horizontally
* centers the contents
* Info: https://piccalil.li/quick-tip/use-css-clamp-to-create-a-more-flexible-wrapper-utility/
*/
.wrapper {
max-width: var(--wrapper-max-width, 85rem);
margin-left: auto;
margin-right: auto;
padding-left: var(--gutter);
padding-right: var(--gutter);
/* position: relative; */
}

View file

@ -0,0 +1,34 @@
{
"title": "Colors",
"description": "Hex color codes that can be shared, cross-platform. They can be converted at point of usage, such as HSL for web or CMYK for print.",
"items": [
{
"name": "Dark",
"value": "#404040"
},
{
"name": "Light",
"value": "#F3F3F3"
},
{
"name": "Light Glare",
"value": "#FFFFFF"
},
{
"name": "Primary",
"value": "#FF5678"
},
{
"name": "Primary Glare",
"value": "#F4D0DF"
},
{
"name": "Secondary",
"value": "#0369A1"
},
{
"name": "Secondary Glare",
"value": "#D8E2F4"
}
]
}

View file

@ -0,0 +1,21 @@
{
"title": "Fonts",
"description": "Each array of fonts creates a priority-based order. The first font in the array should be the ideal font, followed by sensible, web-safe fallbacks",
"items": [
{
"name": "Base",
"description": "System fonts for body copy and globally set text.",
"value": ["Outfit", "Segoe UI", "Roboto", "Helvetica Neue", "Arial", "sans-serif"]
},
{
"name": "Serif",
"description": "Expressive sections, like quotes",
"value": ["Charter", "serif"]
},
{
"name": "Mono",
"description": "Expressive sections, like quotes",
"value": ["RobotoMono", "monospace"]
}
]
}

View file

@ -0,0 +1,79 @@
{
"title": "Spacing",
"description": "Consistent spacing sizes, based on a ratio, with min and max sizes. This allows you to set spacing based on the context size. For example, min for mobile and max for desktop browsers.",
"meta": {
"scaleGenerator": "https://utopia.fyi/space/calculator?c=320,16,1.2,1350,20,1.414,8,1,&s=0.75|0.5|0.25,2|3|5|8|13,s-l"
},
"items": [
{
"name": "XS",
"min": 12,
"max": 15
},
{
"name": "S",
"min": 16,
"max": 20
},
{
"name": "M",
"min": 24,
"max": 30
},
{
"name": "L",
"min": 32,
"max": 40
},
{
"name": "XL",
"min": 48,
"max": 60
},
{
"name": "2XL",
"min": 64,
"max": 80
},
{
"name": "3XL",
"min": 96,
"max": 120
},
{
"name": "XS - S",
"min": 12,
"max": 20
},
{
"name": "S - M",
"min": 16,
"max": 30
},
{
"name": "M - L",
"min": 24,
"max": 40
},
{
"name": "L - XL",
"min": 32,
"max": 60
},
{
"name": "L - 2xl",
"min": 32,
"max": 80
},
{
"name": "XL - 2XL",
"min": 48,
"max": 80
},
{
"name": "2XL - 3XL",
"min": 64,
"max": 120
}
]
}

View file

@ -0,0 +1,44 @@
{
"title": "Text Sizes",
"description": "A minimum and maximum text size size allows you to pick the right size from a ratio, depending on the context size. The min and max sizes are in pixels and should be converted to appropiate sizes, per context. For example, for web, the should be converted to REM units (pixelSize / baseSize)",
"meta": {
"scaleGenerator": "https://utopia.fyi/type/calculator/?c=320,16,1.2,1350,20,1.414,8,1,&s=0.75|0.5|0.25,1.5|2|3|4|6,s-l"
},
"items": [
{
"name": "Step 0",
"min": 16,
"max": 20
},
{
"name": "Step 1",
"min": 19,
"max": 28
},
{
"name": "Step 2",
"min": 23,
"max": 40
},
{
"name": "Step 3",
"min": 28,
"max": 57
},
{
"name": "Step 4",
"min": 33,
"max": 80
},
{
"name": "Step 5",
"min": 40,
"max": 113
},
{
"name": "Step 6",
"min": 48,
"max": 159
}
]
}

View file

@ -0,0 +1,6 @@
{
"title": "Viewports",
"description": "The min and maximum viewports used to generate fluid type and space scales.",
"min": 320,
"max": 1350
}

28
src/_assets/filters/md.js Normal file
View file

@ -0,0 +1,28 @@
const md = require('markdown-it')();
/**
* Render content as inline markdown if single line, or full
* markdown if multiline
* @param {string} [content]
* @param {import('markdown-it').Options} [opts]
* @return {string|undefined}
*/
module.exports = (content, opts) => {
if (!content) {
return;
}
if (opts) {
md.set(opts);
}
let inline = !content.includes('\n');
// If there's quite a bit of content, we want to make sure
// it's marked up for readability purposes
if (inline && content.length > 200) {
inline = false;
}
return inline ? md.renderInline(content) : md.render(content);
};

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,12 @@
---
permalink: /_redirects
eleventyExcludeFromCollections: true
excludeFromSitemap: true
---
{% for page in collections.all %}
{% if page.url and page.data.redirectFrom %}
{% for oldUrl in page.data.redirectFrom %}
{{ oldUrl }} {{ page.url }}
{% endfor %}
{% endif %}
{% endfor %}

View file

@ -0,0 +1,6 @@
---
permalink: /google35901daa0ceb7399.html
eleventyExcludeFromCollections: true
excludeFromSitemap: true
---
google-site-verification: google35901daa0ceb7399.html

View file

@ -0,0 +1,14 @@
---
permalink: /humans.txt
eleventyExcludeFromCollections: true
excludeFromSitemap: true
---
/* TEAM */
Developer: {{ meta.author }}
Contact: {{ meta.authorEmail }}
Site: {{ meta.siteURL }}
{% if meta.meta_data.twitterCreator %}Twitter: {{ meta.meta_data.twitterCreator }}{% endif %}
/* SITE */
Doctype: HTML5

View file

@ -0,0 +1,9 @@
---
permalink: /robots.txt
eleventyExcludeFromCollections: true
excludeFromSitemap: true
---
User-agent: *
Disallow: /404.html
Disallow: /google35901daa0ceb7399.html
Sitemap: {{ meta.siteURL }}/sitemap.xml

View file

@ -0,0 +1,21 @@
---
permalink: /sitemap.xml
eleventyExcludeFromCollections: true
excludeFromSitemap: true
---
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
{% for page in collections.all %}
{% if page.url and page.data.excludeFromSitemap != true %}
{% if page.data.lastUpdated %}{% set lastmod = page.data.lastUpdated %}
{% else %}{% set lastmod = page.date %}
{% endif %}
<url>
<loc>{{ meta.siteURL }}{{ page.url }}</loc>
<lastmod>{{ lastmod | toIsoString }}</lastmod>
</url>
{% endif %}
{% endfor %}
</urlset>

View file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 871 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 100 100"><text x="50%" y="50%" dominant-baseline="central" text-anchor="middle" font-size="90">🧡</text></svg>

After

Width:  |  Height:  |  Size: 190 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -0,0 +1,26 @@
{
"name": "Lene Saile - Frontend developer",
"short_name": "Lene Saile",
"start_url": "https://www.lene.dev",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "/maskable.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#FF7F5C",
"background_color": "#FF7F5C",
"display": "standalone"
}

View file

@ -0,0 +1,10 @@
<svg class="w-6 h-6" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<title>indicates an external link</title>
<path
d="M5.63605 18.364L18.364 5.63603M18.364 5.63603L8.46446 5.63604M18.364 5.63603V15.5355"
stroke="#888"
stroke-width="3"
stroke-linecap="round"
stroke-linejoin="round"
></path>
</svg>

After

Width:  |  Height:  |  Size: 346 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 B

BIN
src/_assets/images/lene.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

View file

@ -0,0 +1,64 @@
'use strict';
// ------------------- cards redundant click, accessible whole card clickable solution by Heydon Pickering
const cards = [...document.querySelectorAll('.card')];
cards.forEach(card => {
card.style.cursor = 'pointer';
let down,
up,
link = card.querySelector('a');
card.onmousedown = () => (down = +new Date());
card.onmouseup = () => {
up = +new Date();
if (up - down < 200) {
link.click();
}
};
});
// ------------------- responsive accessible nav
const nav = document.querySelector('nav');
const list = nav.querySelector('ul');
const burgerClone = document.querySelector('#burger-template').content.cloneNode(true);
const svg = nav.querySelector('svg');
const button = burgerClone.querySelector('button');
button.addEventListener('click', e => {
const isOpen = button.getAttribute('aria-expanded') === 'false';
button.setAttribute('aria-expanded', isOpen);
});
// avoid DRY: disabling menu
const disableMenu = () => {
button.setAttribute('aria-expanded', false);
button.focus();
};
// close on escape
nav.addEventListener('keyup', e => {
if (e.code === 'Escape') {
disableMenu();
}
});
// close if clicked outside of event target
document.addEventListener('click', e => {
const isClickInsideElement = nav.contains(e.target);
if (!isClickInsideElement) {
disableMenu();
}
});
nav.insertBefore(burgerClone, list);
// ------------------- web components
document.querySelectorAll('img').forEach(img => {
if (img.complete) {
img.removeAttribute('data-is-loading');
return;
}
img.addEventListener('load', () => img.removeAttribute('data-is-loading'));
});

View file

@ -0,0 +1,341 @@
const islandOnceCache = new Map();
class Island extends HTMLElement {
static tagName = 'is-land';
static fallback = {
':not(:defined)': (readyPromise, node, prefix) => {
// remove from document to prevent web component init
let cloned = document.createElement(prefix + node.localName);
for (let attr of node.getAttributeNames()) {
cloned.setAttribute(attr, node.getAttribute(attr));
}
let children = Array.from(node.childNodes);
for (let child of children) {
cloned.append(child); // Keep the *same* child nodes, clicking on a details->summary child should keep the state of that child
}
node.replaceWith(cloned);
return readyPromise.then(() => {
// restore children (not cloned)
for (let child of Array.from(cloned.childNodes)) {
node.append(child);
}
cloned.replaceWith(node);
});
}
};
static autoinit = {
'petite-vue': function (library) {
library.createApp().mount(this);
},
'vue': function (library) {
library.createApp().mount(this);
},
'svelte': function (mod) {
new mod.default({target: this});
},
'svelte-ssr': function (mod) {
new mod.default({target: this, hydrate: true});
},
'preact': function (mod) {
mod.default(this);
}
};
constructor() {
super();
this.attrs = {
autoInitType: 'autoinit',
import: 'import',
template: 'data-island',
ready: 'ready'
};
this.conditionMap = {
'visible': Conditions.visible,
'idle': Conditions.idle,
'interaction': Conditions.interaction,
'media': Conditions.media,
'save-data': Conditions.saveData
};
// Internal promises
this.ready = new Promise((resolve, reject) => {
this.readyResolve = resolve;
this.readyReject = reject;
});
}
static getParents(el, selector, stopAt = false) {
let nodes = [];
while (el) {
if (el.matches && el.matches(selector)) {
if (stopAt && el === stopAt) {
break;
}
nodes.push(el);
}
el = el.parentNode;
}
return nodes;
}
static async ready(el) {
let parents = Island.getParents(el, Island.tagName);
let imports = await Promise.all(parents.map(el => el.wait()));
// return innermost module import
if (imports.length) {
return imports[0];
}
}
async forceFallback() {
let prefix = Island.tagName + '--';
let promises = [];
if (window.Island) {
Object.assign(Island.fallback, window.Island.fallback);
}
for (let selector in Island.fallback) {
// Reverse here as a cheap way to get the deepest nodes first
let components = Array.from(this.querySelectorAll(selector)).reverse();
// with thanks to https://gist.github.com/cowboy/938767
for (let node of components) {
if (!node.isConnected || node.localName === Island.tagName) {
continue;
}
let readyPromise = Island.ready(node);
promises.push(Island.fallback[selector](readyPromise, node, prefix));
}
}
return promises;
}
wait() {
return this.ready;
}
getConditions() {
let map = {};
for (let key of Object.keys(this.conditionMap)) {
if (this.hasAttribute(`on:${key}`)) {
map[key] = this.getAttribute(`on:${key}`);
}
}
return map;
}
async connectedCallback() {
// Keep fallback content without initializing the components
// TODO improvement: only run this for not-eager components?
await this.forceFallback();
await this.hydrate();
}
getTemplates() {
return this.querySelectorAll(`:scope template[${this.attrs.template}]`);
}
replaceTemplates(templates) {
// replace <template> with the live content
for (let node of templates) {
if (Island.getParents(node, Island.tagName, this).length > 0) {
continue;
}
let value = node.getAttribute(this.attrs.template);
// get rid of the rest of the content on the island
if (value === 'replace') {
let children = Array.from(this.childNodes);
for (let child of children) {
this.removeChild(child);
}
this.appendChild(node.content);
break;
} else {
if (value === 'once' && node.innerHTML) {
if (islandOnceCache.has(node.innerHTML)) {
node.remove();
return;
}
islandOnceCache.set(node.innerHTML, true);
}
node.replaceWith(node.content);
}
}
}
async hydrate() {
let conditions = [];
if (this.parentNode) {
// wait for all parents before hydrating
conditions.push(Island.ready(this.parentNode));
}
let attrs = this.getConditions();
for (let condition in attrs) {
if (this.conditionMap[condition]) {
conditions.push(this.conditionMap[condition](attrs[condition], this));
}
}
// Loading conditions must finish before dependencies are loaded
await Promise.all(conditions);
this.replaceTemplates(this.getTemplates());
let mod;
// [dependency="my-component-code.js"]
let importScript = this.getAttribute(this.attrs.import);
if (importScript) {
// we could resolve import maps here manually but youd still have to use the full URL in your scripts import anyway
mod = await import(importScript);
}
if (mod) {
// Use `import=""` for when import maps are available e.g. `import="petite-vue"`
let fn =
Island.autoinit[this.getAttribute(this.attrs.autoInitType) || importScript];
if (fn) {
await fn.call(this, mod);
}
}
this.readyResolve({
import: mod
});
this.setAttribute(this.attrs.ready, '');
}
}
class Conditions {
static visible(noop, el) {
if (!('IntersectionObserver' in window)) {
// runs immediately
return;
}
return new Promise(resolve => {
let observer = new IntersectionObserver(entries => {
let [entry] = entries;
if (entry.isIntersecting) {
observer.unobserve(entry.target);
resolve();
}
});
observer.observe(el);
});
}
// TODO make sure this runs after all of the conditions have finished, otherwise it will
// finish way before the other lazy loaded promises and will be the same as a noop when
// on:interaction or on:visible finishes much later
static idle() {
let onload = new Promise(resolve => {
if (document.readyState !== 'complete') {
window.addEventListener('load', () => resolve(), {once: true});
} else {
resolve();
}
});
if (!('requestIdleCallback' in window)) {
// run immediately
return onload;
}
// both idle and onload
return Promise.all([
new Promise(resolve => {
requestIdleCallback(() => {
resolve();
});
}),
onload
]);
}
static interaction(eventOverrides, el) {
let events = ['click', 'touchstart'];
// event overrides e.g. on:interaction="mouseenter"
if (eventOverrides) {
events = (eventOverrides || '').split(',').map(entry => entry.trim());
}
return new Promise(resolve => {
function resolveFn(e) {
resolve();
// cleanup the other event handlers
for (let name of events) {
el.removeEventListener(name, resolveFn);
}
}
for (let name of events) {
el.addEventListener(name, resolveFn, {once: true});
}
});
}
static media(query) {
let mm = {
matches: true
};
if (query && 'matchMedia' in window) {
mm = window.matchMedia(query);
}
if (mm.matches) {
return;
}
return new Promise(resolve => {
mm.addListener(e => {
if (e.matches) {
resolve();
}
});
});
}
static saveData(expects) {
// return early if API does not exist
if (
!('connection' in navigator) ||
navigator.connection.saveData === (expects !== 'false')
) {
return Promise.resolve();
}
// dangly promise
return new Promise(() => {});
}
}
// Should this auto define? Folks can redefine later using { component } export
if ('customElements' in window) {
window.customElements.define(Island.tagName, Island);
window.Island = Island;
}
export const component = Island;
export const ready = Island.ready;

8
src/_data/global.js Normal file
View file

@ -0,0 +1,8 @@
module.exports = {
random() {
const segment = () => {
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
};
return `${segment()}-${segment()}-${segment()}`;
}
};

73
src/_data/helpers.js Normal file
View file

@ -0,0 +1,73 @@
module.exports = {
/**
* Returns back some attributes based on wether the
* link is active or a parent of an active item
*
* @param {String} itemUrl The link in question
* @param {String} pageUrl The page context
* @returns {String} The attributes or empty
*/
getLinkActiveState(itemUrl, pageUrl) {
let response = '';
if (itemUrl === pageUrl) {
response = ' aria-current="page"';
}
if (itemUrl.length > 1 && pageUrl.indexOf(itemUrl) === 0) {
response += ' data-state="active"';
}
return response;
},
/**
* Filters out the passed item from the passed collection
* and randomises and limits them based on flags
*
* @param {Array} collection The 11ty collection we want to take from
* @param {Object} item The item we want to exclude (often current page)
* @param {Number} limit=3 How many items we want back
* @param {Boolean} random=true Wether or not this should be randomised
* @returns {Array} The resulting collection
*/
getSiblingContent(collection, item, limit = 3, random = true) {
let filteredItems = collection.filter(x => x.url !== item.url);
if (random) {
let counter = filteredItems.length;
while (counter > 0) {
// Pick a random index
let index = Math.floor(Math.random() * counter);
counter--;
let temp = filteredItems[counter];
// Swap the last element with the random one
filteredItems[counter] = filteredItems[index];
filteredItems[index] = temp;
}
}
// Lastly, trim to length
if (limit > 0) {
filteredItems = filteredItems.slice(0, limit);
}
return filteredItems;
},
/**
* Take an array of keys and return back items that match.
* Note: items in the collection must have a key attribute in
* Front Matter
*
* @param {Array} collection 11ty collection
* @param {Array} keys collection of keys
* @returns {Array} result collection or empty
*/
filterCollectionByKeys(collection, keys) {
return collection.filter(x => keys.includes(x.data.key));
}
};

36
src/_data/meta.js Normal file
View file

@ -0,0 +1,36 @@
module.exports = {
siteName: 'Lene Saile',
siteDescription:
'Frontend developer based in Madrid. I enjoy working with Jamstack, vanilla Javascript and modern CSS. I do my very best to improve in terms of accessibility and performance. ',
siteType: 'Person', // schema
siteURL: 'https://www.lene.dev',
locale: 'en_EN',
lang: 'en',
skipContent: 'Skip to content',
author: 'Lene Saile',
authorEmail: 'hola@lenesaile.com',
authorWebsite: 'https://www.lenesaile.com',
meta_data: {
twitterSite: '@lenesaile',
twitterCreator: '@lenesaile',
opengraph_default: '/_assets/images/opengraph-default.jpg'
},
pagination: {
itemsPerPage: 20
},
address: {
firma: 'Lene Saile',
street: 'c/ Humilladero 25, 2C',
city: 'Madrid',
state: 'Madrid',
zip: '28005',
mobileDisplay: '+34 644 959496',
mobileCall: ' +34644959496',
email: 'hola@lenesaile.com',
cif: ''
},
menu: {
closedText: 'Menu'
},
env: process.env.ELEVENTY_ENV === 'production'
};

17
src/_data/navigation.json Normal file
View file

@ -0,0 +1,17 @@
{
"items": [
{
"text": "Home",
"url": "/"
},
{
"text": "Markdown",
"url": "/markdown/"
},
{
"text": "WebC",
"url": "/test/"
}
]
}

6
src/_data/site.json Normal file
View file

@ -0,0 +1,6 @@
{
"name": "Build Excellent Websites",
"url": "https://buildexcellentwebsit.es",
"authorName": "Andy Bell",
"authorEmail": "andy@set.studio"
}

View file

@ -0,0 +1 @@
<svg aria-hidden="true" focusable="false" viewBox="0 0 120 139" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)"><path d="m2.932 101.287 56.338 34.42 58.252-33.307L59.27 69.092 2.932 101.287Z" fill="#DBDBDB" fill-opacity=".2"/><path d="M59.27 2.475v66.618l58.252 33.307V35.782L59.27 2.475ZM59.27 2.475v66.618L2.932 101.287V35.782L59.27 2.475Z" fill="#F3F3F3" fill-opacity=".3"/><path d="M117.522 38.26c-.432 0-.856-.113-1.231-.328l-57.01-32.6-55.557 32.26A2.479 2.479 0 0 1 .082 34.82a2.475 2.475 0 0 1 1.15-1.507L58.026.337a2.474 2.474 0 0 1 2.474 0l58.244 33.299a2.474 2.474 0 0 1-1.231 4.624h.008Z" fill="#fff"/><path d="M56.792 4.949V2.474A2.473 2.473 0 0 1 59.27 0a2.479 2.479 0 0 1 2.477 2.474V4.95M59.27 29.19a2.482 2.482 0 0 1-2.478-2.474v-3.629a2.473 2.473 0 0 1 2.478-2.474 2.479 2.479 0 0 1 2.477 2.474v3.629a2.473 2.473 0 0 1-2.477 2.474Zm0-10.885a2.48 2.48 0 0 1-2.478-2.474v-3.626a2.473 2.473 0 0 1 2.478-2.474 2.48 2.48 0 0 1 2.477 2.474v3.626a2.473 2.473 0 0 1-2.477 2.474ZM59.27 61.836a2.48 2.48 0 0 1-2.478-2.475v-3.625a2.472 2.472 0 0 1 2.478-2.475 2.479 2.479 0 0 1 2.477 2.475v3.625a2.473 2.473 0 0 1-2.477 2.475Zm0-10.882a2.479 2.479 0 0 1-2.478-2.475v-3.628a2.472 2.472 0 0 1 2.478-2.474 2.479 2.479 0 0 1 2.477 2.474v3.628a2.473 2.473 0 0 1-2.477 2.475Zm0-10.882a2.479 2.479 0 0 1-2.478-2.475V33.97a2.472 2.472 0 0 1 2.478-2.474 2.479 2.479 0 0 1 2.477 2.474v3.628a2.472 2.472 0 0 1-1.529 2.286 2.48 2.48 0 0 1-.948.189ZM59.27 71.567a2.48 2.48 0 0 1-2.478-2.474v-2.475a2.473 2.473 0 0 1 2.478-2.474 2.48 2.48 0 0 1 2.477 2.474v2.475a2.473 2.473 0 0 1-2.477 2.474ZM117.522 104.874a2.476 2.476 0 0 1-2.477-2.474V35.782a2.476 2.476 0 0 1 4.229-1.75c.465.464.726 1.094.726 1.75V102.4a2.473 2.473 0 0 1-2.478 2.474ZM2.478 104.543A2.479 2.479 0 0 1 0 102.069V35.454a2.473 2.473 0 0 1 2.478-2.474 2.479 2.479 0 0 1 2.477 2.474v66.615a2.474 2.474 0 0 1-2.477 2.474Z" fill="#fff"/><path d="m116.602 99.682 2.152 1.23a2.475 2.475 0 0 1-2.46 4.296l-2.152-1.23M109.023 100.675a2.509 2.509 0 0 1-1.229-.325l-3.172-1.816a2.468 2.468 0 0 1-1.16-1.499 2.466 2.466 0 0 1 .876-2.607 2.475 2.475 0 0 1 2.747-.19l3.172 1.816a2.473 2.473 0 0 1-1.234 4.621Zm-9.52-5.444c-.431 0-.855-.112-1.229-.325L95.1 93.09a2.475 2.475 0 1 1 2.463-4.296l3.175 1.816a2.473 2.473 0 0 1-1.234 4.621ZM89.982 89.787c-.432 0-.856-.113-1.231-.328l-3.172-1.813a2.476 2.476 0 0 1-.92-3.378 2.478 2.478 0 0 1 3.382-.918l3.173 1.816a2.475 2.475 0 0 1-1.232 4.62Zm-9.523-5.45c-.43 0-.854-.113-1.228-.328l-3.173-1.813a2.48 2.48 0 0 1-1.23-2.462 2.475 2.475 0 0 1 1.807-2.078 2.483 2.483 0 0 1 1.883.239l3.176 1.815a2.476 2.476 0 0 1 1.171 2.79 2.474 2.474 0 0 1-2.406 1.837ZM70.954 78.9c-.431 0-.855-.112-1.229-.328l-3.175-1.815a2.476 2.476 0 0 1-.965-3.404 2.475 2.475 0 0 1 3.428-.89l3.172 1.816a2.476 2.476 0 0 1 1.162 2.783 2.474 2.474 0 0 1-2.393 1.838ZM38.6 84.232a2.479 2.479 0 0 1-2.398-1.829 2.472 2.472 0 0 1 1.148-2.786l3.09-1.792a2.478 2.478 0 0 1 3.67 2.772 2.474 2.474 0 0 1-1.18 1.506l-3.087 1.792c-.378.22-.807.337-1.244.337Zm9.264-5.382a2.48 2.48 0 0 1-2.39-1.83 2.472 2.472 0 0 1 1.145-2.783l3.087-1.795a2.48 2.48 0 0 1 3.744 1.801 2.471 2.471 0 0 1-1.26 2.48l-3.088 1.796c-.377.216-.803.33-1.237.33ZM10.799 100.373a2.48 2.48 0 0 1-2.396-1.83 2.472 2.472 0 0 1 1.15-2.785l3.087-1.795a2.48 2.48 0 0 1 3.701 1.808 2.474 2.474 0 0 1-1.21 2.47l-3.09 1.796a2.478 2.478 0 0 1-1.242.336Zm9.268-5.379a2.483 2.483 0 0 1-2.39-1.83 2.475 2.475 0 0 1 1.14-2.785l3.09-1.792a2.481 2.481 0 0 1 3.423.875 2.473 2.473 0 0 1-.933 3.403l-3.087 1.792c-.377.22-.807.337-1.243.337Zm9.253-5.385a2.48 2.48 0 0 1-2.39-1.83 2.472 2.472 0 0 1 1.144-2.782L31.16 83.2a2.48 2.48 0 0 1 3.388.895 2.473 2.473 0 0 1-.896 3.384l-3.09 1.795a2.482 2.482 0 0 1-1.243.334ZM5.864 103.627l-2.14 1.241a2.48 2.48 0 0 1-3.702-1.808 2.474 2.474 0 0 1 1.21-2.47l2.143-1.242" fill="#fff"/><path d="M59.27 138.182c-.445 0-.881-.119-1.264-.346L1.214 104.198a2.467 2.467 0 0 1-1.183-1.517 2.472 2.472 0 0 1 1.81-3.048 2.48 2.48 0 0 1 1.9.31l55.552 32.901 56.998-32.591a2.488 2.488 0 0 1 1.933-.33 2.476 2.476 0 0 1 1.309 4.003 2.497 2.497 0 0 1-.788.618l-58.244 33.313c-.375.213-.8.325-1.231.325Z" fill="#fff"/><path d="M59.27 138.182a2.48 2.48 0 0 1-2.478-2.475V69.092a2.473 2.473 0 0 1 2.478-2.474 2.48 2.48 0 0 1 2.477 2.474v66.615a2.474 2.474 0 0 1-2.477 2.475Z" fill="#fff"/><path d="M59.273 71.567a2.48 2.48 0 0 1-2.383-1.844 2.472 2.472 0 0 1 1.163-2.777l58.238-33.31a2.48 2.48 0 0 1 3.623 2.796 2.476 2.476 0 0 1-1.16 1.5L60.501 71.238a2.462 2.462 0 0 1-1.228.328Z" fill="#fff"/><path d="M59.267 71.567a2.48 2.48 0 0 1-1.26-.346L1.213 37.583a2.477 2.477 0 0 1-.868-3.391 2.475 2.475 0 0 1 2.483-1.188c.322.046.632.155.912.321l56.793 33.638a2.475 2.475 0 0 1-1.267 4.604Z" fill="#fff"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h120v138.182H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

View file

@ -0,0 +1 @@
<svg viewBox="0 0 85 96" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M0 23.817v45.841a3.09 3.09 0 0 0 1.521 2.662L41.71 96l40.87-23.683a3.09 3.09 0 0 0 1.541-2.673V23.817L43.258.409a3.09 3.09 0 0 0-3.091.01L0 23.818Zm5.617 6.357v38.04L38.9 87.825V48.777L5.617 30.174Zm38.9 18.616v39.09l33.986-19.693V30.109L44.518 48.79Zm30.98-23.439L41.734 6.01 8.514 25.358l33.206 18.56L75.498 25.35Z" fill="currentColor"/><path fill-rule="evenodd" clip-rule="evenodd" d="M41.709 59.627c6.515 0 11.796-5.281 11.796-11.796S48.224 36.034 41.71 36.034s-11.797 5.282-11.797 11.797c0 6.515 5.282 11.796 11.797 11.796Zm0 6.18c9.928 0 17.976-8.048 17.976-17.976 0-9.928-8.048-17.976-17.976-17.976-9.928 0-17.976 8.048-17.976 17.976 0 9.928 8.048 17.976 17.976 17.976Z" fill="currentColor"/><circle cx="41.709" cy="47.831" r="12.358" fill="var(--spot-color)"/></svg>

After

Width:  |  Height:  |  Size: 895 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -0,0 +1 @@
<svg aria-hidden="true" focusable="false" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M112.135 59.995c.335 22.204-15.257 43.47-36.562 49.776-19.461 6.256-42.288-.178-55.305-16.023C7.045 78.514 3.98 55.41 13.057 37.352 21.455 19.31 40.945 7.29 60.837 7.885c20.018.004 39.136 12.854 46.934 31.252 2.898 6.546 4.412 13.697 4.364 20.858Z" fill="#fff" fill-opacity=".25"/><path d="M60.064 11.735c-17.118-.098-33.978 9.586-42.152 24.744-9.559 16.443-7.764 38.392 4.09 53.214 11.62 15.408 32.994 22.148 51.424 16.645 20.176-5.494 35.133-25.448 34.803-46.342a46.607 46.607 0 0 0-1.678-12.634c-4.867-18.694-21.89-33.459-41.175-35.278a44.2 44.2 0 0 0-5.312-.349Zm.092 4.36c4.904.026 9.793.91 14.416 2.548v65.641H47.525V17.918a39.082 39.082 0 0 1 2.832-.695 40.673 40.673 0 0 1 9.799-1.129ZM42.242 19.83V99.98C26.846 93.153 15.945 76.89 16.23 59.895a41.795 41.795 0 0 1 .19-4.189c1.306-15.495 11.713-29.625 25.82-35.876Zm37.613 1.074c6.617 3.348 12.398 8.335 16.469 14.616 6.433 9.223 8.655 20.91 6.778 31.884H79.855v-46.5Z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -0,0 +1,2 @@
<svg aria-hidden="true" focusable="false" viewBox="0 0 101 95" fill="none" xmlns="http://www.w3.org/2000/svg"><path opacity=".16" d="M8.304 22.314.543 26.782l38.746 22.302 7.826-4.424-38.81-22.346ZM47.051 62.538l-7.762 4.469.064 26.819 7.762-4.47-.064-26.818ZM47.051 44.917l-7.762 4.47.064 8.682 7.762-4.469-.064-8.683Z" fill="#FD308F"/><path opacity=".1" d="m92.581 22.31-38.81 22.347 7.749 4.467 38.819-22.352-7.758-4.462ZM53.917 44.773l-.145 44.579 7.761 4.47.141-44.587-7.757-4.462Z" fill="#00D9EE"/><path opacity=".13" d="m11.735 9.044-.157 8.848 38.809 22.17 38.81-22.17-.2-8.833-38.61 22.367L11.735 9.044Z" fill="#00FFCF"/><path opacity=".11" d="M65.743 4.767 42.607 17.89l7.763 4.47 15.324-8.657.049-8.936Z" fill="#00FFCF"/><path opacity=".07" d="m84.785 44.898-15.49 8.704v9.14l23.326-13.476-7.836-4.368Z" fill="#00D9EE"/><path opacity=".05" d="m92.52 48.782-7.735-3.885 7.736-4.52v8.404Z" fill="#00D9EE"/><path opacity=".07" d="M92.523 58.065 69.443 71.46l-.211 8.958 31.126-17.984-7.835-4.369Z" fill="#00D9EE"/><path opacity=".1" d="m15.896 44.658-7.762 4.47-.063-8.683 7.825 4.213Z" fill="#FD308F"/><path opacity=".1" d="m15.383 44.917-7.142 4.212 31.048 17.878 7.762-4.47-31.668-17.62ZM8.097 58.344.956 62.556 31.59 80.423v-8.942L8.097 58.344Z" fill="#FD308F"/><path d="M39.354 94.45 0 71.788v-9.565l7.219-4.16L0 53.903v-27.43l8.305-4.785 39.36 22.66v9.565l-7.225 4.16 7.224 4.16v27.435l-8.31 4.781ZM1.087 71.162l38.267 22.036 7.224-4.16V62.851L38.27 58.07l8.307-4.784v-8.313L8.305 22.94l-7.218 4.16v26.187l8.304 4.782-8.304 4.784v8.31Z" fill="#FD308F"/><path d="M39.334 49.754.244 27.244l.542-.937 38.548 22.196 7.492-4.313.544.936-8.036 4.628ZM39.832 66.998h-1.087v26.87h1.087v-26.87ZM32.134 81.36.264 63.008l.543-.937 30.241 17.413v-7.69L8.028 58.537l.543-.938 23.563 13.568V81.36ZM15.41 44.273 8 48.663l.554.932 7.41-4.392-.555-.931Z" fill="#FD308F"/><path d="M39.289 67.633 7.698 49.442v-9.91l31.047 17.605v-8.053h1.087V59L8.784 41.394v7.423L39.29 66.382l7.49-4.313.542.938-8.032 4.626Z" fill="#FD308F"/><path d="m61.532 94.448-8.305-4.783V44.348l39.348-22.66 8.305 4.782v27.44l-.27.157L69.84 71.79v7.687L100.886 61.6V71.79l-.27.156-39.084 22.503Zm-7.223-5.41 7.223 4.16L99.8 71.162v-7.687L68.756 81.351v-10.19l.27-.156 30.777-17.719V27.09l-7.223-4.16L54.315 44.97l-.006 44.067Z" fill="#00D9EE"/><path d="M68.756 63.507v-10.22l.27-.157 24.167-13.912v10.084L68.756 63.507Zm1.087-9.594v7.71l22.26-12.944v-7.585l-22.26 12.819Z" fill="#00D9EE"/><path d="m84.956 44.385-.506.959 7.871 4.132.506-.96-7.87-4.131ZM61.52 49.75l-8.036-4.626.543-.94 7.493 4.314 38.547-22.196.543.939L61.52 49.75Z" fill="#00D9EE"/><path d="M62.012 49.068h-1.087v44.785h1.087V49.068ZM92.821 57.586l-.486.97 8.07 4.021.486-.97-8.07-4.02Z" fill="#00D9EE"/><path d="m58.205 1.25 6.685 3.854v9.821l1.614-.896L81.45 5.734l6.664 3.837v7.692L50.387 38.987 12.665 17.265v-7.69l6.684-3.85 14.998 8.63.543.308.544-.308L58.204 1.25Zm0-1.25L34.89 13.425 19.35 4.474 11.58 8.949v8.938l38.807 22.352 38.81-22.348V8.947l-7.74-4.46-15.482 8.593V4.474L58.205 0Z" fill="#00FFCF"/><path d="m50.365 22.982-8.844-5.093L65.126 4.296l.543.938-21.976 12.655 6.682 3.847 14.536-8.06.528.947-15.074 8.36ZM50.93 39.898h-1.086v-8.285L11.834 9.726l.542-.939 38.555 22.2v8.91Z" fill="#00FFCF"/><path d="M88.468 8.893 50.011 30.94l.544.943L89.01 9.836l-.543-.943Z" fill="#00FFCF"/></svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View file

@ -0,0 +1 @@
<svg viewBox="0 0 254 254" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M126.998 69c-25.603 0-41.603 12.779-48.004 38.332 9.604-12.774 20.802-17.572 33.603-14.375 7.304 1.826 12.525 7.111 18.299 12.967 9.415 9.536 20.308 20.576 44.101 20.576 25.603 0 41.602-12.779 48.003-38.332-9.599 12.774-20.801 17.568-33.598 14.375-7.304-1.826-12.525-7.111-18.304-12.972C161.684 80.04 150.79 69 126.998 69Zm-47.995 57.5c-25.607 0-41.602 12.77-48.003 38.327 9.599-12.779 20.801-17.572 33.603-14.375 7.3 1.822 12.52 7.112 18.299 12.963C92.317 172.96 103.21 184 127.002 184c25.603 0 41.603-12.774 48.004-38.332-9.604 12.779-20.802 17.572-33.603 14.375-7.304-1.821-12.521-7.111-18.299-12.967-9.415-9.536-20.308-20.576-44.1-20.576Z" fill="currentColor"/></svg>

After

Width:  |  Height:  |  Size: 794 B

View file

@ -0,0 +1 @@
<svg viewBox="0 0 254 254" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#utopia)"><path d="M69.464 59.675c11.848.595 18.407.638 28.963.364 20.595-.589 22.419-.535 24.923.595 2.28 1.072 4.007 3.183 4.007 5.053 0 3.05-2.414 4.914-11.387 8.783-6.436 2.781-8.249 4.287-9.069 7.379-.317 1.41-.499 8.874-.499 23.626 0 24.113.536 38.373 1.641 44.063 1.502 8.011 4.42 15.16 7.745 19.071 1.958 2.278 8.335 5.557 13.623 6.967 4.784 1.275 15.629 1.409 20.456.182 9.751-2.369 15.554-8.242 18.638-18.937 2.183-7.502 3.003-17.893 3.368-42.789.365-25.356-.225-30.822-3.824-35.007-1.55-1.822-3.143-2.733-12.803-7.148-4.859-2.16-7.095-5.782-5.223-8.333 2.547-3.413 7.562-4.458 17.86-3.751 9.041.695 18.124.634 27.155-.182 11.708-1.136 15.553-.777 18.316 1.774 3.915 3.595.59 8.922-7.38 11.928-12.031 4.506-13.125 5.825-13.672 17.024-1.55 31.615-3.556 54.309-5.696 64.597-4.51 21.531-15.307 34.595-33.714 40.726-17.27 5.733-36.042 5.733-54.219 0-8.753-2.733-14.583-6.19-20.686-12.154-11.44-11.199-14.766-22.988-15.554-55.402-.67-28.958-1.218-44.074-1.625-46.062-.773-3.339-3.283-5.246-10.545-8.118-3.325-1.27-6.79-2.91-7.793-3.639-4.553-3.322-4.602-7.282-.09-9.608 3.893-2.004 5.078-2.047 27.084-1.002Z" fill="currentColor"/></g><defs><clipPath id="utopia"><path fill="currentColor" transform="translate(39 59)" d="M0 0h176v141H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,10 @@
<footer class="site-foot" role="contentinfo">
<div class="wrapper">
<div class="site-foot__inner">
{% include "icons/keyboard.svg" %} &copy; {% year %} {{ meta.siteName }}
<a href="/imprint/"> Imprint </a>
<a href="/privacy/"> Privacy </a><br />
</div>
</div>
</footer>

View file

@ -0,0 +1,17 @@
<a href="#main" class="skip-link">{{ meta.skipContent }}</a>
<header class="wrapper | relative" role="banner">
<div class="sidebar | ontop spot-color-primary">
<a href="/" class="logo | no-underline">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path
d="M11.645 20.91l-.007-.003-.022-.012a15.247 15.247 0 01-.383-.218 25.18 25.18 0 01-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0112 5.052 5.5 5.5 0 0116.313 3c2.973 0 5.437 2.322 5.437 5.25 0 3.925-2.438 7.111-4.739 9.256a25.175 25.175 0 01-4.244 3.17 15.247 15.247 0 01-.383.219l-.022.012-.007.004-.003.001a.752.752 0 01-.704 0l-.003-.001z"
/>
</svg>
Lene Saile
</a>
{% include "partials/menu.njk" %}
</div>
</header>

Some files were not shown because too many files have changed in this diff Show more