├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── etc └── gulp │ └── rollup.js ├── fractal.config.js ├── gulpfile.js ├── package-lock.json ├── package.json └── src ├── app.webmanifest ├── assets ├── icons │ ├── icon.ico │ ├── icon.png │ └── icon.svg ├── images │ ├── gravatar.png │ └── logo-perch.png ├── scripts │ ├── app.js │ ├── modules │ │ ├── focusing.js │ │ ├── menu.js │ │ └── webfont-loader.js │ └── prism.js ├── styles │ ├── app.css │ ├── base │ │ ├── _disabled.css │ │ ├── _embedded.css │ │ ├── _forms.css │ │ ├── _grouping.css │ │ ├── _sections.css │ │ ├── _tables.css │ │ └── _text.css │ ├── config │ │ ├── _constants.css │ │ └── _media.css │ ├── docs │ │ └── styleguide.css │ ├── helpers │ │ ├── _mixins.css │ │ └── _typography.css │ └── theme.css └── vectors │ ├── clip.svg │ ├── corner.svg │ ├── diamond.svg │ ├── menu.svg │ ├── next.svg │ ├── prev.svg │ ├── search.svg │ ├── topic-business.svg │ ├── topic-code.svg │ ├── topic-content.svg │ ├── topic-design.svg │ ├── topic-process.svg │ └── topic-ux.svg ├── components ├── _partials │ └── _preview.html ├── common │ ├── article │ │ ├── article.config.yaml │ │ ├── article.css │ │ └── article.html │ ├── author │ │ ├── author.config.yaml │ │ ├── author.css │ │ └── author.html │ ├── avatar │ │ ├── avatar.config.yaml │ │ ├── avatar.css │ │ └── avatar.html │ ├── backdrop │ │ ├── backdrop.css │ │ └── backdrop.html │ ├── button │ │ ├── button.config.yaml │ │ ├── button.css │ │ └── button.html │ ├── comment-form │ │ └── comment-form.html │ ├── comment │ │ ├── comment.config.yaml │ │ ├── comment.css │ │ └── comment.html │ ├── continue │ │ ├── continue.config.yaml │ │ ├── continue.css │ │ └── continue.html │ ├── field │ │ ├── field--combined.html │ │ ├── field--textarea.html │ │ ├── field.config.yaml │ │ ├── field.css │ │ └── field.html │ ├── listing │ │ ├── listing.config.yaml │ │ ├── listing.css │ │ └── listing.html │ ├── meta │ │ ├── meta.config.yaml │ │ ├── meta.css │ │ └── meta.html │ ├── preface │ │ ├── preface.config.yaml │ │ ├── preface.css │ │ └── preface.html │ ├── promo │ │ ├── promo.config.yaml │ │ ├── promo.css │ │ └── promo.html │ ├── search-form │ │ └── search-form.html │ ├── section │ │ ├── section.config.yaml │ │ ├── section.css │ │ └── section.html │ └── summary │ │ ├── summary.config.yaml │ │ ├── summary.css │ │ └── summary.html ├── components.config.yaml ├── global │ ├── banner │ │ ├── banner.css │ │ └── banner.html │ ├── contentinfo │ │ ├── contentinfo.css │ │ └── contentinfo.html │ ├── main │ │ └── main.css │ ├── menu │ │ ├── menu.css │ │ └── menu.html │ ├── search │ │ ├── search.css │ │ └── search.html │ ├── site-nav │ │ ├── site-nav.css │ │ └── site-nav.html │ ├── topics-nav │ │ ├── topics-nav.config.yaml │ │ ├── topics-nav.css │ │ └── topics-nav.html │ └── traverse-nav │ │ ├── traverse-nav.config.yaml │ │ ├── traverse-nav.css │ │ └── traverse-nav.html ├── og-image │ ├── og-image.config.yaml │ ├── og-image.css │ └── og-image.html ├── scopes │ ├── embed │ │ ├── embed.config.yaml │ │ ├── embed.css │ │ └── embed.html │ ├── gallery │ │ ├── gallery.css │ │ └── gallery.html │ ├── note │ │ ├── note.config.yaml │ │ ├── note.css │ │ └── note.html │ ├── prose │ │ ├── prose.config.yaml │ │ ├── prose.css │ │ └── prose.html │ └── syntax │ │ ├── syntax.config.yaml │ │ ├── syntax.css │ │ └── syntax.html ├── templates │ ├── _page.html │ ├── article-template │ │ ├── article-template.config.yaml │ │ └── article-template.html │ ├── content-template │ │ ├── content-template.config.yaml │ │ └── content-template.html │ ├── index-template │ │ ├── index-template.config.yaml │ │ └── index-template.html │ └── listing-template │ │ ├── listing-template.config.yaml │ │ └── listing-template.html └── utilities │ ├── hidden │ ├── hidden.css │ └── hidden.html │ └── utilities.config.yaml ├── docs ├── _partials │ └── _palette-sample.md ├── index.md ├── tokens.config.js └── tokens.md ├── humans.txt ├── robots.txt └── tokens ├── borders.json ├── breakpoints.json ├── colors.json ├── fonts.json ├── layers.json ├── sizes.json └── spaces.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Folders 2 | node_modules 3 | www 4 | tmp 5 | 6 | # Files 7 | .DS_Store 8 | .publish 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Paul Robert Lloyd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bits, the front-end component library for 24 ways 2 | 3 | ## Requirements 4 | Bits is built upon [Fractal](https://github.com/frctl/fractal), a tool that enables the rapid development of components, templates and pages. Fractal uses a number of ES6 features, so this project requires [Node.js](https://nodejs.org/) v4.0+ to be installed locally. A global install of Gulp is also recommended. 5 | 6 | ## Installation 7 | To get the project up and running, and view components in the browser, complete the following steps: 8 | 9 | 1. Download and install Node: 10 | 2. Clone this repo: `git clone git@github.com:24ways/frontend.git` (SSH) or `git clone https://github.com/24ways/frontend.git` (HTTPS) 11 | 3. [Optional] Install Gulp globally: `npm install gulp -g` 12 | 4. [Optional] Install Fractal globally: `npm install fractal -g` 13 | 5. Install project dependancies: `npm install` 14 | 6. Start the development environment: `npm start` 15 | 7. Open your browser and visit 16 | 17 | ## Development 18 | When developing components, you may want assets automatically compiled and the browser to refresh automatically. To do this, run the following task: 19 | 20 | * `npm run dev` 21 | 22 | ## Creating a static build 23 | To create a static instance of this project, run the following task: 24 | 25 | * `npm run build` 26 | 27 | This will create a folder called `www`, into which the required files will be created. 28 | 29 | ## Deployment 30 | To make this project publicly accessible, you can deploy a static instance by running the following task: 31 | 32 | * `npm run publish` 33 | 34 | This will publish the contents of `public` to your `gh-pages` branch. 35 | 36 | ## Repo structure 37 | Sometimes it’s helpful to know what all these different files are for… 38 | 39 | ``` 40 | / 41 | ├─ src/ 42 | │ ├─ assets/ # Assets 43 | │ │ ├─ icons/ # Favicon and home screen icons 44 | │ │ ├─ images/ # Raster images (used in component examples) 45 | │ │ ├─ scripts/ # JavaScript files 46 | │ │ ├─ styles/ # CSS files 47 | │ │ └─ vectors/ # SVG images, icons and logos 48 | │ │ 49 | │ ├─ components/ # Components 50 | │ │ ├─ _partials/ # …that render component previews 51 | │ │ ├─ common/ # …that may appear anywhere 52 | │ │ ├─ global/ # …that appear on every page 53 | │ │ ├─ layouts/ # …that govern macro layout 54 | │ │ ├─ scopes/ # …that style undecorated markup 55 | │ │ ├─ templates/ # …that combine components to render page types 56 | │ │ └─ utilities/ # …that have a single purpose/role 57 | │ │ 58 | │ ├─ docs/ # Documentation 59 | │ │ ├─ _partials/ # Partials for rendering documentation 60 | │ │ └─ … # Documentation files 61 | │ │ 62 | │ └─ tokens/ # Design tokens 63 | │ 64 | ├─ tmp/ # Files required for dynamic builds (ignored by Git) 65 | ├─ www/ # Public build (ignored by Git) 66 | │ 67 | ├─ .editorconfig # Code style definitions 68 | ├─ .gitignore # List of files and folders not tracked by Git 69 | ├─ .eslintrc # Linting preferences for JavasScript 70 | ├─ fractal.configjs # Configuration for Fractal 71 | ├─ gulpfile.js # Configuration for Gulp tasks 72 | ├─ LICENSE # License information for this project 73 | ├─ package.json # Project manifest 74 | └─ README.md # This file 75 | ``` 76 | -------------------------------------------------------------------------------- /etc/gulp/rollup.js: -------------------------------------------------------------------------------- 1 | const rollup = require('rollup'); 2 | const commonjs = require('@rollup/plugin-commonjs'); 3 | const {nodeResolve} = require('@rollup/plugin-node-resolve'); 4 | const closure = require('rollup-plugin-closure-compiler-js'); 5 | 6 | module.exports = (modules, logger, callback) => { 7 | modules.forEach((module, i) => { 8 | rollup.rollup({ 9 | input: module.input, 10 | plugins: [ 11 | nodeResolve({ 12 | browser: true 13 | }), 14 | commonjs(), 15 | closure() 16 | ] 17 | }) 18 | .then(bundle => { 19 | bundle.write({ 20 | file: module.file, 21 | format: 'iife', 22 | sourcemap: true, 23 | name: module.name 24 | }); 25 | 26 | if (i === modules.length - 1) { 27 | callback(); 28 | } 29 | }) 30 | .catch(error => logger.log(error)); 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /fractal.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const paths = { 4 | build: path.join(__dirname, 'www'), 5 | src: path.join(__dirname, 'src'), 6 | static: path.join(__dirname, 'tmp') 7 | }; 8 | 9 | const fractal = require('@frctl/fractal').create(); 10 | 11 | const mandelbrot = require('@frctl/mandelbrot')({ 12 | favicon: '/assets/icons/icon.ico', 13 | lang: 'en-gb', 14 | nav: ['search', 'components', 'docs'], 15 | styles: ['default', '/assets/styles/theme.css'], 16 | static: { 17 | mount: 'fractal' 18 | } 19 | }); 20 | 21 | const mdAbbr = require('markdown-it-abbr'); 22 | const mdFootnote = require('markdown-it-footnote'); 23 | const md = require('markdown-it')({ 24 | html: true, 25 | xhtmlOut: true, 26 | typographer: true 27 | }).use(mdAbbr).use(mdFootnote); 28 | const nunjucksDate = require('nunjucks-date'); 29 | const nunjucks = require('@frctl/nunjucks')({ 30 | filters: { 31 | date: nunjucksDate, 32 | markdown(string) { 33 | return md.render(string); 34 | }, 35 | markdownInline(string) { 36 | return md.renderInline(string); 37 | }, 38 | slugify(string) { 39 | return string.toLowerCase().replace(/\W+/g, ''); 40 | }, 41 | stringify() { 42 | return JSON.stringify(this, null, '\t'); 43 | } 44 | }, 45 | paths: [`${paths.static}/assets/vectors`] 46 | }); 47 | 48 | // Project config 49 | fractal.set('project.title', 'Bits of 24 ways'); 50 | 51 | // Components config 52 | fractal.components.engine(nunjucks); 53 | fractal.components.set('default.preview', '@preview'); 54 | fractal.components.set('default.status', null); 55 | fractal.components.set('ext', '.html'); 56 | fractal.components.set('path', `${paths.src}/components`); 57 | 58 | // Docs config 59 | fractal.docs.engine(nunjucks); 60 | fractal.docs.set('ext', '.md'); 61 | fractal.docs.set('path', `${paths.src}/docs`); 62 | 63 | // Web UI config 64 | fractal.web.theme(mandelbrot); 65 | fractal.web.set('static.path', paths.static); 66 | fractal.web.set('builder.dest', paths.build); 67 | fractal.web.set('builder.urls.ext', null); 68 | 69 | // Export config 70 | module.exports = fractal; 71 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------- 2 | // Dependencies 3 | // -------------------------------------------------------- 4 | 5 | // Utils 6 | const fs = require('fs'); 7 | const path = require('path'); 8 | const del = require('del'); 9 | const gulp = require('gulp'); 10 | const util = require('gulp-util'); 11 | 12 | // CSS 13 | const postcss = require('gulp-postcss'); 14 | const apply = require('postcss-apply'); 15 | const assets = require('postcss-assets'); 16 | const autoprefixer = require('autoprefixer'); 17 | const calc = require('postcss-calc'); 18 | const colorFunction = require('postcss-color-function'); 19 | const customMedia = require('postcss-custom-media'); 20 | const importer = require('postcss-easy-import'); 21 | const mapper = require('postcss-map'); 22 | const mediaMinMax = require('postcss-media-minmax'); 23 | const nano = require('cssnano'); 24 | const nested = require('postcss-nested'); 25 | const responsiveType = require('postcss-responsive-type'); 26 | const simpleVars = require('postcss-simple-vars'); 27 | 28 | // Misc 29 | const ghPages = require('gulp-gh-pages'); 30 | const imagemin = require('gulp-imagemin'); 31 | const sourcemaps = require('gulp-sourcemaps'); 32 | 33 | // JavaScript 34 | const rollup = require('./etc/gulp/rollup'); 35 | 36 | // Fractal 37 | const pkg = require('./package.json'); 38 | const fractal = require('./fractal.config.js'); 39 | 40 | const logger = fractal.cli.console; 41 | 42 | // -------------------------------------------------------- 43 | // Configuration 44 | // -------------------------------------------------------- 45 | 46 | // Paths 47 | const paths = { 48 | build: path.join(__dirname, 'www'), 49 | dest: path.join(__dirname, 'tmp'), 50 | src: path.join(__dirname, 'src'), 51 | modules: path.join(__dirname, 'node_modules') 52 | }; 53 | 54 | // PostCSS plugins 55 | const processors = [ 56 | importer({ 57 | glob: true 58 | }), 59 | mapper({ 60 | maps: [ 61 | `${paths.src}/tokens/borders.json`, 62 | `${paths.src}/tokens/breakpoints.json`, 63 | `${paths.src}/tokens/colors.json`, 64 | `${paths.src}/tokens/fonts.json`, 65 | `${paths.src}/tokens/layers.json`, 66 | `${paths.src}/tokens/sizes.json`, 67 | `${paths.src}/tokens/spaces.json` 68 | ] 69 | }), 70 | assets({ 71 | loadPaths: [`${paths.src}/assets/vectors`] 72 | }), 73 | simpleVars, 74 | apply, 75 | calc, 76 | customMedia, 77 | colorFunction, 78 | mediaMinMax, 79 | nested, 80 | responsiveType, 81 | autoprefixer, 82 | nano 83 | ]; 84 | 85 | // -------------------------------------------------------- 86 | // Tasks 87 | // -------------------------------------------------------- 88 | 89 | // Build static site 90 | function build() { 91 | const builder = fractal.web.builder(); 92 | 93 | builder.on('progress', (completed, total) => logger.update(`Exported ${completed} of ${total} items`, 'info')); 94 | builder.on('error', error => logger.error(error.message)); 95 | 96 | return builder.build().then(() => { 97 | logger.success('Fractal build completed!'); 98 | }); 99 | } 100 | 101 | // Serve dynamic site 102 | function serve() { 103 | const server = fractal.web.server({ 104 | sync: true, 105 | syncOptions: { 106 | https: true 107 | } 108 | }); 109 | 110 | server.on('error', error => logger.error(error.message)); 111 | 112 | return server.start().then(() => { 113 | logger.success(`Fractal server is now running at ${server.url}`); 114 | }); 115 | } 116 | 117 | // Clean 118 | function clean() { 119 | return del(`${paths.dest}/assets/`); 120 | } 121 | 122 | // Deploy to GitHub pages 123 | function deploy() { 124 | // Generate CNAME file from `homepage` value in package.json 125 | const cname = pkg.homepage.replace(/.*?:\/\//g, ''); 126 | fs.writeFileSync(`${paths.build}/CNAME`, cname); 127 | 128 | // Push contents of build folder to `gh-pages` branch 129 | return gulp.src(`${paths.build}/**/*`) 130 | .pipe(ghPages({ 131 | force: true 132 | })); 133 | } 134 | 135 | // Meta 136 | function meta() { 137 | return gulp.src(`${paths.src}/*.{txt,json}`) 138 | .pipe(gulp.dest(paths.dest)); 139 | } 140 | 141 | // Icons 142 | function icons() { 143 | return gulp.src(`${paths.src}/assets/icons/**/*`) 144 | .pipe(imagemin()) 145 | .pipe(gulp.dest(`${paths.dest}/assets/icons`)); 146 | } 147 | 148 | // Images 149 | function images() { 150 | return gulp.src(`${paths.src}/assets/images/**/*`) 151 | .pipe(imagemin({ 152 | progressive: true 153 | })) 154 | .pipe(gulp.dest(`${paths.dest}/assets/images`)); 155 | } 156 | 157 | // Vectors 158 | function vectors() { 159 | return gulp.src(`${paths.src}/assets/vectors/**/*`) 160 | .pipe(gulp.dest(`${paths.dest}/assets/vectors`)); 161 | } 162 | 163 | // Scripts 164 | function scripts(callback) { 165 | const modules = [{ 166 | input: `${paths.src}/assets/scripts/app.js`, 167 | file: `${paths.dest}/assets/scripts/app.js`, 168 | name: 'app' 169 | }, { 170 | input: `${paths.src}/assets/scripts/prism.js`, 171 | file: `${paths.dest}/assets/scripts/prism.js`, 172 | name: 'prism' 173 | }]; 174 | 175 | rollup(modules, util, callback); 176 | } 177 | 178 | // Styles 179 | function styles() { 180 | return gulp.src(`${paths.src}/assets/styles/*.css`) 181 | .pipe(sourcemaps.init()) 182 | .pipe(postcss(processors)) 183 | .pipe(sourcemaps.write('./')) 184 | .pipe(gulp.dest(`${paths.dest}/assets/styles`)); 185 | } 186 | 187 | // Watch 188 | function watch() { 189 | serve(); 190 | gulp.watch(`${paths.src}/assets/icons`, icons); 191 | gulp.watch(`${paths.src}/assets/images`, images); 192 | gulp.watch(`${paths.src}/assets/vectors`, images); 193 | gulp.watch(`${paths.src}/**/*.js`, scripts); 194 | gulp.watch(`${paths.src}/**/*.css`, styles); 195 | } 196 | 197 | // Task sets 198 | const compile = gulp.series(clean, gulp.parallel(meta, icons, images, vectors, scripts, styles)); 199 | 200 | gulp.task('start', gulp.series(compile, serve)); 201 | gulp.task('build', gulp.series(compile, build)); 202 | gulp.task('dev', gulp.series(compile, watch)); 203 | gulp.task('publish', gulp.series(build, deploy)); 204 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "24ways-frontend", 3 | "version": "2017.0.0", 4 | "description": "Bits, the front-end component library for 24 ways", 5 | "keywords": [ 6 | "styleguide", 7 | "patterns", 8 | "library" 9 | ], 10 | "homepage": "http://bits.24ways.org", 11 | "bugs": "https://github.com/24ways/frontend/issues", 12 | "license": "SEE LICENSE IN LICENSE", 13 | "author": "Paul Robert Lloyd", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/24ways/frontend.git" 17 | }, 18 | "main": "fractal.config.js", 19 | "scripts": { 20 | "prestart": "npm install", 21 | "start": "node_modules/.bin/gulp start", 22 | "build": "node_modules/.bin/gulp build", 23 | "dev": "node_modules/.bin/gulp dev", 24 | "publish": "node_modules/.bin/gulp publish", 25 | "test": "xo || stylelint src/**/*.css" 26 | }, 27 | "browserslist": [ 28 | ">2%" 29 | ], 30 | "stylelint": { 31 | "extends": "stylelint-config-standard", 32 | "rules": { 33 | "property-no-unknown": [ 34 | true, 35 | { 36 | "ignoreProperties": [ 37 | "font-range" 38 | ] 39 | } 40 | ] 41 | } 42 | }, 43 | "xo": { 44 | "space": true, 45 | "envs": "browser" 46 | }, 47 | "dependencies": { 48 | "@frctl/fractal": "^1.3.0", 49 | "@frctl/mandelbrot": "^1.4.0", 50 | "@frctl/nunjucks": "^2.0.2", 51 | "@rollup/plugin-commonjs": "^13.0.0", 52 | "@rollup/plugin-node-resolve": "^8.1.0", 53 | "autoprefixer": "^9.8.4", 54 | "cssnano": "^4.1.10", 55 | "del": "^5.1.0", 56 | "fontfaceobserver": "^2.1.0", 57 | "gulp": "^4.0.2", 58 | "gulp-gh-pages": "^0.5.0", 59 | "gulp-imagemin": "^7.1.0", 60 | "gulp-postcss": "^8.0.0", 61 | "gulp-sourcemaps": "^2.6.5", 62 | "gulp-util": "^3.0.0", 63 | "markdown-it": "^11.0.0", 64 | "markdown-it-abbr": "^1.0.0", 65 | "markdown-it-footnote": "^3.0.2", 66 | "nunjucks-date": "^1.5.0", 67 | "postcss-apply": "^0.12.0", 68 | "postcss-assets": "^5.0.0", 69 | "postcss-calc": "^7.0.2", 70 | "postcss-color-function": "^4.1.0", 71 | "postcss-custom-media": "^7.0.8", 72 | "postcss-custom-properties": "^9.1.1", 73 | "postcss-easy-import": "^3.0.0", 74 | "postcss-map": "^0.11.0", 75 | "postcss-media-minmax": "^4.0.0", 76 | "postcss-nested": "4.2.3", 77 | "postcss-responsive-type": "^1.0.0", 78 | "postcss-simple-vars": "^5.0.2", 79 | "prismjs": "^1.20.0", 80 | "rollup": "^2.20.0", 81 | "rollup-plugin-closure-compiler-js": "^1.0.0" 82 | }, 83 | "devDependencies": { 84 | "stylelint": "^13.6.1", 85 | "stylelint-config-standard": "^20.0.0", 86 | "xo": "^0.32.1" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/app.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "lang": "en-gb", 3 | "name": "24 ways", 4 | "short_name": "24 ways", 5 | "icons": [{ 6 | "src": "/assets/icons/icon.png", 7 | "sizes": "180x180", 8 | "type": "image/png" 9 | }], 10 | "theme_color": "#302", 11 | "background_color": "#f04", 12 | "display": "browser", 13 | "scope": "/", 14 | "start_url": "/" 15 | } 16 | -------------------------------------------------------------------------------- /src/assets/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/24ways/frontend/ad5e2bed43c5164c1283a69bc18cddd62b07410b/src/assets/icons/icon.ico -------------------------------------------------------------------------------- /src/assets/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/24ways/frontend/ad5e2bed43c5164c1283a69bc18cddd62b07410b/src/assets/icons/icon.png -------------------------------------------------------------------------------- /src/assets/icons/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/gravatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/24ways/frontend/ad5e2bed43c5164c1283a69bc18cddd62b07410b/src/assets/images/gravatar.png -------------------------------------------------------------------------------- /src/assets/images/logo-perch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/24ways/frontend/ad5e2bed43c5164c1283a69bc18cddd62b07410b/src/assets/images/logo-perch.png -------------------------------------------------------------------------------- /src/assets/scripts/app.js: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import {loadWebfonts} from './modules/webfont-loader'; 3 | import {menu} from './modules/menu'; 4 | 5 | // Run 6 | menu(); 7 | loadWebfonts(); 8 | -------------------------------------------------------------------------------- /src/assets/scripts/modules/focusing.js: -------------------------------------------------------------------------------- 1 | // Thanks to https://github.com/edenspiekermann/a11y-dialog for focus trapping 2 | 3 | const focusableElements = [ 4 | 'a[href]', 5 | 'area[href]', 6 | 'input:not([disabled])', 7 | 'select:not([disabled])', 8 | 'textarea:not([disabled])', 9 | 'button:not([disabled])', 10 | 'iframe', 11 | 'object', 12 | 'embed', 13 | '[contenteditable]', 14 | '[tabindex]:not([tabindex^="-"])' 15 | ]; 16 | 17 | function toArray(collection) { 18 | return Array.prototype.slice.call(collection); 19 | } 20 | 21 | function $$(selector, context) { 22 | return toArray((context || document).querySelectorAll(selector)); 23 | } 24 | 25 | function getFocusableChildren(node) { 26 | return $$(focusableElements.join(','), node).filter(child => { 27 | return Boolean(child.offsetWidth || child.offsetHeight || child.getClientRects().length); 28 | }); 29 | } 30 | 31 | function createFirstFocusableChild(node) { 32 | const newDiv = document.createElement('div'); 33 | newDiv.setAttribute('tabindex', '0'); 34 | newDiv.style.cssText = 'outline:none;'; 35 | const firstChild = node.firstChild; 36 | firstChild.before(newDiv); 37 | return newDiv; 38 | } 39 | 40 | function getCurrentFocusable(node, event) { 41 | const focusableChildren = getFocusableChildren(node); 42 | let focusableElement; 43 | 44 | if (focusableChildren.length > 0) { 45 | const focusedItemIndex = focusableChildren.indexOf(safeActiveElement()); 46 | if (event.shiftKey && focusedItemIndex === 0) { 47 | focusableElement = focusableChildren[focusableChildren.length - 1]; 48 | } else if (!event.shiftKey && focusedItemIndex === focusableChildren.length - 1) { 49 | focusableElement = focusableChildren[0]; 50 | } 51 | } 52 | 53 | return focusableElement; 54 | } 55 | 56 | function trapTabKey(node, event) { 57 | const focusableElement = getCurrentFocusable(node, event); 58 | if (focusableElement) { 59 | focusableElement.focus(); 60 | event.preventDefault(); 61 | } 62 | } 63 | 64 | export function safeActiveElement() { 65 | try { 66 | return document.activeElement; 67 | } catch {} 68 | } 69 | 70 | export function bindKeypress(isShown, onExit, node, event) { 71 | if (isShown && event.which === 27) { 72 | event.preventDefault(); 73 | onExit(); 74 | } 75 | 76 | if (isShown && event.which === 9) { 77 | trapTabKey(node, event); 78 | } 79 | } 80 | 81 | export function setInitialFocus(node) { 82 | const firstFocusableChild = getFocusableChildren(node)[0] || createFirstFocusableChild(node); 83 | if (firstFocusableChild) { 84 | firstFocusableChild.focus(); 85 | } 86 | } 87 | 88 | export function removeFocus(node) { 89 | const focusableChildren = getFocusableChildren(node); 90 | if (focusableChildren.length > 0) { 91 | const focusedItemIndex = focusableChildren.indexOf(safeActiveElement()); 92 | if (focusedItemIndex !== -1) { 93 | focusableChildren[focusedItemIndex].blur(); 94 | } 95 | } 96 | } 97 | 98 | export function maintainFocus(isShown, node, event) { 99 | if (isShown && !node.contains(event.target)) { 100 | setInitialFocus(node); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/assets/scripts/modules/menu.js: -------------------------------------------------------------------------------- 1 | import * as focusing from './focusing'; 2 | 3 | export function menu() { 4 | // Set timeout so that transitions don't run on page load 5 | const menuElement = document.querySelector('.c-menu'); 6 | setTimeout(() => { 7 | menuElement.classList.remove('no-transition'); 8 | }, 10); 9 | 10 | // Set up menu button 11 | // Mark drawer as being closed 12 | const buttonElement = document.querySelector('.c-menu__button'); 13 | buttonElement.setAttribute('aria-expanded', false); 14 | 15 | // Set up menu drawer 16 | const drawerElement = document.querySelector('.c-menu__drawer'); 17 | drawerElement.setAttribute('role', 'dialog'); 18 | drawerElement.setAttribute('aria-hidden', 'true'); 19 | drawerElement.hidden = true; 20 | 21 | // Set up backdrop 22 | const backdropElement = document.createElement('div'); 23 | document.body.append(backdropElement); 24 | backdropElement.className = 'c-backdrop'; 25 | backdropElement.setAttribute('tabindex', -1); 26 | 27 | // Set up body state 28 | document.body.dataset.menuExpanded = false; 29 | 30 | // Focusing 31 | const focusRegion = drawerElement; 32 | let previousFocusedElement; 33 | 34 | function handleKeypress(event_) { 35 | focusing.bindKeypress(true, () => { 36 | handleRemoveFocus(); 37 | }, focusRegion, event_); 38 | } 39 | 40 | function handleMaintainFocus(event_) { 41 | focusing.maintainFocus(true, focusRegion, event_); 42 | } 43 | 44 | function handleSetFocus() { 45 | previousFocusedElement = focusing.safeActiveElement(); 46 | focusing.setInitialFocus(focusRegion); 47 | document.addEventListener('keydown', handleKeypress); 48 | document.body.addEventListener('focus', handleMaintainFocus, true); 49 | } 50 | 51 | function handleRemoveFocus() { 52 | document.removeEventListener('keydown', handleKeypress); 53 | document.body.removeEventListener('focus', handleMaintainFocus, true); 54 | focusing.removeFocus(focusRegion); 55 | previousFocusedElement.focus(); 56 | } 57 | 58 | // Inertia 59 | function handleInert(state) { 60 | Array.from(document.body.children).forEach(child => { 61 | if (child !== menuElement) { 62 | child.inert = state; 63 | console.log(state); 64 | } 65 | }); 66 | } 67 | 68 | function toggleMenu(state) { 69 | if (state === 'true') { // Open menu 70 | drawerElement.setAttribute('aria-hidden', false); 71 | drawerElement.hidden = false; 72 | handleSetFocus(); 73 | handleInert(true); 74 | } else { // Close menu 75 | setTimeout(() => { 76 | // Leave time for animation to complete before changing state 77 | drawerElement.setAttribute('aria-hidden', true); 78 | drawerElement.hidden = true; 79 | }, 450); 80 | handleRemoveFocus(); 81 | handleInert(false); 82 | } 83 | 84 | // …and only then update the attribute for `aria-expanded` 85 | buttonElement.setAttribute('aria-expanded', state); 86 | 87 | // …and update global value so other elements can query state 88 | document.body.dataset.menuExpanded = state; 89 | } 90 | 91 | if (buttonElement) { 92 | // Remove script and applied style that hides drawer during load 93 | drawerElement.style.display = ''; 94 | document.querySelector('.c-menu__onload').remove(); 95 | 96 | // Toggle drawer on clicking button 97 | buttonElement.addEventListener('click', event_ => { 98 | const state = buttonElement.getAttribute('aria-expanded') === 'false' ? 'true' : 'false'; 99 | toggleMenu(state); 100 | 101 | event_.preventDefault(); 102 | }); 103 | 104 | // Close menu if escape key is pressed 105 | window.addEventListener('keyup', event_ => { 106 | if (event_.key === 'Escape') { 107 | toggleMenu(false); 108 | handleRemoveFocus(); 109 | } 110 | }); 111 | 112 | // Close menu if backdrop (area outside menu) is clicked 113 | backdropElement.addEventListener('click', event_ => { 114 | const state = buttonElement.getAttribute('aria-expanded') === 'false' ? 'true' : 'false'; 115 | toggleMenu(state); 116 | event_.preventDefault(); 117 | }); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/assets/scripts/modules/webfont-loader.js: -------------------------------------------------------------------------------- 1 | import FontFaceObserver from 'fontfaceobserver'; 2 | 3 | export function loadWebfonts() { 4 | // Setup 5 | const classLoaded = 'fonts-loaded'; 6 | const storageId = 'fonts-loaded'; 7 | const fonts = [ 8 | (new FontFaceObserver('Source Sans Pro', { 9 | weight: 'normal', 10 | style: 'normal' 11 | })).load(), 12 | (new FontFaceObserver('Source Sans Pro', { 13 | weight: '700', 14 | style: 'normal' 15 | })).load(), 16 | (new FontFaceObserver('Source Serif Pro', { 17 | weight: 'normal', 18 | style: 'normal' 19 | })).load(), 20 | (new FontFaceObserver('Source Code Pro', { 21 | weight: 'normal', 22 | style: 'normal' 23 | })).load() 24 | ]; 25 | 26 | // Events 27 | function eventFontsLoaded() { 28 | document.documentElement.classList.add(classLoaded); 29 | sessionStorage[storageId] = true; 30 | } 31 | 32 | // Init 33 | function init() { 34 | Promise.all(fonts) 35 | .then(eventFontsLoaded) 36 | .catch(error => { 37 | console.error(error); 38 | }); 39 | } 40 | 41 | init(); 42 | } 43 | -------------------------------------------------------------------------------- /src/assets/scripts/prism.js: -------------------------------------------------------------------------------- 1 | // Import modules 2 | import prism from 'prismjs'; 3 | 4 | // Run 5 | prism; // eslint-disable-line no-unused-expressions 6 | -------------------------------------------------------------------------------- /src/assets/styles/app.css: -------------------------------------------------------------------------------- 1 | /* ===================================================== 2 | Application styles 3 | ===================================================== */ 4 | 5 | @import 'config/**/*.css'; 6 | @import 'helpers/**/*.css'; 7 | @import 'base/**/*.css'; 8 | @import '../../components/**/*.css'; 9 | -------------------------------------------------------------------------------- /src/assets/styles/base/_disabled.css: -------------------------------------------------------------------------------- 1 | /* ===================================================== 2 | Disabled elements 3 | w3c.github.io/html/disabled-elements.html 4 | ===================================================== */ 5 | 6 | /** 7 | * Use default cursor for disabled elements. 8 | */ 9 | [disabled] { 10 | cursor: default; 11 | opacity: 0.75; 12 | } 13 | 14 | /** 15 | * Add the correct display in IE 10-. 16 | */ 17 | [hidden] { 18 | display: none !important; 19 | } 20 | -------------------------------------------------------------------------------- /src/assets/styles/base/_embedded.css: -------------------------------------------------------------------------------- 1 | /* ===================================================== 2 | Embedded content 3 | w3c.github.io/html/semantics-embedded-content.html 4 | ===================================================== */ 5 | 6 | /** 7 | * Remove the border on images inside links in IE 10-. 8 | */ 9 | img { 10 | border: 0; 11 | 12 | @media print { 13 | page-break-inside: avoid; 14 | } 15 | } 16 | 17 | /** 18 | * Hide the overflow in IE. 19 | */ 20 | svg:not(:root) { 21 | display: block; 22 | overflow: hidden; 23 | fill: currentColor; 24 | } 25 | 26 | /** 27 | * Correct `inline-block` display not defined in IE 8/9 28 | */ 29 | audio, 30 | canvas, 31 | video { 32 | display: inline-block; 33 | } 34 | 35 | iframe { 36 | border: 0; 37 | } 38 | -------------------------------------------------------------------------------- /src/assets/styles/base/_forms.css: -------------------------------------------------------------------------------- 1 | /* ===================================================== 2 | Forms 3 | w3c.github.io/html/sec-forms.html 4 | ===================================================== */ 5 | 6 | /** 7 | * 1. Remove the margin in Firefox and Safari. 8 | * 2. Change text/font properties to `inherit` in all browsers (opinionated). 9 | */ 10 | button, 11 | input, 12 | optgroup, 13 | select, 14 | textarea { 15 | margin: 0; /* 1 */ 16 | font: inherit; /* 2 */ 17 | text-transform: inherit; /* 2 */ 18 | } 19 | 20 | /** 21 | * Remove the default vertical scrollbar in IE. 22 | */ 23 | textarea { 24 | overflow: auto; 25 | } 26 | 27 | /** 28 | * 1. Show the overflow in Edge. 29 | * 2. Show the overflow in Edge, Firefox, and IE. 30 | * 3. Ensure inputs inherit text colour of parent element 31 | */ 32 | button, 33 | input, /* 1 */ 34 | select { /* 2 */ 35 | overflow: visible; 36 | color: inherit; /* 3 */ 37 | } 38 | 39 | /** 40 | * Change the cursor in all browsers (opinionated). 41 | */ 42 | button, 43 | [type=submit], 44 | label { 45 | cursor: pointer; 46 | } 47 | 48 | /** 49 | * 1. Correct the inability to style clickable types in iOS. 50 | * 2. Remove default styling (opinionated). 51 | */ 52 | button, 53 | [type=reset], 54 | [type=submit] { 55 | -webkit-appearance: button; /* 1 */ 56 | background: none; /* 2 */ 57 | border: 0; /* 2 */ 58 | padding: 0; /* 2 */ 59 | } 60 | 61 | /** 62 | * Remove the inner border and padding in Firefox. 63 | * 64 | * 1. Restore the focus styles unset by the previous rule. 65 | */ 66 | button::-moz-focus-inner, 67 | [type=button]::-moz-focus-inner, 68 | [type=reset]::-moz-focus-inner, 69 | [type=submit]::-moz-focus-inner { 70 | border: 0; 71 | padding: 0; 72 | outline: 1px dotted ButtonText; /* 1 */ 73 | } 74 | 75 | /** 76 | * https://thatemil.com/blog/2015/01/03/reset-your-fieldset/ 77 | * 1. Reset minimum width based on content inside fieldset in 78 | * WebKit/Blink/Firefox 79 | * 2. Adjust display mode to ensure 1 works in Firefox 80 | * 3. Remove default margin and border 81 | * 4. Fix margin-collapsing behavior coupled with rendering of legend element 82 | */ 83 | fieldset { 84 | min-width: 0; /* 1 */ 85 | margin: 0; /* 3 */ 86 | border: 0; /* 3 */ 87 | padding: 0.01em 0 0; /* 4 */ 88 | 89 | body:not(:-moz-handler-blocked) & { 90 | display: table-cell; /* 2 */ 91 | } 92 | 93 | @media print { 94 | display: none; 95 | } 96 | } 97 | 98 | /** 99 | * 1. Correct the text wrapping in Edge and IE. 100 | * 2. Correct the color inheritance from `fieldset` elements in IE. 101 | * 3. Remove the padding so developers are not caught out when they zero out 102 | * `fieldset` elements in all browsers. 103 | */ 104 | legend { 105 | box-sizing: border-box; /* 1 */ 106 | color: inherit; /* 2 */ 107 | display: table; /* 1 */ 108 | max-width: 100%; /* 1 */ 109 | padding: 0; /* 3 */ 110 | white-space: normal; /* 1 */ 111 | } 112 | 113 | /** 114 | * 1. Add the correct box sizing in IE 10-. 115 | * 2. Remove the padding in IE 10-. 116 | */ 117 | [type=checkbox], 118 | [type=radio] { 119 | box-sizing: border-box; /* 1 */ 120 | padding: 0; /* 2 */ 121 | } 122 | 123 | /** 124 | * 1. Correct the odd appearance in Chrome and Safari. 125 | * 2. Correct the outline style in Safari. 126 | */ 127 | [type=search] { 128 | -webkit-appearance: none; /* 1 */ 129 | outline-offset: -2px; /* 2 */ 130 | } 131 | 132 | /** 133 | * Remove the inner padding and cancel buttons in Chrome and Safari on macOS. 134 | */ 135 | [type=search]::-webkit-search-cancel-button, 136 | [type=search]::-webkit-search-decoration { 137 | -webkit-appearance: none; 138 | } 139 | -------------------------------------------------------------------------------- /src/assets/styles/base/_grouping.css: -------------------------------------------------------------------------------- 1 | /* ===================================================== 2 | Grouping content 3 | w3c.github.io/html/grouping-content.html 4 | ===================================================== */ 5 | 6 | /** 7 | * 1. Add the correct box sizing in Firefox. 8 | * 2. Show the overflow in Edge and IE. 9 | */ 10 | hr { 11 | box-sizing: content-box; /* 1 */ 12 | height: 0; /* 1 */ 13 | overflow: visible; /* 2 */ 14 | border: 0; 15 | } 16 | 17 | p, 18 | pre, 19 | blockquote, 20 | ul, 21 | ol, 22 | dl, 23 | figure { 24 | margin: 0; 25 | } 26 | 27 | pre { 28 | overflow: auto; 29 | white-space: pre; 30 | hyphens: none; 31 | tab-size: 2; 32 | } 33 | 34 | blockquote { 35 | @media print { 36 | page-break-inside: avoid; 37 | } 38 | } 39 | 40 | /** 41 | * Add the correct display in IE 9-. 42 | */ 43 | figure, 44 | figcaption { 45 | display: block; 46 | } 47 | 48 | /** 49 | * Default to ’bare’ lists (opinionated). 50 | */ 51 | ol, 52 | ul { 53 | padding: 0; 54 | list-style: none; 55 | } 56 | 57 | /** 58 | * Default to no indenting on defintion data (opinionated). 59 | */ 60 | dd { 61 | margin: 0; 62 | } 63 | -------------------------------------------------------------------------------- /src/assets/styles/base/_sections.css: -------------------------------------------------------------------------------- 1 | /* ===================================================== 2 | Sections 3 | w3c.github.io/html/sections.html 4 | ===================================================== */ 5 | 6 | /** 7 | * 1. Prevent adjustment of font size on orientation change. 8 | */ 9 | html { 10 | box-sizing: border-box; 11 | height: 100%; 12 | text-size-adjust: 100%; /* 1 */ 13 | position: relative; 14 | overflow-x: hidden; 15 | } 16 | 17 | *, 18 | *::before, 19 | *::after { 20 | box-sizing: inherit; 21 | } 22 | 23 | *:focus { 24 | outline: 2px solid $color-focus; 25 | } 26 | 27 | @media print { 28 | @page { 29 | margin: 2cm; 30 | } 31 | 32 | * { 33 | background: none !important; 34 | color: black !important; 35 | text-shadow: none !important; 36 | box-shadow: none !important; 37 | } 38 | } 39 | 40 | body { 41 | margin: 0; 42 | height: 100%; 43 | background-color: white; 44 | font-family: sans-serif; 45 | -moz-osx-font-smoothing: grayscale; 46 | -webkit-font-smoothing: antialiased; 47 | line-height: 1.5; 48 | color: $color-text; 49 | 50 | .fonts-loaded & { 51 | font-family: map(fonts, family-sans); 52 | } 53 | } 54 | 55 | /** 56 | * Add the correct display in IE 9-. 57 | */ 58 | article, 59 | aside, 60 | footer, 61 | header, 62 | main, 63 | nav, 64 | section { 65 | display: block; 66 | } 67 | 68 | h1, 69 | h2, 70 | h3, 71 | h4, 72 | h5, 73 | h6 { 74 | margin: 0; 75 | text-rendering: optimizeLegibility; 76 | font-size: inherit; 77 | font-weight: inherit; 78 | 79 | @media print { 80 | page-break-after: avoid; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/assets/styles/base/_tables.css: -------------------------------------------------------------------------------- 1 | /* ===================================================== 2 | Tablular data 3 | w3c.github.io/html/tabular-data.html 4 | ===================================================== */ 5 | 6 | table { 7 | border-collapse: collapse; 8 | border-spacing: 0; 9 | font-variant-numeric: tabular-nums; 10 | } 11 | 12 | td, 13 | th { 14 | text-align: left; 15 | vertical-align: top; 16 | } 17 | 18 | @media print { 19 | thead { 20 | display: table-header-group; 21 | } 22 | 23 | tr, 24 | img { 25 | page-break-inside: avoid; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/assets/styles/base/_text.css: -------------------------------------------------------------------------------- 1 | /* ===================================================== 2 | Text-level semantics 3 | w3c.github.io/html/textlevel-semantics.html 4 | ===================================================== */ 5 | 6 | /** 7 | * 1. Remove grey background on active links in IE 10. 8 | */ 9 | a { 10 | background-color: transparent; /* 1 */ 11 | text-decoration: none; 12 | text-decoration-color: color($color-link a(33%)); 13 | color: $color-link; 14 | transition: all 0.2s ease; 15 | } 16 | 17 | /** 18 | * 1. Improve readability when focused and also mouse hovered in all browsers 19 | */ 20 | a:active, 21 | a:hover { 22 | color: $color-link--hover; 23 | text-decoration-color: color($color-link--hover a(33%)); 24 | outline: 0; /* 1 */ 25 | } 26 | 27 | a:active { 28 | color: $color-link--active; 29 | text-decoration-color: color($color-link--active a(33%)); 30 | } 31 | 32 | @media print { 33 | a, 34 | a:visited { 35 | color: black !important; 36 | } 37 | } 38 | 39 | /* Add the correct font size in all browsers */ 40 | small { 41 | font-size: 100%; 42 | } 43 | 44 | abbr[title] { 45 | text-decoration: underline dotted rgba(0, 0, 0, 0.33); 46 | 47 | &:hover { 48 | cursor: help; 49 | } 50 | } 51 | 52 | /** 53 | * 1. Correct the inheritance and scaling of font size in all browsers. 54 | * 2. Correct the odd `em` font sizing in all browsers. 55 | */ 56 | code, 57 | samp { 58 | font-family: map(fonts, family-monospace); /* 1 */ 59 | font-size: 0.875em; /* 2 */ 60 | } 61 | 62 | kbd { 63 | font-family: map(fonts, family-system); 64 | } 65 | 66 | /** 67 | * Prevent `sub` and `sup` elements from affecting line height in all browsers. 68 | */ 69 | sub, 70 | sup { 71 | font-size: 75%; 72 | line-height: 0; 73 | position: relative; 74 | vertical-align: baseline; 75 | } 76 | 77 | sub { 78 | bottom: -0.25rem; 79 | } 80 | 81 | sup { 82 | top: -0.5rem; 83 | } 84 | 85 | /** 86 | * Address styling not present in IE 8/9 87 | */ 88 | mark { 89 | background: #ffc; 90 | } 91 | -------------------------------------------------------------------------------- /src/assets/styles/config/_constants.css: -------------------------------------------------------------------------------- 1 | /* ===================================================== 2 | Constants 3 | ===================================================== */ 4 | 5 | /* Banner */ 6 | $banner-height--small: map(sizes, large); 7 | $banner-height--large: map(sizes, xlarge); 8 | 9 | /* Navigation */ 10 | $navigation-width--large: map(sizes, xlarge); 11 | $navigation-duration: 0.4s; 12 | $navigation-color: white; 13 | $navigation-color--offset: map(colors, neutral, lightest); 14 | $navigation-color--text: map(colors, neutral, dark); 15 | $navigation-color--shadow: color(map(colors, neutral, darker) a(25%)); 16 | 17 | /* Backdrop */ 18 | $backdrop-color: color(map(colors, neutral, darkest) a(33%)); 19 | 20 | /* Author images */ 21 | $author-duration: 0.5s; 22 | 23 | /* Text */ 24 | $color-text: map(colors, neutral, darker); 25 | 26 | /* Prose */ 27 | $prose-color--background: color(map(colors, neutral, base) a(15%)); 28 | $prose-color--rule: color(map(colors, neutral, base) a(25%)); 29 | $prose-color--shadow: color(map(colors, neutral, base) a(25%)); 30 | 31 | /* Links */ 32 | $color-link: map(colors, secondary, base); 33 | $color-link--hover: map(colors, secondary, dark); 34 | $color-link--active: map(colors, secondary, light); 35 | $color-ui-link: map(colors, primary, base); 36 | $color-ui-link--hover: map(colors, primary, light); 37 | $color-ui-link--active: map(colors, primary, dark); 38 | 39 | /* Focus */ 40 | $color-focus: map(colors, secondary, light); 41 | 42 | /* Themes */ 43 | $color-year: hsl(348, 100%, 16%); 44 | $color-year--dark: hsl(348, 100%, 8%); 45 | $color-year--dark-alpha: hsla(348, 100%, 8%, 0.8); 46 | $color-day: hsl(360, 80%, 60%); 47 | $color-day--light: hsl(360, 60%, 98%); 48 | $color-day--dark: hsl(360, 100%, 24%); 49 | $color-day--dark-alpha: hsl(360, 100%, 24%, 0.33); 50 | -------------------------------------------------------------------------------- /src/assets/styles/config/_media.css: -------------------------------------------------------------------------------- 1 | /* ===================================================== 2 | Media 3 | ===================================================== */ 4 | 5 | /* Xtra-small */ 6 | @custom-media --upto-xsmall-screen screen and (width < map(breakpoints, xsmall)); 7 | @custom-media --from-xsmall-screen screen and (width >= map(breakpoints, xsmall)); 8 | 9 | /* Small */ 10 | @custom-media --upto-small-screen screen and (width < map(breakpoints, small)); 11 | @custom-media --from-small-screen screen and (width >= map(breakpoints, small)); 12 | 13 | /* Medium */ 14 | @custom-media --upto-medium-screen screen and (width < map(breakpoints, medium)); 15 | @custom-media --from-medium-screen screen and (width >= map(breakpoints, medium)); 16 | 17 | /* Large */ 18 | @custom-media --upto-large-screen screen and (width < map(breakpoints, large)); 19 | @custom-media --from-large-screen screen and (width >= map(breakpoints, large)); 20 | 21 | /* Shallow/Deep */ 22 | @custom-media --upto-shallow-screen screen and (height < 28rem); 23 | @custom-media --from-shallow-screen screen and (height >= 28rem); 24 | @custom-media --upto-deep-screen screen and (height < 36rem); 25 | @custom-media --from-deep-screen screen and (height >= 36rem); 26 | 27 | /* Other */ 28 | @custom-media --from-xlarge-screen screen and (width >= map(breakpoints, xlarge)); 29 | @custom-media --from-max-screen screen and (width >= map(breakpoints, max)); 30 | -------------------------------------------------------------------------------- /src/assets/styles/docs/styleguide.css: -------------------------------------------------------------------------------- 1 | /* Palette container */ 2 | .palette { 3 | margin: 0 -0.25em; 4 | display: flex; 5 | flex-wrap: wrap; 6 | } 7 | 8 | /* Colour swatch */ 9 | .color { 10 | flex: 1 1 4em; 11 | margin: 0 0.25em 1.5em; 12 | 13 | svg { 14 | width: 100%; 15 | height: 5em; 16 | border-radius: map(borders, radius-default); 17 | } 18 | 19 | code { 20 | display: inline-block; 21 | color: $color-text; 22 | } 23 | 24 | text { 25 | font: 0.75em/1em 'Hack', 'Consolas', 'Monaco', monospace; 26 | font-style: normal; 27 | text-shadow: 0 0 2px $color-text; 28 | } 29 | } 30 | 31 | .color--base { 32 | order: -1; 33 | margin-right: 1em; 34 | } 35 | -------------------------------------------------------------------------------- /src/assets/styles/helpers/_mixins.css: -------------------------------------------------------------------------------- 1 | /* ===================================================== 2 | General mixins 3 | ===================================================== */ 4 | 5 | :root { 6 | --hidden: { 7 | position: absolute; 8 | overflow: hidden; 9 | clip: rect(0 0 0 0); 10 | height: 1px; 11 | width: 1px; 12 | margin: -1px; 13 | border: 0; 14 | padding: 0; 15 | white-space: nowrap; 16 | } 17 | 18 | --navigation-link: { 19 | color: $navigation-color--text; 20 | 21 | &:hover { 22 | color: $color-ui-link--hover; 23 | background-color: color(map(colors, neutral, lightest) a(60%)); 24 | } 25 | 26 | &:active { 27 | color: $color-ui-link--active; 28 | background-color: color(map(colors, neutral, lightest) a(80%)); 29 | } 30 | } 31 | 32 | --focusable: { 33 | &:focus { 34 | outline: 0; 35 | box-shadow: inset 0 0 0 2px $color-focus; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/assets/styles/helpers/_typography.css: -------------------------------------------------------------------------------- 1 | /* ===================================================== 2 | Typography mixins 3 | ===================================================== */ 4 | 5 | :root { 6 | /* Title: Typically used on titles of containers and modules */ 7 | --typeset-title: { 8 | font-size: responsive 2rem 4rem; 9 | font-range: 30rem 60rem; 10 | font-weight: 700; 11 | font-variant: common-ligatures lining-nums; 12 | letter-spacing: -0.03125em; 13 | line-height: 0.95; 14 | 15 | @media print { 16 | font-size: 3rem; 17 | } 18 | } 19 | 20 | --typeset-summary-title: { 21 | font-size: 1.25rem; 22 | font-weight: 700; 23 | font-variant: common-ligatures lining-nums; 24 | letter-spacing: -0.03125em; 25 | line-height: 0.95; 26 | } 27 | 28 | /* Prose: Typically used for running text */ 29 | --typeset-prose: { 30 | font-family: serif; 31 | font-variant: common-ligatures oldstyle-nums; 32 | line-height: 1.5; 33 | letter-spacing: -0.0125em; 34 | hyphens: auto; 35 | hanging-punctuation: first; 36 | 37 | .fonts-loaded & { 38 | font-family: map(fonts, family-serif); 39 | } 40 | } 41 | 42 | /* Prose: */ 43 | --typescale-prose: { 44 | font-size: responsive 0.9375rem 1.25rem; 45 | font-range: 30rem 60rem; 46 | 47 | @media print { 48 | font-size: 0.9375rem; 49 | } 50 | } 51 | 52 | /* Heading: Typically used for headings within areas of prose content */ 53 | --typeset-heading: { 54 | font-family: sans-serif; 55 | font-variant: common-ligatures lining-nums; 56 | line-height: 1.25; 57 | letter-spacing: -0.025em; 58 | 59 | .fonts-loaded & { 60 | font-family: map(fonts, family-sans); 61 | } 62 | } 63 | 64 | /* Lede: Typically used for introductory text */ 65 | --typeset-lede: { 66 | font-size: responsive 1.125rem 1.5rem; 67 | font-range: 30rem 60rem; 68 | font-family: sans-serif; 69 | font-weight: 400; 70 | font-variant: common-ligatures oldstyle-nums; 71 | line-height: 1.25; 72 | letter-spacing: -0.025em; 73 | 74 | @media print { 75 | font-size: 1.125rem; 76 | } 77 | 78 | .fonts-loaded & { 79 | font-family: map(fonts, family-sans); 80 | } 81 | } 82 | 83 | /* Caption: Typically used for supporting text */ 84 | --typeset-caption: { 85 | font-size: responsive 0.875rem 1rem; 86 | font-range: 30rem 60rem; 87 | font-family: sans-serif; 88 | font-weight: 400; 89 | font-variant: common-ligatures lining-nums; 90 | line-height: 1.25; 91 | 92 | @media print { 93 | font-size: 0.875rem; 94 | } 95 | 96 | .fonts-loaded & { 97 | font-family: map(fonts, family-sans); 98 | } 99 | } 100 | 101 | /* Label: Typically used on UI labels and controls */ 102 | --typeset-label: { 103 | font-size: 0.8125rem; 104 | line-height: calc(16 / 13); 105 | } 106 | 107 | /* Label: Typically used on UI labels and controls */ 108 | --typeset-ui: { 109 | font-family: sans-serif; 110 | font-size: 1rem; 111 | line-height: calc(20 / 16); 112 | 113 | .fonts-loaded & { 114 | font-family: map(fonts, family-sans); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/assets/styles/theme.css: -------------------------------------------------------------------------------- 1 | /* ===================================================== 2 | Fractal UI styles 3 | ===================================================== */ 4 | 5 | @import 'config/**/*.css'; 6 | @import 'docs/**/*.css'; 7 | 8 | /* Theme overrides */ 9 | a { 10 | color: $color-link; 11 | 12 | &:hover { 13 | color: $color-link--hover; 14 | } 15 | 16 | &:active { 17 | color: $color-link--active; 18 | } 19 | } 20 | 21 | .Pen-previewLink svg { 22 | fill: currentColor; 23 | } 24 | 25 | .Header { 26 | background-color: map(colors, primary, dark); 27 | } 28 | 29 | .Prose { 30 | tr td:first-child { 31 | min-width: 12em; 32 | padding-right: 1em; 33 | white-space: nowrap; 34 | } 35 | 36 | tr td:last-child { 37 | width: 100%; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/assets/vectors/clip.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/vectors/corner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/assets/vectors/diamond.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/assets/vectors/menu.svg: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /src/assets/vectors/next.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/assets/vectors/prev.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/assets/vectors/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/vectors/topic-business.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/assets/vectors/topic-code.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/assets/vectors/topic-content.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/assets/vectors/topic-design.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/assets/vectors/topic-process.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/assets/vectors/topic-ux.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/components/_partials/_preview.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | {%- if _target.context.prism -%} 28 | 29 | {%- endif -%} 30 | 31 | 32 | 33 | 34 | 35 | 36 | {%- if _target.context.title -%} 37 | 38 | {%- else -%} 39 | 40 | {%- endif -%} 41 | {%- if _target.context.description -%} 42 | 43 | {%- else -%} 44 | 45 | {%- endif -%} 46 | {%- if _target.context.url -%} 47 | 48 | {%- else -%} 49 | 50 | {%- endif -%} 51 | {%- if _target.context.avatar -%} 52 | 53 | {%- else -%} 54 | 55 | {%- endif -%} 56 | {%- if _target.context.handle -%} 57 | 58 | {%- endif -%} 59 | 60 | 61 | 62 | 65 | 76 | 77 | {{ _target.context.title }} ◆ 24 ways 78 | 79 | 80 | 81 | {{ yield | safe }} 82 | 83 | 84 | -------------------------------------------------------------------------------- /src/components/common/article/article.config.yaml: -------------------------------------------------------------------------------- 1 | context: 2 | title: "Starting Your HTML5 Project on the Right Foot (and Keeping It There)" 3 | author: Drew McLellan 4 | -------------------------------------------------------------------------------- /src/components/common/article/article.css: -------------------------------------------------------------------------------- 1 | .c-article { 2 | position: relative; 3 | overflow-x: hidden; 4 | background-color: $color-day--light; 5 | background-color: var(--color-day--light, $color-day--light); 6 | 7 | @media (--from-large-screen) { 8 | padding-top: 9rem; 9 | padding-bottom: map(spaces, large); 10 | } 11 | 12 | a { 13 | color: $color-day--dark; 14 | color: var(--color-day--dark, $color-day--dark); 15 | 16 | &:hover { 17 | color: $color-day; 18 | color: var(--color-day, $color-day); 19 | } 20 | } 21 | } 22 | 23 | .c-article__header { 24 | padding: map(spaces, medium) map(spaces, large) map(spaces, large); 25 | position: relative; 26 | 27 | @media (--from-large-screen) { 28 | min-height: 16rem; 29 | margin-top: -1px; 30 | padding-bottom: map(spaces, xxlarge); 31 | padding-left: 25%; 32 | border-top: 1px solid $prose-color--rule; 33 | } 34 | } 35 | 36 | .c-article__title { 37 | @apply --typeset-title; 38 | 39 | position: relative; 40 | margin-bottom: map(spaces, small); 41 | z-index: map(layers, overlay); 42 | } 43 | 44 | .c-article__byline { 45 | @apply --typescale-prose; 46 | 47 | .c-avatar { 48 | position: absolute; 49 | margin: 0; 50 | 51 | @media (--upto-large-screen) { 52 | top: -25%; 53 | right: -12.5%; 54 | width: 50%; 55 | opacity: 0.1; 56 | filter: saturate(0) contrast(2); 57 | mix-blend-mode: multiply; 58 | } 59 | 60 | @media (--from-large-screen) { 61 | top: -40%; 62 | right: 80%; 63 | width: 27.5%; 64 | margin-right: calc(map(spaces, medium) * -1); 65 | } 66 | 67 | @media print { 68 | display: none; 69 | } 70 | } 71 | } 72 | 73 | .c-article__main { 74 | padding: 0 map(spaces, large) map(spaces, large); 75 | 76 | @media (--from-large-screen) { 77 | margin-left: 25%; 78 | padding-left: 0; 79 | } 80 | } 81 | 82 | .c-article__footer { 83 | position: relative; 84 | z-index: map(layers, default); 85 | padding: 0 map(spaces, large); 86 | 87 | @media (--from-large-screen) { 88 | float: left; 89 | width: 20%; 90 | padding: 0; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/components/common/article/article.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{ title }}

4 | 10 |
11 | 12 |
13 | {% render "@meta" %} 14 |
15 | 16 |
17 | {% render "@note" %} 18 | {% render "@prose--article" %} 19 |
20 | 21 | {% render "@section--author" %} 22 | {% render "@section--sponsor" %} 23 | {% if related %} 24 | {% render "@section--related" %} 25 | {% endif %} 26 | {% if comments %} 27 | {% render "@section--comments" %} 28 | {% render "@comment-form" %} 29 | {% endif %} 30 |
31 | -------------------------------------------------------------------------------- /src/components/common/author/author.config.yaml: -------------------------------------------------------------------------------- 1 | context: 2 | author: Drew McLellan 3 | -------------------------------------------------------------------------------- /src/components/common/author/author.css: -------------------------------------------------------------------------------- 1 | .c-author { 2 | display: block; 3 | position: relative; 4 | overflow: hidden; 5 | } 6 | 7 | .c-author__meta { 8 | @apply --typeset-label; 9 | 10 | display: inline-block; 11 | position: absolute; 12 | right: 0; 13 | bottom: 0; 14 | left: 0; 15 | padding: map(spaces, small); 16 | background-color: color(map(colors, neutral, darkest) a(75%)); 17 | color: white; 18 | 19 | @media print { 20 | color: white !important; 21 | background-color: black !important; 22 | } 23 | 24 | .c-author:hover & { 25 | text-decoration: underline color(white a(66%)); 26 | } 27 | } 28 | 29 | .c-author__image { 30 | display: block; 31 | height: auto; 32 | width: 100%; 33 | transition: transform $author-duration ease-out; 34 | 35 | .c-author:hover & { 36 | transform: scale(1.1); 37 | 38 | @media (prefers-reduced-motion) { 39 | transform: none; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/components/common/author/author.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ author }} 4 | 5 | -------------------------------------------------------------------------------- /src/components/common/avatar/avatar.config.yaml: -------------------------------------------------------------------------------- 1 | context: 2 | src: https://cloud.24ways.org/authors/drewmclellan280.jpg 3 | alt: Drew McLellan 4 | size: 280 5 | -------------------------------------------------------------------------------- /src/components/common/avatar/avatar.css: -------------------------------------------------------------------------------- 1 | .c-avatar { 2 | display: inline-block; 3 | height: auto; 4 | max-height: 21rem; 5 | max-width: 21rem; 6 | margin: map(spaces, medium); 7 | clip-path: url('/assets/vectors/clip.svg#avatar'); 8 | clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%); 9 | shape-outside: polygon(50% 0, 100% 50%, 50% 100%, 0 50%) content-box; 10 | shape-margin: map(spaces, medium); 11 | } 12 | -------------------------------------------------------------------------------- /src/components/common/avatar/avatar.html: -------------------------------------------------------------------------------- 1 | {{ alt }} 2 | -------------------------------------------------------------------------------- /src/components/common/backdrop/backdrop.css: -------------------------------------------------------------------------------- 1 | .c-backdrop { 2 | position: fixed; 3 | top: 0; 4 | right: 0; 5 | bottom: 0; 6 | left: 0; 7 | z-index: map(layers, backdrop); 8 | transition: opacity $navigation-duration ease-in; 9 | background-color: $backdrop-color; 10 | opacity: 0; 11 | pointer-events: none; 12 | 13 | [data-menu-expanded=true] & { 14 | opacity: 1; 15 | pointer-events: fill; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/components/common/backdrop/backdrop.html: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /src/components/common/button/button.config.yaml: -------------------------------------------------------------------------------- 1 | collated: true 2 | variants: 3 | - name: default 4 | context: 5 | label: Button 6 | - name: disabled 7 | context: 8 | label: Disabled button 9 | disabled: true 10 | - name: more 11 | context: 12 | label: Show me another 24 ways… 13 | href: ?page=2 14 | mods: [more] 15 | -------------------------------------------------------------------------------- /src/components/common/button/button.css: -------------------------------------------------------------------------------- 1 | .c-button { 2 | @apply --typeset-ui; 3 | 4 | display: inline-block; 5 | padding: map(spaces, medium) map(spaces, large); 6 | background-color: $color-ui-link; 7 | text-align: center; 8 | text-transform: uppercase; 9 | color: white; 10 | 11 | &:hover { 12 | background-color: $color-ui-link--hover; 13 | color: white; 14 | } 15 | 16 | &:active { 17 | background-color: $color-ui-link--active; 18 | color: white; 19 | } 20 | 21 | @media (-ms-high-contrast: active) { 22 | border: 1px solid; 23 | } 24 | 25 | @media print { 26 | display: none; 27 | } 28 | } 29 | 30 | .c-button--more { 31 | display: block; 32 | } 33 | -------------------------------------------------------------------------------- /src/components/common/button/button.html: -------------------------------------------------------------------------------- 1 | {%- if href -%} 2 | {{ label }} 3 | {%- else -%} 4 | 5 | {%- endif -%} 6 | -------------------------------------------------------------------------------- /src/components/common/comment-form/comment-form.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | Impress us 6 | 7 |
8 |
9 |

Be friendly / use Textile

10 |
11 |

12 | 13 | 14 |

15 |

16 | 17 | 18 |

19 |

20 | 21 | 22 |

23 |

24 | 25 | 26 |

27 |

28 | 29 | 30 | 31 |

32 |
33 |
34 |
35 |
36 | -------------------------------------------------------------------------------- /src/components/common/comment/comment.config.yaml: -------------------------------------------------------------------------------- 1 | collated: true 2 | default: helpful 3 | variants: 4 | - name: helpful 5 | context: 6 | id: c0001 7 | href: https://adactio.com/ 8 | author: Jeremy Keith 9 | datetime: "2012-12-01T12:24:48-00:00" 10 | avatar: 11 | src: "https://www.gravatar.com/avatar/5ad82c5ba0264363974af89deb743c20?s=96" 12 | size: 64 13 | content: | 14 | This is great stuff! I'm terrible at regular expressions—my brain just doesn't seem to want to remember any of it—but this article contains the clearest description of regular expressions I've come across. 15 | 16 | I thought I'd share a useful little rewrite rule that I use for cache-busting JavaScript and CSS files. You know the story: you make a change in your JavaScript or CSS and you want to let the browser know that it should grab the new version instead of using what it's got in its cache. 17 | 18 | Now, I could potentially just use a query string when I point to my JS and CSS files ( e.g. /js/myscript.js?20131201 ) …but that can cause issues with proxy servers. 19 | 20 | Instead what I what I do is point to files like this: `/js/myscript.20131201.js` 21 | 22 | Then I need to tell the server to look for the **actual** file in `/js/myscript.js` 23 | 24 | Here's the rewrite rule I'm using: 25 | 26 | ``` 27 | RewriteCond %{REQUEST_FILENAME} !-f 28 | RewriteRule ^(.).(d).(js|css)$ $1.$3 [L] 29 | ``` 30 | 31 | It's basically telling the server that, if the JS or CSS file doesn't actually exist and it matches the pattern of having two dots before the file extension (with only numbers after the first dot), to look at the bit before the first dot, and look at the bit after the second dot, but to ignore the bit in between (the numbers). 32 | 33 | The server serves up the right file, but browsers fetch the new version because, as far as they're concerned, this looks like a brand new file that they haven't got in their cache. 34 | 35 | That was a terrible explanation, wasn't it? I now have even more appreciation for how clearly and concise this article is. 36 | - name: unhelpful 37 | context: 38 | id: c0002 39 | href: https://paulrobertlloyd.com/ 40 | author: Paul Robert Lloyd 41 | datetime: "2012-12-12T12:24:48-00:00" 42 | mods: [unhelpful] 43 | avatar: 44 | src: "https://www.gravatar.com/avatar/15091a37bacfa4bdd011282627eaca2b?s=96" 45 | size: 64 46 | content: | 47 | You fool! Everything you've written here is wrong! 48 | -------------------------------------------------------------------------------- /src/components/common/comment/comment.css: -------------------------------------------------------------------------------- 1 | $comment-avatar--small: map(sizes, large); 2 | $comment-avatar--large: map(sizes, xlarge); 3 | 4 | .c-comment { 5 | position: relative; 6 | margin-bottom: map(spaces, large); 7 | padding: map(spaces, large); 8 | padding-left: $comment-avatar--small; 9 | background-color: white; 10 | box-shadow: 1px 1px 0 $prose-color--shadow; 11 | 12 | @media (-ms-high-contrast: active) { 13 | border: 1px solid; 14 | } 15 | 16 | @media (--from-medium-screen) { 17 | padding-left: $comment-avatar--large; 18 | } 19 | 20 | @media (--upto-large-screen) { 21 | margin-left: calc(calc(map(spaces, large) - map(spaces, xsmall)) * -1); 22 | margin-right: calc(calc(map(spaces, large) - map(spaces, xsmall)) * -1); 23 | } 24 | 25 | &.c-comment--unhelpful { 26 | background-color: transparent; 27 | box-shadow: 0 0 0 1px $prose-color--shadow; 28 | } 29 | 30 | .c-avatar { 31 | position: absolute; 32 | top: 0; 33 | left: calc(map(spaces, large) * -1); 34 | width: map(sizes, large); 35 | 36 | @media (--from-medium-screen) { 37 | width: map(sizes, xlarge); 38 | } 39 | } 40 | } 41 | 42 | .c-comment__header { 43 | margin-bottom: map(spaces, large); 44 | 45 | @media print { 46 | border-top: 1px solid black; 47 | padding-top: map(spaces, small); 48 | } 49 | } 50 | 51 | .c-comment__title { 52 | @apply --typeset-summary-title; 53 | } 54 | 55 | .c-comment__meta { 56 | @apply --typeset-label; 57 | 58 | @media (--from-small-screen) { 59 | position: absolute; 60 | top: 1.75rem; 61 | right: map(spaces, large); 62 | } 63 | } 64 | 65 | .c-comment__main { 66 | margin-bottom: map(spaces, large); 67 | } 68 | 69 | .c-comment__footer { 70 | @apply --typeset-label; 71 | 72 | border-top: 1px solid $prose-color--shadow; 73 | padding-top: map(spaces, medium); 74 | text-align: right; 75 | } 76 | -------------------------------------------------------------------------------- /src/components/common/comment/comment.html: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /src/components/common/continue/continue.config.yaml: -------------------------------------------------------------------------------- 1 | context: 2 | label: More articles by Drew 3 | href: /authors/drewmclellan/ 4 | -------------------------------------------------------------------------------- /src/components/common/continue/continue.css: -------------------------------------------------------------------------------- 1 | .c-continue { 2 | @apply --typeset-ui; 3 | 4 | font-weight: 700; 5 | 6 | &::after { 7 | display: inline-block; 8 | vertical-align: middle; 9 | height: 0.375em; 10 | width: 0.375em; 11 | border: 0.1875em solid; 12 | border-color: currentColor currentColor transparent transparent; 13 | transform: rotate(45deg); 14 | content: ''; 15 | } 16 | } 17 | 18 | .c-continue--ajax { 19 | &::after { 20 | content: '\2026'; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/components/common/continue/continue.html: -------------------------------------------------------------------------------- 1 | {{ label }} 2 | -------------------------------------------------------------------------------- /src/components/common/field/field--combined.html: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | {% render "@button", { label: "Search", submit: true } %} 5 |

6 | -------------------------------------------------------------------------------- /src/components/common/field/field--textarea.html: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 |

5 | -------------------------------------------------------------------------------- /src/components/common/field/field.config.yaml: -------------------------------------------------------------------------------- 1 | collated: true 2 | -------------------------------------------------------------------------------- /src/components/common/field/field.css: -------------------------------------------------------------------------------- 1 | .c-field { 2 | @apply --typeset-ui; 3 | 4 | position: relative; 5 | 6 | &:not(:last-of-type) { 7 | margin-bottom: map(spaces, medium); 8 | } 9 | 10 | @media print { 11 | display: none; 12 | } 13 | } 14 | 15 | .c-field__icon { 16 | height: 2.5rem; 17 | width: 2.5rem; 18 | } 19 | 20 | .c-field__input { 21 | width: 100%; 22 | border: 1px solid $prose-color--rule; 23 | border-radius: 0; 24 | padding: map(spaces, medium); 25 | } 26 | 27 | .c-field__label { 28 | display: block; 29 | position: absolute; 30 | top: 0; 31 | bottom: 0; 32 | left: 0; 33 | width: map(sizes, xlarge); 34 | padding: map(spaces, medium); 35 | font-weight: 700; 36 | 37 | + .c-field__input { 38 | padding-left: calc(map(sizes, xlarge) + map(spaces, medium)); 39 | } 40 | } 41 | 42 | .c-field__label--icon { 43 | padding: map(spaces, small); 44 | 45 | + .c-field__input { 46 | padding-left: map(sizes, large); 47 | } 48 | } 49 | 50 | .c-field--combined { 51 | display: flex; 52 | } 53 | -------------------------------------------------------------------------------- /src/components/common/field/field.html: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 |

5 | -------------------------------------------------------------------------------- /src/components/common/listing/listing.config.yaml: -------------------------------------------------------------------------------- 1 | variants: 2 | - name: default 3 | context: 4 | mods: [summaries] 5 | component: summary 6 | items: 7 | - "@summary" 8 | - "@summary" 9 | - "@summary" 10 | - "@summary" 11 | - name: inset 12 | context: 13 | mods: [summaries, inset] 14 | component: summary 15 | items: 16 | - "@summary" 17 | - "@summary" 18 | - "@summary" 19 | - "@summary" 20 | - name: authors 21 | context: 22 | mods: [authors] 23 | component: author 24 | items: 25 | - "@author" 26 | - "@author" 27 | - "@author" 28 | - "@author" 29 | -------------------------------------------------------------------------------- /src/components/common/listing/listing.css: -------------------------------------------------------------------------------- 1 | .c-listing { 2 | display: flex; 3 | flex-wrap: wrap; 4 | 5 | @supports (display: grid) { 6 | display: grid; 7 | grid-gap: map(spaces, xsmall); 8 | } 9 | 10 | > * { 11 | display: flex; 12 | flex: 1; 13 | margin: 0 map(spaces, xsmall) map(spaces, xsmall) 0; 14 | 15 | @supports (display: grid) { 16 | margin: 0; 17 | } 18 | } 19 | } 20 | 21 | .c-listing--authors { 22 | grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr)); 23 | 24 | > * { 25 | flex: 8rem 0 0; 26 | } 27 | } 28 | 29 | .c-listing--summaries { 30 | grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); 31 | 32 | > * { 33 | flex: 16em 1 0; 34 | } 35 | } 36 | 37 | .c-listing--inset { 38 | padding: map(spaces, xsmall) 0 0 map(spaces, xsmall); 39 | 40 | @supports (display: grid) { 41 | padding: map(spaces, xsmall); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/components/common/listing/listing.html: -------------------------------------------------------------------------------- 1 |
    2 | {%- for item in items -%} 3 |
  1. 4 | {%- render "@" + component, item -%} 5 |
  2. 6 | {%- endfor -%} 7 |
8 | -------------------------------------------------------------------------------- /src/components/common/meta/meta.config.yaml: -------------------------------------------------------------------------------- 1 | context: 2 | items: 3 | - "" 4 | - "Published in [Code](/topics/code/)" 5 | - "[8 comments](#comments)" 6 | -------------------------------------------------------------------------------- /src/components/common/meta/meta.css: -------------------------------------------------------------------------------- 1 | .c-meta { 2 | @apply --typeset-caption; 3 | 4 | display: flex; 5 | flex-flow: row wrap; 6 | margin-bottom: map(spaces, medium); 7 | border-top: 1px solid $prose-color--rule; 8 | padding: map(spaces, small) 0 map(spaces, large); 9 | 10 | @media (--from-large-screen) { 11 | flex-flow: column nowrap; 12 | border: 0; 13 | padding: 0; 14 | } 15 | } 16 | 17 | .c-meta__item { 18 | flex: 0 0 auto; 19 | 20 | @media screen and (max-width: 24.9375em) { 21 | .dt-published span { 22 | display: none; 23 | } 24 | } 25 | 26 | @media (--upto-large-screen) { 27 | &:not(:last-child)::after { 28 | content: '•'; 29 | margin: 0 map(spaces, small); 30 | opacity: 0.5; 31 | } 32 | } 33 | 34 | @media (--from-large-screen) { 35 | display: block; 36 | border-top: 1px solid $prose-color--rule; 37 | padding: map(spaces, small) 0 map(spaces, large); 38 | text-align: right; 39 | } 40 | 41 | @media print { 42 | &:not(:last-child)::after { 43 | content: '•'; 44 | margin: 0 map(spaces, small); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/components/common/meta/meta.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/components/common/preface/preface.config.yaml: -------------------------------------------------------------------------------- 1 | collated: true 2 | context: 3 | title: Preface title 4 | content: Preface content 5 | variants: 6 | - name: topic 7 | context: 8 | title: Business 9 | titleIcon: topic-business 10 | content: | 11 | Where there's muck, there are clients, deliverables and contracts. Occasionally, there's brass. Here are articles on developing and improving relationships with clients; writing contracts and presenting your work; and encouraging success and avoiding failure. 12 | - name: author 13 | context: 14 | title: Rachel Andrew 15 | avatar: 16 | src: https://cloud.24ways.org/authors/rachelandrew280.jpg 17 | size: 280 18 | microformats: true 19 | content: | 20 | Rachel Andrew is a Director of edgeofmyseat.com, a UK web development consultancy and creators of the small content management system, [Perch](https://grabaperch.com/). She is the author of a number of [books](https://rachelandrew.co.uk/books), and is a regular columnist for [A List Apart](http://alistapart.com/author/rachelandrew). 21 | 22 | She curates a popular [email newsletter on CSS Layout](http://csslayout.news/), and will be launching a [CSS Layout online workshop](https://thecssworkshop.com/) in early 2016. 23 | 24 | When not writing about business and technology on her blog at [rachelandrew.co.uk](https://rachelandrew.co.uk) or [speaking at conferences](http://lanyrd.com/profile/rachelandrew/), you will usually find Rachel running up and down one of the giant hills in Bristol. 25 | 26 | Photo: [James Duncan Davidson](http://duncandavidson.com/) 27 | -------------------------------------------------------------------------------- /src/components/common/preface/preface.css: -------------------------------------------------------------------------------- 1 | .c-preface { 2 | overflow: hidden; 3 | display: flex; 4 | flex-direction: column; 5 | padding: map(spaces, medium) map(spaces, large) calc(map(spaces, large) / 2); 6 | background-color: $color-year; 7 | background-color: var(--color-year, $color-year); 8 | background-image: inline('diamond.svg'); 9 | background-repeat: repeat; 10 | 11 | @media (--from-medium-screen) { 12 | min-height: 9rem; 13 | } 14 | 15 | @media (--from-large-screen) { 16 | padding-top: map(spaces, xxlarge); 17 | padding-left: 25%; 18 | } 19 | } 20 | 21 | .c-preface__title { 22 | @apply --typeset-title; 23 | 24 | display: flex; 25 | align-items: flex-start; 26 | position: relative; 27 | margin-top: auto; 28 | color: map(colors, primary, light); 29 | 30 | svg { 31 | width: 1em; 32 | height: 1em; 33 | margin-right: map(spaces, small); 34 | } 35 | } 36 | 37 | .c-preface__main { 38 | position: relative; 39 | margin-top: map(spaces, small); 40 | color: color(white a(75%)); 41 | 42 | .s-prose p:first-of-type { 43 | @apply --typeset-lede; 44 | } 45 | 46 | .s-prose p:last-of-type { 47 | margin-bottom: 0; 48 | } 49 | 50 | .c-avatar { 51 | width: 50%; 52 | float: right; 53 | margin-top: -7.5%; 54 | margin-right: -7.5%; 55 | 56 | @media (--from-large-screen) { 57 | width: auto; 58 | float: left; 59 | margin-left: -33%; 60 | margin-right: map(spaces, medium); 61 | } 62 | } 63 | 64 | a { 65 | border-bottom: 1px solid color(white a(25%)); 66 | color: white; 67 | 68 | &:hover { 69 | border-bottom-color: color(white a(25%)); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/components/common/preface/preface.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | {%- if titleIcon -%} 4 | {% include titleIcon + ".svg" %} 5 | {%- endif -%} 6 | {{ title }} 7 |

8 | {%- if content -%} 9 |
10 |
11 | {%- if avatar -%} 12 | {% render "@avatar", avatar %} 13 | {%- endif -%} 14 | {{ content | markdown | safe }} 15 |
16 |
17 | {%- endif -%} 18 |
19 | -------------------------------------------------------------------------------- /src/components/common/promo/promo.config.yaml: -------------------------------------------------------------------------------- 1 | context: 2 | href: https://grabaperch.com/?ref=24w01 3 | img: 4 | src: /assets/images/logo-perch.png 5 | alt: Perch - A really little CMS 6 | message: The easiest way to publish **fast, flexible HTML5 websites** your clients will love. 7 | url: grabaperch.com 8 | -------------------------------------------------------------------------------- /src/components/common/promo/promo.css: -------------------------------------------------------------------------------- 1 | .c-promo { 2 | display: block; 3 | margin-bottom: map(spaces, small); 4 | } 5 | 6 | .c-promo__image { 7 | height: auto; 8 | max-width: 100%; 9 | } 10 | 11 | .c-promo__message { 12 | margin-bottom: map(spaces, small); 13 | color: $color-text; 14 | } 15 | 16 | .c-promo__url { 17 | @apply --typeset-label; 18 | } 19 | -------------------------------------------------------------------------------- /src/components/common/promo/promo.html: -------------------------------------------------------------------------------- 1 | 2 | {{ img.alt }} 3 |

{{ message | markdownInline | safe }}

4 |

{{ url }}

5 |
6 | -------------------------------------------------------------------------------- /src/components/common/search-form/search-form.html: -------------------------------------------------------------------------------- 1 |
2 | {%- include "@field--combined" -%} 3 |
4 | -------------------------------------------------------------------------------- /src/components/common/section/section.config.yaml: -------------------------------------------------------------------------------- 1 | collated: true 2 | default: topic 3 | variants: 4 | - name: topic 5 | context: 6 | id: code 7 | title: "[Code](/topic/code/)" 8 | mods: [topic] 9 | icon: topic-code 10 | content: | 11 | Poetry? Not likely (though [Dan Cederholm wrote some doggerel](/2006/gravity-defying-page-corners/) back in 2006). Here are articles on hypertext markup language, its attributes and other minutiae; crafty ~~hacks~~ tips for your cascading style sheets; JavaScript legerdemain; and tinkering with application programming interfaces. 12 | - name: archive 13 | context: 14 | id: 2013 15 | title: "[2013](/2013/)" 16 | content: | 17 | Scourge of browser vendors everywhere, WaSP buzzed its last in March. Dave Shea's CSS Zen Garden celebrated its tenth anniversary in May, and Google Glass was released. Ever broad in its interests, 24 ways tamed Grunt, URLs and GitHub Pages, encouraged readers to write and publish books, and leavened all that with goodies on project management, web typography and SVG. 18 | - name: author 19 | context: 20 | id: author 21 | title: "About the author" 22 | content: | 23 | Drew McLellan is lead developer on your favourite content management systems, [Perch and Perch Runway](https://grabaperch.com/). He is Director and Senior Developer at edgeofmyseat.com in Bristol, England, and is formerly Group Lead at the Web Standards Project. When not publishing 24 ways, Drew keeps a [personal site](http://allinthehead.com/) covering web development issues and themes, [takes photos](https://flickr.com/drewm/), [tweets a lot](https://twitter.com/drewm) and tries to stay upright on his bicycle. 24 | 25 | Photo: [James Duncan Davidson](http://duncandavidson.com/) 26 | 27 | More articles by Drew 28 | - name: sponsor 29 | context: 30 | id: sponsor 31 | title: "Brought to you by" 32 | mods: [sponsor] 33 | component: promo 34 | - name: comments 35 | context: 36 | id: comments 37 | title: "2 Comments" 38 | content: | 39 | Comments are ordered by helpfulness, as indicated by you. Help us pick out the gems and discourage asshattery by voting on notable comments. 40 | 41 | Got something to add? Leave a comment below 42 | comments: 43 | - "@comment" 44 | - "@comment--unhelpful" 45 | - name: related 46 | context: 47 | id: related 48 | title: "Related articles" 49 | mods: [related] 50 | component: listing 51 | - name: authors 52 | context: 53 | id: a 54 | title: "A" 55 | component: listing--authors 56 | -------------------------------------------------------------------------------- /src/components/common/section/section.css: -------------------------------------------------------------------------------- 1 | .c-section { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: space-between; 5 | 6 | @media (--from-large-screen) { 7 | flex-direction: row; 8 | } 9 | } 10 | 11 | .c-section__header { 12 | padding: map(spaces, medium) map(spaces, large); 13 | 14 | @media (--from-large-screen) { 15 | flex: 1 0 20%; 16 | padding-right: 0; 17 | border-top: 1px solid $prose-color--rule; 18 | text-align: right; 19 | } 20 | 21 | @media print { 22 | border-top: 2px solid black; 23 | padding-bottom: 0; 24 | } 25 | } 26 | 27 | .c-section__title { 28 | @apply --typeset-ui; 29 | 30 | text-transform: uppercase; 31 | } 32 | 33 | .c-section__main { 34 | margin-bottom: map(spaces, large); 35 | padding: map(spaces, medium) map(spaces, large); 36 | border-top: 1px solid $prose-color--rule; 37 | 38 | @media (--from-large-screen) { 39 | flex: 1 0 75%; 40 | margin-left: 5%; 41 | padding-left: 0; 42 | } 43 | } 44 | 45 | /* Section - Sponsor */ 46 | .c-article .c-section--sponsor { 47 | @media (--from-large-screen) { 48 | flex-direction: column; 49 | position: absolute; 50 | top: 56rem; 51 | left: 0; 52 | width: 20%; 53 | padding: 0; 54 | 55 | .c-section__title { 56 | @apply --typeset-ui; 57 | } 58 | 59 | .c-section__main { 60 | padding-right: 0; 61 | border-top: 0; 62 | } 63 | 64 | .c-promo { 65 | @apply --typeset-caption; 66 | 67 | text-align: right; 68 | } 69 | } 70 | } 71 | 72 | /* Section - Topic */ 73 | .c-section--topic { 74 | .c-section__main { 75 | display: flex; 76 | flex-direction: row; 77 | } 78 | 79 | svg { 80 | flex-shrink: 0; 81 | width: map(sizes, xlarge); 82 | height: map(sizes, xlarge); 83 | margin: map(spaces, small) map(spaces, large) 0 0; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/components/common/section/section.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{ title | markdownInline | safe }}

4 |
5 |
6 | {%- if component -%} 7 | {% render "@" + component, config %} 8 | {%- endif -%} 9 | {%- if icon -%} 10 | {% include icon + ".svg" %} 11 | {%- endif -%} 12 | {%- if content -%} 13 |
14 | {{ content | markdown | safe }} 15 |
16 | {%- endif -%} 17 | {%- if comments -%} 18 | {%- for comment in comments -%} 19 | {% render "@comment", comment %} 20 | {%- endfor -%} 21 | {%- endif -%} 22 |
23 |
24 | -------------------------------------------------------------------------------- /src/components/common/summary/summary.config.yaml: -------------------------------------------------------------------------------- 1 | collated: true 2 | context: 3 | theme: day-15 4 | href: /2015/grid-flexbox-box-alignment-our-new-system-for-layout/ 5 | title: "Grid, Flexbox, Box Alignment: Our New System for Layout" 6 | author: Rachel Andrew 7 | datetime: "2010-12-15T00:00:00-00:00" 8 | comments: 6 9 | content: | 10 | [Rachel Andrew](https://rachelandrew.co.uk/) unwraps the new paradigms of web layout, comparing their features and showing how they can free us from grid-based, `div`-infested frameworks. Beautifully wrapped boxes look lovely under the Christmas tree, but we need to think and break out of them. 11 | variants: 12 | - name: countdown 13 | context: 14 | mods: [countdown] 15 | theme: day-15 16 | href: /2010/real-animation-using-javascript-css3-and-html5-video/ 17 | title: "Real Animation Using JavaScript, CSS3, and HTML5 Video" 18 | author: Dan Mall 19 | datetime: "2010-12-15T00:00:00-00:00" 20 | comments: 11 21 | content: | 22 | [Dan Mall](http://danielmall.com/) breathes life into web standards-based animation. By striving for more than just mechanical movement, we can create more believable animated effects to enhance our users' experience. 23 | - name: short 24 | context: 25 | mods: [countdown] 26 | theme: day-15 27 | href: /2010/real-animation-using-javascript-css3-and-html5-video/ 28 | title: "Real Animation" 29 | author: Dan Mall 30 | datetime: "2010-12-15T00:00:00-00:00" 31 | comments: 11 32 | content: | 33 | *Dan Mall* breathes life into web standards-based animation. 34 | - name: sponsored 35 | context: 36 | mods: [sponsored] 37 | href: mailto:sponsorship@24ways.org?subject=Sponsoring 24 ways on Friday, 1 December 38 | title: "Friday, 1 December" 39 | author: false 40 | datetime: false 41 | content: | 42 | Sponsor this day for $250 USD. 43 | - name: sponsored-taken 44 | context: 45 | mods: [sponsored, taken] 46 | href: false 47 | title: "Saturday, 2 December" 48 | author: false 49 | datetime: false 50 | content: | 51 | Too late, this slot has been taken! 52 | -------------------------------------------------------------------------------- /src/components/common/summary/summary.css: -------------------------------------------------------------------------------- 1 | $summary__author-size: map(sizes, xlarge); 2 | 3 | @keyframes corner-forward { 4 | from { 5 | z-index: map(layers, default); 6 | background-position: -0.5em; 7 | } 8 | 9 | to { 10 | z-index: calc(map(layers, default) + 1); 11 | background-position: -50.5em; 12 | } 13 | } 14 | 15 | @keyframes corner-reverse { 16 | from { 17 | z-index: calc(map(layers, default) + 1); 18 | background-position: -50.5em; 19 | } 20 | 21 | to { 22 | z-index: map(layers, default); 23 | background-position: -0.5em; 24 | } 25 | } 26 | 27 | .c-summary { 28 | display: flex; 29 | flex-direction: column; 30 | flex: 1; 31 | position: relative; 32 | padding: map(spaces, medium) calc(map(spaces, large) - map(spaces, xsmall)); 33 | background-color: white; 34 | box-shadow: 1px 1px 0 $prose-color--shadow; 35 | 36 | @media (-ms-high-contrast: active) { 37 | border: 1px solid; 38 | } 39 | 40 | @media print { 41 | padding-bottom: 0; 42 | height: auto !important; 43 | min-height: 12em; 44 | page-break-inside: avoid; 45 | } 46 | } 47 | 48 | .c-summary__header { 49 | min-height: map(sizes, large); 50 | margin-bottom: map(spaces, medium); 51 | 52 | @media print { 53 | border-top: 1px solid black; 54 | padding-top: map(spaces, medium); 55 | } 56 | } 57 | 58 | .c-summary__title { 59 | @apply --typeset-summary-title; 60 | 61 | padding-right: calc($summary__author-size - map(spaces, medium)); 62 | 63 | a::before { 64 | content: ''; 65 | overflow: hidden; 66 | position: absolute; 67 | top: 0; 68 | right: 0; 69 | bottom: 0; 70 | left: 0; 71 | z-index: map(layers, default); 72 | white-space: nowrap; 73 | text-indent: 200%; 74 | } 75 | 76 | a:focus { 77 | outline: none; 78 | 79 | &::before { 80 | outline: 2px solid $color-focus; 81 | } 82 | } 83 | } 84 | 85 | .c-summary__main { 86 | @apply --typeset-prose; 87 | 88 | max-width: 75ch; 89 | font-size: 0.875rem; 90 | 91 | a, 92 | em { 93 | font-family: map(fonts, family-sans); 94 | font-style: normal; 95 | font-weight: 700; 96 | color: inherit; 97 | } 98 | } 99 | 100 | .c-summary__footer { 101 | @media screen { 102 | margin-top: auto; 103 | padding-top: map(spaces, medium); 104 | } 105 | } 106 | 107 | .c-summary__meta { 108 | @apply --typeset-label; 109 | 110 | color: map(colors, neutral, dark); 111 | 112 | .dt-published { 113 | margin-right: map(spaces, medium); 114 | } 115 | } 116 | 117 | .c-summary__author { 118 | position: absolute; 119 | top: 0; 120 | right: 0; 121 | margin: 0; 122 | pointer-events: none; 123 | 124 | @media print { 125 | display: none; 126 | } 127 | 128 | img { 129 | display: block; 130 | height: $summary__author-size; 131 | width: $summary__author-size; 132 | transition: transform $author-duration ease-out; 133 | 134 | @media (prefers-reduced-motion) { 135 | transition: none; 136 | } 137 | } 138 | 139 | span { 140 | @apply --hidden; 141 | } 142 | } 143 | 144 | .c-summary__author-url { 145 | display: block; 146 | overflow: hidden; 147 | 148 | &::before { 149 | content: ''; 150 | display: block; 151 | position: absolute; 152 | top: 0; 153 | right: 0; 154 | z-index: map(layers, default); 155 | height: $summary__author-size; 156 | width: $summary__author-size; 157 | background-image: linear-gradient(to top right, white, white 50%, transparent 50%, transparent); 158 | } 159 | 160 | &::after { 161 | content: ''; 162 | display: block; 163 | position: absolute; 164 | top: 0; 165 | right: 0; 166 | height: $summary__author-size; 167 | width: $summary__author-size; 168 | background: inline('corner.svg') -0.5em 0 no-repeat; 169 | background-size: size('corner.svg'); 170 | } 171 | 172 | .c-summary:hover &, 173 | &:focus { 174 | img { 175 | transform: scale(1.2); 176 | transform-origin: bottom left; 177 | } 178 | 179 | &::after { 180 | z-index: calc(map(layers, default) + 1); 181 | animation: corner-forward 0.15s steps(10); 182 | animation-fill-mode: forwards; 183 | } 184 | } 185 | 186 | &:focus { 187 | box-shadow: 0 0 2px 0 $color-focus; 188 | } 189 | } 190 | 191 | .c-summary--countdown { 192 | .c-summary__header { 193 | padding-left: calc(map(spaces, large) + map(spaces, xsmall)); 194 | } 195 | 196 | .c-summary__footer { 197 | padding-top: 0; 198 | } 199 | 200 | .dt-published { 201 | @apply --typeset-summary-title; 202 | 203 | font-weight: 400; 204 | position: absolute; 205 | top: map(spaces, medium); 206 | left: calc(map(spaces, medium) + map(spaces, xsmall)); 207 | z-index: 0; 208 | 209 | span { 210 | @apply --hidden; 211 | } 212 | 213 | @media print { 214 | top: 2rem; 215 | } 216 | } 217 | } 218 | 219 | .c-summary--taken { 220 | .c-summary__title { 221 | opacity: 0.66; 222 | text-decoration: line-through; 223 | } 224 | 225 | background-image: repeating-linear-gradient(-45deg, transparent, transparent 4px, color(map(colors, neutral, lightest) a(75%)) 4px, color(map(colors, neutral, lightest) a(75%)) 8px); 226 | } 227 | -------------------------------------------------------------------------------- /src/components/common/summary/summary.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | {%- if href -%} 5 | {{ title }} 6 | {%- else -%} 7 | {{ title }} 8 | {%- endif -%} 9 |

10 | {%- if author -%} 11 |

12 | 13 | 14 | {{ author }} 15 | 16 |

17 | {%- endif -%} 18 |
19 |
20 |

{{ content | markdownInline | safe }}

21 |
22 | {%- if datetime -%} 23 |
24 |

25 | 28 |

29 |
30 | {%- endif -%} 31 |
32 | -------------------------------------------------------------------------------- /src/components/components.config.yaml: -------------------------------------------------------------------------------- 1 | context: 2 | site: 3 | title: 24 ways 4 | handle: 24ways 5 | description: 24 ways is the advent calendar for web geeks. Each day throughout December we publish a daily dose of web design and development goodness to bring you all a little Christmas cheer. 6 | url: https://24ways.org 7 | feed: https://feeds.feedburner.com/24ways 8 | theme_color: "#f04" 9 | theme: 10 | # Year hues = 348 - (4 × year increment) 11 | year: [348, 344, 340, 336, 332, 328, 324, 320, 316, 312, 308, 304, 300, 296, 292, 288] 12 | # Day hues = 360 - (7 × day increment) 13 | day: [360, 353, 346, 339, 332, 325, 318, 311, 304, 297, 290, 283, 276, 269, 262, 255, 248, 241, 234, 227, 220, 213, 206, 199] 14 | -------------------------------------------------------------------------------- /src/components/global/banner/banner.css: -------------------------------------------------------------------------------- 1 | .c-banner { 2 | background-color: $color-year--dark; 3 | background-color: var(--color-year--dark, $color-year--dark); 4 | 5 | .has-js & { 6 | @media (--from-medium-screen) { 7 | margin-right: $navigation-width--large; 8 | } 9 | } 10 | 11 | @media (-ms-high-contrast: active) { 12 | border-bottom: 1px solid; 13 | } 14 | } 15 | 16 | .c-banner__title { 17 | padding: map(spaces, medium) map(spaces, large); 18 | font-size: 1.25em; 19 | line-height: calc(16 / 20); 20 | 21 | .has-js & { 22 | @media (--upto-medium-screen) { 23 | position: fixed; 24 | top: 0; 25 | right: 0; 26 | left: 0; 27 | z-index: calc(map(layers, modal) + 1); 28 | margin-right: map(sizes, large); 29 | } 30 | } 31 | 32 | @media (--from-medium-screen) { 33 | padding: map(spaces, large); 34 | font-size: 1.5em; 35 | line-height: calc(24 / 24); 36 | } 37 | 38 | @media (--from-large-screen) { 39 | padding-left: 25%; 40 | } 41 | } 42 | 43 | .c-banner__home { 44 | text-transform: uppercase; 45 | color: white; 46 | 47 | &:hover { 48 | color: white; 49 | } 50 | 51 | span { 52 | font-size: 0.6667em; 53 | text-transform: lowercase; 54 | white-space: nowrap; 55 | color: map(colors, primary, light); 56 | 57 | @media (--upto-xsmall-screen) { 58 | @apply --hidden; 59 | } 60 | } 61 | } 62 | 63 | .c-banner__skip { 64 | @apply --typeset-ui; 65 | 66 | position: fixed; 67 | top: 0; 68 | left: map(spaces, medium); 69 | z-index: map(layers, overlay); 70 | display: block; 71 | background-color: $navigation-color; 72 | box-shadow: 0 8px 8px 0 $navigation-color--shadow; 73 | padding: map(spaces, xlarge) map(spaces, small) map(spaces, xsmall); 74 | color: $navigation-color--text; 75 | transform: translateY(0); 76 | 77 | &:hover { 78 | color: $color-ui-link--hover; 79 | } 80 | 81 | &:active { 82 | color: $color-ui-link--active; 83 | } 84 | 85 | &:not(:focus) { 86 | @apply --hidden; 87 | 88 | transform: translateY(-100%); 89 | transition: transform 0.5s ease-out; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/components/global/banner/banner.html: -------------------------------------------------------------------------------- 1 |
2 | Skip to content 3 |

4 | 24 ways to impress your friends 5 |

6 |
7 | -------------------------------------------------------------------------------- /src/components/global/contentinfo/contentinfo.css: -------------------------------------------------------------------------------- 1 | .c-contentinfo { 2 | @apply --typeset-label; 3 | 4 | display: flex; 5 | flex-wrap: wrap; 6 | justify-content: space-between; 7 | padding: map(spaces, small) map(spaces, large); 8 | 9 | .has-js & { 10 | @media (--from-medium-screen) { 11 | margin-right: $navigation-width--large; 12 | } 13 | } 14 | 15 | @media (--from-large-screen) { 16 | padding-left: map(spaces, medium); 17 | padding-right: map(spaces, medium); 18 | } 19 | 20 | @media (-ms-high-contrast: active) { 21 | border-top: 1px solid; 22 | } 23 | 24 | a { 25 | white-space: nowrap; 26 | 27 | @media print { 28 | display: none; 29 | } 30 | } 31 | 32 | > * { 33 | @media (--upto-large-screen) { 34 | line-height: 2; 35 | } 36 | } 37 | } 38 | 39 | .c-contentinfo__social a { 40 | margin-right: map(spaces, medium); 41 | } 42 | -------------------------------------------------------------------------------- /src/components/global/contentinfo/contentinfo.html: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /src/components/global/main/main.css: -------------------------------------------------------------------------------- 1 | .c-main { 2 | background-color: $navigation-color--offset; 3 | 4 | .has-js & { 5 | @media (--upto-medium-screen) { 6 | margin-top: $banner-height--small; 7 | } 8 | 9 | @media (--from-medium-screen) { 10 | margin-right: $navigation-width--large; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/components/global/menu/menu.css: -------------------------------------------------------------------------------- 1 | .c-menu { 2 | .has-js & { 3 | position: fixed; 4 | z-index: map(layers, modal); 5 | 6 | /* Create ‘fake’ banner/sidebar, so drawer can slide from underneath it */ 7 | &::before { 8 | position: fixed; 9 | top: 0; 10 | right: 0; 11 | z-index: calc(map(layers, modal) + 1); 12 | display: block; 13 | content: ''; 14 | 15 | @media (--upto-medium-screen) { 16 | left: 0; 17 | background-color: $color-year--dark; 18 | background-color: var(--color-year--dark, $color-year--dark); 19 | height: $banner-height--small; 20 | width: 100%; 21 | } 22 | 23 | @media (--from-medium-screen) { 24 | background-color: white; 25 | box-shadow: inset 1px 0 $navigation-color--offset; 26 | height: 100%; 27 | width: $navigation-width--large; 28 | } 29 | } 30 | } 31 | 32 | &.no-transition * { 33 | transition: none !important; 34 | } 35 | 36 | @media print { 37 | display: none; 38 | } 39 | } 40 | 41 | .c-menu__button { 42 | @apply --focusable; 43 | 44 | display: block; 45 | position: fixed; 46 | top: 0; 47 | right: 0; 48 | z-index: calc(map(layers, modal) + 1); 49 | 50 | .no-js & { 51 | display: none; 52 | } 53 | 54 | @media (--upto-medium-screen) { 55 | padding: map(sizes, xsmall); 56 | color: white; 57 | 58 | &:hover, 59 | &:active { 60 | color: white; 61 | } 62 | } 63 | 64 | @media (--from-medium-screen) { 65 | @apply --navigation-link; 66 | 67 | padding: map(spaces, medium); 68 | box-shadow: inset 0 -1px 0 $navigation-color--offset; 69 | } 70 | 71 | @media (-ms-high-contrast: active) { 72 | border: 1px solid; 73 | } 74 | 75 | @media print { 76 | display: none; 77 | } 78 | } 79 | 80 | .c-menu__drawer { 81 | display: flex; 82 | flex-direction: column; 83 | background-color: $navigation-color; 84 | 85 | [data-menu-expanded] & { 86 | box-shadow: 0 8px 8px 0 $navigation-color--shadow; 87 | transition: all $navigation-duration ease-in-out; 88 | 89 | @media (--upto-medium-screen) { 90 | width: 100vw; 91 | } 92 | 93 | @media (--from-medium-screen) { 94 | position: fixed; 95 | top: 0; 96 | right: 0; 97 | bottom: 0; 98 | width: 18rem; 99 | } 100 | } 101 | 102 | [data-menu-expanded=false] & { 103 | @media (--upto-medium-screen) { 104 | transform: translateY(-100%); 105 | } 106 | 107 | @media (--from-medium-screen) { 108 | transform: translateX(100%); 109 | } 110 | } 111 | 112 | [data-menu-expanded=true] & { 113 | @media (--upto-medium-screen) { 114 | transform: translateY(0); 115 | } 116 | 117 | @media (--from-medium-screen) { 118 | transform: translateX(calc($navigation-width--large * -1)); 119 | } 120 | } 121 | } 122 | 123 | .c-menu__icon { 124 | height: 2.5rem; 125 | width: 2.5rem; 126 | } 127 | 128 | .c-menu__line { 129 | $lines-animation-duration: 0.2s; 130 | $cross-animation-duration: 0.1s; 131 | 132 | transform-origin: 50% 50%; 133 | 134 | /* Transitions: ☰ */ 135 | &:nth-of-type(1), 136 | &:nth-of-type(2), 137 | &:nth-of-type(5), 138 | &:nth-of-type(6) { 139 | transform: translateY(0); 140 | transition: 141 | transform $lines-animation-duration cubic-bezier(0.8, 0, 0.4, 1.8) $cross-animation-duration, 142 | opacity $lines-animation-duration cubic-bezier(1, 0, 1, 0); 143 | 144 | [aria-expanded=true] & { 145 | opacity: 0; 146 | transition: 147 | transform $lines-animation-duration ease-out, 148 | opacity $lines-animation-duration cubic-bezier(1, 0, 1, 0); 149 | } 150 | } 151 | 152 | /* Transitions: × */ 153 | &:nth-of-type(3), 154 | &:nth-of-type(4) { 155 | transform: rotate(0) scaleX(1); 156 | transition: transform $cross-animation-duration ease-out; 157 | 158 | [aria-expanded=true] & { 159 | transition: transform $cross-animation-duration cubic-bezier(0.8, 0, 0.4, 1.8) $lines-animation-duration; 160 | } 161 | } 162 | 163 | /* Transforms */ 164 | [aria-expanded=true] & { 165 | &:nth-of-type(1) { transform: translateY(25%); } 166 | &:nth-of-type(2) { transform: translateY(12.5%); } 167 | &:nth-of-type(3) { transform: rotate(45deg) scaleX(1.3334); } 168 | &:nth-of-type(4) { transform: rotate(-45deg) scaleX(1.3334); } 169 | &:nth-of-type(5) { transform: translateY(-12.5%); } 170 | &:nth-of-type(6) { transform: translateY(-25%); } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/components/global/menu/menu.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | 10 | 13 |
14 | -------------------------------------------------------------------------------- /src/components/global/search/search.css: -------------------------------------------------------------------------------- 1 | .c-search { 2 | .c-field__input { 3 | width: 100%; 4 | box-shadow: inset 0 -1px 0 $navigation-color--offset; 5 | padding-left: map(spaces, xlarge); 6 | background-color: $navigation-color--offset; 7 | 8 | &:not(:focus) { 9 | box-shadow: none; 10 | } 11 | 12 | .has-js & { 13 | @media (--from-medium-screen) { 14 | height: $banner-height--large; 15 | } 16 | } 17 | 18 | @media (-ms-high-contrast: active) { 19 | border: 1px solid; 20 | } 21 | } 22 | 23 | .c-field__button { 24 | @apply --focusable; 25 | 26 | position: absolute; 27 | top: 0; 28 | left: 0; 29 | padding: map(spaces, xsmall) map(spaces, small); 30 | 31 | .has-js & { 32 | @media (--from-medium-screen) { 33 | padding: map(spaces, medium) map(spaces, xsmall); 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/components/global/search/search.html: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /src/components/global/site-nav/site-nav.css: -------------------------------------------------------------------------------- 1 | .c-site-nav { 2 | @media (--from-medium-screen) { 3 | margin-top: auto; 4 | } 5 | } 6 | 7 | .c-site-nav__items { 8 | display: flex; 9 | flex-direction: row; 10 | 11 | .has-js & { 12 | @media (--from-medium-screen) { 13 | flex-direction: column; 14 | } 15 | } 16 | } 17 | 18 | .c-site-nav__item { 19 | flex: 1 0 33.3334%; 20 | } 21 | 22 | .c-site-nav__label { 23 | @apply --focusable; 24 | @apply --navigation-link; 25 | @apply --typeset-label; 26 | 27 | display: block; 28 | box-shadow: 1px 0 0 $navigation-color--offset; 29 | padding: map(spaces, small); 30 | text-align: center; 31 | 32 | &:focus { 33 | position: relative; /* Fix border overlap on focus */ 34 | } 35 | 36 | .no-js & { 37 | box-shadow: 1px 0 0 $navigation-color--offset; 38 | } 39 | 40 | @media (--from-shallow-screen) { 41 | padding: map(spaces, medium); 42 | } 43 | 44 | @media (--from-medium-screen) { 45 | box-shadow: 0 -1px 0 $navigation-color--offset; 46 | text-align: left; 47 | } 48 | 49 | @media (-ms-high-contrast: active) { 50 | border: 1px solid; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/components/global/site-nav/site-nav.html: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /src/components/global/topics-nav/topics-nav.config.yaml: -------------------------------------------------------------------------------- 1 | context: 2 | topics: 3 | - title: Business 4 | url: /topics/business/ 5 | - title: Code 6 | url: /topics/code/ 7 | - title: Content 8 | url: /topics/content/ 9 | - title: Design 10 | url: /topics/design/ 11 | - title: Process 12 | url: /topics/process/ 13 | - title: UX 14 | url: /topics/ux/ 15 | -------------------------------------------------------------------------------- /src/components/global/topics-nav/topics-nav.css: -------------------------------------------------------------------------------- 1 | .c-topics-nav__items { 2 | display: flex; 3 | flex-wrap: wrap; 4 | align-items: flex-start; 5 | align-content: flex-start; 6 | box-shadow: inset 0 1px 0 $navigation-color--offset; 7 | 8 | @media (--from-medium-screen) { 9 | box-shadow: 0 -1px 0 $navigation-color--offset; 10 | } 11 | } 12 | 13 | .c-topics-nav__item { 14 | flex: 1 0 33.3334%; 15 | 16 | .has-js & { 17 | @media (--from-medium-screen) { 18 | flex-basis: 50%; 19 | min-width: 50%; 20 | } 21 | } 22 | 23 | @media (--from-large-screen) { 24 | flex-basis: 16.6667%; 25 | } 26 | 27 | svg { 28 | height: map(sizes, medium); 29 | width: map(sizes, medium); 30 | margin: 0 auto map(spaces, xsmall); 31 | 32 | @media (--upto-deep-screen) { 33 | display: none; 34 | } 35 | 36 | @media (--from-medium-screen) { 37 | height: map(sizes, large); 38 | width: map(sizes, large); 39 | } 40 | } 41 | } 42 | 43 | .c-topics-nav__label { 44 | @apply --focusable; 45 | @apply --navigation-link; 46 | @apply --typeset-ui; 47 | 48 | display: block; 49 | box-shadow: 1px 1px 0 $navigation-color--offset; 50 | padding: map(spaces, small); 51 | text-align: center; 52 | 53 | @media (--from-shallow-screen) { 54 | padding: map(spaces, large) map(spaces, medium); 55 | } 56 | 57 | @media (-ms-high-contrast: active) { 58 | border: 1px solid; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/components/global/topics-nav/topics-nav.html: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /src/components/global/traverse-nav/traverse-nav.config.yaml: -------------------------------------------------------------------------------- 1 | context: 2 | traverse: 3 | type: article 4 | prev: 5 | url: /2015/how-tabs-should-work/ 6 | title: "How Tabs Should Work" 7 | next: 8 | url: /2015/solve-the-hard-problems/ 9 | title: "Solve the Hard Problems" 10 | -------------------------------------------------------------------------------- /src/components/global/traverse-nav/traverse-nav.css: -------------------------------------------------------------------------------- 1 | $traverse-nav__label-width: 18rem; /* 288px */ 2 | 3 | .c-traverse-nav { 4 | display: flex; 5 | 6 | .has-js & { 7 | @media (--from-medium-screen) { 8 | display: block; 9 | position: fixed; 10 | right: 0; 11 | top: $banner-height--large; 12 | z-index: calc(map(layers, modal) + 1); 13 | width: $navigation-width--large; 14 | } 15 | } 16 | } 17 | 18 | .c-traverse-nav__item { 19 | @apply --focusable; 20 | @apply --navigation-link; 21 | 22 | display: flex; 23 | flex: 50% 0 1; 24 | justify-content: center; 25 | position: relative; 26 | width: auto; 27 | box-shadow: inset 0 -1px 0 $navigation-color--offset; 28 | padding: map(spaces, medium); 29 | 30 | @media (-ms-high-contrast: active) { 31 | border: 1px solid; 32 | } 33 | } 34 | 35 | .c-traverse-nav__item[rel] { 36 | &::before, 37 | &::after { 38 | position: absolute; 39 | top: 0; 40 | bottom: 0; 41 | left: 0; 42 | right: 0; 43 | } 44 | 45 | /* Box: covers ear */ 46 | &::before { 47 | z-index: map(layers, underlay); 48 | background-color: $navigation-color; 49 | box-shadow: inset 1px 0 $navigation-color--offset; 50 | content: ''; 51 | } 52 | 53 | /* Ear: slides from under box on hover */ 54 | &::after { 55 | .has-js & { 56 | @media (--from-medium-screen) { 57 | @apply --typeset-label; 58 | 59 | z-index: calc(map(layers, underlay) - 1); 60 | width: $traverse-nav__label-width; 61 | overflow: hidden; 62 | background-color: $color-year--dark-alpha; 63 | background-color: var(--color-year--dark-alpha, $color-year--dark-alpha); 64 | backdrop-filter: blur(4px); 65 | padding: map(spaces, xsmall) map(spaces, small); 66 | white-space: pre-wrap; 67 | color: white; 68 | transition: all 0.3s ease-out; 69 | content: attr(aria-label); 70 | } 71 | } 72 | 73 | [data-menu-expanded=true] & { 74 | display: none; 75 | } 76 | } 77 | 78 | &:hover { 79 | &::after { 80 | left: calc($traverse-nav__label-width * -1); 81 | } 82 | } 83 | } 84 | 85 | .c-traverse-nav__icon { 86 | height: 2.5rem; 87 | width: 2.5rem; 88 | } 89 | -------------------------------------------------------------------------------- /src/components/global/traverse-nav/traverse-nav.html: -------------------------------------------------------------------------------- 1 | {% if traverse %} 2 | 14 | {% endif %} 15 | -------------------------------------------------------------------------------- /src/components/og-image/og-image.config.yaml: -------------------------------------------------------------------------------- 1 | title: Open Graph sharing image 2 | label: OG Image 3 | order: 1 4 | context: 5 | quote: "There's no prize for solving problems you don't have yet, and heading further into the desert in search of water is a survival tactic, not an aspiration." 6 | author: Drew McLellan 7 | avatar: 8 | size: 220 9 | src: https://cloud.24ways.org/authors/drewmclellan280.jpg 10 | -------------------------------------------------------------------------------- /src/components/og-image/og-image.css: -------------------------------------------------------------------------------- 1 | .c-og-image { 2 | width: 600px; 3 | height: 315px; 4 | overflow: hidden; 5 | padding: 2rem; 6 | background-color: $color-year; 7 | background-color: var(--color-year, $color-year); 8 | background-image: inline('diamond.svg'); 9 | background-repeat: repeat; 10 | } 11 | 12 | .c-og-image__quote { 13 | @apply --typeset-prose; 14 | 15 | margin-bottom: map(spaces, small); 16 | font-family: map(fonts, family-serif); 17 | font-size: 2rem; 18 | line-height: 1.15; 19 | text-indent: -0.45em; 20 | color: white; 21 | } 22 | 23 | @supports (hanging-punctuation: first) { 24 | .c-og-image__quote { 25 | text-indent: 0; 26 | } 27 | } 28 | 29 | .c-og-image__quote--small { 30 | font-size: 1.75rem; 31 | } 32 | 33 | .c-og-image__cite { 34 | @apply --typeset-caption; 35 | 36 | font-weight: 600; 37 | font-size: 1.25rem; 38 | color: map(colors, primary, light); 39 | } 40 | 41 | .c-og-image .c-avatar { 42 | float: right; 43 | margin: -2.5rem -3.5rem 0 0; 44 | } 45 | -------------------------------------------------------------------------------- /src/components/og-image/og-image.html: -------------------------------------------------------------------------------- 1 |
2 | {% render "@avatar", avatar %} 3 |
4 |

“{{ quote | markdownInline }}”

5 |
6 |
7 | — {{ author }} 8 |
9 |
10 | -------------------------------------------------------------------------------- /src/components/scopes/embed/embed.config.yaml: -------------------------------------------------------------------------------- 1 | collated: true 2 | context: 3 | src: https://www.youtube-nocookie.com/embed/yyPFT3Ajkzc 4 | variants: 5 | - name: widescreen 6 | context: 7 | mods: [widescreen] 8 | src: https://www.youtube-nocookie.com/embed/rGkQDrUcX18 9 | -------------------------------------------------------------------------------- /src/components/scopes/embed/embed.css: -------------------------------------------------------------------------------- 1 | .s-embed { 2 | position: relative; 3 | overflow: hidden; 4 | height: 0; 5 | max-width: 100%; 6 | padding-bottom: 75%; /* 4:3 */ 7 | 8 | iframe { 9 | position: absolute; 10 | top: 0; 11 | left: 0; 12 | height: 100%; 13 | width: 100%; 14 | } 15 | } 16 | 17 | .s-embed--widescreen { 18 | padding-bottom: 56.25%; /* 16:9 */ 19 | } 20 | -------------------------------------------------------------------------------- /src/components/scopes/embed/embed.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | -------------------------------------------------------------------------------- /src/components/scopes/gallery/gallery.css: -------------------------------------------------------------------------------- 1 | .s-gallery { 2 | display: flex; 3 | 4 | @media (--upto-medium-screen) { 5 | flex-direction: column; 6 | } 7 | 8 | img { 9 | display: block; 10 | max-width: 100%; 11 | } 12 | 13 | > * { 14 | flex: 1 1 auto; 15 | width: 100%; 16 | height: 100%; 17 | } 18 | 19 | > *:not(:last-of-type) { 20 | @media (--upto-medium-screen) { 21 | margin-bottom: 2%; 22 | } 23 | 24 | @media (--from-medium-screen) { 25 | margin-right: 2%; 26 | } 27 | } 28 | 29 | > *:nth-last-of-type(2):first-child, 30 | > *:nth-last-of-type(2):first-child ~ * { 31 | @media (--from-medium-screen) { 32 | width: 48%; 33 | } 34 | } 35 | 36 | > *:nth-last-of-type(3):first-child, 37 | > *:nth-last-of-type(3):first-child ~ * { 38 | @media (--from-medium-screen) { 39 | width: 31.3334%; 40 | } 41 | } 42 | 43 | > *:nth-last-of-type(4):first-child, 44 | > *:nth-last-of-type(4):first-child ~ * { 45 | @media (--from-medium-screen) { 46 | width: 23%; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/components/scopes/gallery/gallery.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 11 | 12 | 18 | 19 | 24 | -------------------------------------------------------------------------------- /src/components/scopes/note/note.config.yaml: -------------------------------------------------------------------------------- 1 | context: 2 | content: | 3 | **A note from the editors:** While brilliant for its time, this article no longer reflects modern best practices. 4 | -------------------------------------------------------------------------------- /src/components/scopes/note/note.css: -------------------------------------------------------------------------------- 1 | .s-note { 2 | @apply --typeset-caption; 3 | 4 | margin-bottom: map(spaces, large); 5 | border: solid $prose-color--rule; 6 | border-width: 1px 0; 7 | padding: map(spaces, medium) 0; 8 | 9 | p:not(:last-child) { 10 | margin: map(spaces, medium); 11 | font-size: 1rem; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/components/scopes/note/note.html: -------------------------------------------------------------------------------- 1 |
2 | {{ content | markdown | safe }} 3 |
4 | -------------------------------------------------------------------------------- /src/components/scopes/prose/prose.config.yaml: -------------------------------------------------------------------------------- 1 | context: 2 | content: | 3 |

Rachel Andrew is a Director of edgeofmyseat.com, a UK web development consultancy and creators of the small content management system, Perch. She is the author of a number of books, and is a regular columnist for A List Apart.

4 | 5 | She curates a popular [email newsletter on CSS Layout](http://csslayout.news/), and will be launching a [CSS Layout online workshop](https://thecssworkshop.com/) in early 2016. 6 | 7 | When not writing about business and technology on her blog at [rachelandrew.co.uk](https://rachelandrew.co.uk) or [speaking at conferences](http://lanyrd.com/profile/rachelandrew/), you will usually find Rachel running up and down one of the giant hills in Bristol. 8 | 9 | Photo: [James Duncan Davidson](http://duncandavidson.com/) 10 | variants: 11 | - name: article 12 | context: 13 | mods: [article] 14 | content: | 15 |

Video is a bigger part of the web experience than ever before. With native browser support for HTML5 video elements freeing us from the tyranny of plugins, and the availability of faster internet connections to the workplace, home and mobile networks, it’s now pretty straightforward to publish video in a way that can be consumed in all sorts of ways on all sorts of different web devices.

16 | 17 | I recently worked on a project where the client had shot some dedicated video shorts to publish on their site. They also had some five-second motion graphics produced to top and tail the videos with context and branding. This pretty common requirement is a great idea on the web, where a user might land at your video having followed a link and be viewing a page without much context. 18 | 19 |
20 |

[I]t appears probable that the progenitors of man, either the males or females or both sexes, before acquiring the power of expressing their mutual love in articulate language, endeavoured to charm each other with musical notes and rhythm.

21 |
—Charles DARWIN, The Descent of Man, and Selection in Relation to Sex, 1871
22 |
23 | 24 | Known as _bumpers_, these short introduction clips help brand a video and make it look a lot more professional. 25 | 26 |
27 | Index cards 28 |
Index cards represent each feature the rental property software would launch with.
29 |
30 | 31 |
32 | 36 |
Index cards represent each feature the rental property software would launch with.
37 |
38 | 39 |
40 | 45 |
Index cards represent each feature the rental property software would launch with.
46 |
47 | 48 |
49 | 55 |
Index cards represent each feature the rental property software would launch with.
56 |
57 | 58 | ## Heading 2 59 | The simplest way to add bumpers to a video would be to edit them on to the start and end of the video file itself. Cooking the bumpers into the video file is easy, but should you ever want to update them it can become a real headache. If the branding needs updating, for example, you'd need to re-edit and re-encode all your videos. Not a fun task. 60 | 61 | Save the document by pressing Ctrl + S 62 | 63 | There are many, many options for recording your screen, including QuickTime Player on Mac OS X (FileNew Screen Recording), GifGrabber, or Giffing Tool on Windows. 64 | 65 | Just sample text. 66 | 67 | ## Heading 2… 68 | ### …followed by a heading 3 69 | What if the bumpers could be added dynamically? That would enable you to use the same bumper for multiple videos (decreasing download time for users who might watch more than one) and to update the bumpers whenever you wanted. You could change them seasonally, update them for special promotions, run different advertising slots, perform multivariate testing, or even target different bumpers to different users. 70 | 71 | [View an example](#) 72 | 73 | > The responsive projects I've worked on have had a lot of success combining design and development into one hybrid phase, bringing the two teams into one highly collaborative group. 74 | > 75 | > * A list inside a blockquote 76 | > * Is a very fine thing 77 | > 78 | > A lot of success combining design and development into one hybrid phase, bringing the two teams into one highly collaborative group. 79 | > 80 | > 1. How about an ordered list 81 | > 2. Inside a blockquote, too? 82 | 83 | ### Heading 3 84 | The trade-off, of course, is that if you dynamically add your bumpers, there's a chance that a user in a given circumstance might not see the bumper. For example, if the main video feature was uploaded to YouTube, you'd have no way to control the playback. As always, you need to weigh up the pros and cons and make your choice. 85 | 86 | #### Heading 4 87 | If you wanted to dynamically add bumpers to your HTML5 video, how would you go about it? That was the question I found myself needing to answer for this particular client project. 88 | 89 | ##### Heading 5 90 | My initial thought was to treat it just like an image slideshow. If I were building a slideshow that moved between images, I'd use CSS absolute positioning with `z-index` to stack the images up on top of each other in a pile, with the first image on top. To transition to the second image, I'd use JavaScript to fade the top image out, revealing the second image beneath it. 91 | 92 |
93 |
94 | 95 |
96 |
Example of responsive video embed.
97 |
98 | 99 | Now that video is just a native object in the DOM, just like an image, why not do the same? Stack the videos up with the opening bumper on top, listen for the video's `onended` event, and fade it out to reveal the main feature behind. Good idea, right? 100 | 101 | | The Very Best `Eggnog` | Serves 12 | Serves 24 | 102 | |--------------------------------|-----------------|-----------| 103 | | Milk | 1 quart | 2 quart | 104 | | Cinnamon Sticks | 1 | 2 | 105 | | Vanilla Bean, Split | 1 | 2 | 106 | | Cloves | 5 | 10 | 107 | | Mace | 10 | 20 | 108 | | Egg Yolks | 12 | 24 | 109 | | Cups Sugar | 1 ½ cups | 3 cups | 110 | | Dark Rum | 1 ½ cups | 3 cups | 111 | | Brandy | 1 ½ cups | 3 cups | 112 | | Vanilla | 1 tbsp | 2 tbsp | 113 | | | 1 quart | 2 quart | 114 | 115 | Remember that this is the web. It's never going to be that easy. The problem here is that many non-desktop devices use native, dedicated video players. Think about watching a video on a mobile phone -- when you play the video, the phone often goes full-screen in its native player, leaving the web page behind. There's no opportunity to fade or switch `z-index`, as the video isn't being viewed in the page. Your page is left powerless. Powerless! 116 | 117 | iOS full-screen media player 118 | 119 | So what can we do? What can we control? 120 | 121 | *** 122 | 123 | Those of us with particularly long memories might recall a time before CSS, when we'd have to use JavaScript to perform image rollovers. As CSS background images weren't a practical reality, we would use lots of `` elements, and perform a rollover by modifying the `src` attribute of the image. 124 | 125 |
126 | Index cards 127 |
Index cards represent each feature the rental property software would launch with.
128 |
129 | 130 | Turns out, this old trick of modifying the source can help us out with video, too. In most cases, modifying the `src` attribute of a `