6 | *
7 | * @package air-light
8 | */
9 |
10 | namespace Air_Light;
11 |
12 | ?>
13 |
14 |
15 | >
16 |
17 |
18 |
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 | '' . esc_html__( 'Pages:', 'air-light' ), 'after' => '
' ) );
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 |
42 |
43 |
44 |
45 |
46 |
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 '';
39 | }
40 |
41 | echo '
';
42 | }
43 |
--------------------------------------------------------------------------------
/inc/template-tags/single-comment.php:
--------------------------------------------------------------------------------
1 |
13 |
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 |    
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 | 
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 |
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 |