first commit
155
.eleventy.js
Normal 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
|
|
@ -0,0 +1 @@
|
|||
node_modules
|
||||
16
.gitignore
vendored
Normal 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
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"printWidth": 90,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"bracketSpacing": false,
|
||||
"quoteProps": "consistent",
|
||||
"trailingComma": "none",
|
||||
"arrowParens": "avoid"
|
||||
}
|
||||
9
config/collections/index.js
Normal 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
|
|
@ -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
|
|
@ -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
|
||||
};
|
||||
47
config/plugins/markdown.js
Normal 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;
|
||||
12
config/shortcodes/aside/index.js
Normal 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;
|
||||
44
config/shortcodes/image/index.js
Normal 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;
|
||||
74
config/shortcodes/imagePlaceholder/index.js
Normal 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;
|
||||
12
config/shortcodes/index.js
Normal 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
|
||||
};
|
||||
30
config/shortcodes/insertion/index.js
Normal 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;
|
||||
10
config/shortcodes/youtube-lite/index.js
Normal 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;
|
||||
20
config/transforms/index.js
Normal 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
|
|
@ -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
|
|
@ -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
74
package.json
Normal 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
|
|
@ -0,0 +1,8 @@
|
|||
module.exports = {
|
||||
plugins: [
|
||||
require('postcss-import-ext-glob'),
|
||||
require('postcss-import'),
|
||||
require('tailwindcss'),
|
||||
require('cssnano')
|
||||
]
|
||||
};
|
||||
93
readme.md
Normal 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
|
||||
39
src/_assets/css-utils/clamp-generator.js
Normal 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;
|
||||
20
src/_assets/css-utils/tokens-to-tailwind.js
Normal 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;
|
||||
1
src/_assets/css/blocks/button.css
Normal file
|
|
@ -0,0 +1 @@
|
|||
/* A blank block because there is *always* a button */
|
||||
12
src/_assets/css/blocks/card.css
Normal 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);
|
||||
}
|
||||
245
src/_assets/css/blocks/code.css
Normal 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%;
|
||||
}
|
||||
6
src/_assets/css/blocks/curve.css
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
.curve {
|
||||
display: block;
|
||||
height: 3.5em;
|
||||
width: 100%;
|
||||
fill: var(--spot-color, var(--color-light));
|
||||
}
|
||||
24
src/_assets/css/blocks/features.css
Normal 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;
|
||||
}
|
||||
64
src/_assets/css/blocks/island.css
Normal 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;
|
||||
}
|
||||
119
src/_assets/css/blocks/menu.css
Normal 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));
|
||||
}
|
||||
37
src/_assets/css/blocks/prose.css
Normal 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);
|
||||
}
|
||||
19
src/_assets/css/blocks/section.css
Normal 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%;
|
||||
}
|
||||
14
src/_assets/css/blocks/signoff.css
Normal 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);
|
||||
}
|
||||
16
src/_assets/css/blocks/site-foot.css
Normal 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;
|
||||
}
|
||||
34
src/_assets/css/blocks/skip-link.css
Normal 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;
|
||||
}
|
||||
11
src/_assets/css/compositions/cluster.css
Normal 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);
|
||||
}
|
||||
33
src/_assets/css/compositions/grid.css
Normal 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);
|
||||
}
|
||||
44
src/_assets/css/compositions/sidebar.css
Normal 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;
|
||||
}
|
||||
15
src/_assets/css/global.css
Normal 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';
|
||||
47
src/_assets/css/global/fonts.css
Normal 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');
|
||||
}
|
||||
118
src/_assets/css/global/global-styles.css
Normal 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;
|
||||
}
|
||||
66
src/_assets/css/global/reset.css
Normal 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;
|
||||
}
|
||||
11
src/_assets/css/global/variables.css
Normal 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;
|
||||
}
|
||||
8
src/_assets/css/utilities/flow.css
Normal 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);
|
||||
}
|
||||
9
src/_assets/css/utilities/region.css
Normal 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));
|
||||
}
|
||||
14
src/_assets/css/utilities/wrapper.css
Normal 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; */
|
||||
}
|
||||
34
src/_assets/design-tokens/colors.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
21
src/_assets/design-tokens/fonts.json
Normal 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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
79
src/_assets/design-tokens/spacing.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
44
src/_assets/design-tokens/text-sizes.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
6
src/_assets/design-tokens/viewports.json
Normal 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
|
|
@ -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);
|
||||
};
|
||||
BIN
src/_assets/fonts/charter/charter_bold.woff2
Normal file
BIN
src/_assets/fonts/charter/charter_bold_italic.woff2
Normal file
BIN
src/_assets/fonts/charter/charter_italic.woff2
Normal file
BIN
src/_assets/fonts/charter/charter_regular.woff2
Normal file
BIN
src/_assets/fonts/outfit/outfit-v5-latin-700-webfont.woff
Normal file
BIN
src/_assets/fonts/outfit/outfit-v5-latin-700-webfont.woff2
Normal file
BIN
src/_assets/fonts/outfit/outfit-v5-latin-regular.woff
Normal file
BIN
src/_assets/fonts/outfit/outfit-v5-latin-regular.woff2
Normal file
12
src/_assets/helperfiles/_redirects.njk
Normal 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 %}
|
||||
6
src/_assets/helperfiles/google35901daa0ceb7399.html
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
permalink: /google35901daa0ceb7399.html
|
||||
eleventyExcludeFromCollections: true
|
||||
excludeFromSitemap: true
|
||||
---
|
||||
google-site-verification: google35901daa0ceb7399.html
|
||||
14
src/_assets/helperfiles/humans.njk
Normal 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
|
||||
9
src/_assets/helperfiles/robots.njk
Normal 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
|
||||
21
src/_assets/helperfiles/sitemap.njk
Normal 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>
|
||||
0
src/_assets/images/.gitkeep
Normal file
BIN
src/_assets/images/favicon/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
src/_assets/images/favicon/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
src/_assets/images/favicon/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
src/_assets/images/favicon/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 871 B |
BIN
src/_assets/images/favicon/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src/_assets/images/favicon/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
src/_assets/images/favicon/favicon.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
1
src/_assets/images/favicon/favicon.svg
Normal 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 |
BIN
src/_assets/images/favicon/maskable.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
26
src/_assets/images/favicon/site.webmanifest
Normal 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"
|
||||
}
|
||||
10
src/_assets/images/icn-external.svg
Normal 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 |
BIN
src/_assets/images/image-placeholder.png
Normal file
|
After Width: | Height: | Size: 96 B |
BIN
src/_assets/images/lene.jpg
Normal file
|
After Width: | Height: | Size: 510 KiB |
BIN
src/_assets/images/opengraph-default.jpg
Normal file
|
After Width: | Height: | Size: 74 KiB |
64
src/_assets/scripts/app.js
Normal 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'));
|
||||
});
|
||||
341
src/_assets/scripts/is-land.js
Normal 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 you’d still have to use the full URL in your script’s 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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"items": [
|
||||
{
|
||||
"text": "Home",
|
||||
"url": "/"
|
||||
},
|
||||
|
||||
{
|
||||
"text": "Markdown",
|
||||
"url": "/markdown/"
|
||||
},
|
||||
{
|
||||
"text": "WebC",
|
||||
"url": "/test/"
|
||||
}
|
||||
]
|
||||
}
|
||||
6
src/_data/site.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "Build Excellent Websites",
|
||||
"url": "https://buildexcellentwebsit.es",
|
||||
"authorName": "Andy Bell",
|
||||
"authorEmail": "andy@set.studio"
|
||||
}
|
||||
1
src/_includes/icons/cube.svg
Normal 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 |
1
src/_includes/icons/design-tokens.svg
Normal 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 |
1
src/_includes/icons/every-layout.svg
Normal file
|
After Width: | Height: | Size: 11 KiB |
10
src/_includes/icons/keyboard.svg
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
4
src/_includes/icons/kezboard 2.svg
Normal file
|
After Width: | Height: | Size: 13 KiB |
1
src/_includes/icons/polypane.svg
Normal 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 |
2
src/_includes/icons/set.svg
Normal 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 |
1
src/_includes/icons/tailwind.svg
Normal 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 |
1
src/_includes/icons/utopia.svg
Normal 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 |
10
src/_includes/partials/footer.njk
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<footer class="site-foot" role="contentinfo">
|
||||
<div class="wrapper">
|
||||
<div class="site-foot__inner">
|
||||
{% include "icons/keyboard.svg" %} © {% year %} {{ meta.siteName }}
|
||||
<a href="/imprint/"> Imprint </a>
|
||||
|
||||
<a href="/privacy/"> Privacy </a><br />
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
17
src/_includes/partials/header.njk
Normal 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>
|
||||