├── src ├── vue │ └── .gitkeep ├── js │ ├── tests │ │ └── .gitkeep │ ├── globals.d.ts │ ├── shims-vue.d.ts │ ├── app.ts │ ├── types.ts │ ├── apolloClient.ts │ ├── admin.ts │ ├── scriptLoader.ts │ ├── modules │ │ ├── menu.ts │ │ └── alerts.ts │ └── main.ts ├── public │ ├── img │ │ └── .gitkeep │ └── svgs │ │ └── .gitkeep ├── css │ ├── components │ │ └── .gitkeep │ ├── layouts │ │ └── .gitkeep │ ├── base │ │ ├── README.md │ │ ├── admin.css │ │ ├── social-icons.css │ │ ├── alerts.css │ │ ├── menu.css │ │ ├── structure.css │ │ └── wp-classes.css │ └── main.css ├── sprite │ ├── email.svg │ ├── facebook.svg │ ├── icon-hamburger.svg │ ├── x.svg │ ├── youtube.svg │ ├── linkedin.svg │ ├── vimeo.svg │ ├── icon-close.svg │ ├── pinterest.svg │ ├── instagram.svg │ ├── tiktok.svg │ └── soundcloud.svg └── README.md ├── .npmrc ├── .prettierignore ├── index.php ├── cypress ├── support │ ├── helpers │ │ ├── index.ts │ │ └── accessibility-helper.ts │ ├── vue.ts │ ├── e2e.ts │ ├── baseUrl.ts │ ├── index.d.ts │ ├── checkElementExists.ts │ └── assertions.ts ├── fixtures │ └── example.json ├── tsconfig.json ├── e2e │ ├── alert.spec.ts │ ├── navigation.spec.ts │ └── axe.spec.ts └── plugins │ └── index.ts ├── screenshot.png ├── templates ├── modules │ ├── general-content.php │ └── form.php ├── content-single.php ├── email │ └── email-body.html ├── content-page.php ├── partials │ └── alert.php └── README.md ├── .env ├── .editorconfig ├── stylelint.config.ts ├── vite-env.d.ts ├── prettier.config.cjs ├── CONTRIBUTING.md ├── home.php ├── 404.php ├── style.css ├── .git-ftp-ignore ├── .eslintrc.cjs ├── .gitignore ├── tsconfig.json ├── tailwind.config.ts ├── cypress.config.ts ├── postcss.config.cts ├── page.php ├── single.php ├── inc ├── lib │ ├── FragmentCache.php │ ├── vite.php │ ├── README.md │ ├── shortcodes.php │ ├── assets.php │ ├── helpers.php │ ├── init.php │ ├── AjaxForm.php │ └── clean.php └── acf-json │ ├── group_62586c11ea98d.json │ ├── group_64c9841305284.json │ ├── group_653fdd4a51868.json │ ├── group_621fb69bc75ee.json │ ├── group_653fdbc6d93b5.json │ ├── group_62583ddaa0897.json │ ├── group_653fde8f6b970.json │ ├── group_653fd7cec6a73.json │ └── group_62582b4fb2898.json ├── composer.json ├── footer.php ├── LICENSE.md ├── archive.php ├── header.php ├── phpcs.xml ├── package.json ├── vite.config.ts ├── functions.php ├── .github └── workflows │ └── dev.yml ├── README.md ├── CHANGELOG.md └── composer.lock /src/vue/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/js/tests/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/public/img/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /src/css/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/css/layouts/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/public/svgs/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | { 2 | return cy.wrap(Cypress.vueWrapper); 3 | }); 4 | -------------------------------------------------------------------------------- /src/js/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue'; 3 | export default Vue; 4 | } 5 | -------------------------------------------------------------------------------- /src/css/base/README.md: -------------------------------------------------------------------------------- 1 | These are the base styles that make Tofino work. 2 | 3 | You probably don't want to edit them. 4 | -------------------------------------------------------------------------------- /templates/modules/general-content.php: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | -------------------------------------------------------------------------------- /cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | import './vue'; 2 | import './assertions'; 3 | // import './baseUrl'; 4 | import './checkElementExists'; 5 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | VITE_ASSET_URL=http://tofino.test 2 | VITE_THEME_PATH=/wp-content/themes/tofino 3 | DEPLOYMENT_PATH= 4 | SSH_LOGIN= 5 | DEPLOY=FALSE -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = lf 3 | insert_final_newline = true 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | 8 | [*.php] 9 | indent_size = 2 -------------------------------------------------------------------------------- /cypress/support/baseUrl.ts: -------------------------------------------------------------------------------- 1 | const baseUrl = Cypress.env('baseUrl'); 2 | 3 | Cypress.Commands.add('baseUrl', (value = '') => { 4 | cy.visit(baseUrl + '/' + value); 5 | }); 6 | -------------------------------------------------------------------------------- /stylelint.config.ts: -------------------------------------------------------------------------------- 1 | import { Config } from 'stylelint'; 2 | 3 | const stylelintConfig: Config = { 4 | extends: 'stylelint-config-recommended', 5 | }; 6 | 7 | export default stylelintConfig; 8 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /cypress/support/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | declare namespace Cypress { 3 | interface Chainable { 4 | checkElementExists(elm: string, callback: () => void): Chainable; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue'; 5 | const component: DefineComponent<{}, {}, any>; 6 | export default component; 7 | } 8 | -------------------------------------------------------------------------------- /prettier.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 100, 3 | singleQuote: true, 4 | semi: true, 5 | trailingComma: 'es5', 6 | tailwindConfig: './tailwind.config.ts', 7 | plugins: ['prettier-plugin-tailwindcss'], 8 | }; 9 | -------------------------------------------------------------------------------- /src/css/base/admin.css: -------------------------------------------------------------------------------- 1 | .maintenance-mode-alert { 2 | @apply fixed inset-0 z-[99999] bg-red-600 p-5 text-center text-white; 3 | 4 | h1 { 5 | @apply text-white; 6 | } 7 | 8 | p { 9 | @apply text-sm; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/css/base/social-icons.css: -------------------------------------------------------------------------------- 1 | .social-icons { 2 | @apply list-none pl-0; 3 | 4 | li { 5 | @apply inline-block; 6 | } 7 | 8 | svg { 9 | @apply h-6 w-6; 10 | } 11 | 12 | a { 13 | @apply block; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["es5", "dom"], 5 | "types": ["cypress", "node"], 6 | "esModuleInterop": true 7 | }, 8 | "include": ["**/*.ts", "./support/index.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /templates/content-single.php: -------------------------------------------------------------------------------- 1 |
2 |
4 |

5 | 7 |
8 |
9 | -------------------------------------------------------------------------------- /src/css/main.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss/base'; 2 | 3 | @import 'tailwindcss/components'; 4 | 5 | @import 'base/alerts'; 6 | @import 'base/menu'; 7 | @import 'base/social-icons'; 8 | @import 'base/structure'; 9 | 10 | @import 'tailwindcss/utilities'; 11 | -------------------------------------------------------------------------------- /src/sprite/email.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/sprite/facebook.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/css/base/alerts.css: -------------------------------------------------------------------------------- 1 | .alert { 2 | @apply hidden bg-blue-400 p-2; 3 | 4 | &.bottom { 5 | @apply fixed bottom-0 left-0 right-0 z-10; 6 | } 7 | 8 | &.top { 9 | @apply relative; 10 | } 11 | 12 | svg { 13 | @apply fill-current; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/sprite/icon-hamburger.svg: -------------------------------------------------------------------------------- 1 | 2 | Open Menu Icon 3 | 4 | -------------------------------------------------------------------------------- /src/sprite/x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/sprite/youtube.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/css/base/menu.css: -------------------------------------------------------------------------------- 1 | /* Menu Styles */ 2 | .inactive { 3 | @apply hidden; 4 | } 5 | 6 | .sticky-top { 7 | @apply sticky top-0 z-20; 8 | } 9 | 10 | #menu-header-menu { 11 | @apply mt-10 lg:mt-0 lg:inline-flex; 12 | 13 | .menu-item { 14 | @apply text-center lg:mr-4 lg:text-left; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/sprite/linkedin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/js/app.ts: -------------------------------------------------------------------------------- 1 | // Import local deps 2 | import scripts from './main'; 3 | 4 | // Import CSS 5 | import '@/css/main.css'; 6 | 7 | // DOM Ready 8 | window.addEventListener('DOMContentLoaded', () => { 9 | scripts.init(); 10 | }); 11 | 12 | // Fully loaded 13 | window.addEventListener('load', () => { 14 | scripts.loaded(); 15 | }); 16 | -------------------------------------------------------------------------------- /templates/modules/form.php: -------------------------------------------------------------------------------- 1 | $form_id]); 8 | } 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | Updates and pull requests should be done on the dev branch. 4 | 5 | New features should be done in a separate branch and sent as a pull request. 6 | 7 | Nothing should be merged into the master branch until it has been tested on dev and (preferably) approved by all devs. 8 | 9 | No breaking changes except at major versions. 10 | -------------------------------------------------------------------------------- /cypress/support/checkElementExists.ts: -------------------------------------------------------------------------------- 1 | Cypress.Commands.add('checkElementExists', (elm, callback) => { 2 | cy.get('body').then(($body) => { 3 | if ($body.find(elm).length > 0) { 4 | callback(); 5 | } else { 6 | cy.log('Element not found, stopping further tests in this spec.', elm); 7 | 8 | Cypress.runner.stop(); 9 | } 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/sprite/vimeo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /home.php: -------------------------------------------------------------------------------- 1 | {} 13 | 14 | // Define Script interface 15 | export interface Script { 16 | selector: string; 17 | src: string; 18 | type: 'vue' | 'ts'; 19 | } 20 | -------------------------------------------------------------------------------- /src/sprite/icon-close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/sprite/pinterest.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/css/base/structure.css: -------------------------------------------------------------------------------- 1 | html { 2 | @apply h-full; 3 | } 4 | 5 | main { 6 | @apply flex-auto; 7 | } 8 | 9 | footer { 10 | @apply bg-gray-100; 11 | } 12 | 13 | body { 14 | @apply flex min-h-full flex-col font-roboto antialiased; 15 | 16 | &.no-fout { 17 | @apply invisible; 18 | 19 | .wf-inactive &, 20 | .wf-active & { 21 | @apply visible; 22 | } 23 | } 24 | } 25 | 26 | .highlight-violation { 27 | @apply outline-1 outline-red-500; 28 | } 29 | -------------------------------------------------------------------------------- /404.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 | 6 | 404 7 | 8 | 9 |

10 | 11 |

12 | 13 | 14 | Back to home 15 | 16 |
17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | /* 2 | Theme Name: Tofino 3 | Theme URI: https://github.com/creativedotdesign/tofino 4 | Description: Tofino is a WordPress starter theme. Contribute on GitHub 5 | Version: 4.2.0 6 | Author: Creative Dot 7 | Author URI: https://creativedotdesign.com 8 | Text Domain: tofino 9 | 10 | License: MIT License 11 | License URI: http://opensource.org/licenses/MIT 12 | */ 13 | -------------------------------------------------------------------------------- /src/js/apolloClient.ts: -------------------------------------------------------------------------------- 1 | import { ApolloClient, createHttpLink, InMemoryCache } from '@apollo/client/core'; 2 | 3 | // HTTP connection to the API 4 | const httpLink = createHttpLink({ 5 | // You should use an absolute URL here 6 | uri: '/graphql', 7 | }); 8 | 9 | // Cache implementation 10 | const cache = new InMemoryCache(); 11 | 12 | // Create the apollo client 13 | export const apolloClient = new ApolloClient({ 14 | link: httpLink, 15 | cache, 16 | }); 17 | 18 | export default apolloClient; 19 | -------------------------------------------------------------------------------- /templates/email/email-body.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 |
6 | %message%

7 | %form_content%
8 | 9 | %ip_address%
10 | %referrer% 11 |
14 | 15 | -------------------------------------------------------------------------------- /.git-ftp-ignore: -------------------------------------------------------------------------------- 1 | /src/ 2 | node_modules 3 | .vscode 4 | .editorconfig 5 | .env 6 | .eslintrc.cjs 7 | .git 8 | .github 9 | .gitignore 10 | .gitkeep 11 | .git-ftp-ignore 12 | .git-ftp-include 13 | .gitattributes 14 | .gitignore 15 | .prettierignore 16 | .npmrc 17 | composer.json 18 | composer.lock 19 | package.json 20 | package-lock.json 21 | postcss.config.cts 22 | tsconfig.json 23 | phpcs.xml 24 | **/*.md 25 | /cypress/ 26 | cypress.config.ts 27 | prettier.config.cjs 28 | stylelint.config.ts 29 | tailwind.config.ts 30 | vite.config.ts 31 | vite-env.d.ts 32 | -------------------------------------------------------------------------------- /src/sprite/instagram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'eslint:recommended', 4 | 'plugin:@typescript-eslint/eslint-recommended', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'plugin:vue/vue3-recommended', 7 | 'plugin:prettier/recommended', 8 | 'plugin:cypress/recommended', 9 | ], 10 | globals: { 11 | browser: true, 12 | tofinoJS: true, 13 | }, 14 | parserOptions: { 15 | parser: '@typescript-eslint/parser', 16 | }, 17 | plugins: ['@typescript-eslint'], 18 | env: { 19 | 'vue/setup-compiler-macros': true, 20 | node: true, 21 | es2021: true, 22 | es6: true, 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore all files starting with . 2 | .* 3 | 4 | # track this file .gitignore (i.e. do NOT ignore it) 5 | !.gitignore 6 | !.env 7 | !.gitkeep 8 | !.gitattributes 9 | !.stylelintrc.js 10 | !.env 11 | !.prettierrc.js 12 | !.git-ftp-ignore 13 | !.github 14 | !.npmrc 15 | !.prettierignore 16 | !.eslintrc.cjs 17 | 18 | # ignore node/grunt dependency directories 19 | node_modules/ 20 | 21 | # ignore composer dependency directories 22 | /vendor/ 23 | 24 | # ignore dist directories 25 | dist/ 26 | 27 | # ignore npm debug log file 28 | npm-debug.log 29 | yarn-debug.log 30 | 31 | # cypress 32 | cypress/videos/ 33 | cypress/screenshots/ 34 | cypress/axe-reports/ 35 | cypress/downloads/ 36 | 37 | # vitest 38 | *.ts.snap 39 | -------------------------------------------------------------------------------- /cypress/support/assertions.ts: -------------------------------------------------------------------------------- 1 | const isInViewport = (_chai) => { 2 | function assertIsInViewport() { 3 | const subject = this._obj; 4 | 5 | const windowHeight = Cypress.$(cy.state('window')).height(); 6 | const bottomOfCurrentViewport = windowHeight; 7 | const rect = subject[0].getBoundingClientRect(); 8 | 9 | this.assert( 10 | (rect.top > 0 && rect.top < bottomOfCurrentViewport) || 11 | (rect.bottom > 0 && rect.bottom < bottomOfCurrentViewport), 12 | 'expected #{this} to be in viewport', 13 | 'expected #{this} to not be in viewport', 14 | subject 15 | ); 16 | } 17 | 18 | _chai.Assertion.addMethod('inViewport', assertIsInViewport); 19 | }; 20 | 21 | chai.use(isInViewport); 22 | -------------------------------------------------------------------------------- /templates/content-page.php: -------------------------------------------------------------------------------- 1 |
7 | 8 |
9 | 10 |
11 | 17 |
18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "es2020", 5 | "importHelpers": true, 6 | "isolatedModules": true, 7 | "moduleResolution": "node", 8 | "allowSyntheticDefaultImports": true, 9 | "jsx": "preserve", 10 | "baseUrl": ".", 11 | "paths": { 12 | "@/*": ["./src/*"] 13 | }, 14 | "types": [ 15 | "vite/client", // if using vite-plugin-typescript 16 | "jest", // if using jest 17 | "node" // if using node 18 | ], 19 | "strict": true, // Enable strict type-checking options 20 | "skipLibCheck": true, // Skip type checking of declaration files 21 | "noImplicitAny": false // Bypass raising errors on `any` type 22 | }, 23 | "include": ["src/**/**.ts", "src/**/*.vue"] 24 | } 25 | -------------------------------------------------------------------------------- /src/css/base/wp-classes.css: -------------------------------------------------------------------------------- 1 | /* WordPress Generated Classes 2 | http://codex.wordpress.org/CSS#WordPress_Generated_Classes */ 3 | 4 | /* Media alignment */ 5 | .alignnone { 6 | @apply mx-0 max-w-full; 7 | } 8 | 9 | .aligncenter { 10 | @apply mx-auto block h-auto; 11 | } 12 | 13 | .alignleft, 14 | .alignright { 15 | @apply mb-3; 16 | } 17 | 18 | .alignleft { 19 | @apply mr-3 sm:float-left; 20 | } 21 | 22 | .alignright { 23 | @apply ml-3 sm:float-right; 24 | } 25 | 26 | /* Enable responsive images in WP content */ 27 | img { 28 | &[class*='wp-image-'] { 29 | @apply h-auto w-full md:w-auto; 30 | } 31 | } 32 | 33 | /* Text meant only for screen readers */ 34 | .screen-reader-text { 35 | @apply sr-only; 36 | } 37 | 38 | .wp-caption .wp-caption-text { 39 | @apply !text-base !font-normal; 40 | } 41 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss'; 2 | 3 | export default { 4 | content: [ 5 | './header.php', 6 | './footer.php', 7 | './404.php', 8 | './functions.php', 9 | './inc/**/*.php', 10 | './templates/**/*.php', 11 | './src/public/svgs/**/*.svg', 12 | './src/**/*.vue', 13 | ], 14 | theme: { 15 | container: { 16 | center: true, 17 | }, 18 | screens: { 19 | sm: '640px', 20 | md: '768px', 21 | lg: '1024px', 22 | xl: '1366px', 23 | }, 24 | fontFamily: { 25 | roboto: ['Roboto'], 26 | }, 27 | }, 28 | plugins: [ 29 | // eslint-disable-next-line @typescript-eslint/no-var-requires 30 | require('@tailwindcss/forms')({ 31 | strategy: 'class', 32 | }), 33 | ], 34 | } satisfies Config; 35 | -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress'; 2 | import * as dotenv from 'dotenv'; 3 | import setupPlugins from './cypress/plugins/index'; 4 | 5 | dotenv.config(); 6 | 7 | export default defineConfig({ 8 | video: false, 9 | enableScreenshots: true, 10 | axeIgnoreContrast: true, 11 | e2e: { 12 | // We've imported your old cypress plugins here. 13 | // You may want to clean this up later by importing these. 14 | setupNodeEvents(on, config) { 15 | setupPlugins(on, config); 16 | }, 17 | baseUrl: process.env.VITE_ASSET_URL, 18 | specPattern: 'cypress/e2e/**/*{spec,cy}.{js,ts}', 19 | }, 20 | // env: { 21 | // baseUrl: process.env.VITE_ASSET_URL, 22 | // }, 23 | component: { 24 | setupNodeEvents(on, config) {}, 25 | specPattern: 'src/**/*{spec,cy}.{js,ts}', 26 | }, 27 | }); 28 | -------------------------------------------------------------------------------- /cypress/e2e/alert.spec.ts: -------------------------------------------------------------------------------- 1 | describe('Alert', () => { 2 | it('Check if Alert is visible', () => { 3 | cy.visit('/'); 4 | 5 | cy.checkElementExists('[data-alert-id]', () => { 6 | cy.get('[data-alert-id]').should('be.visible'); 7 | }); 8 | }); 9 | 10 | it('Check Alert is not visable on scroll', () => { 11 | cy.checkElementExists('[data-alert-id]', () => { 12 | cy.scrollTo(0, '50%'); 13 | 14 | cy.get('[data-alert-id]').should('not.inViewport'); 15 | }); 16 | }); 17 | 18 | it('Check Alert closes', () => { 19 | cy.checkElementExists('[data-alert-id]', () => { 20 | cy.get('[data-alert-id] .js-close').click(); 21 | 22 | cy.get('[data-alert-id]').should('not.exist'); 23 | 24 | cy.getCookie('tofino-alert-closed').should('have.property', 'value', 'yes'); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /templates/partials/alert.php: -------------------------------------------------------------------------------- 1 | 5 | 6 | { 5 | if (document.querySelector('.maintenance-mode-alert')) { 6 | const button: HTMLElement | null = document.querySelector('.maintenance-mode-alert button'); 7 | 8 | if (button) { 9 | button.addEventListener('click', () => { 10 | const date = new Date(); 11 | 12 | date.setTime(date.getTime() + 1 * 24 * 60 * 60 * 1000); 13 | 14 | const expires = 'expires=' + date.toUTCString(); 15 | 16 | document.cookie = 'tofino_maintenance_alert_dismissed=true;' + expires + '; path=/'; 17 | 18 | const alert: HTMLElement | null = document.querySelector('.maintenance-mode-alert'); 19 | 20 | if (alert) { 21 | // Hide the alert 22 | alert.style.display = 'none'; 23 | } 24 | }); 25 | } 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | # Assets 2 | 3 | ## /img 4 | 5 | Images will be compressed, optimised and converted to progressive loading using Imagemin. 6 | 7 | ## /js 8 | 9 | Javascript files belong here. 10 | 11 | Scripts will be minified when `npm run build` is run. 12 | 13 | ## /css 14 | 15 | CSS files go here. Similarly to scripts, these will not automatically be added to `/dist`. To add your styles into `dist/css/main.css`, add your file name into `main.css`. 16 | 17 | ## /svgs 18 | 19 | ### Single SVGs 20 | 21 | SVGs added to `public/svgs` will be minified and copied to dist/svgs. 22 | 23 | ### Sprites 24 | 25 | SVGs added to `sprite` will be processed by the main build task and output as a single SVG just before the closing `` tag. 26 | 27 | ### Font Loader 28 | 29 | All fonts should be loaded using the [Web Font Loader](https://github.com/typekit/webfontloader). 30 | 31 | A theme option has been added to disable FOUT (Flash of un-styled text). 32 | -------------------------------------------------------------------------------- /postcss.config.cts: -------------------------------------------------------------------------------- 1 | interface PostCSSConfig { 2 | plugins: { 3 | [key: string]: object; 4 | }; 5 | } 6 | 7 | interface Asset { 8 | url: string; 9 | } 10 | 11 | const getPostCSSConfig = (env: string): PostCSSConfig => { 12 | /* eslint-disable */ 13 | require('dotenv').config(); 14 | 15 | const postcssConfig: PostCSSConfig = { 16 | plugins: { 17 | 'postcss-import': {}, 18 | 'postcss-url': { 19 | url: (asset: Asset) => { 20 | if (env === 'production') { 21 | return asset.url.replace('$fonts', `${process.env.VITE_THEME_PATH}/dist/fonts`); 22 | } else { 23 | return asset.url.replace('$fonts', `${process.env.VITE_THEME_PATH}/src/public/fonts`); 24 | } 25 | }, 26 | }, 27 | 'tailwindcss/nesting': {}, 28 | tailwindcss: {}, 29 | autoprefixer: {}, 30 | }, 31 | }; 32 | 33 | return postcssConfig; 34 | }; 35 | 36 | module.exports = getPostCSSConfig; 37 | -------------------------------------------------------------------------------- /src/sprite/tiktok.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/sprite/soundcloud.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /page.php: -------------------------------------------------------------------------------- 1 | key = $key; 25 | $this->ttl = $ttl; 26 | } 27 | 28 | public function output() 29 | { 30 | $output = get_transient($this->key); 31 | if (!empty($output)) { // It was in the cache 32 | echo $output; 33 | return true; 34 | } else { 35 | ob_start(); 36 | return false; 37 | } 38 | } 39 | 40 | public function store() 41 | { 42 | $output = ob_get_flush(); // Flushes the buffers 43 | set_transient($this->key, $output, $this->ttl); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "creativedotdesign/tofino", 3 | "type": "wordpress-theme", 4 | "license": "MIT", 5 | "description": "A WordPress starter theme for jumpstarting custom theme development.", 6 | "homepage": "https://github.com/creativedotdesign/tofino", 7 | "authors": [ 8 | { 9 | "name": "Daniel Hewes", 10 | "email": "daniel@creativedotdesign.com", 11 | "homepage": "https://github.com/danimalweb" 12 | }, 13 | { 14 | "name": "Jake Gully", 15 | "email": "chimpytk@gmail.com", 16 | "homepage": "https://github.com/mrchimp" 17 | } 18 | ], 19 | "keywords": [ 20 | "WordPress", 21 | "theme", 22 | "tofino" 23 | ], 24 | "require": { 25 | "php": ">=8.2.0", 26 | "composer/installers": "~v2.2.0", 27 | "respect/validation": "^2.2.4" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "Tofino\\": "inc/lib/" 32 | } 33 | }, 34 | "config": { 35 | "allow-plugins": { 36 | "composer/installers": true 37 | } 38 | }, 39 | "require-dev": { 40 | "php-stubs/acf-pro-stubs": "^6.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /footer.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
6 | 'nav_menu', 9 | 'theme_location' => 'footer_navigation', 10 | 'depth' => 1, 11 | 'container' => '', 12 | 'container_class' => '', 13 | 'container_id' => '', 14 | 'menu_class' => 'footer-nav', 15 | 'items_wrap' => '
    %3$s
', 16 | ]); ?> 17 | 24 | 25 |
26 |
27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/js/scriptLoader.ts: -------------------------------------------------------------------------------- 1 | import { createApp, defineAsyncComponent } from 'vue'; 2 | import { createPinia } from 'pinia'; 3 | import { Scripts } from './types'; 4 | 5 | export const loadScripts = async (scripts: Scripts) => { 6 | // Loop through the scripts and import the src 7 | scripts.forEach(({ selector, src, type }) => { 8 | const el: HTMLElement | null = document.querySelector(selector); 9 | 10 | if (el) { 11 | if (type === 'vue') { 12 | createApp({ 13 | components: { 14 | [src]: defineAsyncComponent(() => import(`../vue/${src}.vue`)), 15 | }, 16 | }) 17 | .use(createPinia()) 18 | .mount(el); 19 | 20 | // console.log(`Tofino Theme: Loaded ${selector} for Vue component ${src}.vue.`); 21 | } else if (type === 'ts') { 22 | // Dynamically Import Typescript File 23 | import(`./modules/${src}.ts`).then(({ default: script }) => { 24 | script(); 25 | }); 26 | 27 | // console.log(`Tofino Theme: Loaded ${selector} for script ${src}.ts.`); 28 | } 29 | } 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /src/js/modules/menu.ts: -------------------------------------------------------------------------------- 1 | // Scroll lock 2 | import { disableBodyScroll, enableBodyScroll, clearAllBodyScrollLocks } from 'body-scroll-lock'; 3 | 4 | export default () => { 5 | // Menu Toggle 6 | const buttons: NodeListOf | null = document.querySelectorAll('.js-menu-toggle'); 7 | const menu: HTMLElement | null = document.getElementById('main-menu'); 8 | 9 | if (menu) { 10 | buttons.forEach((el) => { 11 | el.addEventListener('click', () => { 12 | // Toggle the hide class 13 | menu.classList.toggle('inactive'); 14 | 15 | if (menu.classList.contains('inactive')) { 16 | enableBodyScroll(menu); 17 | 18 | document.body.classList.remove('menu-open'); 19 | } else { 20 | disableBodyScroll(menu); 21 | 22 | document.body.classList.add('menu-open'); 23 | } 24 | }); 25 | }); 26 | 27 | // Close menu on ESC key 28 | document.onkeydown = (e) => { 29 | if (e.key === 'Escape' && !menu.classList.contains('inactive')) { 30 | menu.classList.add('inactive'); 31 | 32 | document.body.classList.remove('menu-open'); 33 | 34 | clearAllBodyScrollLocks(); 35 | } 36 | }; 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) Ben Word and Scott Walkinshaw from Roots/Sage - https://roots.io/ 2 | 3 | Modifications and new functionality Copyright (c) Daniel Hewes from Creative Dot - https://creativedot.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /inc/acf-json/group_62586c11ea98d.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": "group_62586c11ea98d", 3 | "title": "General Content", 4 | "fields": [ 5 | { 6 | "key": "field_62586c16b4c24", 7 | "label": "General Content", 8 | "name": "general_content", 9 | "type": "wysiwyg", 10 | "instructions": "", 11 | "required": 0, 12 | "conditional_logic": 0, 13 | "wrapper": { 14 | "width": "", 15 | "class": "", 16 | "id": "" 17 | }, 18 | "default_value": "", 19 | "tabs": "all", 20 | "toolbar": "full", 21 | "media_upload": 0, 22 | "delay": 0 23 | } 24 | ], 25 | "location": [ 26 | [ 27 | { 28 | "param": "post_type", 29 | "operator": "==", 30 | "value": "post" 31 | } 32 | ] 33 | ], 34 | "menu_order": 0, 35 | "position": "normal", 36 | "style": "default", 37 | "label_placement": "top", 38 | "instruction_placement": "label", 39 | "hide_on_screen": "", 40 | "active": false, 41 | "description": "", 42 | "show_in_rest": 0, 43 | "modified": 1649962023 44 | } -------------------------------------------------------------------------------- /src/js/main.ts: -------------------------------------------------------------------------------- 1 | // Import Font loader 2 | import * as WebFont from 'webfontloader'; 3 | import { WebFontInterface } from '@/js/types'; 4 | import 'virtual:svg-icons-register'; 5 | import { loadScripts } from '@/js/scriptLoader'; 6 | import { Scripts } from '@/js/types'; 7 | 8 | export default { 9 | init() { 10 | // JavaScript to be fired on all pages 11 | 12 | // Config for WebFontLoader 13 | const fontConfig: WebFontInterface = { 14 | classes: false, 15 | events: false, 16 | google: { 17 | families: ['Roboto:300,400,500,700'], 18 | display: 'swap', 19 | version: 1.0, 20 | }, 21 | }; 22 | 23 | // Load Fonts 24 | WebFont.load(fontConfig); 25 | 26 | // Define the selectors and src for dynamic imports 27 | const scripts: Scripts = [ 28 | { 29 | selector: '.alert', // Alert 30 | src: 'alerts', 31 | type: 'ts', 32 | }, 33 | { 34 | selector: '#main-menu', // Main menu 35 | src: 'menu', 36 | type: 'ts', 37 | }, 38 | ]; 39 | 40 | // Load the scripts 41 | loadScripts(scripts); 42 | 43 | this.finalize(); 44 | }, 45 | finalize() { 46 | // JavaScript to be fired after init 47 | }, 48 | loaded() { 49 | // Javascript to be fired once fully loaded 50 | }, 51 | }; 52 | -------------------------------------------------------------------------------- /archive.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | slug) ? get_queried_object()->slug : null; 5 | $taxonomy = isset(get_queried_object()->taxonomy) ? get_queried_object()->taxonomy : null; 6 | $post_type = isset(get_queried_object()->name) ? get_queried_object()->name : null; 7 | 8 | if (locate_template('templates/archive-' . $slug . '.php') != '') { // archive-{category-slug} 9 | get_template_part('templates/archive', $slug); // e.g. templates/archive-category-slug.php 10 | } elseif (locate_template('templates/archive-' . $taxonomy . '-' . $slug . '.php') != '') { // archive-{taxonomy}-{term} 11 | get_template_part('templates/archive', $taxonomy . '-' . $slug); 12 | } elseif (locate_template('templates/archive-' . $post_type . '-' . $taxonomy . '-' . $slug . '.php') != '') { // archive-{posttype}-{taxonomy}-{term} 13 | get_template_part('templates/archive', $post_type . '-' . $taxonomy . '-' . $slug); 14 | } elseif (locate_template('templates/archive-' . $taxonomy . '.php') != '') { // archive-{taxonomy} 15 | get_template_part('templates/archive', $taxonomy); 16 | } elseif ($post_type && (locate_template('templates/archive-' . $post_type . '.php') != '')) { // archive-{posttype} 17 | get_template_part('templates/archive', $post_type); 18 | } else { 19 | echo ('

' . __('Error: Unable to locate an archive template. Did you create the file in /templates?.', 'tofino') . '

'); 20 | } ?> 21 | 22 | -------------------------------------------------------------------------------- /inc/acf-json/group_64c9841305284.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": "group_64c9841305284", 3 | "title": "Media Attachments", 4 | "fields": [ 5 | { 6 | "key": "field_64c9841323a4a", 7 | "label": "Media Credit", 8 | "name": "media_credit", 9 | "aria-label": "", 10 | "type": "text", 11 | "instructions": "", 12 | "required": 0, 13 | "conditional_logic": 0, 14 | "wrapper": { 15 | "width": "", 16 | "class": "", 17 | "id": "" 18 | }, 19 | "show_in_graphql": 0, 20 | "default_value": "", 21 | "maxlength": "", 22 | "placeholder": "", 23 | "prepend": "", 24 | "append": "" 25 | } 26 | ], 27 | "location": [ 28 | [ 29 | { 30 | "param": "attachment", 31 | "operator": "==", 32 | "value": "image" 33 | } 34 | ] 35 | ], 36 | "menu_order": 0, 37 | "position": "normal", 38 | "style": "default", 39 | "label_placement": "top", 40 | "instruction_placement": "label", 41 | "hide_on_screen": "", 42 | "active": true, 43 | "description": "", 44 | "show_in_rest": 0, 45 | "show_in_graphql": 0, 46 | "graphql_field_name": "test", 47 | "map_graphql_types_from_location_rules": 0, 48 | "graphql_types": "", 49 | "modified": 1704223836 50 | } 51 | -------------------------------------------------------------------------------- /inc/acf-json/group_653fdd4a51868.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": "group_653fdd4a51868", 3 | "title": "Footer", 4 | "fields": [ 5 | { 6 | "key": "field_653fdd4a8347c", 7 | "label": "Text", 8 | "name": "footer_text", 9 | "aria-label": "", 10 | "type": "textarea", 11 | "instructions": "", 12 | "required": 0, 13 | "conditional_logic": 0, 14 | "wrapper": { 15 | "width": "", 16 | "class": "", 17 | "id": "" 18 | }, 19 | "default_value": "[copyright]", 20 | "acfe_textarea_code": 0, 21 | "maxlength": "", 22 | "rows": 4, 23 | "placeholder": "", 24 | "new_lines": "" 25 | } 26 | ], 27 | "location": [ 28 | [ 29 | { 30 | "param": "post_type", 31 | "operator": "==", 32 | "value": "post" 33 | } 34 | ] 35 | ], 36 | "menu_order": 0, 37 | "position": "normal", 38 | "style": "default", 39 | "label_placement": "left", 40 | "instruction_placement": "label", 41 | "hide_on_screen": "", 42 | "active": false, 43 | "description": "", 44 | "show_in_rest": 0, 45 | "acfe_display_title": "", 46 | "acfe_autosync": [ 47 | "json" 48 | ], 49 | "acfe_form": 0, 50 | "acfe_meta": "", 51 | "acfe_note": "", 52 | "modified": 1698684435 53 | } 54 | -------------------------------------------------------------------------------- /src/js/modules/alerts.ts: -------------------------------------------------------------------------------- 1 | export default () => { 2 | const getCookie = (cookieName: string) => { 3 | const value = `; ${document.cookie}`; 4 | const parts: string[] = value.split(`; ${cookieName}=`); 5 | 6 | if (parts.length === 2) { 7 | return parts.pop()?.split(';').shift(); 8 | } 9 | }; 10 | 11 | const alerts: NodeListOf = document.querySelectorAll('.alert'); 12 | 13 | if (alerts) { 14 | const expires = tofinoJS.cookieExpires; 15 | 16 | alerts.forEach((element) => { 17 | const alertId = element.dataset.alertId; 18 | 19 | if (!getCookie('tofino-alert-' + alertId + '-closed')) { 20 | // Show the alert using JS based on the cookie (fixes html caching issue) 21 | element.style.display = 'block'; 22 | } 23 | 24 | const closeIcon: HTMLElement | null = element.querySelector('.js-close'); 25 | 26 | if (closeIcon) { 27 | closeIcon.addEventListener('click', () => { 28 | const expiresValue: string = expires[alertId]; 29 | 30 | if (expiresValue) { 31 | const date: Date = new Date(); 32 | date.setTime( 33 | date.getTime() + parseInt(tofinoJS.cookieExpires, 10) * 24 * 60 * 60 * 1000 34 | ); 35 | const expires: string = 'expires=' + date.toUTCString(); 36 | document.cookie = 'tofino-alert-' + alertId + '-closed=yes;' + expires + '; path=/'; 37 | } else { 38 | document.cookie = 'tofino-alert-' + alertId + '-closed=yes;max=age=0; path=/'; 39 | } 40 | 41 | element.remove(); 42 | }); 43 | } 44 | }); 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /cypress/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv'; 2 | import axios from 'axios'; 3 | import { createHtmlReport } from 'axe-html-reporter'; 4 | 5 | dotenv.config(); 6 | 7 | export default (on: Cypress.PluginEvents, config: Cypress.PluginConfigOptions) => { 8 | on('task', { 9 | processAccessibilityViolations(violations: any[]) { 10 | createHtmlReport({ 11 | results: { violations: violations }, 12 | options: { 13 | outputDir: './cypress/axe-reports', 14 | reportFileName: 'a11yReport.html', 15 | }, 16 | }); 17 | 18 | return null; 19 | }, 20 | }); 21 | 22 | on('task', { 23 | async sitemapLocations() { 24 | try { 25 | const response = await axios.get(`${process.env.VITE_ASSET_URL}/page-sitemap.xml`, { 26 | headers: { 27 | 'Content-Type': 'application/xml', 28 | }, 29 | }); 30 | 31 | // Check respose status 32 | if (response.status !== 200) { 33 | throw new Error('Error fetching sitemap'); 34 | } 35 | 36 | const xml = response.data; 37 | const locs = [...xml.matchAll(`(.|\n)*?`)].map(([loc]) => 38 | loc.replace('', '').replace('', '') 39 | ); 40 | 41 | return locs; 42 | } catch (error) { 43 | console.error('Error fetching sitemap:', error); 44 | throw error; 45 | } 46 | }, 47 | }); 48 | 49 | on('task', { 50 | log(message: string) { 51 | console.log(message); 52 | 53 | return null; 54 | }, 55 | table(message: any) { 56 | console.table(message); 57 | 58 | return null; 59 | }, 60 | }); 61 | 62 | return config; 63 | }; 64 | -------------------------------------------------------------------------------- /templates/README.md: -------------------------------------------------------------------------------- 1 | # templates 2 | 3 | Template files go here. The naming of your template files is important. By naming a file correctly you can make it be used without any further work. 4 | 5 | ## Possible formats 6 | 7 | - `content-single-{post-type}-{post-slug}.php` 8 | - `content-single-{post-type}.php` 9 | - `content-single.php` 10 | - `content-page-{slug}.php` 11 | - `content-page.php` 12 | - `archive-{category}.php` 13 | - `archive-{taxonomy}-{term}.php` 14 | - `archive-{posttype}-{taxonomy}-{term}.php` 15 | - `archive-{post-type}.php` 16 | - `archive.php` 17 | 18 | ## Default Templates 19 | 20 | Wordpress gives some default templates in the root theme folder. Apart from `header.php` and `footer.php` _you shouldn't have to edit these or create any new ones_ - just create your own templates in this `templates` subdirectory. 21 | 22 | Therefore _do not edit or create these files_: 23 | 24 | - archive.php 25 | - index.php 26 | - single.php 27 | - page.php 28 | 29 | ## Front page 30 | 31 | Front page is the first page you see when arriving on the website. 32 | 33 | To create a custom front page: 34 | 35 | 1. Create a new page 36 | 2. Select it in Settings > Reading 37 | 3. Create a custom template e.g. `templates/content-page-home.php` 38 | 39 | ## Home 40 | 41 | The home page is the default post archive page. 42 | 43 | To create a custom home page: 44 | 45 | 1. Create a new page 46 | 2. Select it in Settings > Reading 47 | 3. Create a custom template e.g. `templates/archive.php` 48 | 49 | ## Frontpage with Home page 50 | 51 | If you want to set both a static Frontpage and a Home page. For example you have a blog separate to the main website content and Frontpage. 52 | 53 | Create a page called home.php in the root including header.php and footer.php with a get_template_part pointing to a new template you'd create e.g. `templates/content-page-blog.php` 54 | -------------------------------------------------------------------------------- /header.php: -------------------------------------------------------------------------------- 1 | 2 | > 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | > 11 | 12 | 13 | 14 | 15 | 16 |
17 | 53 |
54 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Tofino Coding Standards 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | templates/* 32 | 33 | 34 | 35 | 36 | templates/* 37 | 38 | 39 | 40 | 41 | templates/* 42 | 404.php 43 | archive.php 44 | page.php 45 | single.php 46 | home.php 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tofino", 3 | "private": true, 4 | "version": "4.2.0", 5 | "description": "A WordPress starter theme for jumpstarting custom theme development.", 6 | "keywords": [ 7 | "WordPress", 8 | "theme", 9 | "tofino" 10 | ], 11 | "scripts": { 12 | "dev": "vite", 13 | "build": "vite build", 14 | "tests": "vitest", 15 | "tests-ui": "vitest --ui", 16 | "cy": "cypress open", 17 | "cy-run": "npx cypress run --browser chrome", 18 | "ct": "npx cypress open-ct" 19 | }, 20 | "author": "Daniel Hewes ", 21 | "repository": { 22 | "type": "git", 23 | "url": "git://github.com/creativedotdesign/tofino.git" 24 | }, 25 | "bugs": { 26 | "url": "https://github.com/creativedotdesign/tofino/issues" 27 | }, 28 | "homepage": "https://creativedotdesign.com", 29 | "licenses": [ 30 | { 31 | "type": "MIT", 32 | "url": "http://opensource.org/licenses/MIT" 33 | } 34 | ], 35 | "engines": { 36 | "node": ">=20.0.0", 37 | "npm": ">=8.0.0" 38 | }, 39 | "type": "module", 40 | "devDependencies": { 41 | "@tailwindcss/forms": "^0.5.7", 42 | "@tailwindcss/typography": "^0.5.10", 43 | "@types/body-scroll-lock": "^3.1.2", 44 | "@types/browser-sync": "^2.29.0", 45 | "@types/jest": "^29.5.11", 46 | "@types/node": "^20.10.6", 47 | "@types/webfontloader": "^1.6.38", 48 | "@typescript-eslint/eslint-plugin": "^6.17.0", 49 | "@vitest/coverage-istanbul": "^1.1.1", 50 | "@vitest/ui": "^1.1.1", 51 | "@vue/apollo-composable": "^4.0.0-beta.12", 52 | "@vue/test-utils": "^2.4.3", 53 | "autoprefixer": "^10.4.16", 54 | "axe-core": "^4.8.3", 55 | "axe-html-reporter": "^2.2.3", 56 | "cypress": "^13.6.2", 57 | "cypress-axe": "^1.5.0", 58 | "dotenv": "^16.3.1", 59 | "eslint": "^8.56.0", 60 | "eslint-config-prettier": "^9.1.0", 61 | "eslint-plugin-cypress": "^2.15.1", 62 | "eslint-plugin-prettier": "^5.1.2", 63 | "eslint-plugin-vue": "^9.19.2", 64 | "jsdom": "^23.0.1", 65 | "postcss": "^8.4.32", 66 | "postcss-import": "^16.0.0", 67 | "postcss-url": "^10.1.3", 68 | "prettier": "^3.1.1", 69 | "prettier-plugin-tailwindcss": "^0.5.10", 70 | "stylelint": "^16.1.0", 71 | "stylelint-config-recommended": "^14.0.0", 72 | "stylelint-prettier": "^5.0.0", 73 | "ts-node": "^10.9.2", 74 | "vite": "^5.0.10", 75 | "vite-plugin-browser-sync": "^2.0.1", 76 | "vite-plugin-chunk-split": "^0.5.0", 77 | "vite-plugin-eslint": "^1.8.1", 78 | "vite-plugin-svg-icons": "^2.0.1", 79 | "vitest": "^1.1.1" 80 | }, 81 | "dependencies": { 82 | "body-scroll-lock": "^3.1.5", 83 | "pinia": "^2.1.7", 84 | "tailwindcss": "^3.4.0", 85 | "vue": "^3.4.4", 86 | "webfontloader": "github:creativedotdesign/webfontloader#feature/google-fonts-v2" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig, loadEnv } from 'vite'; 3 | import eslintPlugin from 'vite-plugin-eslint'; 4 | import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'; 5 | import VitePluginBrowserSync from 'vite-plugin-browser-sync'; 6 | import { chunkSplitPlugin } from 'vite-plugin-chunk-split'; 7 | import path from 'path'; 8 | 9 | export default ({ mode }: { mode: string }) => { 10 | process.env = { ...process.env, ...loadEnv(mode, process.cwd()) }; 11 | 12 | return defineConfig({ 13 | publicDir: path.resolve(__dirname, './src/public'), 14 | root: path.resolve(__dirname, './src'), 15 | base: process.env.NODE_ENV === 'production' ? `${process.env.VITE_THEME_PATH}/dist/` : '/', 16 | build: { 17 | outDir: path.resolve(__dirname, 'dist'), 18 | emptyOutDir: true, 19 | manifest: true, 20 | // minify: false, 21 | sourcemap: process.env.NODE_ENV === 'production' ? false : 'inline', 22 | target: 'es2018', 23 | rollupOptions: { 24 | input: { 25 | app: '/js/app.ts', 26 | admin: '/js/admin.ts', 27 | }, 28 | external: ['jquery'], 29 | output: { 30 | globals: { 31 | jquery: 'jQuery', 32 | }, 33 | }, 34 | }, 35 | }, 36 | plugins: [ 37 | eslintPlugin(), 38 | chunkSplitPlugin({ 39 | strategy: 'unbundle', 40 | customChunk: (args) => { 41 | const { id } = args; 42 | if (id.includes('node_modules')) { 43 | return id.toString().split('node_modules/')[1].split('/')[0].toString(); 44 | } 45 | return null; 46 | }, 47 | }), 48 | VitePluginBrowserSync({ 49 | bs: { 50 | online: true, 51 | notify: false, 52 | proxy: { 53 | target: process.env.VITE_ASSET_URL, 54 | ws: true, 55 | proxyReq: [ 56 | (proxyReq) => { 57 | proxyReq.setHeader('Browser-Sync', true); 58 | }, 59 | ], 60 | }, 61 | }, 62 | }), 63 | createSvgIconsPlugin({ 64 | iconDirs: [path.resolve(process.cwd(), 'src/sprite')], 65 | symbolId: 'icon-[name]', 66 | customDomId: 'tofino-sprite', 67 | }), 68 | ], 69 | define: { __VUE_PROD_DEVTOOLS__: false }, 70 | test: { 71 | include: [`${__dirname}/src/js/tests/*.ts`], 72 | globals: true, 73 | watch: false, 74 | environment: 'jsdom', 75 | coverage: { 76 | provider: 'istanbul', 77 | }, 78 | }, 79 | server: { 80 | host: true, 81 | cors: true, 82 | strictPort: true, 83 | port: 3000, 84 | hmr: { 85 | port: 3000, 86 | protocol: 'ws', 87 | }, 88 | }, 89 | resolve: { 90 | alias: { 91 | '@': path.resolve(__dirname, './src'), 92 | vue: 'vue/dist/vue.esm-bundler.js', 93 | }, 94 | }, 95 | }); 96 | }; 97 | -------------------------------------------------------------------------------- /cypress/e2e/navigation.spec.ts: -------------------------------------------------------------------------------- 1 | // Import tailwindcss config file 2 | import resolveConfig from 'tailwindcss/resolveConfig'; 3 | import tailwindConfig from '../../tailwind.config'; 4 | 5 | const fullConfig = resolveConfig(tailwindConfig); 6 | const screens = fullConfig.theme.screens; 7 | 8 | describe('Navigation Tests', () => { 9 | it('Mobile Menu, check functionality', () => { 10 | cy.visit('/'); 11 | 12 | // Mobile Viewport 13 | const mobileHeight = 812; 14 | 15 | cy.viewport(parseInt(screens.sm, 10), mobileHeight); 16 | 17 | // Make sure there's no scroll lock on body 18 | cy.get('body').should('not.have.css', 'overflow', 'hidden'); 19 | 20 | // Open menu 21 | cy.get('button[data-cy="open-mobile-menu"]').click(); 22 | 23 | // Make sure there is scroll lock on body 24 | cy.get('body').should('have.css', 'overflow', 'hidden'); 25 | 26 | // Check if open menu is viewport height 27 | cy.get('#main-menu').invoke('height').should('equal', mobileHeight); 28 | 29 | // Tablet Viewport 30 | const tabletHeight = 1024; 31 | 32 | cy.viewport(parseInt(screens.md, 10), tabletHeight); 33 | 34 | // Check if open menu is still viewport height 35 | cy.get('[id="main-menu"]').invoke('height').should('equal', tabletHeight); 36 | 37 | // Click close menu 38 | cy.get('nav').find('button[data-cy="close-mobile-menu"]').click(); 39 | 40 | // Check if open state is hidden 41 | cy.get('#main-menu').should('not.be.visible'); 42 | 43 | // Check that scroll lock is removed 44 | cy.get('body').should('not.have.css', 'overflow', 'hidden'); 45 | 46 | // Check ESC closes menu 47 | cy.get('button[data-cy="open-mobile-menu"]').click(); 48 | cy.get('body').type('{esc}', { force: true }); 49 | 50 | // Check if open state is hidden 51 | cy.get('#main-menu').should('not.be.visible'); 52 | 53 | // Check that scroll lock is removed 54 | cy.get('body').should('not.have.css', 'overflow', 'hidden'); 55 | }); 56 | 57 | it('Navbar is sticky or not sticky', () => { 58 | cy.visit('/'); 59 | 60 | // Desktop Viewport 61 | cy.viewport(parseInt(screens.lg, 10), 660); 62 | 63 | cy.get('body').then(($body) => { 64 | if ($body.hasClass('menu-fixed')) { 65 | // Check header for .nav-stuck class 66 | cy.get('header').should('have.class', 'sticky-top'); 67 | 68 | // Check if visible 69 | cy.get('header').should('be.visible'); 70 | 71 | // Scroll Down 30%, check if visible 72 | cy.scrollTo(0, '30%'); 73 | cy.get('header').should('be.visible'); 74 | 75 | // Scroll to bottom, check if visible 76 | cy.scrollTo('bottom'); 77 | cy.get('header').should('be.visible'); 78 | 79 | // Scroll to top, check if visible 80 | cy.scrollTo('top'); 81 | cy.get('header').should('be.visible'); 82 | } else { 83 | // Check if not visible 84 | cy.get('header').should('not.have.class', 'sticky-top'); 85 | } 86 | }); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /functions.php: -------------------------------------------------------------------------------- 1 |

' . __('Theme Error', 'tofino') . ' - ' . __('Composer autoload file not found. Run composer install on the command line.', 'tofino') . '

'; 78 | } 79 | 80 | 81 | // Admin notice for missing dist directory. 82 | function missing_dist_error_notice() 83 | { 84 | echo '

' . __('Theme Error', 'tofino') . ' - ' . __('/dist directory not found. You probably want to run npm install and npm run prod on the command line.', 'tofino') . '

'; 85 | } 86 | 87 | 88 | // Admin notice for missing ACF plugin. 89 | function missing_acf_plugin_notice() 90 | { 91 | echo '

' . __('Missing Plugin', 'tofino') . ' - ' . __('Advanced Custom Fields Pro plugin not found. Please install it.', 'tofino') . '

'; 92 | } 93 | -------------------------------------------------------------------------------- /.github/workflows/dev.yml: -------------------------------------------------------------------------------- 1 | name: Development Build 2 | 3 | on: 4 | push: 5 | branches: [dev] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | defaults: 11 | run: 12 | shell: bash 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | with: 17 | fetch-depth: 2 18 | 19 | # Set up Node.js version 20 20 | - name: Set up Node.js 20 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: '20' 24 | cache: 'npm' 25 | 26 | - name: Get Composer Cache Directory 27 | id: composer-cache 28 | run: | 29 | echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT 30 | - uses: actions/cache@v3 31 | with: 32 | path: ${{ steps.composer-cache.outputs.dir }} 33 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} 34 | restore-keys: | 35 | ${{ runner.os }}-composer- 36 | 37 | - name: Install Composer Dependencies 38 | run: composer install -o 39 | 40 | - name: Get npm cache directory 41 | id: npm-cache-dir 42 | shell: bash 43 | run: echo "dir=$(npm config get cache)" >> ${GITHUB_OUTPUT} 44 | - uses: actions/cache@v3 45 | id: npm-cache # use this to check for `cache-hit` ==> if: steps.npm-cache.outputs.cache-hit != 'true' 46 | with: 47 | path: ${{ steps.npm-cache-dir.outputs.dir }} 48 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 49 | restore-keys: | 50 | ${{ runner.os }}-node- 51 | 52 | - name: Install NPM Dependencies 53 | run: npm install 54 | 55 | - name: Build Scripts 56 | run: npm run build 57 | 58 | - name: Get .env Variables 59 | id: dotenv 60 | uses: falti/dotenv-action@v1.0.4 61 | 62 | - name: Sync Files to Server 63 | env: 64 | dest: '${{ steps.dotenv.outputs.SSH_LOGIN }}:${{ steps.dotenv.outputs.DEPLOYMENT_PATH }}${{ steps.dotenv.outputs.VITE_THEME_PATH }}' 65 | deploy: '${{ steps.dotenv.outputs.DEPLOY }}' 66 | if: ${{ env.deploy == 'TRUE' }} 67 | run: | 68 | echo "${{ secrets.DEPLOY_KEY }}" > deploy_key 69 | chmod 600 ./deploy_key 70 | 71 | # Deleting 'dist' folder on the server if it exists 72 | echo "Checking and deleting 'dist' folder on the server if it exists..." 73 | ssh -i ./deploy_key -o StrictHostKeyChecking=no ${{ steps.dotenv.outputs.SSH_LOGIN }} 'if [ -d "${{ steps.dotenv.outputs.DEPLOYMENT_PATH }}${{ steps.dotenv.outputs.VITE_THEME_PATH }}/dist" ]; then rm -rf ${{ steps.dotenv.outputs.DEPLOYMENT_PATH }}${{ steps.dotenv.outputs.VITE_THEME_PATH }}/dist; fi' 74 | 75 | # Starting rsync operation 76 | echo "Starting rsync operation..." 77 | 78 | rsync -chav --delete \ 79 | -e 'ssh -i ./deploy_key -o StrictHostKeyChecking=no' \ 80 | --exclude-from='.git-ftp-ignore' \ 81 | --exclude /deploy_key \ 82 | ./ ${{env.dest}} 83 | -------------------------------------------------------------------------------- /cypress/e2e/axe.spec.ts: -------------------------------------------------------------------------------- 1 | import 'cypress-axe'; 2 | import resolveConfig from 'tailwindcss/resolveConfig'; 3 | import tailwindConfig from '../../tailwind.config'; 4 | import { screenshotViolations, cypressLog, terminalLog } from '../support/helpers/index'; 5 | 6 | // Type for viewport sizes 7 | type ViewportSize = { 8 | name: string; 9 | width: number; 10 | height: number; 11 | }; 12 | 13 | // Type for violation callback 14 | type ViolationCallback = (violations: any[]) => void; // Replace 'any' with the actual type of violations if known 15 | 16 | const fullConfig = resolveConfig(tailwindConfig); 17 | const screens: { [key: string]: string } = fullConfig.theme.screens; 18 | 19 | let allViolations: any[] = []; // Replace 'any' with the actual type of violations 20 | 21 | before(() => { 22 | allViolations = []; // Reset before the test suite runs 23 | }); 24 | 25 | export const createAccessibilityCallback = ( 26 | pageName: string, 27 | breakpointName: string 28 | ): ViolationCallback => { 29 | cy.task('log', `Running accessibility checks for ${pageName} at ${breakpointName} breakpoint`); 30 | 31 | return (violations) => { 32 | cypressLog(violations); 33 | terminalLog(violations); 34 | 35 | if (Cypress.config('enableScreenshots')) { 36 | screenshotViolations(violations, pageName, breakpointName); 37 | } 38 | 39 | allViolations.push(...violations); 40 | }; 41 | }; 42 | 43 | const viewportSizes: ViewportSize[] = [ 44 | { 45 | name: 'Mobile', 46 | width: 320, 47 | height: 812, 48 | }, 49 | { 50 | name: 'Tablet', 51 | width: parseInt(screens.md, 10), 52 | height: 1024, 53 | }, 54 | { 55 | name: 'Desktop', 56 | width: parseInt(screens.lg, 10), 57 | height: 660, 58 | }, 59 | ]; 60 | 61 | describe('Accessibility Tests', () => { 62 | it('should be accessible', () => { 63 | cy.task('sitemapLocations').then((pages) => { 64 | pages.forEach((page) => { 65 | cy.visit(page); 66 | cy.injectAxe(); 67 | 68 | if (Cypress.config('axeIgnoreContrast')) { 69 | cy.configureAxe({ 70 | rules: [ 71 | { 72 | id: 'color-contrast', 73 | enabled: false, 74 | }, 75 | ], 76 | }); 77 | } 78 | 79 | const url = new URL(page); 80 | const path = url.pathname; 81 | 82 | viewportSizes.forEach((viewport) => { 83 | cy.viewport(viewport.width, viewport.height); 84 | 85 | cy.checkA11y( 86 | null, 87 | null, 88 | // { 89 | // runOnly: { 90 | // type: 'tag', 91 | // values: ['wcag2a', 'wcag2aa', 'best-practice', 'section508'], 92 | // }, 93 | // }, 94 | createAccessibilityCallback(path, viewport.name), 95 | true // Do not fail the test when there are accessibility failures 96 | ); 97 | }); 98 | }); 99 | }); 100 | }); 101 | }); 102 | 103 | after(() => { 104 | // Send the accumulated violations to the custom task 105 | cy.task('processAccessibilityViolations', allViolations); 106 | }); 107 | -------------------------------------------------------------------------------- /inc/acf-json/group_621fb69bc75ee.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": "group_621fb69bc75ee", 3 | "title": "Contact Submissions", 4 | "fields": [ 5 | { 6 | "key": "field_621fb6b9f7959", 7 | "label": "First Name", 8 | "name": "contact_form_firstname", 9 | "type": "text", 10 | "instructions": "", 11 | "required": 0, 12 | "conditional_logic": 0, 13 | "wrapper": { 14 | "width": "", 15 | "class": "", 16 | "id": "" 17 | }, 18 | "default_value": "", 19 | "placeholder": "", 20 | "prepend": "", 21 | "append": "", 22 | "maxlength": "" 23 | }, 24 | { 25 | "key": "field_621fb6e4f795a", 26 | "label": "Last Name", 27 | "name": "contact_form_lastname", 28 | "type": "text", 29 | "instructions": "", 30 | "required": 0, 31 | "conditional_logic": 0, 32 | "wrapper": { 33 | "width": "", 34 | "class": "", 35 | "id": "" 36 | }, 37 | "default_value": "", 38 | "placeholder": "", 39 | "prepend": "", 40 | "append": "", 41 | "maxlength": "" 42 | }, 43 | { 44 | "key": "field_621fb6fff795b", 45 | "label": "Email", 46 | "name": "contact_form_email", 47 | "type": "email", 48 | "instructions": "", 49 | "required": 0, 50 | "conditional_logic": 0, 51 | "wrapper": { 52 | "width": "", 53 | "class": "", 54 | "id": "" 55 | }, 56 | "default_value": "", 57 | "placeholder": "", 58 | "prepend": "", 59 | "append": "" 60 | }, 61 | { 62 | "key": "field_621fb720f795c", 63 | "label": "Phone", 64 | "name": "contact_form_phone", 65 | "type": "text", 66 | "instructions": "", 67 | "required": 0, 68 | "conditional_logic": 0, 69 | "wrapper": { 70 | "width": "", 71 | "class": "", 72 | "id": "" 73 | }, 74 | "default_value": "", 75 | "placeholder": "", 76 | "prepend": "", 77 | "append": "", 78 | "maxlength": "" 79 | }, 80 | { 81 | "key": "field_621fb72ff795d", 82 | "label": "Message", 83 | "name": "contact_form_message", 84 | "type": "textarea", 85 | "instructions": "", 86 | "required": 0, 87 | "conditional_logic": 0, 88 | "wrapper": { 89 | "width": "", 90 | "class": "", 91 | "id": "" 92 | }, 93 | "default_value": "", 94 | "placeholder": "", 95 | "maxlength": "", 96 | "rows": 8, 97 | "new_lines": "", 98 | "acfe_textarea_code": 0 99 | } 100 | ], 101 | "location": [ 102 | [ 103 | { 104 | "param": "post_type", 105 | "operator": "==", 106 | "value": "contact_submission" 107 | } 108 | ] 109 | ], 110 | "menu_order": 0, 111 | "position": "normal", 112 | "style": "default", 113 | "label_placement": "left", 114 | "instruction_placement": "label", 115 | "hide_on_screen": "", 116 | "active": true, 117 | "description": "", 118 | "show_in_rest": 0, 119 | "modified": 1650052210 120 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Tofino 2 | 3 | # Tofino 4 | 5 | A WordPress starter theme for jumpstarting custom theme development. 6 | 7 | Developed by [Daniel Hewes](https://github.com/danimalweb), [Jake Gully](https://github.com/mrchimp). 8 | 9 | Ongoing development is sponsored by [Creative Dot](https://creativdotdesign.com) 10 | 11 | Heavily inspired the by awesome WordPress starter theme [Sage](https://github.com/roots/sage) by [Roots](https://github.com/roots) from [Ben Word](https://github.com/retlehs) and [Scott Walkinshaw](https://github.com/swalkinshaw). 12 | 13 | ## Requirements 14 | 15 | | Prerequisite | How to check | How to install | 16 | | ----------------- | ------------- | ----------------------------------------------- | 17 | | PHP >= 8.2.0 | `php -v` | [php.net](http://php.net/manual/en/install.php) | 18 | | Node.js >= 20.0.0 | `node -v` | [nodejs.org](http://nodejs.org/) | 19 | | Composer >= 2.0.0 | `composer -V` | [getcomposer.org](http://getcomposer.org) | 20 | 21 | ## Installation 22 | 23 | - Download the latest [tagged release](https://github.com/creativedotdesign/tofino/releases). 24 | - Clone the git repo and run the following commands: 25 | 26 | ``` 27 | composer install 28 | npm install 29 | npm run dev 30 | ``` 31 | 32 | Note that the Vite Dev Server runs on port 3000. You access the website via the hostname and Vite will HMR or refresh automatically. If the Vite Dev Server is not running the website will pull it's assets from the /dist directory. 33 | 34 | Important: You MUST set `WP_ENVIRONMENT_TYPE` to `development` or `local` in your wp-config.php file for the Vite Dev Server to work. Local by Flywheel does this automatically. 35 | 36 | ## Features 37 | 38 | - [TailwindCSS](http://tailwindcss.com/) (v3.4) 39 | - Multilingual ready (WPML) 40 | - Responsive 41 | - General Options via ACF 42 | - Admin login screen logo 43 | - Custom Dashboard Widget 44 | - Social links 45 | - Sticky header menu 46 | - Client Data (Address, Telephone number, Email address, Company number) 47 | - Footer text 48 | - Alert Bar with top/bottom positions 49 | - Maintenance mode popup 50 | - Custom 404 page 51 | - [Advanced Custom Fields](https://www.advancedcustomfields.com/resources/getting-started/) 52 | - ACF JSON Folder 53 | - [TypeScript](https://www.typescriptlang.org/) 54 | - [Vite](https://vitejs.dev/guide/) build script 55 | - [Vitest](https://vitest.dev/) for testing Vue components 56 | - [Cypress](https://www.cypress.io/) for Integration and E2E tests 57 | - [Composer](https://getcomposer.org/) for PHP package management 58 | - Namespaced functions 59 | - Auto post type / slug based template routing 60 | - Shortcodes 61 | - SVG Sprite 62 | - [Web Font Loader](https://github.com/typekit/webfontloader) load Google, Typekit and custom fonts 63 | - VueJS v3.x with Composition API 64 | - Pinia State Management 65 | - Form support via Tofino Form Builder plugin 66 | - AjaxForm PHP Class 67 | - Fragment Cache PHP Class 68 | 69 | ## Documentation 70 | 71 | Docs are provided by README.md files in each directory. 72 | 73 | ## Deployment 74 | 75 | We use [GitHub Actions](https://github.com/features/actions). The deployment script is issued the following commands: 76 | 77 | ``` 78 | composer install 79 | npm install 80 | npm run build 81 | ``` 82 | 83 | The following files and directories should not be deployed on the server: 84 | 85 | ``` 86 | src 87 | node_modules 88 | .vscode 89 | .editorconfig 90 | .env 91 | .eslintrc.cjs 92 | .git 93 | .github 94 | .gitignore 95 | .gitkeep 96 | .git-ftp-ignore 97 | .git-ftp-include 98 | .gitattributes 99 | .gitignore 100 | .prettierignore 101 | .npmrc 102 | composer.json 103 | composer.lock 104 | package.json 105 | package-lock.json 106 | postcss.config.cts 107 | tsconfig.json 108 | vite.config.ts 109 | phpcs.xml 110 | \*.md 111 | cypress 112 | cypress.config.ts 113 | prettier.config.cjs 114 | stylelint.config.ts 115 | tailwind.config.ts 116 | vite.config.ts 117 | vite-env.d.ts 118 | ``` 119 | -------------------------------------------------------------------------------- /inc/acf-json/group_653fdbc6d93b5.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": "group_653fdbc6d93b5", 3 | "title": "Client Data", 4 | "fields": [ 5 | { 6 | "key": "field_653fdbc721c1c", 7 | "label": "Telephone Number", 8 | "name": "telephone_number", 9 | "aria-label": "", 10 | "type": "text", 11 | "instructions": "", 12 | "required": 0, 13 | "conditional_logic": 0, 14 | "wrapper": { 15 | "width": "", 16 | "class": "", 17 | "id": "" 18 | }, 19 | "default_value": "", 20 | "maxlength": "", 21 | "placeholder": "", 22 | "prepend": "", 23 | "append": "" 24 | }, 25 | { 26 | "key": "field_653fdc00ceac3", 27 | "label": "Email Address", 28 | "name": "email_address", 29 | "aria-label": "", 30 | "type": "text", 31 | "instructions": "", 32 | "required": 0, 33 | "conditional_logic": 0, 34 | "wrapper": { 35 | "width": "", 36 | "class": "", 37 | "id": "" 38 | }, 39 | "default_value": "", 40 | "maxlength": "", 41 | "placeholder": "", 42 | "prepend": "", 43 | "append": "" 44 | }, 45 | { 46 | "key": "field_653fdc0aceac4", 47 | "label": "Company Name", 48 | "name": "company_name", 49 | "aria-label": "", 50 | "type": "text", 51 | "instructions": "", 52 | "required": 0, 53 | "conditional_logic": 0, 54 | "wrapper": { 55 | "width": "", 56 | "class": "", 57 | "id": "" 58 | }, 59 | "default_value": "", 60 | "maxlength": "", 61 | "placeholder": "", 62 | "prepend": "", 63 | "append": "" 64 | }, 65 | { 66 | "key": "field_653fdc17ceac5", 67 | "label": "Address", 68 | "name": "address", 69 | "aria-label": "", 70 | "type": "textarea", 71 | "instructions": "", 72 | "required": 0, 73 | "conditional_logic": 0, 74 | "wrapper": { 75 | "width": "", 76 | "class": "", 77 | "id": "" 78 | }, 79 | "default_value": "", 80 | "acfe_textarea_code": 0, 81 | "maxlength": "", 82 | "rows": 4, 83 | "placeholder": "", 84 | "new_lines": "" 85 | }, 86 | { 87 | "key": "field_653fdc3dceac6", 88 | "label": "Company Number", 89 | "name": "company_number", 90 | "aria-label": "", 91 | "type": "text", 92 | "instructions": "", 93 | "required": 0, 94 | "conditional_logic": 0, 95 | "wrapper": { 96 | "width": "", 97 | "class": "", 98 | "id": "" 99 | }, 100 | "default_value": "", 101 | "maxlength": "", 102 | "placeholder": "", 103 | "prepend": "", 104 | "append": "" 105 | } 106 | ], 107 | "location": [ 108 | [ 109 | { 110 | "param": "post_type", 111 | "operator": "==", 112 | "value": "post" 113 | } 114 | ] 115 | ], 116 | "menu_order": 0, 117 | "position": "normal", 118 | "style": "default", 119 | "label_placement": "left", 120 | "instruction_placement": "label", 121 | "hide_on_screen": "", 122 | "active": false, 123 | "description": "", 124 | "show_in_rest": 0, 125 | "acfe_display_title": "", 126 | "acfe_autosync": [ 127 | "json" 128 | ], 129 | "acfe_form": 0, 130 | "acfe_meta": "", 131 | "acfe_note": "", 132 | "modified": 1698683988 133 | } 134 | -------------------------------------------------------------------------------- /inc/acf-json/group_62583ddaa0897.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": "group_62583ddaa0897", 3 | "title": "__Page Modules", 4 | "fields": [ 5 | { 6 | "key": "field_62586c9af1a1a", 7 | "label": "Content Modules", 8 | "name": "content_modules", 9 | "aria-label": "", 10 | "type": "flexible_content", 11 | "instructions": "", 12 | "required": 0, 13 | "conditional_logic": 0, 14 | "wrapper": { 15 | "width": "", 16 | "class": "", 17 | "id": "" 18 | }, 19 | "show_in_graphql": 1, 20 | "layouts": { 21 | "layout_62586ca1c1a98": { 22 | "key": "layout_62586ca1c1a98", 23 | "name": "general_content", 24 | "label": "General Content", 25 | "display": "block", 26 | "sub_fields": [ 27 | { 28 | "key": "field_62586caef1a1b", 29 | "label": "General Content", 30 | "name": "general_content", 31 | "aria-label": "", 32 | "type": "clone", 33 | "instructions": "", 34 | "required": 0, 35 | "conditional_logic": 0, 36 | "wrapper": { 37 | "width": "", 38 | "class": "", 39 | "id": "" 40 | }, 41 | "clone": [ 42 | "group_62586c11ea98d" 43 | ], 44 | "display": "seamless", 45 | "layout": "block", 46 | "prefix_label": 0, 47 | "prefix_name": 0 48 | } 49 | ], 50 | "min": "", 51 | "max": "" 52 | }, 53 | "layout_64c975f9f72ae": { 54 | "key": "layout_64c975f9f72ae", 55 | "name": "form", 56 | "label": "Form", 57 | "display": "block", 58 | "sub_fields": [ 59 | { 60 | "key": "field_64c97600f72b1", 61 | "label": "Form", 62 | "name": "form", 63 | "aria-label": "", 64 | "type": "clone", 65 | "instructions": "", 66 | "required": 0, 67 | "conditional_logic": 0, 68 | "wrapper": { 69 | "width": "", 70 | "class": "", 71 | "id": "" 72 | }, 73 | "clone": [ 74 | "group_63bee2ff52da2" 75 | ], 76 | "display": "seamless", 77 | "layout": "block", 78 | "prefix_label": 0, 79 | "prefix_name": 0 80 | } 81 | ], 82 | "min": "", 83 | "max": "" 84 | } 85 | }, 86 | "min": "", 87 | "max": "", 88 | "button_label": "Add Row" 89 | } 90 | ], 91 | "location": [ 92 | [ 93 | { 94 | "param": "post_type", 95 | "operator": "==", 96 | "value": "page" 97 | } 98 | ] 99 | ], 100 | "menu_order": 0, 101 | "position": "acf_after_title", 102 | "style": "seamless", 103 | "label_placement": "top", 104 | "instruction_placement": "label", 105 | "hide_on_screen": [ 106 | "the_content", 107 | "discussion", 108 | "comments", 109 | "revisions", 110 | "slug", 111 | "author", 112 | "format", 113 | "page_attributes", 114 | "categories", 115 | "tags", 116 | "send-trackbacks" 117 | ], 118 | "active": true, 119 | "description": "", 120 | "show_in_rest": 0, 121 | "show_in_graphql": 0, 122 | "graphql_field_name": "__pageModules", 123 | "map_graphql_types_from_location_rules": 0, 124 | "graphql_types": "", 125 | "modified": 1693961110 126 | } 127 | -------------------------------------------------------------------------------- /inc/lib/vite.php: -------------------------------------------------------------------------------- 1 | '; 85 | } 86 | 87 | add_action('wp_head', function () use (&$res) { 88 | echo $res; 89 | }); 90 | } 91 | 92 | private static function cssTag(string $entry): string 93 | { 94 | // not needed on dev, it's inject by Vite 95 | if (self::isDevServerRunning() || self::isBrowserSyncRunning()) { 96 | return ''; 97 | } 98 | 99 | $tags = ''; 100 | 101 | foreach (self::cssUrls($entry) as $url) { 102 | wp_register_style("tofino/$entry", $url); 103 | wp_enqueue_style("tofino/$entry", $url); 104 | } 105 | 106 | return $tags; 107 | } 108 | 109 | // Helpers to locate files 110 | private static function getManifest(): array 111 | { 112 | $file = get_stylesheet_directory() . '/dist/.vite/manifest.json'; 113 | 114 | if (!file_exists($file)) { 115 | return []; 116 | } 117 | 118 | $content = @file_get_contents(get_stylesheet_directory() . '/dist/.vite/manifest.json'); 119 | 120 | return json_decode($content, true); 121 | } 122 | 123 | private static function assetUrl(string $entry): string 124 | { 125 | $manifest = self::getManifest(); 126 | 127 | return isset($manifest[$entry]) 128 | ? self::base_path() . $manifest[$entry]['file'] 129 | : self::base_path() . $entry; 130 | } 131 | 132 | private static function getPublicURLBase() 133 | { 134 | return self::isDevServerRunning() ? '/dist/' : self::base_path(); 135 | } 136 | 137 | private static function importsUrls(string $entry): array 138 | { 139 | $urls = []; 140 | $manifest = self::getManifest(); 141 | 142 | if (!empty($manifest[$entry]['imports'])) { 143 | foreach ($manifest[$entry]['imports'] as $imports) { 144 | $urls[] = self::getPublicURLBase() . $manifest[$imports]['file']; 145 | } 146 | } 147 | return $urls; 148 | } 149 | 150 | private static function cssUrls(string $entry): array 151 | { 152 | $urls = []; 153 | $manifest = self::getManifest(); 154 | 155 | if (!empty($manifest[$entry]['css'])) { 156 | foreach ($manifest[$entry]['css'] as $file) { 157 | $urls[] = self::getPublicURLBase() . $file; 158 | } 159 | } 160 | return $urls; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /inc/lib/README.md: -------------------------------------------------------------------------------- 1 | # lib 2 | 3 | Custom php code (that doesn't come from composer) goes here. The following files are base tofino code which you _probably_ don't want to edit: 4 | 5 | - `AjaxForm.php` 6 | - `assets.php` 7 | - `FragmentCache.php` 8 | - `helpers.php` 9 | - `init.php` 10 | 11 | ## Assets 12 | 13 | The enqueuing of JS and CSS files happens in this file. 14 | 15 | If you need to add a new local JS variable, find the function named `localize_scripts` in this file. 16 | 17 | ## Helpers 18 | 19 | Helper functions / wrappers to assist with development. Current functions include: `get_id_by_slug`, `get_page_name`, `get_complete_meta` and sanitize functions used in the Theme Options. 20 | 21 | ## Init 22 | 23 | Theme setup functions. Includes a PHP Version Check, registration of navigation menus, `add_theme_support`, global content width and hide admin bar on the front end. 24 | 25 | ## Fragment Cache 26 | 27 | Cache a template fragment. 28 | Uses Transients. If persistent caching is configured, then the transients functions will use the wp_cache. 29 | 30 | Example usage: 31 | 32 | ``` 33 | $frag = new \Tofino\FragmentCache('unique-key', 3600); // Second param is TTL in seconds 34 | 35 | if (!$frag->output()) { // Testing for a return of false 36 | functions_that_do_stuff_live(); 37 | these_should_echo(); 38 | 39 | // IMPORTANT YOU CANNOT FORGET THIS. If you do, the site will break. 40 | $frag->store(); 41 | } else { 42 | echo frag->output(); 43 | } 44 | ``` 45 | 46 | # Shortcodes 47 | 48 | The following shortcodes are available as shortcodes in WordPress content and PHP functions in your template files. 49 | 50 | ## [copyright] 51 | 52 | Generate copyright string, probably for use in the footer. 53 | 54 | Example usage: 55 | 56 | `[copyright]` or `copyright();` 57 | 58 | HTML output: 59 | 60 | ``` 61 | © 2020 62 | ``` 63 | 64 | ## [svg] 65 | 66 | Generate SVG sprite code for files from assets/svgs/sprites 67 | 68 | You can add custom Titles and Classes to the SVG via the shortcode attributes. 69 | 70 | You can only retireve the Title and Classes from an existing SVG using file, this does not work when using a sprite. 71 | 72 | Example usage: 73 | 74 | `[svg sprite="facebook"]` or `[svg sprite="facebook" class="icon-facebook" title="Facebook" id="fb" preserveAspectRatio="align"]` 75 | 76 | or 77 | 78 | ``` 79 | svg([ 80 | 'sprite' => 'facebook', 81 | 'class' => 'icon-facebook' 82 | ]); 83 | ``` 84 | 85 | HTML output: 86 | 87 | ``` 88 | 89 | 90 | 91 | ``` 92 | 93 | ## [social_icons] 94 | 95 | Generate a `