├── .nvmrc ├── sass ├── parts │ └── .gitkeep ├── components │ ├── .gitkeep │ ├── _index.scss │ ├── _button-size-large.scss │ ├── _button-size-small.scss │ ├── _link.scss │ ├── _button-style-ghost.scss │ └── _button.scss ├── views │ ├── _front-page.scss │ ├── _404.scss │ ├── _search.scss │ └── _single.scss ├── gutenberg │ ├── blocks │ │ ├── _core-buttons.scss │ │ ├── _button-file.scss │ │ ├── _core-heading.scss │ │ ├── _core-list.scss │ │ ├── _boxed.scss │ │ ├── _core-video.scss │ │ ├── _core-paragraph.scss │ │ ├── _error.scss │ │ ├── _core-separator.scss │ │ ├── _core-columns.scss │ │ ├── _core-pullquote.scss │ │ ├── _core-image.scss │ │ ├── _core-blockquote.scss │ │ └── _core-table.scss │ ├── formatting │ │ ├── _paragraph.scss │ │ ├── _img.scss │ │ ├── _link.scss │ │ ├── _caption.scss │ │ └── _align.scss │ ├── _formatting.scss │ ├── layout │ │ ├── _button.scss │ │ └── _content.scss │ ├── _blocks.scss │ └── variables │ │ └── _typography.scss ├── navigation │ ├── _navigation.scss │ ├── _nav-click-mobile.scss │ └── _nav-toggle.scss ├── helpers │ ├── _index.scss │ ├── _box-model.scss │ ├── _variablefont.scss │ ├── _images.scss │ ├── _font.scss │ └── _clamp.scss ├── layout │ ├── _print.scss │ ├── _wordpress.scss │ ├── _site-header.scss │ ├── _site-footer.scss │ └── _typography.scss ├── variables │ ├── _index.scss │ ├── _colors.scss │ ├── _breakpoints.scss │ ├── _spacings.scss │ ├── _forms.scss │ └── _typography.scss ├── features │ ├── _gallery.scss │ ├── _top.scss │ └── _pagination.scss ├── gutenberg-editor-styles.scss ├── global.scss └── base │ └── _accessibility.scss ├── svg ├── block-icons │ └── .gitkeep ├── breadcrumbs-arrow.svg ├── slider-left-arrow.svg ├── slider-right-arrow.svg ├── mobile-nav-arrow-down.svg ├── chevron-right.svg ├── chevron-left.svg ├── chevron-up.svg └── logo.svg ├── template-parts ├── blocks │ └── .gitkeep └── header │ ├── branding.php │ └── navigation.php ├── .accessibilityrc ├── .stylelintignore ├── inc ├── functions │ └── index.php ├── template-tags │ ├── wp-body-open.php │ ├── edit-link.php │ ├── entry-footer.php │ └── single-comment.php ├── template-tags.php ├── hooks │ ├── forms.php │ ├── general.php │ ├── acf-blocks.php │ ├── native-gutenberg-blocks.php │ └── scripts-styles.php ├── includes.php ├── includes │ ├── nav-walker-footer.php │ └── post-type.php ├── hooks.php ├── taxonomies │ └── your-taxonomy.php └── post-types │ └── your-post-type.php ├── fonts ├── inter.ttf ├── inter.woff2 ├── inter-400.ttf ├── inter-400.woff ├── inter-500.ttf ├── inter-500.woff ├── inter-600.ttf ├── inter-600.woff ├── inter-700.ttf ├── inter-700.woff ├── inter-400.woff2 ├── inter-500.woff2 ├── inter-600.woff2 ├── inter-700.woff2 ├── monasansvf.woff ├── monasansvf.woff2 ├── inter-400-italic.ttf ├── inter-400-italic.woff └── inter-400-italic.woff2 ├── screenshot.png ├── .prettierrc ├── .jshintignore ├── bin ├── tasks │ ├── dependencies.sh │ ├── project.sh │ ├── askvars.sh │ ├── wsl-packages.sh │ ├── footer.sh │ ├── logo.sh │ ├── get-theme.sh │ ├── permissions.sh │ ├── imports.sh │ ├── variables.sh │ ├── cleanups.sh │ ├── self-update.sh │ ├── replaces-wsl.sh │ ├── header.sh │ ├── additions.sh │ └── replaces.sh ├── air-pack.sh ├── newtheme-wsl.sh ├── newtheme.sh ├── newtheme-popos.sh ├── newtheme-laragon.sh └── air-move-in.sh ├── .browserslistrc ├── js ├── src │ ├── modules │ │ ├── navigation │ │ │ ├── add-multiple-event-listeners.js │ │ │ ├── is-out-of-viewport.js │ │ │ ├── a11y-add-dropdown-toggle-labels.js │ │ │ ├── calculate-dropdown-toggle-height.js │ │ │ ├── a11y-add-dropdown-toggle-labels-click.js │ │ │ ├── check-for-submenu-overflow.js │ │ │ ├── close-sub-menu.js │ │ │ ├── convert-dropdown-menu-items.js │ │ │ ├── a11y-focus-trap.js │ │ │ ├── close-sub-menu-handler.js │ │ │ └── calculate-burger-menu-position.js │ │ ├── localization.js │ │ ├── a11y-focus-search-field.js │ │ ├── a11y-skip-link.js │ │ ├── top.js │ │ └── anchors.js │ ├── front-end.js │ └── gutenberg-editor.js └── prod │ └── gutenberg-editor.js ├── .gitignore ├── .editorconfig ├── gulp ├── tasks │ ├── lintstyles.js │ ├── phpcs.js │ ├── husky.js │ ├── js.js │ ├── blocks.js │ ├── devstyles.js │ ├── watch.js │ └── prodstyles.js ├── webpack.config.dev.js ├── helpers │ └── handle-errors.js ├── webpack.config.prod.js └── config.js ├── .eslintrc.js ├── composer.json ├── gulpfile.js ├── .github └── workflows │ ├── js.yml │ ├── html.yml │ ├── php8.3.yml │ ├── php.yml │ └── styles.yml ├── sidebar.php ├── .husky └── pre-commit-config ├── .hintrc ├── page.php ├── front-page.php ├── header.php ├── 404.php ├── LICENSE.md ├── single.php ├── .svgo.yml ├── readme.txt ├── index.php ├── style.css ├── footer.php ├── package.json └── search.php /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.17.0 2 | -------------------------------------------------------------------------------- /sass/parts/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sass/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /svg/block-icons/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /template-parts/blocks/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.accessibilityrc: -------------------------------------------------------------------------------- 1 | options: { 2 | browser: true 3 | } -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bin 3 | *.* 4 | !*.scss 5 | -------------------------------------------------------------------------------- /inc/functions/index.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /sass/gutenberg/formatting/_paragraph.scss: -------------------------------------------------------------------------------- 1 | // General paragraphs 2 | p { 3 | color: var(--color-paragraph); 4 | line-height: var(--typography-paragraph-line-height); 5 | } 6 | -------------------------------------------------------------------------------- /sass/gutenberg/formatting/_img.scss: -------------------------------------------------------------------------------- 1 | img { 2 | max-width: 100%; 3 | } 4 | 5 | // For images that have width and height set, T-23188 6 | img[width][height] { 7 | height: auto; 8 | } 9 | -------------------------------------------------------------------------------- /sass/navigation/_navigation.scss: -------------------------------------------------------------------------------- 1 | // Navigation core files 2 | @use 'nav-mobile'; 3 | @use 'nav-desktop'; 4 | 5 | // Click navigation 6 | // @use 'nav-click-desktop'; 7 | // @use 'nav-click-mobile'; 8 | -------------------------------------------------------------------------------- /sass/gutenberg/blocks/_button-file.scss: -------------------------------------------------------------------------------- 1 | @use '../../components' as *; 2 | 3 | .wp-block-file .wp-block-file__button { 4 | @include button-size-small(); 5 | font-size: .8em; 6 | padding: .3em .8em; 7 | } 8 | -------------------------------------------------------------------------------- /svg/slider-left-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | js/all.js 2 | js/src/*.js 3 | gulpfile.js 4 | gulp/*.js 5 | gulp/*/*.js 6 | js/prod/*.js 7 | js/dev/*.js 8 | js/src/modules/lazyload.js 9 | js/src/modules/sticky-nav.js 10 | js/src/modules/navigation.js 11 | -------------------------------------------------------------------------------- /bin/tasks/dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "${YELLOW}Installing theme npm packages (like JS/CSS dependencies) for the new theme...${TXTRESET}" 3 | cd ${PROJECT_THEME_PATH} 4 | rm -f package-lock.json 5 | npm update 6 | npm install 7 | -------------------------------------------------------------------------------- /bin/tasks/project.sh: -------------------------------------------------------------------------------- 1 | # Only copy .nvmrc to project root for consistency 2 | echo "${YELLOW}Setting up project configuration...${TXTRESET}" 3 | cp ${PROJECT_THEME_PATH}/.nvmrc ${PROJECT_PATH}/ 2>/dev/null || echo "No .nvmrc found in theme" 4 | -------------------------------------------------------------------------------- /svg/slider-right-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /sass/gutenberg/_formatting.scss: -------------------------------------------------------------------------------- 1 | // Formatting 2 | @use 'gutenberg/formatting/align'; 3 | @use 'gutenberg/formatting/caption'; 4 | @use 'gutenberg/formatting/img'; 5 | @use 'gutenberg/formatting/paragraph'; 6 | @use 'gutenberg/formatting/link'; 7 | -------------------------------------------------------------------------------- /sass/components/_index.scss: -------------------------------------------------------------------------------- 1 | // Components index file 2 | // This file forwards all component definitions for use with @use 3 | @forward 'button'; 4 | @forward 'button-size-small'; 5 | @forward 'button-size-large'; 6 | @forward 'button-style-ghost'; 7 | @forward 'link'; 8 | -------------------------------------------------------------------------------- /sass/helpers/_index.scss: -------------------------------------------------------------------------------- 1 | // Helpers index file 2 | // This file forwards all helper definitions for use with @use 3 | 4 | @forward 'sanitize'; 5 | @forward 'box-model'; 6 | @forward 'clamp'; 7 | @forward 'images'; 8 | @forward 'font'; 9 | @forward 'variablefont'; 10 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | last 1 edge version 2 | last 1 safari version 3 | last 1 chrome version 4 | last 1 firefox version 5 | not firefox > 90 6 | not dead 7 | not ie > 0 8 | not ucandroid > 0 9 | not firefoxandroid > 0 10 | not samsung > 0 11 | not op_mob > 0 12 | > 1% in FI 13 | -------------------------------------------------------------------------------- /svg/mobile-nav-arrow-down.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /js/src/modules/navigation/add-multiple-event-listeners.js: -------------------------------------------------------------------------------- 1 | // Event listener helper function 2 | function addMultipleEventListeners(element, events, handler) { 3 | events.forEach((e) => element.addEventListener(e, handler)); 4 | } 5 | 6 | export default addMultipleEventListeners; 7 | -------------------------------------------------------------------------------- /svg/chevron-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /svg/chevron-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /svg/chevron-up.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | templates/ 3 | content/ 4 | move_out.sh 5 | move_in.sh 6 | theme_tests.sh 7 | bower_components/ 8 | node_modules/ 9 | dandelion.yml 10 | sass/navigation/luxbar 11 | sass/navigation/_luxbar.scss 12 | .DS_Store 13 | html 14 | w3cErrors/ 15 | .vscode 16 | package-lock.json 17 | blocks/*/node_modules 18 | -------------------------------------------------------------------------------- /sass/gutenberg/formatting/_link.scss: -------------------------------------------------------------------------------- 1 | @use '../../components' as *; 2 | 3 | // Default links everywhere 4 | a { 5 | @include link(); 6 | } 7 | 8 | // Define elements where you do not want to use default link styles 9 | .entry-footer, 10 | .site-header, 11 | .site-footer { 12 | a { 13 | text-decoration: none; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | indent_size = 2 4 | end_of_line = lf 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | 9 | [*.md] 10 | charset = unset 11 | end_of_line = unset 12 | insert_final_newline = unset 13 | trim_trailing_whitespace = unset 14 | indent_style = unset 15 | indent_size = unset 16 | -------------------------------------------------------------------------------- /inc/template-tags/wp-body-open.php: -------------------------------------------------------------------------------- 1 | { 2 | const urlSearch = window.location.search; 3 | const urlParams = new URLSearchParams(urlSearch); 4 | if (urlParams.has('s')) { 5 | const searchField = document.querySelector('main input[name="s"]'); 6 | if (searchField) { 7 | searchField.focus({ preventScroll: true }); 8 | } 9 | } 10 | }; 11 | 12 | export default initA11yFocusSearchField; 13 | -------------------------------------------------------------------------------- /gulp/tasks/phpcs.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | const { src } = require('gulp'); 3 | const phpcsplugin = require('gulp-phpcs'); 4 | const config = require('../config.js'); 5 | 6 | // Task 7 | function phpcs(cb) { 8 | return src(config.phpcs.src) 9 | 10 | // Validate files using PHP Code Sniffer 11 | .pipe(phpcsplugin(config.phpcs.opts)) 12 | 13 | // Log all problems that was found 14 | .pipe(phpcsplugin.reporter('log')); 15 | cb(); 16 | } 17 | 18 | exports.phpcs = phpcs; 19 | -------------------------------------------------------------------------------- /sass/components/_button-size-large.scss: -------------------------------------------------------------------------------- 1 | // stylelint-disable number-max-precision, rem-over-px/rem-over-px 2 | @mixin button-size-large() { 3 | font-size: var(--typography-size-18); 4 | line-height: 1.66; 5 | padding-bottom: calc(17px - calc(var(--border-width-input-field) * 2)); 6 | padding-left: calc(21px - calc(var(--border-width-input-field) * 2)); 7 | padding-right: calc(21px - calc(var(--border-width-input-field) * 2)); 8 | padding-top: calc(17px - calc(var(--border-width-input-field) * 2)); 9 | } 10 | -------------------------------------------------------------------------------- /sass/components/_button-size-small.scss: -------------------------------------------------------------------------------- 1 | // stylelint-disable number-max-precision, rem-over-px/rem-over-px 2 | @mixin button-size-small() { 3 | font-size: var(--typography-size-16); 4 | line-height: 20.9091px; 5 | padding-bottom: calc(10px - calc(var(--border-width-input-field) * 2)); 6 | padding-left: calc(21px - calc(var(--border-width-input-field) * 2)); 7 | padding-right: calc(21px - calc(var(--border-width-input-field) * 2)); 8 | padding-top: calc(10px - calc(var(--border-width-input-field) * 2)); 9 | } 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | ignorePatterns: ['**/js/dev/*.js', '**/js/prod/*.js', '**/node_modules/*.js', '**/gulp/**/*.js', '**/gulp/*.js', 'gulpfile.js'], 4 | parser: '@babel/eslint-parser', 5 | parserOptions: { 6 | requireConfigFile: false, 7 | babelOptions: { 8 | presets: ['@wordpress/babel-preset-default'] 9 | } 10 | }, 11 | rules: { 12 | indent: ['error', 2], 13 | }, 14 | env: { 15 | browser: true, 16 | jquery: true, 17 | es6: true, 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /sass/views/_404.scss: -------------------------------------------------------------------------------- 1 | @use '../variables' as *; 2 | @use '../helpers/clamp' as *; 3 | 4 | // Error 404 title 5 | .block-error-404 { 6 | .container { 7 | @media (min-width: $container-mobile) { 8 | padding-bottom: 10%; 9 | padding-top: 10%; 10 | } 11 | } 12 | 13 | .content { 14 | text-align: center; 15 | } 16 | 17 | h1 { 18 | font-size: clamp-calc($container-mobile, 1600px, 60px, 120px); 19 | line-height: var(--typography-heading-core-line-height); 20 | margin-bottom: 1.25rem; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "allow-plugins": { 4 | "dealerdirect/phpcodesniffer-composer-installer": true 5 | } 6 | }, 7 | "require-dev": { 8 | "wp-coding-standards/wpcs": "^3.0", 9 | "phpcompatibility/php-compatibility": "dev-develop as 9.3.5", 10 | "phpcsstandards/phpcsutils": "^1.0", 11 | "squizlabs/php_codesniffer": "*", 12 | "phpcsstandards/phpcsextra": "^1.2.0", 13 | "dealerdirect/phpcodesniffer-composer-installer": "^0.7", 14 | "phpcompatibility/phpcompatibility-wp": "*" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /sass/features/_gallery.scss: -------------------------------------------------------------------------------- 1 | // Classic WordPress galleries 2 | // @source https://github.com/Automattic/_s/blob/50ce93c7cd478871c9ae7504f0ef4748f5de449f/sass/media/_galleries.scss 3 | .gallery { 4 | display: grid; 5 | grid-gap: 1.5em; 6 | margin-bottom: 1.5em; 7 | } 8 | 9 | .gallery-item { 10 | display: inline-block; 11 | text-align: center; 12 | width: 100%; 13 | } 14 | 15 | @for $i from 2 through 9 { 16 | .gallery-columns-#{$i} { 17 | grid-template-columns: repeat($i, 1fr); 18 | } 19 | } 20 | 21 | .gallery-caption { 22 | display: block; 23 | } 24 | -------------------------------------------------------------------------------- /sass/gutenberg/blocks/_core-list.scss: -------------------------------------------------------------------------------- 1 | // Default list block 2 | ul, 3 | ol { 4 | list-style-position: outside; 5 | width: calc((100% - calc(var(--spacing-container-padding-inline) * 2))); 6 | } 7 | 8 | ul { 9 | // list-style-type: '\2022 '; 10 | padding-inline-start: 1.2em; 11 | } 12 | 13 | ol { 14 | padding-inline-start: 1.25em; 15 | } 16 | 17 | // Colorized markers 18 | li::marker { 19 | color: var(--color-link-text); 20 | // Resetting the font in numbering magically removes the gap 21 | font-family: system-ui, sans-serif; 22 | font-size: inherit; 23 | } 24 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | // Export tasks 2 | exports.js = require('./gulp/tasks/js.js').js; 3 | exports.phpcs = require('./gulp/tasks/phpcs.js').phpcs; 4 | exports.lintstyles = require('./gulp/tasks/lintstyles.js').lintstyles; 5 | exports.devstyles = require('./gulp/tasks/devstyles.js').devstyles; 6 | exports.prodstyles = require('./gulp/tasks/prodstyles.js').prodstyles; 7 | exports.husky = require('./gulp/tasks/husky.js').husky; 8 | exports.watch = require('./gulp/tasks/watch.js').watch; 9 | exports.default = require('./gulp/tasks/watch.js').watch; 10 | exports.blocks = require('./gulp/tasks/blocks.js').buildBlocks; 11 | -------------------------------------------------------------------------------- /sass/gutenberg/blocks/_boxed.scss: -------------------------------------------------------------------------------- 1 | @use '../../variables' as *; 2 | 3 | // Boxed block style 4 | .wp-block.is-style-boxed, 5 | .is-style-boxed { 6 | border: 2px solid var(--color-paragraph); 7 | color: var(--color-paragraph); 8 | font-size: var(--typography-size-17); 9 | font-style: normal; 10 | font-weight: var(--typography-weight-medium); 11 | line-height: var(--typography-paragraph-line-height); 12 | padding: 1.25rem; 13 | width: calc(100% - 2.5rem); 14 | } 15 | 16 | @media (min-width: $width-max-article + 40px) { 17 | .is-style-boxed { 18 | transform: translate(1.25rem, 0); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /sass/gutenberg/blocks/_core-video.scss: -------------------------------------------------------------------------------- 1 | @use '../../variables' as *; 2 | // Core/video block 3 | .wp-block-video video { 4 | max-width: 100%; 5 | width: 100%; 6 | } 7 | 8 | @media (max-width: $width-grid-base + 40px) { 9 | .wp-block-video.alignwide { 10 | width: calc(100% - calc(var(--spacing-container-padding-inline) * 2)); 11 | } 12 | } 13 | 14 | .wp-block-video.alignwide { 15 | padding-left: 0; 16 | padding-right: 0; 17 | } 18 | 19 | .wp-block-video.alignfull { 20 | margin-left: 0; 21 | margin-right: 0; 22 | max-width: 100%; 23 | padding-left: 0; 24 | padding-right: 0; 25 | width: 100%; 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/js.yml: -------------------------------------------------------------------------------- 1 | name: JS 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | name: Test JS 8 | runs-on: ubuntu-22.04 9 | 10 | steps: 11 | - name: Checkout the repository 12 | uses: actions/checkout@v3 13 | 14 | - name: Read .nvmrc 15 | run: echo ::set-output name=NVMRC::$(cat .nvmrc) 16 | id: nvm 17 | 18 | - name: Setup node 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: '${{ steps.nvm.outputs.NVMRC }}' 22 | 23 | - name: Install packages and test JS 24 | run: | 25 | npm i 26 | npx eslint -c .eslintrc.js js/src/ 27 | -------------------------------------------------------------------------------- /sass/helpers/_images.scss: -------------------------------------------------------------------------------- 1 | // Styling background-images 2 | // For custom sized image background, use: class="image image-background" 3 | // For bg as layer, use: class="image image-background image-background-layer" 4 | .image-background > img, 5 | .image-background > img[width][height] { 6 | object-fit: cover; 7 | } 8 | 9 | .image-background { 10 | overflow: hidden; 11 | position: relative; 12 | } 13 | 14 | .image-background > img, 15 | .image-background > img[width][height], 16 | .image-background-layer { 17 | height: 100%; 18 | left: 0; 19 | position: absolute; 20 | top: 0; 21 | width: 100%; 22 | z-index: 0; 23 | } 24 | -------------------------------------------------------------------------------- /sidebar.php: -------------------------------------------------------------------------------- 1 | 19 | 20 | 23 | -------------------------------------------------------------------------------- /sass/gutenberg/layout/_button.scss: -------------------------------------------------------------------------------- 1 | // Buttons inside Gutenberg 2 | // We have to override default wp-admin blue buttons 3 | @use '../../components' as *; 4 | 5 | .wp-block .acf-block-preview { 6 | button, 7 | .button, 8 | input[type="reset"], 9 | input[type="submit"], 10 | input[type="button"] { 11 | @include button(); 12 | 13 | // Button sizes 14 | &.button-small { 15 | @include button-size-small(); 16 | } 17 | 18 | &.button-large { 19 | @include button-size-large(); 20 | } 21 | 22 | // Button styles 23 | &.button-ghost { 24 | @include button-style-ghost(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /inc/template-tags.php: -------------------------------------------------------------------------------- 1 | (invalid HTML) 20 | add_filter( 'gform_force_hooks_js_output', '__return_false' ); 21 | -------------------------------------------------------------------------------- /sass/gutenberg/blocks/_core-paragraph.scss: -------------------------------------------------------------------------------- 1 | @use '../../variables' as *; 2 | // Core/paragraph block 3 | .has-larger-font-size, 4 | .has-large-font-size { 5 | line-height: var(--typography-heading-line-height); 6 | } 7 | 8 | .has-large-font-size { 9 | @media (max-width: $container-mobile) { 10 | font-size: var(--typography-size-18); 11 | } 12 | } 13 | 14 | .has-larger-font-size { 15 | @media (max-width: $container-mobile) { 16 | font-size: var(--typography-size-22); 17 | } 18 | } 19 | 20 | @media (max-width: $width-grid-base + 40px) { 21 | .has-background { 22 | width: calc(100% - calc(var(--spacing-container-padding-inline) * 2)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /sass/gutenberg/blocks/_error.scss: -------------------------------------------------------------------------------- 1 | .block-error { 2 | --color-error-block-border: #adb2ad; // Color picked from ACF 3 | --color-error-block-background: #f9f9f9; // Color picked from ACF 4 | background-color: var(--color-error-block-background); 5 | border: 1px solid var(--color-error-block-border); 6 | border-left: 1rem solid var(--color-error); 7 | 8 | .container { 9 | padding-bottom: 1.25rem; 10 | padding-top: 1.25rem; 11 | } 12 | 13 | p { 14 | font-size: var(--typography-size-14); 15 | } 16 | 17 | p.error-message { 18 | font-size: var(--typography-size-16); 19 | } 20 | 21 | h2 { 22 | font-size: var(--typography-size-h4); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /gulp/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | externals: { 4 | jquery: 'jQuery' // Available and loaded through WordPress. 5 | }, 6 | mode: 'development', 7 | module: { 8 | rules: [{ 9 | test: /.js$/, 10 | exclude: /node_modules/, 11 | use: [{ 12 | loader: 'babel-loader', 13 | options: { 14 | presets: [ 15 | ['@babel/preset-env', { 16 | modules: false, 17 | useBuiltIns: 'usage', 18 | corejs: 3, 19 | targets: { 20 | esmodules: true 21 | } 22 | }] 23 | ] 24 | } 25 | }] 26 | }] 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /bin/tasks/wsl-packages.sh: -------------------------------------------------------------------------------- 1 | # Install node for WSL if not installed 2 | if [ ! -d .nvm/versions/node ]; then 3 | echo "${YELLOW}Installing/updating npm${TXTRESET}" 4 | sudo apt-get install curl -y 5 | curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash 6 | export NVM_DIR="$HOME/.nvm" 7 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm 8 | [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion 9 | nvm install node 10 | nvm install --lts 11 | fi 12 | 13 | if [ ! -f /usr/bin/git ]; then 14 | echo "${YELLOW}Git is not installed. Installing git${TXTRESET}" 15 | sudo apt install git -y 16 | fi 17 | -------------------------------------------------------------------------------- /bin/tasks/footer.sh: -------------------------------------------------------------------------------- 1 | echo "" 2 | echo "${BOLDGREEN}All done!${TXTRESET} Theme created: ${THEME_NAME}" 3 | echo "" 4 | echo "Front end: https://${PROJECT_NAME}.test" 5 | echo "WP-admin: https://${PROJECT_NAME}.test/wp/wp-login.php" 6 | echo "" 7 | echo "Theme generated. Go to https://$PROJECT_NAME.test/wp/wp-admin/themes.php and activate your theme, then run ${GREEN}gulp${TXTRESET} in project root ${GREEN}${PROJECTS_HOME}/${PROJECT_NAME}${TXTRESET} and start coding! Your project can be found at ${GREEN}$PROJECT_PATH${TXTRESET} and your theme can be found at ${GREEN}$PROJECT_THEME_PATH${TXTRESET}" 8 | echo "" 9 | echo "Thanks for using newtheme start script ${SCRIPT_LABEL}, v${SCRIPT_VERSION}!" 10 | echo "" 11 | -------------------------------------------------------------------------------- /inc/template-tags/edit-link.php: -------------------------------------------------------------------------------- 1 | 18 | 19 | 25 | esc_html__( 'Sidebar', 'air-light' ), 18 | 'id' => 'sidebar-1', 19 | 'description' => esc_html__( 'Add widgets here.', 'air-light' ), 20 | 'before_widget' => '
', 21 | 'after_widget' => '
', 22 | 'before_title' => '

', 23 | 'after_title' => '

', 24 | ) ); 25 | } // end widgets_init 26 | -------------------------------------------------------------------------------- /template-parts/header/branding.php: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 | 15 |

16 | 17 | 18 | 19 | 20 |

21 | 22 | 23 |

24 | 25 |

26 | 27 | 28 |
29 | -------------------------------------------------------------------------------- /.hintrc: -------------------------------------------------------------------------------- 1 | { 2 | "formatters": [ 3 | "css", 4 | "summary", 5 | "html", 6 | "js" 7 | ], 8 | "parsers": [ 9 | "css", 10 | "sass", 11 | "html", 12 | "javascript" 13 | ], 14 | "extends": [ 15 | "web-recommended", 16 | "accessibility" 17 | ], 18 | "hints": { 19 | "axe/aria": "error", 20 | "axe/color": "error", 21 | "axe/forms": "error", 22 | "axe/language": "error", 23 | "axe/name-role-value": "error", 24 | "axe/parsing": "error", 25 | "axe/semantics": "error", 26 | "axe/sensory-and-visual-cues": "error", 27 | "axe/structure": "error", 28 | "axe/text-alternatives": "error", 29 | "button-type": "error", 30 | "compat-api/css": "error", 31 | "compat-api/html": "error" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /page.php: -------------------------------------------------------------------------------- 1 | 23 | 24 |
25 | 29 |
30 | 31 | = (document.documentElement.clientHeight || document.body.clientHeight); 13 | out.right = bounding.right 14 | >= (document.documentElement.clientWidth || document.body.clientWidth); 15 | out.any = out.top || out.left || out.bottom || out.right; 16 | 17 | return out; 18 | }; 19 | 20 | export default isOutOfViewport; 21 | -------------------------------------------------------------------------------- /sass/variables/_breakpoints.scss: -------------------------------------------------------------------------------- 1 | // stylelint-disable rem-over-px/rem-over-px 2 | // Layout widths 3 | $width-max-layout: 100%; 4 | $width-max-article: 800px; 5 | $width-grid-base: 1440px; 6 | $width-max-mobile: 1030px; 7 | 8 | // Breakpoints for containers 9 | $container-ipad: 770px; 10 | $container-ipad-landscape: 1024px; 11 | $container-mobile: 600px; 12 | $container-desktop: 1200px; 13 | $container-small-mobile: 420px; 14 | $container-tiny-mobile: 354px; 15 | 16 | // Gutenberg widths 17 | $width-wide: 1200px; 18 | $width-full: 100%; 19 | 20 | // CSS variables for JS. Remove this negative 1px value or add 1px if needed depending on when you want to stop the JS from controling the position of the navigation. 21 | :root { 22 | --width-max-mobile: #{$width-max-mobile - 1px}; 23 | } 24 | -------------------------------------------------------------------------------- /gulp/helpers/handle-errors.js: -------------------------------------------------------------------------------- 1 | // Better CSS error reporting 2 | const notify = require('gulp-notify'); 3 | 4 | // General error handling 5 | const handleError = function () { 6 | 7 | return function (err) { 8 | if (typeof err !== 'undefined') { 9 | var notifyMessage = ''; 10 | 11 | if (err.plugin === 'gulp-dart-sass') { 12 | // Message in notification 13 | notifyMessage = err.relativePath + '\n' + err.line + ':' + err.column; 14 | } 15 | 16 | if (err.plugin == 'gulp-stylelint') { 17 | notifyMessage = 'CSS linter found errors.'; 18 | } 19 | 20 | notify({ 21 | title: 'Gulp task failed — see console', 22 | message: notifyMessage 23 | }).write(err); 24 | } 25 | }; 26 | }; 27 | 28 | module.exports = { 29 | handleError 30 | }; 31 | -------------------------------------------------------------------------------- /js/src/modules/a11y-skip-link.js: -------------------------------------------------------------------------------- 1 | import MoveTo from 'moveto'; 2 | 3 | const initA11ySkipLink = () => { 4 | // Go through all the headings of the page and select the first one 5 | const a11ySkipLinkTarget = document.querySelectorAll('h1, h2, h3, h4, h5, h6')[0]; 6 | const a11ySkipLink = document.querySelectorAll('.skip-link')[0]; 7 | 8 | // Register trigger element 9 | // eslint-disable-next-line no-unused-vars, no-restricted-globals 10 | const moveTo = new MoveTo(); 11 | 12 | // When clicked, move focus to the target element 13 | 14 | if (a11ySkipLink) { 15 | a11ySkipLink.addEventListener('click', () => { 16 | a11ySkipLinkTarget.setAttribute('tabindex', '-1'); 17 | a11ySkipLinkTarget.focus(); 18 | moveTo.move(a11ySkipLinkTarget); 19 | }); 20 | } 21 | }; 22 | 23 | export default initA11ySkipLink; 24 | -------------------------------------------------------------------------------- /js/src/modules/navigation/a11y-add-dropdown-toggle-labels.js: -------------------------------------------------------------------------------- 1 | // Add proper link labels for screen readers 2 | function a11yAddDropdownToggleLabels(items) { 3 | items.forEach((li) => { 4 | // If .dropdown-class does not exist then do nothing 5 | if (!li.querySelector('.dropdown')) { 6 | return; 7 | } 8 | 9 | // Get the dropdown-button 10 | const dropdownButton = li.querySelector('.dropdown-toggle'); 11 | 12 | // Get the link text that is children of this item 13 | const linkText = li.querySelector('.dropdown').innerText; 14 | 15 | // Add the aria-label to the dropdown button 16 | // eslint-disable-next-line camelcase, no-undef 17 | dropdownButton.setAttribute('aria-label', `${air_light_screenReaderText.expand_for} ${linkText}`); 18 | }); 19 | } 20 | 21 | export default a11yAddDropdownToggleLabels; 22 | -------------------------------------------------------------------------------- /bin/tasks/imports.sh: -------------------------------------------------------------------------------- 1 | # Import required variables 2 | source ${SCRIPTS_LOCATION}/tasks/variables.sh 3 | 4 | # Script header 5 | source ${SCRIPTS_LOCATION}/tasks/header.sh 6 | 7 | # Ask names and credentials 8 | source ${SCRIPTS_LOCATION}/tasks/askvars.sh 9 | 10 | # Get latest Air-light version with updates and copy it over to your project 11 | source ${SCRIPTS_LOCATION}/tasks/get-theme.sh $@ 12 | 13 | # Set needed file and directory permissions 14 | source ${SCRIPTS_LOCATION}/tasks/permissions.sh 15 | 16 | # Get and install theme dependencies 17 | source ${SCRIPTS_LOCATION}/tasks/dependencies.sh 18 | 19 | # Set up project configuration 20 | source ${SCRIPTS_LOCATION}/tasks/project.sh 21 | 22 | # Clean up leftover development files from Air-light 23 | source ${SCRIPTS_LOCATION}/tasks/cleanups.sh 24 | 25 | # Add media folder, generate README.md for project etc. 26 | source ${SCRIPTS_LOCATION}/tasks/additions.sh 27 | -------------------------------------------------------------------------------- /inc/includes.php: -------------------------------------------------------------------------------- 1 | 23 | 24 |
25 | 29 |
30 | 31 | p:first-child { 5 | margin-top: 0; 6 | } 7 | 8 | h1, 9 | h2, 10 | h3 { 11 | + .wp-block-columns { 12 | margin-top: 0; 13 | } 14 | } 15 | 16 | @for $i from 1 through 9 { 17 | .wp-block-columns.has-#{$i}-columns.alignfull { 18 | padding-left: var(--spacing-container-padding-inline); 19 | padding-right: var(--spacing-container-padding-inline); 20 | width: 100%; 21 | } 22 | 23 | .wp-block-columns.has-#{$i}-columns { 24 | @media (max-width: 900px) { 25 | display: grid; 26 | grid-gap: 1em; 27 | grid-template-columns: repeat($i, 1fr); 28 | width: 100%; 29 | 30 | .wp-block-column { 31 | margin: 0; 32 | width: 100%; 33 | } 34 | } 35 | 36 | @media (max-width: $container-mobile) { 37 | grid-template-columns: repeat(1, 1fr); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /bin/tasks/cleanups.sh: -------------------------------------------------------------------------------- 1 | echo "${YELLOW}Cleaning up...${TXTRESET}" 2 | # We do not want Air-light to be in git any more since this a new theme 3 | cd $PROJECT_THEME_PATH && rm -rf .git 4 | 5 | # DEV-334: Theme keeps its own gulp/stylelint setup, remove bin since new theme doesn't need it 6 | # Keep theme's own .stylelintrc, gulp, gulpfile.js 7 | # Remove bin (new themes don't need the starter script) 8 | rm -rf ${PROJECT_THEME_PATH}/bin 9 | rm -f ${PROJECT_THEME_PATH}/.gitignore 10 | rm -f ${PROJECT_THEME_PATH}/readme.txt 11 | rm -f ${PROJECT_THEME_PATH}/languages/* 12 | rm ${PROJECT_THEME_PATH}/README.md 13 | rm ${PROJECT_THEME_PATH}/LICENSE.md 14 | 15 | # Remove demo content leftover files 16 | rm ${PROJECT_THEME_PATH}/sass/layout/_wordpress.scss 17 | 18 | echo "${YELLOW}Remove things we need to remove anyway in each start...${TXTRESET}" 19 | rm ${PROJECT_THEME_PATH}/sass/layout/_site-footer.scss 20 | touch ${PROJECT_THEME_PATH}/sass/layout/_site-footer.scss 21 | rm -rf ${PROJECT_THEME_PATH}/template-parts/footer 22 | -------------------------------------------------------------------------------- /sass/global.scss: -------------------------------------------------------------------------------- 1 | @charset 'utf-8'; 2 | 3 | // Reset and Helpers 4 | @use 'helpers'; 5 | 6 | // Variables 7 | @use 'variables'; 8 | 9 | // Accessibility 10 | @use 'base/accessibility'; 11 | 12 | // Style components 13 | @use 'components'; 14 | 15 | // Navigation 16 | @use 'navigation/navigation'; 17 | 18 | // Structure 19 | @use 'layout/general'; 20 | @use 'layout/print'; 21 | @use 'layout/typography'; 22 | @use 'layout/site-header'; 23 | @use 'layout/site-footer'; 24 | @use 'layout/forms'; 25 | @use 'layout/wordpress'; 26 | 27 | // Global features 28 | @use 'features/top'; 29 | @use 'features/gallery'; 30 | @use 'features/pagination'; 31 | @use 'features/gravity-forms'; 32 | 33 | // Views 34 | @use 'views/front-page'; 35 | @use 'views/search'; 36 | @use 'views/404' as error404; 37 | @use 'views/single'; 38 | @use 'views/comments'; 39 | 40 | // Gutenberg layout outside blocks 41 | @use 'gutenberg/layout/content'; 42 | 43 | // Gutenberg blocks 44 | @use 'gutenberg/blocks'; 45 | 46 | // Gutenberg formatting 47 | @use 'gutenberg/formatting'; 48 | -------------------------------------------------------------------------------- /sass/views/_search.scss: -------------------------------------------------------------------------------- 1 | @use '../variables' as *; 2 | 3 | // Search 4 | .block-search, 5 | .block-search-results { 6 | background-color: var(--color-white); 7 | } 8 | 9 | .block-search form { 10 | display: grid; 11 | /* autoprefixer: off */ 12 | grid-gap: 0.625rem; 13 | grid-template-columns: 9fr 1fr; 14 | 15 | input, 16 | label { 17 | width: 100%; 18 | } 19 | } 20 | 21 | .block-search-results { 22 | > .container { 23 | padding-top: 0; 24 | 25 | @media (min-width: $container-ipad-landscape) { 26 | // Make a CSS grid for multiple columns if you use other post type results 27 | display: grid; 28 | } 29 | } 30 | 31 | .row-result { 32 | border-top: 2px solid var(--color-border-forms); 33 | padding: 1.25rem 0; 34 | 35 | &:last-of-type { 36 | border-bottom: 2px solid var(--color-border-forms); 37 | } 38 | } 39 | 40 | h3 { 41 | font-size: var(--typography-size-h4); 42 | margin-bottom: 0.625rem; 43 | } 44 | 45 | p { 46 | margin-top: 0.625rem; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /sass/gutenberg/layout/_content.scss: -------------------------------------------------------------------------------- 1 | @use '../../variables' as *; 2 | 3 | // Gutenberg content 4 | .editor-styles-wrapper, 5 | .article-content { 6 | max-width: 100%; 7 | padding-bottom: var(--spacing-content-padding-block); 8 | padding-left: 0; 9 | padding-right: 0; 10 | padding-top: var(--spacing-content-padding-block); 11 | } 12 | 13 | // Gutenberg core magic for article content 14 | .article-content > [class^='wp-block-'], 15 | .article-content > * { 16 | line-height: var(--typography-paragraph-line-height); 17 | margin-left: auto; 18 | margin-right: auto; 19 | max-width: $width-max-article; 20 | 21 | @media (max-width: $width-max-article + 40px) { 22 | padding-left: var(--spacing-container-padding-inline); 23 | padding-right: var(--spacing-container-padding-inline); 24 | 25 | &.alignfull { 26 | padding-left: var(--spacing-container-padding-inline); 27 | padding-right: var(--spacing-container-padding-inline); 28 | width: calc(100% - calc(var(--spacing-container-padding-inline) * 2)); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /sass/gutenberg/formatting/_caption.scss: -------------------------------------------------------------------------------- 1 | @use '../../variables' as *; 2 | // Captions 3 | // stylelint-disable selector-max-combinators, selector-max-compound-selectors 4 | .editor-styles-wrapper, 5 | .article-content { 6 | figcaption, 7 | cite, 8 | small { 9 | --color-caption: #5c5c6a; 10 | border: 0; 11 | color: var(--color-caption); 12 | font-size: var(--typography-captions-size); 13 | font-style: normal; 14 | margin-bottom: 3.75rem; 15 | margin-left: auto; 16 | margin-right: auto; 17 | margin-top: 1.875rem; 18 | max-width: $width-max-article; 19 | padding-bottom: 0; 20 | text-align: center; 21 | width: 100%; 22 | 23 | @media (max-width: $container-mobile) { 24 | margin-top: 1.25rem; 25 | } 26 | } 27 | 28 | .wp-block-image figcaption, 29 | .wp-block-image .alignright > figcaption, 30 | .wp-block-image .aligncenter > figcaption, 31 | .wp-block-image.is-resized > figcaption { 32 | display: block; 33 | width: 100%; 34 | } 35 | 36 | cite { 37 | margin-bottom: 0; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /sass/layout/_wordpress.scss: -------------------------------------------------------------------------------- 1 | // These styles are required by WordPress.org Theme Check 2 | // REQUIRED: .sticky css class is needed in your theme css. 3 | // REQUIRED: .bypostauthor css class is needed in your theme css. 4 | :root { 5 | --color-border-sticky: #d7e4f2; 6 | --color-bypostauthor: rgb(42 45 62 / .5); 7 | } 8 | 9 | .sticky { 10 | border: 1px dashed var(--color-border-sticky); 11 | margin-bottom: 1.25rem; 12 | padding: 1.25rem; 13 | } 14 | 15 | .comment-list li.bypostauthor, 16 | .bypostauthor { 17 | border: 1px dashed var(--color-bypostauthor); 18 | padding: 1.25rem; 19 | } 20 | 21 | // WordPress captions 22 | // REQUIRED: .wp-caption css class is needed in your theme css. 23 | // REQUIRED: .wp-caption-text css class is needed in your theme css. 24 | .wp-caption { 25 | max-width: 100%; 26 | 27 | img[class*="wp-image-"] { 28 | display: block; 29 | margin: 0 auto; 30 | } 31 | 32 | .wp-caption-text { 33 | font-style: italic; 34 | margin-bottom: 1.25rem; 35 | padding: 0.625rem 0; 36 | text-align: left; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /js/src/modules/navigation/calculate-dropdown-toggle-height.js: -------------------------------------------------------------------------------- 1 | // Calculate mobile nav-toggle height 2 | function calculateDropdownToggleHeight() { 3 | // If .dropdown-toggle not found, bail 4 | if (!document.querySelectorAll('.dropdown-toggle')) { 5 | // eslint-disable-next-line no-console 6 | console.log('Warning: No dropdown-toggles found.'); 7 | 8 | return; 9 | } 10 | 11 | // Find all .dropdown-toggle elements on mobile 12 | const dropdownToggles = document.querySelectorAll('.dropdown-toggle'); 13 | 14 | // Loop through dropdown toggles 15 | dropdownToggles.forEach((dropdownToggle) => { 16 | // Get the height of previous element 17 | 18 | const previousElement = dropdownToggle.previousElementSibling; 19 | if (previousElement) { 20 | const previousElementHeight = previousElement.offsetHeight; 21 | // Set the height of the dropdown toggle 22 | // eslint-disable-next-line no-param-reassign 23 | dropdownToggle.style.height = `${previousElementHeight}px`; 24 | } 25 | }); 26 | } 27 | 28 | export default calculateDropdownToggleHeight; 29 | -------------------------------------------------------------------------------- /header.php: -------------------------------------------------------------------------------- 1 | section and everything up until
6 | * 7 | * @package air-light 8 | */ 9 | 10 | namespace Air_Light; 11 | 12 | ?> 13 | 14 | 15 | > 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | > 26 | 27 | 28 | 29 |
30 | 31 | 37 | 38 |
39 | -------------------------------------------------------------------------------- /.github/workflows/php8.3.yml: -------------------------------------------------------------------------------- 1 | name: PHP 8.3 compatibility 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | PHPCS_DIR: /tmp/phpcs 7 | PHPCOMPAT_DIR: /tmp/phpcompatibility 8 | SNIFFS_DIR: /tmp/sniffs 9 | WPCS_DIR: /tmp/wpcs 10 | 11 | jobs: 12 | build: 13 | name: Test for PHP 8.3 support 14 | runs-on: ubuntu-22.04 15 | 16 | steps: 17 | - name: Checkout the repository 18 | uses: actions/checkout@v3 19 | 20 | - name: Setup PHP with Xdebug 2.x 21 | uses: shivammathur/setup-php@v2 22 | with: 23 | php-version: '8.3' 24 | coverage: xdebug2 25 | 26 | - name: Set up PHPCS and WordPress-Coding-Standards 27 | uses: php-actions/composer@v6 28 | env: 29 | COMPOSER: "composer.json" 30 | with: 31 | php_version: "8.3" 32 | version: "2.3.7" 33 | args: "--ignore-platform-reqs --optimize-autoloader" 34 | 35 | - name: Run PHP_CodeSniffer 36 | run: | 37 | vendor/bin/phpcs -p . --extensions=php --ignore=vendor,node_modules,src,js,css,sass --standard=PHPCompatibility --runtime-set testVersion 8.3 38 | -------------------------------------------------------------------------------- /js/src/modules/navigation/a11y-add-dropdown-toggle-labels-click.js: -------------------------------------------------------------------------------- 1 | // Add proper link labels for screen readers 2 | function a11yAddDropdownToggleLabelsClick(items) { 3 | items.forEach((li) => { 4 | // If .dropdown-toggle does not exist then do nothing 5 | if (!li.querySelector('.dropdown-toggle')) { 6 | return; 7 | } 8 | 9 | // Add helper class to dropdown-toggle 10 | li.querySelector('.dropdown-toggle').classList.add('menu-item-clickable'); 11 | 12 | // Remove .dropdown-toggle class 13 | li.querySelector('.dropdown-toggle').classList.remove('dropdown-toggle'); 14 | 15 | // Get the dropdown-button 16 | const dropdownButton = li.querySelector('.menu-item-clickable'); 17 | 18 | // Get the link text that is children of this item 19 | const linkText = dropdownButton.innerHTML; 20 | // Add the aria-label to the dropdown button 21 | // eslint-disable-next-line camelcase, no-undef 22 | dropdownButton.setAttribute('aria-label', `${air_light_screenReaderText.expand_for} ${linkText}`); 23 | }); 24 | } 25 | 26 | export default a11yAddDropdownToggleLabelsClick; 27 | -------------------------------------------------------------------------------- /sass/gutenberg/blocks/_core-pullquote.scss: -------------------------------------------------------------------------------- 1 | @use '../../variables' as *; 2 | // Core/pullquote block 3 | .wp-block-pullquote { 4 | border-color: var(--color-paragraph); 5 | border-width: 3px; 6 | display: grid; 7 | 8 | [aria-label="Pullquote citation text"], 9 | cite { 10 | display: block; 11 | margin-top: 1.875rem; 12 | } 13 | 14 | @media (max-width: $width-grid-base + 40px) { 15 | width: calc(100% - calc(var(--spacing-container-padding-inline) * 2)); 16 | } 17 | } 18 | 19 | .wp-block-pullquote.alignwide, 20 | .wp-block-pullquote.alignfull { 21 | padding-left: 0; 22 | padding-right: 0; 23 | 24 | blockquote { 25 | justify-self: center; 26 | } 27 | 28 | @media (max-width: $width-grid-base + 40px) { 29 | margin-left: var(--spacing-container-padding-inline); 30 | margin-right: var(--spacing-container-padding-inline); 31 | } 32 | } 33 | 34 | .wp-block-pullquote.alignfull { 35 | margin-left: var(--spacing-container-padding-inline); 36 | margin-right: var(--spacing-container-padding-inline); 37 | width: calc(100% - calc(var(--spacing-container-padding-inline) * 2)); 38 | } 39 | -------------------------------------------------------------------------------- /gulp/tasks/husky.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | const { exec } = require('child_process'); 3 | const { promisify } = require('util'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | 7 | const execAsync = promisify(exec); 8 | 9 | function setupHusky() { 10 | const nodeModulesExists = fs.existsSync(path.join(process.cwd(), 'node_modules')); 11 | const huskyExists = fs.existsSync(path.join(process.cwd(), '.husky', '_', 'husky.sh')); 12 | 13 | let command = ''; 14 | 15 | if (!nodeModulesExists) { 16 | console.log('node_modules not found, running npm install...'); 17 | command = 'npm install && '; 18 | } 19 | 20 | if (!huskyExists) { 21 | console.log('Husky not installed, setting up...'); 22 | command += 'husky && '; 23 | } 24 | 25 | command += 'chmod +x .husky/pre-commit .husky/commit-msg'; 26 | 27 | return execAsync(command) 28 | .then(() => { 29 | console.log('Husky hooks setup completed'); 30 | }) 31 | .catch((error) => { 32 | console.error('Error setting up Husky hooks:', error); 33 | throw error; 34 | }); 35 | } 36 | 37 | exports.husky = setupHusky; -------------------------------------------------------------------------------- /js/src/modules/navigation/check-for-submenu-overflow.js: -------------------------------------------------------------------------------- 1 | // Import required modules 2 | import isOutOfViewport from './is-out-of-viewport'; 3 | 4 | // Check for submenu overflow 5 | function checkForSubmenuOverflow(items) { 6 | // If items not found, bail 7 | if (!items) { 8 | // eslint-disable-next-line no-console 9 | console.log('Warning: No items for sub-menus found.'); 10 | 11 | return; 12 | } 13 | 14 | items.forEach((li) => { 15 | // Find sub menus 16 | const subMenusUnderMenuItem = li.querySelectorAll('.sub-menu'); 17 | 18 | // Loop through sub menus 19 | subMenusUnderMenuItem.forEach((subMenu) => { 20 | // First let's check if submenu exists 21 | if (typeof subMenusUnderMenuItem !== 'undefined') { 22 | // Check if the sub menu is out of viewport or not 23 | const isOut = isOutOfViewport(subMenu); 24 | 25 | // At least one side of the element is out of viewport 26 | if (isOut.right) { 27 | subMenu.classList.add('is-out-of-viewport'); 28 | } 29 | } 30 | }); 31 | }); 32 | } 33 | 34 | export default checkForSubmenuOverflow; 35 | -------------------------------------------------------------------------------- /404.php: -------------------------------------------------------------------------------- 1 | 16 | 17 |
18 | 19 |
20 |
21 |
22 | 23 |

404

24 | 25 |

26 | 27 |
28 |
29 |
30 | 31 |
32 | 33 | 11 | 12 | 34 | -------------------------------------------------------------------------------- /gulp/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | optimization: { 3 | minimize: true, 4 | minimizer: [ 5 | (compiler) => { 6 | const TerserPlugin = require('terser-webpack-plugin'); 7 | new TerserPlugin({ 8 | extractComments: false, 9 | terserOptions: { 10 | compress: {}, 11 | mangle: true, 12 | module: false, 13 | format: { 14 | comments: false, 15 | }, 16 | }, 17 | }).apply(compiler); 18 | }, 19 | ] 20 | }, 21 | externals: { 22 | jquery: 'jQuery' // Available and loaded through WordPress. 23 | }, 24 | mode: 'production', 25 | module: { 26 | rules: [{ 27 | test: /.js$/, 28 | exclude: /node_modules/, 29 | use: [{ 30 | loader: 'babel-loader', 31 | options: { 32 | presets: [ 33 | ['@babel/preset-env', { 34 | modules: false, 35 | useBuiltIns: 'usage', 36 | corejs: 3, 37 | targets: { 38 | esmodules: true 39 | } 40 | }] 41 | ] 42 | } 43 | }] 44 | }] 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /js/src/modules/navigation/close-sub-menu.js: -------------------------------------------------------------------------------- 1 | function closeSubMenu(li) { 2 | // If menu item is not a dropdown then do nothing 3 | if (!li.querySelector('.dropdown-toggle') && !li.querySelector('.sub-menu')) { 4 | return; 5 | } 6 | 7 | // Get the dropdown-button 8 | const dropdownButton = li.querySelector('.dropdown-toggle'); 9 | 10 | // Get the submenu 11 | const subMenu = li.querySelector('.sub-menu'); 12 | 13 | // If the dropdown-menu is not open, bail 14 | if (!subMenu.classList.contains('toggled-on')) { 15 | return; 16 | } 17 | 18 | // Remove the open class from the dropdown-menu 19 | subMenu.classList.remove('toggled-on'); 20 | 21 | // Remove the open class from the dropdown-button 22 | dropdownButton.classList.remove('toggled-on'); 23 | 24 | // Remove the aria-expanded attribute from the dropdown-button 25 | dropdownButton.setAttribute('aria-expanded', 'false'); 26 | 27 | // Get the link text that is children of this item 28 | const linkText = dropdownButton.innerHTML; 29 | 30 | // Add the aria-label to the dropdown button 31 | // eslint-disable-next-line camelcase, no-undef 32 | dropdownButton.setAttribute('aria-label', `${air_light_screenReaderText.expand_for} ${linkText}`); 33 | } 34 | 35 | export default closeSubMenu; 36 | -------------------------------------------------------------------------------- /sass/layout/_site-footer.scss: -------------------------------------------------------------------------------- 1 | // The very bottom of the site. Usually contains supporting 2 | // or secondary navigation, social media icons, contact details 3 | // and such. 4 | 5 | // Please note: These are mostly for demo purposes 6 | // so feel free to remove everything in this file 7 | // and start over. 8 | .site-footer { 9 | // Making sure the footer background is white, even on WordPress.org 10 | background-color: var(--color-white); 11 | border-top: 1px solid #e3e3f0; 12 | color: var(--color-paragraph); 13 | overflow: hidden; 14 | padding: 3.75rem 1.25rem; 15 | 16 | .container { 17 | padding-bottom: 2.5rem; 18 | padding-top: 2.5rem; 19 | } 20 | 21 | p, 22 | a { 23 | color: var(--color-black); 24 | } 25 | 26 | .site-info { 27 | display: grid; 28 | gap: 1rem; 29 | justify-content: start; 30 | } 31 | 32 | .theme-info { 33 | font-size: var(--typography-size-16); 34 | } 35 | 36 | .theme-info a { 37 | display: block; 38 | } 39 | 40 | .powered-by-wordpress, 41 | .theme-info { 42 | display: flex; 43 | gap: 1rem; 44 | } 45 | 46 | .powered-by-wordpress { 47 | font-weight: var(--typography-weight-semibold); 48 | } 49 | 50 | .powered-by-wordpress svg { 51 | height: 1.75rem; 52 | width: 1.75rem; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /sass/gutenberg/_blocks.scss: -------------------------------------------------------------------------------- 1 | @use 'gutenberg/blocks/core-blockquote'; 2 | @use 'gutenberg/blocks/core-buttons'; 3 | @use 'gutenberg/blocks/core-columns'; 4 | @use 'gutenberg/blocks/core-separator'; 5 | @use 'gutenberg/blocks/core-heading'; 6 | @use 'gutenberg/blocks/core-image'; 7 | @use 'gutenberg/blocks/core-list'; 8 | @use 'gutenberg/blocks/core-paragraph'; 9 | @use 'gutenberg/blocks/core-pullquote'; 10 | @use 'gutenberg/blocks/core-table'; 11 | @use 'gutenberg/blocks/core-video'; 12 | @use 'gutenberg/blocks/boxed'; 13 | @use 'gutenberg/blocks/button-file'; 14 | @use 'gutenberg/blocks/error'; 15 | 16 | // List of all blocks: https://wordpress.org/support/article/blocks/ 17 | // Core blocks only meant for article content 18 | .editor-styles-wrapper, 19 | .article-content { 20 | // Core block styles are now available from the top-level @use statements 21 | // Custom Gutenberg block styles are now available from the top-level @use statements 22 | 23 | // Add here those ACF Gutenberg blocks you want to use inside article-content 24 | // @use 'gutenberg/blocks/your-new-acf-block'; 25 | } 26 | 27 | // Blocks only meant for outside article-content 28 | .editor-styles-wrapper, 29 | .site-main { 30 | // Error block styles are now available from the top-level @use statements 31 | 32 | // ACF blocks 33 | // @use 'gutenberg/blocks/your-new-acf-block'; 34 | } 35 | -------------------------------------------------------------------------------- /bin/newtheme-wsl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # WordPress theme starting bash script for Air-light (WSL version) 3 | 4 | # Script specific vars 5 | SCRIPT_LABEL='with WSL support' 6 | SCRIPT_VERSION='1.0.8 (2025-08-28)' 7 | 8 | # Vars needed for this file to function globally 9 | CURRENTFILE=`basename $0` 10 | 11 | # Determine scripts location to get imports right 12 | if [ "$CURRENTFILE" = "newtheme-wsl.sh" ]; then 13 | SCRIPTS_LOCATION="$( pwd )" 14 | source ${SCRIPTS_LOCATION}/tasks/variables.sh 15 | source ${SCRIPTS_LOCATION}/tasks/header.sh 16 | exit 17 | else 18 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 19 | ORIGINAL_FILE=$( readlink $DIR/$CURRENTFILE ) 20 | SCRIPTS_LOCATION=$( dirname $ORIGINAL_FILE ) 21 | fi 22 | 23 | # Import required variables 24 | source ${SCRIPTS_LOCATION}/tasks/wsl-packages.sh 25 | 26 | # Final note about server requirements 27 | echo "" 28 | echo "${WHITE}Using this start script requires you use the following: 29 | https://github.com/digitoimistodude/windows-lemp-setup 30 | https://github.com/digitoimistodude/air-light 31 | ${TXTRESET}" 32 | 33 | # Import required tasks 34 | source ${SCRIPTS_LOCATION}/tasks/imports.sh 35 | 36 | # Replace Air-light with your theme name and other seds (WSL version) 37 | source ${SCRIPTS_LOCATION}/tasks/replaces-wsl.sh 38 | 39 | # The end 40 | source ${SCRIPTS_LOCATION}/tasks/footer.sh 41 | -------------------------------------------------------------------------------- /sass/components/_button.scss: -------------------------------------------------------------------------------- 1 | @use '../helpers' as *; 2 | // stylelint-disable number-max-precision, rem-over-px/rem-over-px 3 | @mixin button() { 4 | appearance: none; 5 | background-color: var(--color-button-background); 6 | border: var(--border-width-input-field) solid var(--color-button-background); 7 | border-radius: var(--border-radius-button); 8 | color: var(--color-button); 9 | cursor: pointer; 10 | display: inline-block; 11 | font-family: var(--typography-family-paragraph); 12 | font-size: var(--typography-size-16); 13 | font-weight: var(--typography-weight-semibold); 14 | line-height: 1.39; 15 | margin-bottom: 0; 16 | max-width: 230px; 17 | overflow: hidden; 18 | padding-bottom: calc(14px - calc(var(--border-width-input-field) * 2)); 19 | padding-left: calc(21px - calc(var(--border-width-input-field) * 2)); 20 | padding-right: calc(21px - calc(var(--border-width-input-field) * 2)); 21 | padding-top: calc(14px - calc(var(--border-width-input-field) * 2)); 22 | position: relative; 23 | text-decoration: none; 24 | text-overflow: ellipsis; 25 | transition: all 150ms cubic-bezier(.25, .46, .45, .94); 26 | white-space: nowrap; 27 | width: auto; 28 | 29 | &.focus, 30 | &:hover, 31 | &:focus { 32 | background-color: var(--color-button-background-hover); 33 | border-color: var(--color-button-background-hover); 34 | color: var(--color-button-hover); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /js/src/front-end.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len, no-param-reassign, no-unused-vars */ 2 | /** 3 | * Air theme JavaScript. 4 | */ 5 | 6 | // Import modules 7 | import reframe from 'reframe.js'; 8 | import { styleExternalLinks, initExternalLinkLabels } from './modules/external-link'; 9 | import initAnchors from './modules/anchors'; 10 | import backToTop from './modules/top'; 11 | import initA11ySkipLink from './modules/a11y-skip-link'; 12 | import initA11yFocusSearchField from './modules/a11y-focus-search-field'; 13 | import { 14 | navSticky, navClick, navDesktop, navMobile, 15 | } from './modules/navigation'; 16 | 17 | // Define Javascript is active by changing the body class 18 | document.body.classList.remove('no-js'); 19 | document.body.classList.add('js'); 20 | 21 | document.addEventListener('DOMContentLoaded', () => { 22 | initAnchors(); 23 | backToTop(); 24 | styleExternalLinks(); 25 | initExternalLinkLabels(); 26 | initA11ySkipLink(); 27 | initA11yFocusSearchField(); 28 | 29 | // Init navigation 30 | // If you want to enable click based navigation, comment navDesktop() and uncomment navClick() 31 | // Remember to enable styles in sass/navigation/navigation.scss 32 | navDesktop(); 33 | // navClick(); 34 | navMobile(); 35 | 36 | // Uncomment if you like to use a sticky navigation 37 | // navSticky(); 38 | 39 | // Fit video embeds to container 40 | reframe('.wp-has-aspect-ratio iframe'); 41 | }); 42 | -------------------------------------------------------------------------------- /.github/workflows/styles.yml: -------------------------------------------------------------------------------- 1 | name: CSS 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | name: Test styles 8 | runs-on: ubuntu-22.04 9 | 10 | steps: 11 | - name: Checkout the repository 12 | uses: actions/checkout@v3 13 | 14 | - name: Read .nvmrc 15 | run: echo ::set-output name=NVMRC::$(cat .nvmrc) 16 | id: nvm 17 | 18 | - name: Setup node 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: '${{ steps.nvm.outputs.NVMRC }}' 22 | 23 | - name: Install packages 24 | run: npm install 25 | 26 | - name: Run stylelint 27 | run: | 28 | npx stylelint . --max-warnings 0 --config .stylelintrc 29 | 30 | - name: Run gulp task devstyles 31 | run: | 32 | # Make the command visible 33 | echo "Running npx gulp devstyles..." 34 | npx gulp devstyles 35 | 36 | # Capture the output 37 | output=$(npx gulp devstyles 2>&1) 38 | 39 | # Save it to a temporary file 40 | echo "$output" > output.txt 41 | 42 | # Check if the output contains the string "DEPRECATED" or "ERROR" or "WARNING" 43 | if grep -q "DEPRECATED" output.txt || grep -q "ERROR" output.txt || grep -q "WARNING" output.txt; then 44 | echo "Error found in output, failing build..." 45 | exit 1 46 | else 47 | echo "No errors found in output." 48 | fi 49 | 50 | # Remove the temporary file 51 | rm output.txt 52 | -------------------------------------------------------------------------------- /single.php: -------------------------------------------------------------------------------- 1 | 17 | 18 |
19 | 20 |
21 |
22 | 23 |

24 | 25 | '' ) ); 29 | 30 | entry_footer(); 31 | 32 | if ( get_edit_post_link() ) { 33 | edit_post_link( sprintf( wp_kses( __( 'Edit %s', 'air-light' ), [ 'span' => [ 'class' => [] ] ] ), get_the_title() ), '' ); 34 | } 35 | 36 | the_post_navigation(); 37 | 38 | // If comments are open or we have at least one comment, load up the comment template. 39 | if ( comments_open() || get_comments_number() ) { 40 | comments_template(); 41 | } ?> 42 | 43 |
44 |
45 | 46 |
47 | 48 | /dev/null && pwd )" 19 | ORIGINAL_FILE=$( readlink "$DIR/$CURRENTFILE" ) 20 | if [ -n "$ORIGINAL_FILE" ]; then 21 | SCRIPTS_LOCATION=$( dirname "$ORIGINAL_FILE" ) 22 | else 23 | # Fallback if readlink fails 24 | SCRIPTS_LOCATION="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 25 | fi 26 | fi 27 | 28 | # Final note about server requirements 29 | echo "" 30 | echo "${WHITE}Using this start script requires you use the following: 31 | https://github.com/digitoimistodude/macos-lemp-setup 32 | https://github.com/digitoimistodude/air-light 33 | ${TXTRESET}" 34 | 35 | # First, let's check updates to self 36 | source ${SCRIPTS_LOCATION}/tasks/self-update.sh 37 | 38 | # Import required tasks 39 | source ${SCRIPTS_LOCATION}/tasks/imports.sh $@ 40 | 41 | # Replace Air-light with your theme name and other seds 42 | source ${SCRIPTS_LOCATION}/tasks/replaces.sh 43 | 44 | # The end 45 | source ${SCRIPTS_LOCATION}/tasks/footer.sh 46 | -------------------------------------------------------------------------------- /inc/includes/nav-walker-footer.php: -------------------------------------------------------------------------------- 1 | item_spacing ) && 'discard' === $args->item_spacing ) { 16 | $t = ''; 17 | $n = ''; 18 | } else { 19 | $t = "\t"; 20 | $n = "\n"; 21 | } 22 | 23 | $indent = ( $depth ) ? str_repeat( $t, $depth ) : ''; 24 | $output .= $indent . '
  • '; 25 | 26 | $atts = array(); 27 | $atts['href'] = ! empty( $item->url ) ? $item->url : ''; 28 | $atts['title'] = ! empty( $item->attr_title ) ? $item->attr_title : ''; 29 | $atts['target'] = ! empty( $item->target ) ? $item->target : ''; 30 | 31 | $attributes = ''; 32 | foreach ( $atts as $attr => $value ) { 33 | if ( ! empty( $value ) ) { 34 | $value = ( 'href' === $attr ) ? esc_url( $value ) : esc_attr( $value ); 35 | $attributes .= ' ' . $attr . '="' . $value . '"'; 36 | } 37 | } 38 | 39 | $title = apply_filters( 'the_title', $item->title, $item->ID ); 40 | $item_output = sprintf( 41 | '%s', 42 | $attributes, 43 | $title 44 | ); 45 | 46 | $output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /gulp/tasks/blocks.js: -------------------------------------------------------------------------------- 1 | const { exec } = require('child_process'); 2 | const { watch } = require('gulp'); 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | // Build all blocks using wp-scripts 7 | function buildBlocks(done) { 8 | // Get all block directories 9 | const blocksDir = path.join(process.cwd(), 'blocks'); 10 | const blockFolders = fs.readdirSync(blocksDir) 11 | .filter(file => fs.statSync(path.join(blocksDir, file)).isDirectory()); 12 | 13 | // Build each block that has package.json 14 | const builds = blockFolders.map(blockName => { 15 | const blockPath = path.join(blocksDir, blockName); 16 | if (fs.existsSync(path.join(blockPath, 'package.json'))) { 17 | return new Promise((resolve, reject) => { 18 | exec('npm run build', { cwd: blockPath }, (err, stdout, stderr) => { 19 | if (err) { 20 | console.error(`Error building block ${blockName}:`, err); 21 | reject(err); 22 | } 23 | console.log(stdout); 24 | console.error(stderr); 25 | resolve(); 26 | }); 27 | }); 28 | } 29 | return Promise.resolve(); 30 | }); 31 | 32 | Promise.all(builds) 33 | .then(() => done()) 34 | .catch(err => done(err)); 35 | } 36 | 37 | // Watch blocks 38 | function watchBlocks() { 39 | watch([ 40 | 'blocks/*/src/**/*.js', 41 | 'blocks/*/src/**/*.scss', 42 | 'blocks/*/src/**/*.php', 43 | 'blocks/*/src/**/*.json', 44 | ], buildBlocks); 45 | } 46 | 47 | exports.buildBlocks = buildBlocks; 48 | exports.watchBlocks = watchBlocks; 49 | -------------------------------------------------------------------------------- /.svgo.yml: -------------------------------------------------------------------------------- 1 | multipass: true 2 | full: true 3 | plugins: 4 | - removeDoctype: true 5 | - removeXMLProcInst: true 6 | - removeComments: true 7 | - removeMetadata: true 8 | - removeXMLNS: true 9 | - removeEditorsNSData: true 10 | - cleanupAttrs: true 11 | - inlineStyles: true 12 | - minifyStyles: true 13 | - convertStyleToAttrs: true 14 | - cleanupIDs: true 15 | - prefixIds: true 16 | - removeRasterImages: true 17 | - removeUselessDefs: true 18 | - cleanupNumericValues: true 19 | - cleanupListOfValues: true 20 | - convertColors: true 21 | - removeUnknownsAndDefaults: true 22 | - removeNonInheritableGroupAttrs: true 23 | - removeUselessStrokeAndFill: true 24 | - removeViewBox: false 25 | - cleanupEnableBackground: true 26 | - removeHiddenElems: true 27 | - removeEmptyText: true 28 | - convertShapeToPath: true 29 | - convertEllipseToCircle: true 30 | - moveElemsAttrsToGroup: true 31 | - moveGroupAttrsToElems: true 32 | - collapseGroups: true 33 | - convertPathData: true 34 | - convertTransform: true 35 | - removeEmptyAttrs: true 36 | - removeEmptyContainers: true 37 | - mergePaths: true 38 | - removeUnusedNS: true 39 | - sortAttrs: true 40 | - sortDefsChildren: true 41 | - removeTitle: true 42 | - removeDesc: true 43 | - removeDimensions: false 44 | - removeAttrs: true 45 | - removeAttributesBySelector: true 46 | - removeElementsByAttr: true 47 | - addClassesToSVGElement: true 48 | - removeStyleElement: true 49 | - removeScriptElement: true 50 | - addAttributesToSVGElement: true 51 | - removeOffCanvasPaths: true 52 | - reusePaths: true 53 | -------------------------------------------------------------------------------- /bin/newtheme-popos.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # WordPress theme starting bash script for Air-light, ported for Pop!_OS, might work on Ubuntu or even Debian, or other forks. 3 | 4 | # Script specific vars 5 | SCRIPT_LABEL='for Pop!_OS' 6 | SCRIPT_VERSION='1.0.0 (2023-09-29)' 7 | 8 | # Vars needed for this file to function globally 9 | CURRENTFILE=`basename $0` 10 | 11 | # Determine scripts location to get imports right 12 | if [ "$CURRENTFILE" = "newtheme-popos.sh" ]; then 13 | SCRIPTS_LOCATION="$( pwd )" 14 | source ${SCRIPTS_LOCATION}/tasks/variables.sh 15 | source ${SCRIPTS_LOCATION}/tasks/header.sh 16 | exit 17 | else 18 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 19 | ORIGINAL_FILE=$( readlink $DIR/$CURRENTFILE ) 20 | # Check if ORIGINAL_FILE is empty before calling dirname 21 | if [ -n "$ORIGINAL_FILE" ]; then 22 | SCRIPTS_LOCATION=$( dirname "$ORIGINAL_FILE" ) 23 | else 24 | echo "Error: Could not determine original file location" 25 | exit 1 26 | fi 27 | fi 28 | 29 | # Final note about server requirements 30 | echo "" 31 | echo "${WHITE}Using this start script requires you use the following: 32 | https://github.com/raikasdev/pop-lemp-setup 33 | https://github.com/digitoimistodude/air-light 34 | ${TXTRESET}" 35 | 36 | # First, let's check updates to self 37 | source ${SCRIPTS_LOCATION}/tasks/self-update.sh 38 | 39 | # Import required tasks 40 | source ${SCRIPTS_LOCATION}/tasks/imports.sh 41 | 42 | # Replace Air-light with your theme name and other seds 43 | source ${SCRIPTS_LOCATION}/tasks/replaces-wsl.sh 44 | 45 | # The end 46 | source ${SCRIPTS_LOCATION}/tasks/footer.sh 47 | -------------------------------------------------------------------------------- /gulp/tasks/devstyles.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | const { 3 | dest, 4 | src 5 | } = require('gulp'); 6 | const bs = require('browser-sync').create(); 7 | const sourcemaps = require('gulp-sourcemaps'); 8 | const sass = require('gulp-sass')( require('sass') ); 9 | const postcss = require('gulp-postcss'); 10 | const autoprefixer = require('autoprefixer'); 11 | const calcFunction = require('postcss-calc'); 12 | const colormin = require('postcss-colormin'); 13 | const discardEmpty = require('postcss-discard-empty'); 14 | const mergeLonghand = require('postcss-merge-longhand'); 15 | const mergeAdjacentRules = require('postcss-merge-rules'); 16 | const minifyGradients = require('postcss-minify-gradients'); 17 | const normalizePositions = require('postcss-normalize-positions'); 18 | const normalizeUrl = require('postcss-normalize-url'); 19 | const uniqueSelectors = require('postcss-unique-selectors'); 20 | const zIndex = require('postcss-zindex'); 21 | const config = require('../config.js'); 22 | 23 | function devstyles() { 24 | return src(config.styles.src) 25 | .pipe(bs.stream()) 26 | .pipe(sourcemaps.init()) 27 | .pipe(sass.sync(config.styles.opts.development)) 28 | .pipe(postcss([ 29 | autoprefixer(), 30 | colormin(), 31 | calcFunction(), 32 | discardEmpty(), 33 | mergeLonghand(), 34 | mergeAdjacentRules(), 35 | minifyGradients(), 36 | normalizePositions(), 37 | normalizeUrl(), 38 | zIndex(), 39 | uniqueSelectors() 40 | ])) 41 | .pipe(sourcemaps.write()) 42 | .pipe(dest(config.styles.development)) 43 | } 44 | 45 | exports.devstyles = devstyles; 46 | -------------------------------------------------------------------------------- /gulp/tasks/watch.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | const { 3 | watch, 4 | series 5 | } = require('gulp'); 6 | const bs = require('browser-sync').create(); 7 | const config = require('../config.js'); 8 | const { 9 | handleError 10 | } = require('../helpers/handle-errors.js'); 11 | const { watchBlocks } = require('./blocks'); 12 | 13 | // Watch task 14 | function watchFiles(done) { 15 | 16 | // Init BrowserSync 17 | bs.init(config.browsersync.src, config.browsersync.opts); 18 | 19 | // Console info 20 | function consoleInfo(path) { 21 | console.log(`\x1b[37m[\x1b[35mfileinfo\x1b[37m] \x1b[37mFile \x1b[34m${path} \x1b[37mwas changed.\x1b[0m`); 22 | } 23 | 24 | // Styles in development environment 25 | const devstyles = watch(config.styles.watch.development, series('devstyles')).on('error', handleError()); 26 | devstyles.on('change', function(path) { consoleInfo(path); }); 27 | 28 | // Styles in production environment 29 | const prodstyles = watch(config.styles.watch.production, series('prodstyles')); 30 | prodstyles.on('change', function(path) { consoleInfo(path); }); 31 | 32 | // JavaScript 33 | const javascript = watch(config.js.watch, series('js')); 34 | javascript.on('change', function(path) { consoleInfo(path); }); 35 | 36 | // PHP 37 | const php = watch(config.php.watch, series('phpcs'), bs.reload); 38 | php.on('change', function(path) { consoleInfo(path); }); 39 | 40 | // Lint styles 41 | watch(config.styles.watch.development, series('lintstyles')); 42 | 43 | // Add block watching 44 | watchBlocks(); 45 | 46 | // Finish task 47 | done(); 48 | }; 49 | 50 | exports.watch = watchFiles; 51 | -------------------------------------------------------------------------------- /sass/gutenberg/blocks/_core-image.scss: -------------------------------------------------------------------------------- 1 | @use '../../variables' as *; 2 | 3 | // Image block 4 | .wp-block-image { 5 | display: block; 6 | margin-bottom: var(--spacing-wp-block-image-margin-block); 7 | margin-top: var(--spacing-wp-block-image-margin-block); 8 | 9 | &.alignwide, 10 | &.alignfull { 11 | padding-left: 0; 12 | padding-right: 0; 13 | } 14 | 15 | .alignwide img, 16 | .alignfull img { 17 | width: 100%; 18 | } 19 | 20 | // No border radius on full width image and wide on small screens 21 | .alignfull img { 22 | border-radius: 0; 23 | } 24 | 25 | > figure { 26 | display: block; 27 | width: auto; 28 | 29 | &.alignleft, 30 | &.alignright { 31 | // Hack for keeping figcaption from flowing over floated image 32 | // This variable is set inline to the corresponding figure with gutenberg-js 33 | // stylelint-disable-next-line csstools/value-no-unknown-custom-properties 34 | max-width: var(--width-child-img); 35 | } 36 | } 37 | 38 | figcaption { 39 | margin-bottom: 1.25rem; 40 | } 41 | 42 | .aligncenter { 43 | text-align: center; 44 | } 45 | 46 | .aligncenter img { 47 | margin-left: auto; 48 | margin-right: auto; 49 | } 50 | 51 | @media (max-width: $width-grid-base + 40px) { 52 | &.alignwide { 53 | width: calc(100% - calc(var(--spacing-container-padding-inline) * 2)); 54 | } 55 | } 56 | 57 | @media (max-width: $container-mobile) { 58 | &.alignleft img, 59 | &.alignright img, 60 | &.aligncenter img { 61 | float: none; 62 | height: auto; 63 | width: 100%; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /sass/gutenberg/formatting/_align.scss: -------------------------------------------------------------------------------- 1 | @use '../../variables' as *; 2 | 3 | // Alignments 4 | .editor-styles-wrapper, 5 | .article-content { 6 | .alignleft > * { 7 | float: left; 8 | } 9 | 10 | .alignright > * { 11 | float: right; 12 | } 13 | 14 | .alignleft > img { 15 | margin-bottom: var(--spacing-paragraphs-margin-block); 16 | margin-right: var(--spacing-container-padding-inline); 17 | margin-top: var(--spacing-paragraphs-margin-block); 18 | 19 | + figcaption { 20 | margin-top: 0; 21 | } 22 | } 23 | 24 | .alignright > img { 25 | margin-bottom: var(--spacing-paragraphs-margin-block); 26 | margin-left: var(--spacing-container-padding-inline); 27 | margin-top: var(--spacing-paragraphs-margin-block); 28 | 29 | + figcaption { 30 | margin-top: 0; 31 | } 32 | } 33 | 34 | .alignwide { 35 | max-width: $width-wide; 36 | padding-left: var(--spacing-container-padding-inline); 37 | padding-right: var(--spacing-container-padding-inline); 38 | width: 100%; 39 | 40 | @media (min-width: $width-wide + 40px) { 41 | padding-left: 0; 42 | padding-right: 0; 43 | } 44 | } 45 | 46 | .alignfull { 47 | max-width: $width-full; 48 | padding-left: 0; 49 | padding-right: 0; 50 | width: $width-full; 51 | 52 | &.wp-block-image img { 53 | border-radius: 0; 54 | } 55 | 56 | @media (min-width: $width-max-article + 40px) { 57 | margin-bottom: var(--spacing-content-padding-block); 58 | margin-top: var(--spacing-content-padding-block); 59 | max-width: $width-full; 60 | width: $width-full; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /sass/variables/_spacings.scss: -------------------------------------------------------------------------------- 1 | @use 'breakpoints' as *; 2 | 3 | // CSS Variables for responsive paddings and margins 4 | :root { 5 | // Gaps 6 | --spacing-grid-gap: 3rem; 7 | 8 | // Paddings 9 | --spacing-container-padding-inline: 1.25rem; 10 | --spacing-container-padding-inline-large: 4rem; 11 | --spacing-container-padding-block: 4rem; 12 | --spacing-site-header-padding-block: 1.25rem; 13 | --spacing-content-padding-block: 5rem; 14 | 15 | // Margins 16 | --spacing-text-margin-block: 2.5rem; 17 | --spacing-wp-block-image-margin-block: 2.5rem; 18 | --spacing-paragraphs-margin-block: 1.6875rem; 19 | 20 | // Mid-sized screens 21 | @media (max-width: $width-grid-base + 150px) { 22 | --spacing-container-padding-inline: 4rem; 23 | } 24 | 25 | // When there's no longer room for container to fit with wider white space 26 | @media (max-width: 700px) { 27 | --spacing-container-padding-inline: 1.25rem; 28 | } 29 | 30 | // When navigation transforms to a responsive hamburger menu 31 | @media (max-width: $width-max-mobile) { 32 | --spacing-site-header-padding-block: 1.25rem; 33 | } 34 | 35 | // iPad 36 | @media (max-width: $container-ipad-landscape) { 37 | --spacing-grid-gap: 2rem; 38 | } 39 | 40 | @media (max-width: $container-ipad) { 41 | --spacing-grid-gap: var(--spacing-container-padding-inline); 42 | --spacing-container-padding-block: 3.125rem; 43 | } 44 | 45 | // Between iPad and a mobile phone 46 | @media (max-width: 600px) { 47 | --spacing-content-padding-block: 3.75rem; 48 | } 49 | 50 | // Vars in mobile 51 | @media (max-width: $container-mobile) { 52 | --spacing-container-padding-block: 2.5rem; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | === air-light === 2 | 3 | Air-light WordPress Theme, Copyright 2018 Digitoimisto Dude Oy, Roni Laukkarinen 4 | Air-light is distributed under the terms of the MIT License 5 | 6 | Bundled header image, Copyright Brady Bellini 7 | License: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication 8 | Source: https://stocksnap.io/photo/nature-mountains-3T1GEXSD0R 9 | 10 | Contributors: Digitoimisto Dude Oy 11 | Tags: one-column, accessibility-ready, translation-ready 12 | 13 | Requires at least: 5.0 14 | Tested up to: 6.8.2 15 | Stable tag: 9.6.2 16 | License: MIT License 17 | License URI: https://opensource.org/licenses/MIT 18 | 19 | A starter theme called air-light. 20 | 21 | == Description == 22 | 23 | Air starter theme is built to be very straightforward, front end developer friendly and only partly modular by its structure. 24 | 25 | == Installation == 26 | 27 | 1. In your admin panel, go to Appearance > Themes and click the Add New button. 28 | 2. Click Upload Theme and Choose File, then select the theme's .zip file. Click Install Now. 29 | 3. Click Activate to use your new theme right away. 30 | 31 | Or just get from GitHub: https://github.com/digitoimistodude/air-light 32 | 33 | == Frequently Asked Questions == 34 | 35 | = Does this theme support any plugins? = 36 | 37 | Yes. There's also a extender, helper plugin Air Helper: https://github.com/digitoimistodude/air-helper 38 | 39 | == Changelog == 40 | 41 | = 1.0 - 29 Jan 2016 = 42 | * Initial release 43 | 44 | == Credits == 45 | 46 | * Based on Underscores https://underscores.me/, (C) 2012-2017 Automattic, Inc., [GPLv2 or later](https://www.gnu.org/licenses/gpl-2.0.html) 47 | * normalize.css https://necolas.github.io/normalize.css/, (C) 2012-2016 Nicolas Gallagher and Jonathan Neal, [MIT](https://opensource.org/licenses/MIT) 48 | -------------------------------------------------------------------------------- /bin/tasks/self-update.sh: -------------------------------------------------------------------------------- 1 | # Temp colors 2 | export YELLOW=$(tput setaf 3) 3 | export GREEN=$(tput setaf 2) 4 | export RED=$(tput setaf 1) 5 | export TXTRESET=$(tput sgr0) 6 | 7 | # Check for symlink 8 | echo "${YELLOW}Running self-updater...${TXTRESET}" 9 | 10 | if [ $0 != '/usr/local/bin/newtheme' ]; then 11 | echo "${TXTRESET}${WHITE}Please do NOT run this script with ${RED}sh $CURRENTFILE${WHITE} or ${RED}bash $CURRENTFILE${WHITE} or ${RED}./$CURRENTFILE${WHITE}. 12 | Run this script globally instead by simply typing: ${GREEN}newtheme${TXTRESET}. If this doesn't work, please run first setup from the bin folder first." 13 | echo "" 14 | exit 15 | fi 16 | 17 | # Check for updates 18 | # Get symlink path from /usr/local/bin/newtheme 19 | SYMLINKPATH=$(sudo readlink /usr/local/bin/newtheme) 20 | 21 | # Get theme bin folder directory from symlink path 22 | THEMEBINFOLDER=$(dirname "$SYMLINKPATH") 23 | 24 | # Go one step back from bin to get theme root folder 25 | THEMEROOTFOLDER=$(dirname "$THEMEBINFOLDER") 26 | 27 | # Go to the theme root folder 28 | cd $THEMEROOTFOLDER 29 | 30 | # Make sure no file mods are being committed 31 | git config core.fileMode false --replace-all 32 | 33 | # Check for updates 34 | git pull origin master 35 | 36 | # Ensure permissions are intact 37 | sudo chmod +x /usr/local/bin/newtheme 38 | 39 | # If there is something preventing the update, show error message 40 | if [ $? -ne 0 ]; then 41 | echo "" 42 | echo "${TXTRESET}${RED}There was an error updating the start script. You have probably made changes to the air-light theme? Please commit those changes, send a PR or stash them. Please check the error message above and try again.${TXTRESET}" 43 | echo "" 44 | 45 | # Stop script 46 | exit 1 47 | 48 | # If there are no errors, add line break 49 | else 50 | echo "" 51 | fi 52 | -------------------------------------------------------------------------------- /sass/gutenberg/blocks/_core-blockquote.scss: -------------------------------------------------------------------------------- 1 | @use '../../variables' as *; 2 | 3 | // Core/blockquote block 4 | blockquote + cite, 5 | blockquote + p > cite { 6 | margin-bottom: 2.5rem; 7 | } 8 | 9 | // General blockquote styles 10 | blockquote { 11 | border: 0 none; 12 | clear: both; 13 | padding-bottom: 1.875rem; 14 | position: relative; 15 | 16 | p { 17 | color: var(--color-paragraph); 18 | font-style: normal; 19 | font-weight: var(--typography-weight-semibold); 20 | margin-bottom: 0; 21 | overflow: visible; 22 | position: relative; 23 | } 24 | 25 | @media (min-width: $container-ipad) { 26 | margin-top: 2.5rem; 27 | padding-bottom: 2.5rem; 28 | } 29 | } 30 | 31 | .wp-block-quote { 32 | border-left: 2px solid var(--color-paragraph); 33 | line-height: var(--typography-paragraph-line-height); 34 | margin-bottom: 2.5rem; 35 | margin-left: auto; 36 | margin-right: auto; 37 | margin-top: 2.5rem; 38 | padding: 2.1875rem 3.75rem; 39 | width: calc(100% - 7.5rem); 40 | 41 | > p { 42 | color: var(--color-paragraph); 43 | line-height: var(--typography-paragraph-line-height); 44 | } 45 | 46 | @media (max-width: $width-max-article + 40px) { 47 | padding: 2.1875rem 1.25rem; 48 | width: calc(100% - calc(var(--spacing-container-padding-inline) * 2)); 49 | } 50 | 51 | @media (max-width: $container-mobile) { 52 | padding: 2.5rem 2.5rem 2.5rem 1.25rem; 53 | } 54 | } 55 | 56 | .wp-block-blockquote.alignwide, 57 | .wp-block-blockquote.alignfull { 58 | padding-left: var(--spacing-container-padding-inline); 59 | padding-right: var(--spacing-container-padding-inline); 60 | width: calc(100% - calc(var(--spacing-container-padding-inline) * 2)); 61 | } 62 | 63 | .wp-block-blockquote blockquote { 64 | padding-bottom: 0; 65 | } 66 | -------------------------------------------------------------------------------- /sass/views/_single.scss: -------------------------------------------------------------------------------- 1 | @use '../helpers' as *; 2 | 3 | .article-content .categories, 4 | .article-content .tags, 5 | .categories, 6 | .tags { 7 | display: flex; 8 | flex-wrap: wrap; 9 | list-style: none; 10 | list-style-type: none; 11 | padding-inline-start: 0; 12 | } 13 | 14 | .categories, 15 | .article-content .categories { 16 | gap: 0.75rem; 17 | } 18 | 19 | .categories a { 20 | background-color: var(--color-paragraph); 21 | border-radius: 1.875rem; 22 | color: var(--color-white); 23 | display: inline-block; 24 | font-size: var(--typography-size-14); 25 | margin: 0; 26 | padding: 0.3125rem 0.9375rem; 27 | transition: all 150ms; 28 | } 29 | 30 | .categories a:hover, 31 | .categories a:focus { 32 | background-color: var(--color-black); 33 | color: var(--color-white); 34 | } 35 | 36 | .article-content .tags, 37 | .tags { 38 | display: flex; 39 | flex-wrap: wrap; 40 | gap: 0.3125rem; 41 | margin-bottom: var(--spacing-text-margin-block); 42 | margin-top: 0; 43 | 44 | // stylelint-disable a11y/font-size-is-readable 45 | a { 46 | background-color: transparent; 47 | border: 1px solid var(--color-paragraph); 48 | border-radius: 1.875rem; 49 | box-shadow: none; 50 | color: var(--color-paragraph); 51 | display: inline-block; 52 | font-size: var(--typography-size-12); 53 | margin-right: 4px; 54 | padding: 0.0625rem 0.5rem; 55 | transition: all 150ms; 56 | white-space: nowrap; 57 | } 58 | 59 | a:hover, 60 | a:focus { 61 | background-color: var(--color-black); 62 | border-color: var(--color-black); 63 | color: var(--color-white); 64 | } 65 | } 66 | 67 | // Next/Previous single post navigation 68 | .post-navigation .nav-links { 69 | display: flex; 70 | flex-wrap: wrap; 71 | justify-content: space-between; 72 | } 73 | -------------------------------------------------------------------------------- /js/src/modules/navigation/convert-dropdown-menu-items.js: -------------------------------------------------------------------------------- 1 | function convertDropdownMenuItems(items) { 2 | items.forEach((li) => { 3 | // Get dropdown toggle button 4 | const dropdownToggle = li.querySelector('.dropdown-toggle'); 5 | 6 | // Get dropdown menu item data 7 | const menuItemTitle = li.querySelector('a > span').innerHTML; 8 | const menuItemLinkElement = li.querySelector('a'); 9 | const menuItemLink = menuItemLinkElement.href; 10 | 11 | // Remove dropdown menu item link 12 | menuItemLinkElement.remove(); 13 | 14 | // Add dropdown menu item title to dropdown toggle button 15 | dropdownToggle.innerHTML = menuItemTitle; 16 | 17 | // Create new nav element 18 | const navElement = document.createElement('li'); 19 | navElement.classList.add('menu-item'); 20 | 21 | // Add dropdown menu item data to nav element 22 | // Create elements 23 | const navElementLink = document.createElement('a'); 24 | const navElementLinkSpan = document.createElement('span'); 25 | 26 | // Add data to elements 27 | // Span 28 | navElementLinkSpan.innerHTML = menuItemTitle; 29 | navElementLinkSpan.setAttribute('itemprop', 'name'); 30 | // Link 31 | navElementLink.setAttribute('itemprop', 'url'); 32 | navElementLink.href = menuItemLink; 33 | navElementLink.classList.add('dropdown-item'); 34 | 35 | // Append elements 36 | navElementLink.appendChild(navElementLinkSpan); 37 | navElement.appendChild(navElementLink); 38 | 39 | // Get the sub menu first child and add the new nav element before it 40 | const subMenuFirstChild = li.querySelector('.sub-menu > li'); 41 | const subMenu = li.querySelector('.sub-menu'); 42 | subMenu.insertBefore(navElement, subMenuFirstChild); 43 | }); 44 | } 45 | 46 | export default convertDropdownMenuItems; 47 | -------------------------------------------------------------------------------- /js/src/modules/navigation/a11y-focus-trap.js: -------------------------------------------------------------------------------- 1 | function a11yFocusTrap(e) { 2 | // Init focusable elements 3 | let focusableElements = []; 4 | 5 | // Define container 6 | const container = document.getElementById('nav'); 7 | 8 | // Define nav-toggle 9 | const navToggle = document.getElementById('nav-toggle'); 10 | 11 | // Get --width-max-mobile from CSS 12 | const widthMaxMobile = getComputedStyle( 13 | document.documentElement, 14 | ).getPropertyValue('--width-max-mobile'); 15 | 16 | // Let's see if we are on mobile viewport 17 | const isMobile = window.matchMedia(`(max-width: ${widthMaxMobile})`).matches; 18 | 19 | // If things are not okay, bail 20 | if (!container || !navToggle || !isMobile) { 21 | return; 22 | } 23 | 24 | // Set focusable elements inside main navigation. 25 | focusableElements = [ 26 | ...container.querySelectorAll( 27 | 'a, button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])', 28 | ), 29 | ] 30 | .filter((el) => !el.hasAttribute('disabled')) 31 | .filter( 32 | (el) => !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length), 33 | ); 34 | 35 | // Get first and last focusable element 36 | const firstFocusableElement = focusableElements[0]; 37 | const lastFocusableElement = focusableElements[focusableElements.length - 1]; 38 | 39 | // On key down on first element, if it's a Shift+Tab, redirect to last element 40 | if (firstFocusableElement === e.target && e.code === 'Tab' && e.shiftKey) { 41 | e.preventDefault(); 42 | lastFocusableElement.focus(); 43 | } 44 | // On key down on last element, if it's a Tab, redirect to first element 45 | if (lastFocusableElement === e.target && e.code === 'Tab' && !e.shiftKey) { 46 | e.preventDefault(); 47 | firstFocusableElement.focus(); 48 | } 49 | } 50 | 51 | export default a11yFocusTrap; 52 | -------------------------------------------------------------------------------- /bin/tasks/replaces-wsl.sh: -------------------------------------------------------------------------------- 1 | echo "${YELLOW}Generating theme files with theme name and textdomain called ${THEME_NAME}${TXTRESET}" 2 | # THE magical sed command by rolle (goes through every single file in theme folder and searches and replaces every air instance with THEME_NAME): 3 | # WSL/Ubuntu version of sed binary, different format than on macOS 4 | # Note: find + -exec sed doesn't work in WSL for some weird reason so we have to use "s;string;replacewith;" format 5 | for i in $(grep -rl air-light * --exclude-dir=node_modules 2>/dev/null); do LC_ALL=C sed -i -e "s;air-light;${THEME_NAME};" $i; done 6 | for i in $(grep -rl Air-light * --exclude-dir=node_modules 2>/dev/null); do LC_ALL=C sed -i -e "s;Air-light;${THEME_NAME};" $i; done 7 | for i in $(grep -rl air * --exclude-dir=node_modules 2>/dev/null); do LC_ALL=C sed -i -e "s;air-light;${THEME_NAME};" $i; done 8 | for i in $(grep -rl air * --exclude-dir=node_modules 2>/dev/null); do LC_ALL=C sed -i -e "s;air_light_;${THEME_NAME}_;" $i; done 9 | for i in $(grep -rl air * --exclude-dir=node_modules 2>/dev/null); do LC_ALL=C sed -i -e "s;Air_light_;${THEME_NAME}_;" $i; done 10 | 11 | # Remove demo content 12 | echo "${YELLOW}Removing demo content...${TXTRESET}" 13 | LC_ALL=C sed -i -e "s;@use 'layout\/wordpress'\;;;" ${PROJECT_THEME_PATH}/sass/global.scss 14 | 15 | read -p "${BOLDYELLOW}Do we use comments in this project? (y/n)${TXTRESET} " yn 16 | if [ "$yn" = "n" ]; then 17 | LC_ALL=C sed -i -e "s;@use 'views\/comments'\;;;" ${PROJECT_THEME_PATH}/sass/global.scss 18 | rm ${PROJECT_THEME_PATH}/sass/views/_comments.scss 19 | else 20 | echo ' ' 21 | fi 22 | 23 | echo "${YELLOW}Running project gulp styles once...${TXTRESET}" 24 | cd ${PROJECT_PATH} 25 | 26 | # NPX to try use the project gulp first (making sure we use right version) 27 | npx gulp devstyles 28 | npx gulp prodstyles 29 | 30 | echo "${YELLOW}Running project gulp scripts task once...${TXTRESET}" 31 | cd ${PROJECT_PATH} 32 | npx gulp js 33 | -------------------------------------------------------------------------------- /gulp/tasks/prodstyles.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | const { 3 | dest, 4 | src 5 | } = require('gulp'); 6 | const sass = require('gulp-sass')( require('sass') ); 7 | const postcss = require('gulp-postcss'); 8 | const autoprefixer = require('autoprefixer'); 9 | const cssnano = require('cssnano'); 10 | const calcFunction = require('postcss-calc'); 11 | const colormin = require('postcss-colormin'); 12 | const discardEmpty = require('postcss-discard-empty'); 13 | const discardUnused = require('postcss-discard-unused'); 14 | const mergeLonghand = require('postcss-merge-longhand'); 15 | const mergeAdjacentRules = require('postcss-merge-rules'); 16 | const minifyGradients = require('postcss-minify-gradients'); 17 | const normalizePositions = require('postcss-normalize-positions'); 18 | const normalizeUrl = require('postcss-normalize-url'); 19 | const uniqueSelectors = require('postcss-unique-selectors'); 20 | const zIndex = require('postcss-zindex'); 21 | const size = require('gulp-size'); 22 | const config = require('../config.js'); 23 | 24 | function prodstyles() { 25 | return src(config.styles.src) 26 | 27 | // Compile first time to CSS to be able to parse CSS files 28 | .pipe(sass(config.styles.opts.development)) 29 | 30 | // Compile SCSS synchronously 31 | .pipe(sass.sync(config.styles.opts.production)) 32 | 33 | // Run PostCSS plugins 34 | .pipe(postcss([ 35 | autoprefixer(), 36 | colormin(), 37 | calcFunction(), 38 | discardEmpty(), 39 | mergeLonghand(), 40 | mergeAdjacentRules(), 41 | minifyGradients(), 42 | normalizePositions(), 43 | normalizeUrl(), 44 | uniqueSelectors(), 45 | zIndex(), 46 | cssnano(config.cssnano) 47 | ])) 48 | 49 | // Output production CSS size 50 | .pipe(size(config.size)) 51 | 52 | // Save the final version for production 53 | .pipe(dest(config.styles.production)); 54 | } 55 | 56 | exports.prodstyles = prodstyles; 57 | -------------------------------------------------------------------------------- /js/src/modules/navigation/close-sub-menu-handler.js: -------------------------------------------------------------------------------- 1 | import closeSubMenu from './close-sub-menu'; 2 | 3 | function closeSubMenuHandler(items) { 4 | // Close open dropdowns when clicking outside of the menu 5 | const page = document.getElementById('page'); 6 | page.addEventListener('click', (e) => { 7 | // If the click is inside the menu, bail 8 | if (e.target.closest('.menu-items')) { 9 | return; 10 | } 11 | 12 | items.forEach((li) => { 13 | closeSubMenu(li); 14 | }); 15 | }); 16 | 17 | // Close open dropdown when pressing escape 18 | items.forEach((li) => { 19 | li.addEventListener('keydown', (keydownMouseoverEvent) => { 20 | if (keydownMouseoverEvent.key === 'Escape') { 21 | closeSubMenu(li); 22 | } 23 | }); 24 | }); 25 | 26 | // Close other dropdowns when opening a new one 27 | items.forEach((li) => { 28 | // Bail if no dropdown 29 | if (!li.classList.contains('menu-item-has-children')) { 30 | return; 31 | } 32 | 33 | const dropdownToggle = li.querySelector('.dropdown-toggle'); 34 | const sameLevelDropdowns = li.parentNode.querySelectorAll(':scope > .menu-item-has-children'); 35 | 36 | // Add event listener to dropdown toggle 37 | dropdownToggle.addEventListener('click', () => { 38 | // We want to close other dropdowns only when a new one is opened 39 | if (!dropdownToggle.classList.contains('toggled-on')) { 40 | return; 41 | } 42 | 43 | sameLevelDropdowns.forEach((sameLevelDropdown) => { 44 | if (sameLevelDropdown !== li) { 45 | // Close all other sub level dropdowns 46 | sameLevelDropdown.querySelectorAll('.menu-item').forEach((subLi) => { 47 | closeSubMenu(subLi); 48 | }); 49 | // Close other same level dropdowns 50 | closeSubMenu(sameLevelDropdown); 51 | } 52 | }); 53 | }); 54 | }); 55 | } 56 | export default closeSubMenuHandler; 57 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | 21 | 22 |
    23 | 24 |
    25 |
    26 | 27 | 28 | 29 | 30 |

    31 | 32 | 33 | 35 |
    > 36 | 37 |

    38 | 39 | 40 | 41 |

    42 | 43 |

    44 | 47 |

    48 | 49 |
    50 | 54 |
    55 | 56 |
    57 | 58 | 59 | 60 | 61 | 62 | 63 |
    64 |
    65 | 66 |
    67 | 68 | { 5 | // Back to top button 6 | const moveToTop = new MoveTo({ 7 | duration: 300, 8 | easing: 'easeOutQuart', 9 | }); 10 | const topButton = document.getElementById('top'); 11 | const focusableElements = document.querySelectorAll( 12 | 'button, a, input, select, textarea, [tabindex]:not([tabindex="-1"])', 13 | ); 14 | 15 | function trackScroll() { 16 | const scrolled = window.pageYOffset; 17 | const scrollAmount = document.documentElement.clientHeight; 18 | 19 | if (scrolled > scrollAmount) { 20 | topButton.classList.add('is-visible'); 21 | } 22 | 23 | if (scrolled < scrollAmount) { 24 | topButton.classList.remove('is-visible'); 25 | } 26 | } 27 | 28 | function scroll(focusVisible) { 29 | // Check if user prefers reduced motion, if so, just scroll to top 30 | const prefersReducedMotion = window.matchMedia( 31 | '(prefers-reduced-motion: reduce)', 32 | ).matches; 33 | 34 | if (prefersReducedMotion) { 35 | focusableElements[0].focus({ focusVisible }); 36 | return; 37 | } 38 | 39 | // Move smoothly to the first focusable element on the page 40 | moveToTop.move(focusableElements[0]); 41 | 42 | // Focus too, if on keyboard 43 | focusableElements[0].focus({ preventScroll: true, focusVisible }); 44 | } 45 | 46 | if (topButton) { 47 | topButton.addEventListener('click', (event) => { 48 | // Don't add hash in the end of the url 49 | event.preventDefault(); 50 | 51 | // Focus without visibility (as user is not using keyboard) 52 | scroll(false); 53 | }); 54 | 55 | topButton.addEventListener('keydown', (event) => { 56 | // Don't propagate keydown event to click event 57 | event.preventDefault(); 58 | 59 | // Scroll with focus visible 60 | scroll(true); 61 | }); 62 | } 63 | 64 | window.addEventListener('scroll', trackScroll); 65 | }; 66 | 67 | export default backToTop; 68 | -------------------------------------------------------------------------------- /js/prod/gutenberg-editor.js: -------------------------------------------------------------------------------- 1 | wp.blocks.registerBlockStyle("core/paragraph",{name:"boxed",label:"Laatikko"}),wp.domReady((()=>{wp.blocks.unregisterBlockVariation("core/embed","amazon-kindle"),wp.blocks.unregisterBlockVariation("core/embed","bluesky"),wp.blocks.unregisterBlockVariation("core/embed","pinterest"),wp.blocks.unregisterBlockVariation("core/embed","crowdsignal"),wp.blocks.unregisterBlockVariation("core/embed","soundcloud"),wp.blocks.unregisterBlockVariation("core/embed","twitter"),wp.blocks.unregisterBlockVariation("core/embed","wordpress"),wp.blocks.unregisterBlockVariation("core/embed","spotify"),wp.blocks.unregisterBlockVariation("core/embed","flickr"),wp.blocks.unregisterBlockVariation("core/embed","animoto"),wp.blocks.unregisterBlockVariation("core/embed","cloudup"),wp.blocks.unregisterBlockVariation("core/embed","vimeo"),wp.blocks.unregisterBlockVariation("core/embed","youtube"),wp.blocks.unregisterBlockVariation("core/embed","dailymotion"),wp.blocks.unregisterBlockVariation("core/embed","imgur"),wp.blocks.unregisterBlockVariation("core/embed","issuu"),wp.blocks.unregisterBlockVariation("core/embed","kickstarter"),wp.blocks.unregisterBlockVariation("core/embed","mixcloud"),wp.blocks.unregisterBlockVariation("core/embed","pocket-casts"),wp.blocks.unregisterBlockVariation("core/embed","reddit"),wp.blocks.unregisterBlockVariation("core/embed","reverbnation"),wp.blocks.unregisterBlockVariation("core/embed","screencast"),wp.blocks.unregisterBlockVariation("core/embed","scribd"),wp.blocks.unregisterBlockVariation("core/embed","smugmug"),wp.blocks.unregisterBlockVariation("core/embed","speaker-deck"),wp.blocks.unregisterBlockVariation("core/embed","tumblr"),wp.blocks.unregisterBlockVariation("core/embed","tiktok"),wp.blocks.unregisterBlockVariation("core/embed","ted"),wp.blocks.unregisterBlockVariation("core/embed","videopress"),wp.blocks.unregisterBlockVariation("core/embed","wolfram-cloud"),wp.blocks.unregisterBlockVariation("core/embed","wordpress-tv"),wp.blocks.unregisterBlockVariation("core/embed","facebook")})),window.addEventListener("load",(()=>{window.acf&&window.acf.addAction("render_block_preview",(function(e){}))})); -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | /*! 2 | Theme Name: Air-light 3 | Theme URI: https://github.com/digitoimistodude/air-light 4 | Author: Digitoimisto Dude Oy 5 | Author URI: https://www.dude.fi 6 | Description: Hi. I'm a starter theme called Air-light, or air, if you like. I'm a theme based on Automattic's underscores and I'm meant for hacking so don't use me as a Parent Theme as-is. Instead try turning me into the next, most awesome, WordPress theme out there. That's what I'm here for. 7 | Version: 9.6.2 8 | 9 | ------8<---------- 10 | Please do this before your actual theme is ready to go live: 11 | 12 | 1. Cut everything between "------8<----------" including the separator 13 | 2. Theme Name should be already replaced, but double check it 14 | 3. Edit Theme URI, Author, Author URI, Description 15 | 4. Add Version as 1.0.0. 16 | 17 | This metadata and guide should not be visible in production. 18 | If you see this, contact the site admin. 19 | 20 | /*--------------------------------------------------------------- 21 | >>> Air-light theme version information, only for AIR developers 22 | ----------------------------------------------------------------- 23 | @version 2025-08-29 24 | @since 2016-01-28 25 | 26 | Tested up to: 6.8.2 27 | Requires PHP: 8.3 28 | License: MIT License 29 | License URI: LICENSE 30 | Text Domain: air-light 31 | Tags: one-column, accessibility-ready, translation-ready 32 | This theme is licensed under MIT. 33 | Use it to make something cool, have fun, and share what you've learned. 34 | Air-light is based on Underscores https://underscores.me/ C) 2016-2022 Digitoimisto Dude Oy 35 | _s is based on Underscores https://underscores.me/, (C) 2012-2020 Automattic, Inc. 36 | Underscores is distributed under the terms of the GNU GPL v2 or later. 37 | Normalizing styles have been helped along thanks to the fine work of 38 | Nicolas Gallagher and Jonathan Neal https://necolas.github.io/normalize.css/ 39 | 40 | Last implemented _s commit: 7226368 (May 15, 2020) 41 | Last checked _s commit: e78a808 (May 16, 2020) 42 | @link https://github.com/Automattic/_s/commits/master 43 | ------8<---------- 44 | */ 45 | -------------------------------------------------------------------------------- /inc/template-tags/entry-footer.php: -------------------------------------------------------------------------------- 1 | '; 15 | 16 | if ( 'post' === get_post_type() ) : 17 | if ( has_category() ) : ?> 18 | 26 |
  • ', '
  • ', '
  • ' ); 31 | } 32 | endif; 33 | 34 | if ( ! is_single() && ! post_password_required() && ( comments_open() || get_comments_number() ) ) { 35 | echo ' 36 | '; 37 | comments_popup_link( sprintf( wp_kses( __( 'Leave a comment on %s', 'air-light' ), array( 'span' => array( 'class' => array() ) ) ), get_the_title() ) ); 38 | echo ''; 39 | } 40 | 41 | echo '
    '; 42 | } 43 | -------------------------------------------------------------------------------- /inc/template-tags/single-comment.php: -------------------------------------------------------------------------------- 1 | 13 |
  • > 14 |
    15 | 16 |

    17 | 18 | comment_approved ) : ?> 19 |

    20 | 21 | 22 |

    23 | 24 | 25 | 26 | 27 |

    28 | 29 | $depth, 'max_depth' => $args['max_depth'] ) ) ); 32 | edit_comment_link( __( '— Edit', 'air-light' ), ' ', '' ); 33 | ?> 34 | 35 |
    36 |
  • 37 | self::ask__( 'Your Taxonomy', 'Taxonomy plural name' ), 23 | 'singular_name' => self::ask__( 'Your Taxonomy', 'Taxonomy singular name' ), 24 | 'search_items' => self::ask__( 'Your Taxonomy', 'Search Your Taxonomies' ), 25 | 'popular_items' => self::ask__( 'Your Taxonomy', 'Popular Your Taxonomies' ), 26 | 'all_items' => self::ask__( 'Your Taxonomy', 'All Your Taxonomies' ), 27 | 'parent_item' => self::ask__( 'Your Taxonomy', 'Parent Your Taxonomy' ), 28 | 'parent_item_colon' => self::ask__( 'Your Taxonomy', 'Parent Your Taxonomy' ), 29 | 'edit_item' => self::ask__( 'Your Taxonomy', 'Edit Your Taxonomy' ), 30 | 'update_item' => self::ask__( 'Your Taxonomy', 'Update Your Taxonomy' ), 31 | 'add_new_item' => self::ask__( 'Your Taxonomy', 'Add New Your Taxonomy' ), 32 | 'new_item_name' => self::ask__( 'Your Taxonomy', 'New Your Taxonomy' ), 33 | 'add_or_remove_items' => self::ask__( 'Your Taxonomy', 'Add or remove Your Taxonomies' ), 34 | 'choose_from_most_used' => self::ask__( 'Your Taxonomy', 'Choose from most used Taxonomies' ), 35 | 'menu_name' => self::ask__( 'Your Taxonomy', 'Your Taxonomy' ), 36 | ]; 37 | 38 | $args = [ 39 | 'labels' => $labels, 40 | 'public' => false, 41 | 'show_in_nav_menus' => true, 42 | 'show_admin_column' => true, 43 | 'hierarchical' => true, 44 | 'show_tagcloud' => false, 45 | 'query_var' => false, 46 | 'pll_translatable' => true, 47 | 'rewrite' => [ 48 | 'slug' => 'your-taxonomy', 49 | ], 50 | ]; 51 | 52 | $this->register_wp_taxonomy( $this->slug, $post_types, $args ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /bin/tasks/header.sh: -------------------------------------------------------------------------------- 1 | # Note about running directly as we can't prevent people running this via sh or bash pre-cmd 2 | if [ "$1" = "--existing" ] || [[ "$1" == --* ]]; then 3 | # Skip dirname/basename for any flag arguments 4 | export DIR_TO_FILE="" 5 | else 6 | # Only try to get directory for non-flag arguments 7 | if [ -n "$1" ] && [[ "$1" != --* ]]; then 8 | export DIR_TO_FILE=$(cd "$(dirname "$1")"; pwd -P)/$(basename "$1") 9 | else 10 | export DIR_TO_FILE=$(cd "$(dirname "${BASH_SOURCE[0]}")"; pwd -P)/$(basename "${BASH_SOURCE[0]}") 11 | fi 12 | fi 13 | 14 | # Get air-light version from CHANGELOG.md first line, format: "### 1.2.3: YYYY-MM-DD" 15 | AIRLIGHT_VERSION=$(grep '^### ' "${SCRIPTS_LOCATION}/../CHANGELOG.md" 2>/dev/null | head -n 1 | cut -d' ' -f2 | tr -d ':' || echo "dev") 16 | 17 | # Get version date from CHANGELOG.md in the air-light root directory 18 | AIRLIGHT_DATE=$(grep '^### ' "${SCRIPTS_LOCATION}/../CHANGELOG.md" 2>/dev/null | head -n 1 | cut -d' ' -f3 || date +%Y-%m-%d) 19 | 20 | # Source the logo 21 | source "$SCRIPTS_LOCATION/tasks/logo.sh" 22 | 23 | # Print the logo 24 | print_logo 25 | 26 | echo "" 27 | echo "-----------------------------------------------------------------------" 28 | echo "newtheme start script ${SCRIPT_LABEL}, v${SCRIPT_VERSION}" 29 | echo "air-light v${AIRLIGHT_VERSION} (${AIRLIGHT_DATE})" 30 | echo "-----------------------------------------------------------------------" 31 | echo "" 32 | if [ ! -f /usr/local/bin/newtheme ]; then 33 | echo "${TXTRESET}${TXTBOLD}ACTION REQUIRED:${TXTRESET}${WHITE} Link this file to system level and start from there with this oneliner:${TXTRESET}" 34 | echo "" 35 | echo "${GREEN}sudo ln -s ${SCRIPTS_LOCATION}/newtheme.sh /usr/local/bin/newtheme && sudo chmod +x /usr/local/bin/newtheme && newtheme${TXTRESET}" 1>&2 36 | echo "" 37 | exit 38 | fi 39 | if [ $0 != '/usr/local/bin/newtheme' ]; then 40 | echo "${TXTRESET}${WHITE}Please do NOT run this script with ${RED}sh $CURRENTFILE${WHITE} or ${RED}bash $CURRENTFILE${WHITE} or ${RED}./$CURRENTFILE${WHITE}. 41 | Run this script globally instead by simply typing: ${GREEN}newtheme${TXTRESET}" 42 | echo "" 43 | exit 44 | fi 45 | 46 | while true; do 47 | read -p "${BOLDYELLOW}Project created? (y/n)${TXTRESET} " yn 48 | case $yn in 49 | [Yy]* ) break;; 50 | [Nn]* ) exit;; 51 | * ) echo "Please answer y or n.";; 52 | esac 53 | done 54 | -------------------------------------------------------------------------------- /sass/navigation/_nav-click-mobile.scss: -------------------------------------------------------------------------------- 1 | @use '../variables' as *; 2 | // stylelint-disable a11y/no-display-none, plugin/file-max-lines 3 | // Import nav-toggle 4 | @use 'nav-toggle'; 5 | @use 'nav-mobile'; 6 | 7 | // Mobile styles 8 | @media screen and (max-width: $width-max-mobile - 1px) { 9 | 10 | // Dropdown toggle 11 | .menu-item-clickable { 12 | --menu-item-clickable-size: .75rem; 13 | align-items: center; 14 | background-color: transparent; 15 | border-bottom: 0; 16 | border-left: 0; 17 | border-right: 0; 18 | display: flex; 19 | gap: .625rem; 20 | justify-content: space-between; 21 | text-align: initial; 22 | width: 100%; 23 | } 24 | 25 | .menu-item-clickable::after { 26 | background-image: url("data:image/svg+xml, %3Csvg xmlns='http://www.w3.org/2000/svg' width='var(--menu-item-clickable-size)' height='var(--menu-item-clickable-size)' viewBox='0 0 12 7'%3E%3Cpath fill-rule='evenodd' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M1.385 1.417L6 5.583m4.615-4.166L6 5.583'/%3E%3C/svg%3E "); 27 | background-position: 50% 50%; 28 | content: ''; 29 | height: var(--menu-item-clickable-size); 30 | transition: transform .35s cubic-bezier(.19, 1, .22, 1); 31 | width: var(--menu-item-clickable-size); 32 | } 33 | 34 | .menu-item-clickable.toggled-on::after { 35 | transform: rotate(-180deg) rotateX(0deg); 36 | } 37 | 38 | .menu-item-clickable:hover { 39 | cursor: pointer; 40 | } 41 | 42 | .menu-item-clickable:focus { 43 | cursor: pointer; 44 | z-index: 100; 45 | } 46 | 47 | .sub-menu .menu-item-clickable { 48 | color: var(--color-sub-menu-mobile); 49 | } 50 | 51 | // Mobile navigation core functionality 52 | .js-nav-active { 53 | overflow: hidden; 54 | 55 | .menu-items-wrapper { 56 | background-color: var(--color-background-menu-items-active); 57 | opacity: 1; 58 | pointer-events: all; 59 | transform: translate3d(0, 0, 0); 60 | visibility: visible; 61 | width: var(--width-navigation); 62 | } 63 | } 64 | 65 | .site-main, 66 | .site-footer { 67 | transition: transform 180ms ease-in-out; 68 | } 69 | 70 | // Push site content and footer to the left 71 | .js-nav-active .site-main, 72 | .js-nav-active .site-footer { 73 | transform: translate3d(calc(var(--width-navigation) * -1), 0, 0); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /js/src/modules/anchors.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign, no-undef */ 2 | 3 | import MoveTo from 'moveto'; 4 | 5 | const initAnchors = () => { 6 | const easeFunctions = { 7 | easeInQuad(t, b, c, d) { t /= d; return c * t * t + b; }, 8 | easeOutQuad(t, b, c, d) { t /= d; return -c * t * (t - 2) + b; }, 9 | }; 10 | 11 | const moveTo = new MoveTo( 12 | { ease: 'easeInQuad' }, 13 | easeFunctions, 14 | ); 15 | 16 | let triggers = document.querySelectorAll('a[href*="#"]:not([href="#"]):not(#top)'); 17 | 18 | triggers = Array.from(triggers); 19 | 20 | triggers.forEach((trigger) => { 21 | moveTo.registerTrigger(trigger); 22 | const targetId = trigger.hash.substring(1); 23 | const target = document.getElementById(targetId); 24 | 25 | trigger.addEventListener('click', (event) => { 26 | event.preventDefault(); // Prevent default behavior of anchor links 27 | 28 | // If the trigger is nav-link, close nav 29 | if (trigger.classList.contains('nav-link') || trigger.classList.contains('dropdown-item')) { 30 | document.body.classList.remove('js-nav-active'); 31 | 32 | // Additional navigation cleanup 33 | const html = document.documentElement; 34 | const container = document.getElementById('main-navigation-wrapper'); 35 | const menu = container?.querySelector('ul'); 36 | const button = document.getElementById('nav-toggle'); 37 | 38 | if (html) html.classList.remove('disable-scroll'); 39 | if (container) container.classList.remove('is-active'); 40 | if (button) { 41 | button.classList.remove('is-active'); 42 | button.setAttribute('aria-expanded', 'false'); 43 | } 44 | if (menu) menu.setAttribute('aria-expanded', 'false'); 45 | } 46 | 47 | // Check if the target element exists on the current page 48 | if (target) { 49 | // Scroll to the target element 50 | moveTo.move(target); 51 | 52 | // Update URL history 53 | window.history.pushState('', '', trigger.hash); 54 | 55 | // Focus on the target element after a delay 56 | setTimeout(() => { 57 | target.setAttribute('tabindex', '-1'); 58 | target.focus(); 59 | }, 500); 60 | } else { 61 | // Navigate to the target page 62 | window.location.href = trigger.href; 63 | } 64 | }); 65 | }); 66 | }; 67 | 68 | export default initAnchors; 69 | -------------------------------------------------------------------------------- /inc/hooks/acf-blocks.php: -------------------------------------------------------------------------------- 1 | 'air-light', 14 | 'title' => __( 'Theme blocks', 'air-light' ), 15 | ], 16 | ] ); 17 | } // end acf_blocks_add_category_in_gutenberg 18 | 19 | function acf_blocks_init() { 20 | if ( ! function_exists( 'acf_register_block_type' ) ) { 21 | return; 22 | } 23 | 24 | if ( ! isset( THEME_SETTINGS['acf_blocks'] ) ) { 25 | return; 26 | } 27 | 28 | $example_data = apply_filters( 'air_acf_blocks_example_data', [] ); 29 | 30 | foreach ( THEME_SETTINGS['acf_blocks'] as $block ) { 31 | // Check if we have added example data via hook 32 | if ( empty( $block['example'] ) && ! empty( $example_data[ $block['name'] ] ) ) { 33 | $block['example'] = [ 34 | 'attributes' => [ 35 | 'mode' => 'preview', 36 | 'data' => $example_data[ $block['name'] ], 37 | ], 38 | ]; 39 | } 40 | 41 | // Check if icon is set, otherwise try to load svg icon 42 | if ( ! isset( $block['icon'] ) || empty( $block['icon'] ) ) { 43 | $icon_path = get_theme_file_path( "svg/block-icons/{$block['name']}.svg" ); 44 | $icon_path = apply_filters( 'air_light_acf_block_icon', $icon_path, $block['name'], $block ); 45 | 46 | if ( file_exists( $icon_path ) ) { 47 | $block['icon'] = get_acf_block_icon_str( $icon_path ); 48 | } 49 | } 50 | 51 | acf_register_block_type( wp_parse_args( $block, THEME_SETTINGS['acf_block_defaults'] ) ); 52 | } 53 | } // end acf_blocks_init 54 | 55 | /** 56 | * Thank you WordPress.org theme repository for not allowing 57 | * file_get_contents even for local files. 58 | */ 59 | function get_acf_block_icon_str( $icon_path ) { 60 | if ( ! file_exists( $icon_path ) ) { 61 | return; 62 | } 63 | 64 | ob_start(); 65 | include $icon_path; 66 | return ob_get_clean(); 67 | } // end get_acf_block_icon_str 68 | 69 | function add_custom_tinymce_toolbars( $toolbars ) { 70 | $toolbars['Small'][1] = [ 'bold', 'italic', 'underline', 'strikethrough', 'link', 'bullist', 'numlist', 'blockquote' ]; 71 | $toolbars['Mini'][1] = [ 'bold', 'italic', 'underline', 'strikethrough', 'link' ]; 72 | return $toolbars; 73 | } // end add_custom_tinymce_toolbars 74 | -------------------------------------------------------------------------------- /inc/includes/post-type.php: -------------------------------------------------------------------------------- 1 | slug = $slug; 39 | $this->translations = []; 40 | } 41 | 42 | 43 | /** 44 | * Registers the post type data and registers it in WordPress. 45 | */ 46 | abstract protected function register(); 47 | 48 | /** 49 | * Registers a custom post type in WordPress. 50 | * 51 | * @see http://codex.wordpress.org/Function_Reference/register_post_type 52 | * @see https://developer.wordpress.org/reference/classes/wp_post_type/ 53 | * 54 | * @param string $slug Post type slug (max. 20 characters, cannot contain 55 | * capital letters or spaces). 56 | * @param array $args Post type arguments. 57 | * @return WP_Post_Type|WP_Error Registered post type or error in case 58 | * of failure. 59 | */ 60 | public function register_wp_post_type( $slug, $args ) { 61 | // Register PolyLang translatable only if it's private 62 | if ( isset( $args['pll_translatable'] ) && $args['pll_translatable'] && false === $args['public'] ) { 63 | add_filter( 'pll_get_post_types', function( $cpts ) use ( $slug ) { 64 | $cpts[ $slug ] = $slug; 65 | 66 | return $cpts; 67 | }, 9, 2 ); 68 | } 69 | 70 | $this->register_translations(); 71 | return register_post_type( $slug, $args ); 72 | } 73 | 74 | // Wrapper for ask__ 75 | public function ask__( $key, $value ) { 76 | $pll_key = "{$key}: {$value}"; 77 | $this->translations[ $pll_key ] = $value; 78 | if ( function_exists( 'ask__' ) ) { 79 | return ask__( $pll_key ); 80 | } 81 | 82 | return $value; 83 | } 84 | 85 | private function register_translations() { 86 | $translations = $this->translations; 87 | 88 | add_filter( 'air_light_translations', function ( $strings ) use ( $translations ) { 89 | return array_merge( $translations, $strings ); 90 | }, 10, 2 ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /sass/base/_accessibility.scss: -------------------------------------------------------------------------------- 1 | @use '../variables/breakpoints' as *; 2 | 3 | // A hidden screen reader texts for readers, focus elements for 4 | // vision impaired and other useful a11y CSS hacks. 5 | 6 | // Text meant only for screen readers. 7 | @mixin screen-reader-text() { 8 | border: 0; 9 | clip: rect(1px, 1px, 1px, 1px); 10 | 11 | // doiuse-disable 12 | clip-path: inset(50%); 13 | height: 1px; 14 | margin: -1px; 15 | overflow: hidden; 16 | padding: 0; 17 | position: absolute; 18 | width: 1px; 19 | 20 | // Many screen reader and browser combinations announce broken words as they would appear visually. 21 | // stylelint-disable-next-line declaration-no-important, max-line-length 22 | word-wrap: normal !important; 23 | 24 | // Focused on mouse (it never can be focused via mouse, because it's already invisible) 25 | &:focus { 26 | opacity: 0; 27 | } 28 | 29 | // Focused on keyboard 30 | &:focus-visible { 31 | background-color: var(--color-white); 32 | border-radius: 0; 33 | box-shadow: 0 0 2px 2px rgb(22 22 22 / .6); 34 | clip: auto; 35 | clip-path: none; 36 | display: block; 37 | font-size: 1.0625rem; 38 | font-weight: var(--typography-weight-bold); 39 | height: auto; 40 | left: 0.3125rem; 41 | line-height: normal; 42 | opacity: 1; 43 | padding: 0.9375rem 1.4375rem 0.875rem; 44 | text-decoration: none; 45 | top: 0.3125rem; 46 | width: auto; 47 | z-index: 100000; // Above WP toolbar. 48 | } 49 | } 50 | 51 | .screen-reader-text { 52 | @include screen-reader-text(); 53 | } 54 | 55 | .skip-link { 56 | margin: 0.3125rem; 57 | } 58 | 59 | // Visually distinct focus color on keyboard 60 | a:focus, 61 | input:focus, 62 | button:focus, 63 | select:focus, 64 | textarea:focus, 65 | div[tabindex]:focus { 66 | // Make sure every focusable element has opacity 100% 67 | opacity: 1; 68 | 69 | // Make sure it's not glued to the element 70 | outline-offset: 0.3125rem; 71 | } 72 | 73 | // Make focus a little more engaging 74 | // @source https://twitter.com/argyleink/status/1387072095159406596 75 | // @link https://codepen.io/argyleink/pen/JjEzeLp 76 | @media (prefers-reduced-motion: no-preference) { 77 | *:focus { 78 | transition: outline-offset .25s ease; 79 | } 80 | } 81 | 82 | // External link icon 83 | .external-link-icon { 84 | margin-left: 0.4375rem; 85 | margin-right: 2px; 86 | 87 | @media (max-width: $container-mobile) { 88 | height: 0.75rem; 89 | margin-left: 4px; 90 | transform: translateY(1px); 91 | width: 0.75rem; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /inc/hooks/native-gutenberg-blocks.php: -------------------------------------------------------------------------------- 1 | 'air-light', 19 | // This can be something like "Customer's blocks" 20 | 'title' => __( 'Air-light blocks', 'air-light' ), 21 | ], 22 | ] 23 | ); 24 | } 25 | add_filter( 'block_categories_all', __NAMESPACE__ . '\register_block_categories', 10, 1 ); 26 | 27 | /** 28 | * Register all native blocks from the blocks directory 29 | */ 30 | function register_native_gutenberg_blocks() { 31 | // Get all directories in the blocks folder 32 | $blocks_dir = get_theme_file_path( '/blocks' ); 33 | $block_folders = array_filter( glob( $blocks_dir . '/*' ), 'is_dir' ); 34 | 35 | foreach ( $block_folders as $block_folder ) { 36 | // Check if block.json exists in the build folder 37 | if ( file_exists( $block_folder . '/build/block.json' ) ) { 38 | // Add error logging to debug block registration 39 | $registration_result = register_block_type( $block_folder . '/build' ); 40 | 41 | if ( is_wp_error( $registration_result ) ) { 42 | error_log( 'Block registration error for ' . basename( $block_folder ) . ': ' . $registration_result->get_error_message() ); 43 | } 44 | } 45 | } 46 | } 47 | add_action( 'init', __NAMESPACE__ . '\register_native_gutenberg_blocks' ); 48 | 49 | /** 50 | * Enqueue all native block assets 51 | */ 52 | function enqueue_block_editor_assets() { 53 | // Get all block asset files 54 | $blocks_dir = get_theme_file_path( '/blocks' ); 55 | $block_folders = array_filter( glob( $blocks_dir . '/*' ), 'is_dir' ); 56 | 57 | foreach ( $block_folders as $block_folder ) { 58 | $block_name = basename( $block_folder ); 59 | $asset_file = get_theme_file_path( "blocks/{$block_name}/build/index.asset.php" ); 60 | 61 | if ( file_exists( $asset_file ) ) { 62 | $asset = require $asset_file; 63 | 64 | wp_enqueue_script( 65 | "air-light-{$block_name}", 66 | get_theme_file_uri( "blocks/{$block_name}/build/index.js" ), 67 | $asset['dependencies'] ?? [ 'wp-blocks', 'wp-element', 'wp-editor' ], 68 | $asset['version'] ?? filemtime( get_theme_file_path( "blocks/{$block_name}/build/index.js" ) ), 69 | true 70 | ); 71 | } 72 | } 73 | } 74 | 75 | add_action( 'enqueue_block_editor_assets', __NAMESPACE__ . '\enqueue_block_editor_assets' ); 76 | -------------------------------------------------------------------------------- /bin/tasks/additions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "${YELLOW}Adding media library folder...${TXTRESET}" 3 | mkdir -p ${PROJECT_PATH}/media 4 | echo "" > ${PROJECT_PATH}/media/index.php 5 | chmod 777 ${PROJECT_PATH}/media 6 | 7 | echo "${YELLOW}Generating default README.md...${TXTRESET}" 8 | 9 | NEWEST_AIR_VERSION="9.5.1" 10 | NEWEST_WORDPRESS_VERSION="6.7.0" 11 | NEWEST_PHP_VERSION="8.3" 12 | CURRENT_DATE=$(LC_TIME=en_US date '+%d %b %Y' |tr ' ' '_'); 13 | echo "# ${PROJECT_NAME} 14 | ![based_on_air_version ${NEWEST_AIR_VERSION}_](https://img.shields.io/badge/based_on_air_version-${NEWEST_AIR_VERSION}_-brightgreen.svg?style=flat-square) ![project_created ${CURRENT_DATE}](https://img.shields.io/badge/project_created-${CURRENT_DATE}-blue.svg?style=flat-square) ![Tested_up_to WordPress_${NEWEST_WORDPRESS_VERSION}](https://img.shields.io/badge/Tested_up_to-WordPress_${NEWEST_WORDPRESS_VERSION}-blue.svg?style=flat-square) ![Compatible_with PHP_${NEWEST_PHP_VERSION}](https://img.shields.io/badge/Compatible_with-PHP_${NEWEST_PHP_VERSION}-green.svg?style=flat-square) 15 | 16 | This project is hand made for customer by Dude. 17 | 18 | ------8<----------
    19 | **Disclaimer:** Please remove this disclaimer after you have edited the README.md, style.css version information and details and screenshot.png. If you see this text in place after the project has been deployed to production, \`git blame\` is in place ;)
    20 | ------8<---------- 21 | 22 | ## Stack 23 | 24 | ### Project is based on 25 | 26 | * [digitoimistodude/dudestack](https://github.com/digitoimistodude/dudestack) 27 | * [digitoimistodude/air-light](https://github.com/digitoimistodude/air-light) 28 | 29 | ### Recommended development environment 30 | 31 | * [digitoimistodude/macos-lemp-setup](https://github.com/digitoimistodude/macos-lemp-setup) 32 | 33 | ## Theme screenshot 34 | 35 | ![Screenshot](/content/themes/${THEME_NAME}/screenshot.png?raw=true \"Screenshot\") 36 | 37 | ## Getting started 38 | 39 | Your local server should be up and running. If you need help, ask your supervisor or refer to **[Internal Development Docs](https://app.gitbook.com/o/PedExJWZmbCiZe4gDwKC/s/VVikkYgIZ9miBzwYDCYh/)** → **[Joining the project later on](https://app.gitbook.com/o/PedExJWZmbCiZe4gDwKC/s/VVikkYgIZ9miBzwYDCYh/project-stages/joining-the-project-later-on)**. 40 | 41 | ### Installation 42 | 43 | In project root: 44 | 45 | \`\`\` 46 | composer install 47 | nvm install 48 | nvm use 49 | npm install 50 | \`\`\` 51 | 52 | In theme directory: 53 | 54 | \`\`\` 55 | npm install 56 | \`\`\` 57 | 58 | Start development from project root: 59 | 60 | \`\`\` 61 | gulp 62 | \`\`\`" > "${PROJECTS_HOME}/${PROJECT_NAME}/README.md" 63 | -------------------------------------------------------------------------------- /footer.php: -------------------------------------------------------------------------------- 1 | 14 | 15 |
    16 | 17 |
    18 | 19 |
    20 | 21 |
    22 | 23 | 24 | 25 | . 26 | 27 | 28 | 29 | 30 | air', esc_attr( AIR_LIGHT_VERSION ) ); ?>. 31 | 32 | 33 |
    34 | 35 |
    36 | 37 |
    38 | 39 |
    40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /bin/newtheme-laragon.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # WordPress theme starting bash script for Air-light (Laragon version) 3 | 4 | # Script specific vars 5 | SCRIPT_LABEL='with Laragon support' 6 | SCRIPT_VERSION='1.0.7' 7 | 8 | # Vars needed for this file to function globally 9 | CURRENTFILE=`basename $0` 10 | 11 | #Variables 12 | PROJECTS_HOME="c:/laragon/www" 13 | STARTER_THEME_PATH_TEMP="${HOME}/air" 14 | DIR_TO_FILE=$(cd "$(dirname "$1")"; pwd -P)/$(basename "$1") 15 | TXTBOLD=$(tput bold) 16 | BOLDYELLOW=${TXTBOLD}$(tput setaf 3) 17 | BOLDGREEN=${TXTBOLD}$(tput setaf 2) 18 | BOLDWHITE=${TXTBOLD}$(tput setaf 7) 19 | YELLOW=$(tput setaf 3) 20 | RED=$(tput setaf 1) 21 | GREEN=$(tput setaf 2) 22 | WHITE=$(tput setaf 7) 23 | TXTRESET=$(tput sgr0) 24 | YEAR=$(date +%y) 25 | CURRENTFILE=`basename $0` 26 | 27 | # Determine scripts location to get imports right 28 | if [ "$CURRENTFILE" = "newtheme-laragon.sh" ]; then 29 | SCRIPTS_LOCATION="$( pwd )" 30 | else 31 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 32 | ORIGINAL_FILE=$( readlink $DIR/$CURRENTFILE ) 33 | SCRIPTS_LOCATION=$( dirname $ORIGINAL_FILE ) 34 | fi 35 | 36 | echo "-----------------------------------------------------" 37 | echo "newtheme start script ${SCRIPT_LABEL}, v${SCRIPT_VERSION}" 38 | echo "-----------------------------------------------------" 39 | echo "" 40 | 41 | while true; do 42 | read -p "${BOLDYELLOW}Project created? (y/n)${TXTRESET} " yn 43 | case $yn in 44 | [Yy]* ) break;; 45 | [Nn]* ) exit;; 46 | * ) echo "Please answer y or n.";; 47 | esac 48 | done 49 | 50 | # Final note about server requirements 51 | echo "" 52 | echo "${WHITE}Using this start script requires you use the following: 53 | https://laragon.org/ Full Version 54 | https://github.com/digitoimistodude/air-light 55 | ${TXTRESET}" 56 | 57 | # Ask names and credentials 58 | source ${SCRIPTS_LOCATION}/tasks/askvars.sh 59 | 60 | # Get latest Air-light version with updates and copy it over to your project 61 | source ${SCRIPTS_LOCATION}/tasks/get-theme.sh 62 | 63 | # Get and install theme dependencies 64 | source ${SCRIPTS_LOCATION}/tasks/dependencies.sh 65 | 66 | # Create latest Air-light development packages for project root level (gulp paths etc.) 67 | source ${SCRIPTS_LOCATION}/tasks/project.sh 68 | 69 | # Clean up leftover development files from Air-light 70 | source ${SCRIPTS_LOCATION}/tasks/cleanups.sh 71 | 72 | # Replace Air-light with your theme name and other seds (WSL version) 73 | source ${SCRIPTS_LOCATION}/tasks/replaces-wsl.sh 74 | 75 | # Add media folder, generate README.md for project etc. 76 | source ${SCRIPTS_LOCATION}/tasks/additions.sh 77 | 78 | # The end 79 | source ${SCRIPTS_LOCATION}/tasks/footer.sh 80 | -------------------------------------------------------------------------------- /inc/post-types/your-post-type.php: -------------------------------------------------------------------------------- 1 | Key: Default value => 'Default value' 21 | 'menu_name' => self::ask__( 'Your Post Type', 'Your Post Type' ), 22 | 'name' => self::ask__( 'Your Post Type', 'Your Post Types' ), 23 | 'singular_name' => self::ask__( 'Your Post Type', 'Your Post Type' ), 24 | 'name_admin_bar' => self::ask__( 'Your Post Type', 'Your Post Type' ), 25 | 'add_new' => self::ask__( 'Your Post Type', 'Add New' ), 26 | 'add_new_item' => self::ask__( 'Your Post Type', 'Add New Your Post Type' ), 27 | 'new_item' => self::ask__( 'Your Post Type', 'New Your Post Type' ), 28 | 'edit_item' => self::ask__( 'Your Post Type', 'Edit Your Post Type' ), 29 | 'view_item' => self::ask__( 'Your Post Type', 'View Your Post Type' ), 30 | 'all_items' => self::ask__( 'Your Post Type', 'All Your Post Types' ), 31 | 'search_items' => self::ask__( 'Your Post Type', 'Search Your Post Types' ), 32 | 'parent_item_colon' => self::ask__( 'Your Post Type', 'Parent Your Post Types:' ), 33 | 'not_found' => self::ask__( 'Your Post Type', 'No your post types found.' ), 34 | 'not_found_in_trash' => self::ask__( 'Your Post Type', 'No your post types found in Trash.' ), 35 | ]; 36 | 37 | // Definition of the post type arguments. For full list see: 38 | // http://codex.wordpress.org/Function_Reference/register_post_type 39 | $args = [ 40 | 'labels' => $generated_labels, 41 | 'menu_icon' => null, 42 | 'public' => true, 43 | 'show_ui' => true, 44 | 'has_archive' => false, 45 | 'exclude_from_search' => false, 46 | 'show_in_rest' => false, 47 | 'pll_translatable' => true, 48 | 'rewrite' => [ 49 | 'with_front' => false, 50 | 'slug' => 'your-post-type', 51 | ], 52 | 'supports' => [ 'title', 'editor', 'thumbnail', 'revisions' ], 53 | 'taxonomies' => [], 54 | ]; 55 | 56 | $this->register_wp_post_type( $this->slug, $args ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /sass/navigation/_nav-toggle.scss: -------------------------------------------------------------------------------- 1 | // 🍔 2 | .hamburger { 3 | --hamburger-layer-height: 2px; 4 | --hamburger-width: 2rem; 5 | --hamburger-color: var(--color-black); 6 | --hamburger-color-active: var(--color-white); 7 | background-color: var(--hamburger-color); 8 | flex-shrink: 0; 9 | height: var(--hamburger-layer-height); 10 | position: relative; 11 | transition: all .2s ease 0s; 12 | width: var(--hamburger-width); 13 | 14 | // Hamburger layers 15 | &::before, 16 | &::after { 17 | background-color: var(--hamburger-color); 18 | content: ''; 19 | display: block; 20 | height: var(--hamburger-layer-height); 21 | position: absolute; 22 | transition: all .2s ease 0s; 23 | width: var(--hamburger-width); 24 | } 25 | 26 | &::before { 27 | top: -0.5625rem; 28 | } 29 | 30 | &::after { 31 | bottom: -0.5625rem; 32 | } 33 | } 34 | 35 | // 🍔 third layer width 36 | body:not(.js-nav-active) .nav-toggle .hamburger::after, 37 | body:not(.js-nav-active) .nav-toggle:focus .hamburger::after { 38 | width: 80%; 39 | } 40 | 41 | // 🍔 third layer hover 42 | body:not(.js-nav-active) .nav-toggle:hover .hamburger::after { 43 | width: 100%; 44 | } 45 | 46 | // Active state for 🍔 47 | .js-nav-active .hamburger::before, 48 | .js-nav-active .hamburger::after { 49 | background-color: var(--hamburger-color-active); 50 | left: 0; 51 | top: 0; 52 | } 53 | 54 | .js-nav-active .hamburger::before { 55 | transform: rotate(-45deg); 56 | } 57 | 58 | .js-nav-active .hamburger::after { 59 | transform: rotate(45deg); 60 | } 61 | 62 | .js-nav-active .hamburger { 63 | background-color: transparent; 64 | } 65 | 66 | // Create visual label out of aria-label 67 | // .nav-toggle::after { 68 | // color: var(--color-black); 69 | // content: attr(aria-label); 70 | // font-size: var(--typography-size-14); 71 | // font-weight: var(--typography-weight-semibold); 72 | // margin-left: 1rem; 73 | // white-space: nowrap; 74 | // } 75 | 76 | // The actual toggle