Compare commits
45
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21db4f31a3 | ||
|
|
d0cb20ce59 | ||
|
|
44cf74b6c4 | ||
|
|
935dd74194 | ||
|
|
e31ed29962 | ||
|
|
0613e33411 | ||
|
|
d0dd9fa98a | ||
|
|
2538a07028 | ||
|
|
868c2cc70a | ||
|
|
d4df6d8678 | ||
|
|
81547254fc | ||
|
|
fe52e51bd9 | ||
|
|
cc463c6131 | ||
|
|
22c35d0f56 | ||
|
|
afa594aa0a | ||
|
|
ca9c5f215c | ||
|
|
c6bf4d0ebf | ||
|
|
a50ea9f0ae | ||
|
|
ba5e6fcd86 | ||
|
|
1905a0cfd0 | ||
|
|
7d381e2d2e | ||
|
|
6dcd1233be | ||
|
|
aaef1ee4f8 | ||
|
|
8125d1c818 | ||
|
|
2d9f92618b | ||
|
|
bb8c35a0a3 | ||
|
|
9005aeed1a | ||
|
|
664e227f15 | ||
|
|
353030d2dc | ||
|
|
5ab42f02ab | ||
|
|
b0ed0d7a5a | ||
|
|
1caef627a0 | ||
|
|
5619522bf6 | ||
|
|
3fae5e54ac | ||
|
|
248179ac58 | ||
|
|
631ff4e1d0 | ||
|
|
83d4540a07 | ||
|
|
0e502b64cb | ||
|
|
8c4e6d8d08 | ||
|
|
1fef4c22a4 | ||
|
|
ccccd87544 | ||
|
|
969cdb14a0 | ||
|
|
af8e13d3a8 | ||
|
|
6f828ad3d8 | ||
|
|
bf4c2cf2e3 |
@@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
||||
@@ -0,0 +1,46 @@
|
||||
name: Deploy to Dreamhost
|
||||
on:
|
||||
workflow_dispatch:
|
||||
# Remove "workflow_dispatch" above and uncomment the lines below to enable deploy on push
|
||||
# only do this once you're actually ready to start deploying to Dreamhost
|
||||
# push:
|
||||
# branches:
|
||||
# - main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ !contains(github.event.head_commit.message, '#skipGA') }}
|
||||
|
||||
steps:
|
||||
- name: Install rsync
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get -y install rsync
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Cache node modules
|
||||
id: cache-npm
|
||||
uses: actions/setup-node@v6
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- name: Install npm dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Run build task
|
||||
run: npm run build
|
||||
|
||||
- name: Deploy to Dreamhost
|
||||
uses: SamKirkland/web-deploy@v1
|
||||
with:
|
||||
target-server: iad1-shared-e1-31.dreamhost.com
|
||||
ssh-port: 22
|
||||
remote-user: zarathos
|
||||
private-ssh-key: ${{ secrets.REMOTE_PRIVATE_KEY }}
|
||||
destination-path: work.keithsolomon.net/wp-content/themes/portfolio-2026
|
||||
rsync-options: --archive --verbose --compress --delete-after --human-readable --exclude=.git* --exclude=.git/ --exclude=.vscode/ --exclude=database/ --exclude=.gitignore --exclude=DOCKER_ENV --exclude=docker_tag --exclude=output.log --exclude=vendor/ --exclude=node_modules/ --exclude=notes/ --exclude=docs/
|
||||
@@ -1,64 +0,0 @@
|
||||
name: Deploy to WPEngine
|
||||
on:
|
||||
workflow_dispatch
|
||||
# Remove "workflow_dispatch" above and uncomment the lines below to enable deploy on push
|
||||
# only do this once you're actually ready to start deploying to Flywheel
|
||||
# push:
|
||||
# branches:
|
||||
# - main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ !contains(github.event.head_commit.message, '#skipGA') }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Get Composer Cache Directory
|
||||
id: composer-cache
|
||||
run: |
|
||||
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-composer-
|
||||
|
||||
- name: Install PHP Composer
|
||||
uses: php-actions/composer@v6
|
||||
with:
|
||||
php_version: "8.2"
|
||||
|
||||
- name: Cache node modules
|
||||
id: cache-npm
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
# npm cache files are stored in `~/.npm` on Linux/macOS
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ env.cache-name }}-
|
||||
${{ runner.os }}-build-
|
||||
${{ runner.os }}-
|
||||
|
||||
- name: Install npm dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Run build task
|
||||
run: npm run build
|
||||
|
||||
- name: Clean up node modules (not needed to deploy)
|
||||
run: rm -rf node_modules
|
||||
|
||||
- name: Deploy to WPE
|
||||
uses: wpengine/github-action-wpe-site-deploy@v3
|
||||
with:
|
||||
WPE_SSHG_KEY_PRIVATE: ${{ secrets.WPE_SSHG_KEY_PRIVATE }}
|
||||
WPE_ENV: <change-me>
|
||||
FLAGS: '-azvr --inplace --delete --exclude=".*"'
|
||||
REMOTE_PATH: "wp-content/themes/<change-me>"
|
||||
+3
-2
@@ -5,10 +5,11 @@ node_modules
|
||||
static/dist/
|
||||
.env
|
||||
bak/
|
||||
testimonials.class.php
|
||||
resource-filter/
|
||||
notes/
|
||||
docs/
|
||||
phpcs-results.txt
|
||||
.claude/
|
||||
.superpowers/
|
||||
|
||||
# Playwright
|
||||
/test-results/
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
/**
|
||||
* Page Not Found
|
||||
*
|
||||
* @package SoloFrameEvo
|
||||
* @package KsPortfolio
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
get_header();
|
||||
?>
|
||||
@@ -20,8 +20,8 @@ get_header();
|
||||
<p>
|
||||
<form role="search" method="get" class="search-form" action="<?php echo esc_url( home_url( '/' ) ); ?>">
|
||||
<label>
|
||||
<span class="screen-reader-text"><?php echo esc_html__( 'Search for:', 'label' ); ?></span>
|
||||
Try searching for it: <input type="search" class="search-field border border-secondary px-2 rounded-md" placeholder="<?php echo esc_attr_x( 'Search …', 'placeholder' ); ?>" value="<?php echo get_search_query(); ?>" name="s" title="<?php echo esc_attr__( 'Search for:', 'label' ); ?>" />
|
||||
<span class="screen-reader-text"><?php echo esc_html__( 'Search for:', 'ks-portfolio' ); ?></span>
|
||||
Try searching for it: <input type="search" class="search-field border border-secondary px-2 rounded-md" placeholder="<?php echo esc_attr_x( 'Search …', 'placeholder' ); ?>" value="<?php echo get_search_query(); ?>" name="s" title="<?php echo esc_attr__( 'Search for:', 'ks-portfolio' ); ?>" />
|
||||
</label>
|
||||
<x-button
|
||||
btnClasses="search-submit button text-center"
|
||||
|
||||
+32
-179
@@ -57,27 +57,6 @@
|
||||
"append": "",
|
||||
"maxlength": ""
|
||||
},
|
||||
{
|
||||
"key": "field_684ad36ebaf40",
|
||||
"label": "Fax",
|
||||
"name": "fax",
|
||||
"aria-label": "",
|
||||
"type": "text",
|
||||
"instructions": "",
|
||||
"required": 0,
|
||||
"conditional_logic": 0,
|
||||
"wrapper": {
|
||||
"width": "",
|
||||
"class": "",
|
||||
"id": ""
|
||||
},
|
||||
"default_value": "",
|
||||
"maxlength": "",
|
||||
"allow_in_bindings": 1,
|
||||
"placeholder": "",
|
||||
"prepend": "",
|
||||
"append": ""
|
||||
},
|
||||
{
|
||||
"key": "field_5fd3e0437bff0",
|
||||
"label": "Address",
|
||||
@@ -97,26 +76,6 @@
|
||||
"maxlength": "",
|
||||
"rows": 3,
|
||||
"new_lines": "br"
|
||||
},
|
||||
{
|
||||
"key": "field_60381dcb082b5",
|
||||
"label": "Hours",
|
||||
"name": "hours",
|
||||
"aria-label": "",
|
||||
"type": "textarea",
|
||||
"instructions": "",
|
||||
"required": 0,
|
||||
"conditional_logic": 0,
|
||||
"wrapper": {
|
||||
"width": "",
|
||||
"class": "",
|
||||
"id": ""
|
||||
},
|
||||
"default_value": "",
|
||||
"placeholder": "",
|
||||
"maxlength": "",
|
||||
"rows": 3,
|
||||
"new_lines": "br"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -125,8 +84,8 @@
|
||||
"label": "Social Media",
|
||||
"name": "social_media",
|
||||
"aria-label": "",
|
||||
"type": "group",
|
||||
"instructions": "",
|
||||
"type": "repeater",
|
||||
"instructions": "Add social media links. The service icon will be auto-detected from the URL domain.",
|
||||
"required": 0,
|
||||
"conditional_logic": 0,
|
||||
"wrapper": {
|
||||
@@ -134,105 +93,19 @@
|
||||
"class": "",
|
||||
"id": ""
|
||||
},
|
||||
"layout": "row",
|
||||
"collapsed": "",
|
||||
"min": 0,
|
||||
"max": 0,
|
||||
"layout": "table",
|
||||
"button_label": "Add Social Link",
|
||||
"sub_fields": [
|
||||
{
|
||||
"key": "field_677bf0e692d3c",
|
||||
"label": "Facebook",
|
||||
"name": "facebook",
|
||||
"aria-label": "",
|
||||
"type": "url",
|
||||
"instructions": "",
|
||||
"required": 0,
|
||||
"conditional_logic": 0,
|
||||
"wrapper": {
|
||||
"width": "",
|
||||
"class": "",
|
||||
"id": ""
|
||||
},
|
||||
"default_value": "",
|
||||
"allow_in_bindings": 1,
|
||||
"placeholder": ""
|
||||
},
|
||||
{
|
||||
"key": "field_677bf0e692d3e",
|
||||
"label": "Instagram",
|
||||
"name": "instagram",
|
||||
"aria-label": "",
|
||||
"type": "url",
|
||||
"instructions": "",
|
||||
"required": 0,
|
||||
"conditional_logic": 0,
|
||||
"wrapper": {
|
||||
"width": "",
|
||||
"class": "",
|
||||
"id": ""
|
||||
},
|
||||
"default_value": "",
|
||||
"allow_in_bindings": 1,
|
||||
"placeholder": ""
|
||||
},
|
||||
{
|
||||
"key": "field_677bf0e692d3d",
|
||||
"label": "X\/Twitter",
|
||||
"name": "twitter",
|
||||
"aria-label": "",
|
||||
"type": "url",
|
||||
"instructions": "",
|
||||
"required": 0,
|
||||
"conditional_logic": 0,
|
||||
"wrapper": {
|
||||
"width": "",
|
||||
"class": "",
|
||||
"id": ""
|
||||
},
|
||||
"default_value": "",
|
||||
"allow_in_bindings": 1,
|
||||
"placeholder": ""
|
||||
},
|
||||
{
|
||||
"key": "field_677bf0e692d41",
|
||||
"label": "LinkedIn",
|
||||
"name": "linkedin",
|
||||
"aria-label": "",
|
||||
"type": "url",
|
||||
"instructions": "",
|
||||
"required": 0,
|
||||
"conditional_logic": 0,
|
||||
"wrapper": {
|
||||
"width": "",
|
||||
"class": "",
|
||||
"id": ""
|
||||
},
|
||||
"default_value": "",
|
||||
"allow_in_bindings": 1,
|
||||
"placeholder": ""
|
||||
},
|
||||
{
|
||||
"key": "field_677bf0e692d40",
|
||||
"label": "Youtube",
|
||||
"name": "youtube",
|
||||
"label": "URL",
|
||||
"name": "url",
|
||||
"aria-label": "",
|
||||
"type": "url",
|
||||
"instructions": "",
|
||||
"required": 0,
|
||||
"conditional_logic": 0,
|
||||
"wrapper": {
|
||||
"width": "",
|
||||
"class": "",
|
||||
"id": ""
|
||||
},
|
||||
"default_value": "",
|
||||
"allow_in_bindings": 1,
|
||||
"placeholder": ""
|
||||
},
|
||||
{
|
||||
"key": "field_677bf0e692d3f",
|
||||
"label": "Pinterest",
|
||||
"name": "pinterest",
|
||||
"aria-label": "",
|
||||
"type": "url",
|
||||
"instructions": "",
|
||||
"instructions": "Enter the full URL to your social media profile",
|
||||
"required": 0,
|
||||
"conditional_logic": 0,
|
||||
"wrapper": {
|
||||
@@ -286,6 +159,24 @@
|
||||
"max_height": "",
|
||||
"max_size": "",
|
||||
"mime_types": ""
|
||||
},
|
||||
{
|
||||
"key": "field_6a846bd4360da",
|
||||
"label": "Resume Link",
|
||||
"name": "resume_link",
|
||||
"aria-label": "",
|
||||
"type": "url",
|
||||
"instructions": "",
|
||||
"required": 0,
|
||||
"conditional_logic": 0,
|
||||
"wrapper": {
|
||||
"width": "",
|
||||
"class": "",
|
||||
"id": ""
|
||||
},
|
||||
"default_value": "",
|
||||
"allow_in_bindings": 0,
|
||||
"placeholder": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -330,26 +221,6 @@
|
||||
"max_size": "",
|
||||
"mime_types": ""
|
||||
},
|
||||
{
|
||||
"key": "field_64761ea8cb0dd",
|
||||
"label": "Footer Description",
|
||||
"name": "footer_description",
|
||||
"aria-label": "",
|
||||
"type": "wysiwyg",
|
||||
"instructions": "",
|
||||
"required": 0,
|
||||
"conditional_logic": 0,
|
||||
"wrapper": {
|
||||
"width": "",
|
||||
"class": "",
|
||||
"id": ""
|
||||
},
|
||||
"default_value": "",
|
||||
"tabs": "all",
|
||||
"toolbar": "full",
|
||||
"media_upload": 1,
|
||||
"delay": 0
|
||||
},
|
||||
{
|
||||
"key": "field_61df3348c3795",
|
||||
"label": "Copyright Text",
|
||||
@@ -370,27 +241,6 @@
|
||||
"placeholder": "",
|
||||
"prepend": "",
|
||||
"append": ""
|
||||
},
|
||||
{
|
||||
"key": "field_684af923d5f68",
|
||||
"label": "Credit Text",
|
||||
"name": "credit_text",
|
||||
"aria-label": "",
|
||||
"type": "text",
|
||||
"instructions": "",
|
||||
"required": 0,
|
||||
"conditional_logic": 0,
|
||||
"wrapper": {
|
||||
"width": "",
|
||||
"class": "",
|
||||
"id": ""
|
||||
},
|
||||
"default_value": "Web Design & Development by <a href=\"https:\/\/vincentdesign.ca\/\"><strong>Vincent Design<\/strong><\/a>",
|
||||
"maxlength": "",
|
||||
"allow_in_bindings": 1,
|
||||
"placeholder": "",
|
||||
"prepend": "",
|
||||
"append": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -455,5 +305,8 @@
|
||||
"active": true,
|
||||
"description": "",
|
||||
"show_in_rest": 0,
|
||||
"modified": 1749744328
|
||||
"display_title": "",
|
||||
"allow_ai_access": false,
|
||||
"ai_description": "",
|
||||
"modified": 1787502285
|
||||
}
|
||||
|
||||
@@ -2,6 +2,32 @@
|
||||
"key": "group_60bfdb328901d",
|
||||
"title": "Homepage Hero",
|
||||
"fields": [
|
||||
{
|
||||
"key": "field_6a84d6008c2b1",
|
||||
"label": "Hero Image",
|
||||
"name": "hero_image",
|
||||
"aria-label": "",
|
||||
"type": "image",
|
||||
"instructions": "",
|
||||
"required": 0,
|
||||
"conditional_logic": 0,
|
||||
"wrapper": {
|
||||
"width": "",
|
||||
"class": "",
|
||||
"id": ""
|
||||
},
|
||||
"return_format": "array",
|
||||
"library": "all",
|
||||
"min_width": "",
|
||||
"min_height": "",
|
||||
"min_size": "",
|
||||
"max_width": "",
|
||||
"max_height": "",
|
||||
"max_size": "",
|
||||
"mime_types": "",
|
||||
"allow_in_bindings": 0,
|
||||
"preview_size": "medium"
|
||||
},
|
||||
{
|
||||
"key": "field_60bfdb328cfae",
|
||||
"label": "Heading",
|
||||
@@ -102,5 +128,8 @@
|
||||
"active": true,
|
||||
"description": "",
|
||||
"show_in_rest": 0,
|
||||
"modified": 1742332828
|
||||
"display_title": "",
|
||||
"allow_ai_access": false,
|
||||
"ai_description": "",
|
||||
"modified": 1787090455
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
/**
|
||||
* Project Type Taxonomy Archive Template (plugin override)
|
||||
*
|
||||
* The projects-portfolio plugin's template loader looks for this filename
|
||||
* (archive-project-type.php) via locate_template(); when present it is used
|
||||
* in place of the plugin's bundled template. We mirror the pattern used by
|
||||
* the post-type archive and the Recent Projects block: a flex-row head
|
||||
* with the term title on the left and an "All Projects" link on the right,
|
||||
* a divider, then the project card grid.
|
||||
*
|
||||
* @package KsPortfolio
|
||||
*/
|
||||
|
||||
namespace KsPortfolio;
|
||||
|
||||
get_header();
|
||||
|
||||
$queried = get_queried_object();
|
||||
$term_name = $queried && ! is_wp_error( $queried ) ? $queried->name : __( 'Projects', 'ks-portfolio' );
|
||||
?>
|
||||
<section class="tax-project container mx-auto my-section">
|
||||
<?php if ( have_posts() ) : ?>
|
||||
<div class="tax-project__grid grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<?php
|
||||
while ( have_posts() ) :
|
||||
the_post();
|
||||
get_template_part( 'views/components/project-card' );
|
||||
endwhile;
|
||||
?>
|
||||
</div>
|
||||
|
||||
<div class="tax-project__pagination">
|
||||
<?php
|
||||
the_posts_pagination(
|
||||
array(
|
||||
'mid_size' => 2,
|
||||
'prev_text' => '« ' . esc_html__( 'Previous', 'ks-portfolio' ),
|
||||
'next_text' => esc_html__( 'Next', 'ks-portfolio' ) . ' »',
|
||||
)
|
||||
);
|
||||
?>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<div class="tax-project__empty">
|
||||
<h2 class="tax-project__empty-title"><?php echo esc_html__( 'Nothing here yet…', 'ks-portfolio' ); ?></h2>
|
||||
<p class="tax-project__empty-desc"><?php echo esc_html__( 'No published projects found in this category.', 'ks-portfolio' ); ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
|
||||
<?php
|
||||
get_footer();
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
/**
|
||||
* Project Archive Template
|
||||
*
|
||||
* @package KsPortfolio
|
||||
*/
|
||||
|
||||
namespace KsPortfolio;
|
||||
|
||||
get_header();
|
||||
|
||||
$hero_intro = get_field( 'archive_intro', (int) get_queried_object_id() );
|
||||
if ( ! $hero_intro ) {
|
||||
$hero_intro = __( 'A comprehensive catalog of WordPress plugins, custom themes, and open-source contributions developed for scalable enterprise environments. Focused on modern development practices, performant architectures, and secure integrations.', 'ks-portfolio' );
|
||||
}
|
||||
?>
|
||||
<section class="archive-projects">
|
||||
<div class="container mx-auto my-section">
|
||||
<header class="archive-projects__head">
|
||||
<h1 class="archive-projects__title"><?php echo esc_html__( 'Project Archive', 'ks-portfolio' ); ?></h1>
|
||||
<p class="archive-projects__intro"><?php echo esc_html( $hero_intro ); ?></p>
|
||||
</header>
|
||||
|
||||
<?php if ( have_posts() ) : ?>
|
||||
<div class="archive-projects__grid grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<?php
|
||||
while ( have_posts() ) :
|
||||
the_post();
|
||||
get_template_part( 'views/components/project-card' );
|
||||
endwhile;
|
||||
?>
|
||||
</div>
|
||||
|
||||
<div class="archive-projects__pagination">
|
||||
<?php
|
||||
the_posts_pagination(
|
||||
array(
|
||||
'mid_size' => 2,
|
||||
'prev_text' => '« ' . esc_html__( 'Previous', 'ks-portfolio' ),
|
||||
'next_text' => esc_html__( 'Next', 'ks-portfolio' ) . ' »',
|
||||
)
|
||||
);
|
||||
?>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<div class="archive-projects__empty">
|
||||
<h2 class="archive-projects__empty-title"><?php echo esc_html__( 'Nothing here yet…', 'ks-portfolio' ); ?></h2>
|
||||
<p class="archive-projects__empty-desc"><?php echo esc_html__( 'No published projects found.', 'ks-portfolio' ); ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php
|
||||
get_footer();
|
||||
@@ -1,438 +0,0 @@
|
||||
# Architecture
|
||||
|
||||
A deep dive into how SoloFrame Evo is organized, how it boots, and the conventions it uses.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Architecture](#architecture)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Bootstrap Flow](#bootstrap-flow)
|
||||
- [Architectural Layers](#architectural-layers)
|
||||
- [1. Entry Layer](#1-entry-layer)
|
||||
- [2. Service Layer](#2-service-layer)
|
||||
- [3. UI Templates and Components](#3-ui-templates-and-components)
|
||||
- [4. Styling](#4-styling)
|
||||
- [5. Client Scripts](#5-client-scripts)
|
||||
- [6. Data Layer](#6-data-layer)
|
||||
- [7. Infrastructure](#7-infrastructure)
|
||||
- [Namespace Conventions](#namespace-conventions)
|
||||
- [Global Variables](#global-variables)
|
||||
- [WordPress Hooks Cleanup](#wordpress-hooks-cleanup)
|
||||
- [Enqueue System](#enqueue-system)
|
||||
- [Frontend (`enqFEAssets()`)](#frontend-enqfeassets)
|
||||
- [Admin (`enqBEAssets()`)](#admin-enqbeassets)
|
||||
- [Block Editor (`enqEditorAssets()`)](#block-editor-enqeditorassets)
|
||||
- [theme.json Design System](#themejson-design-system)
|
||||
- [Colors](#colors)
|
||||
- [Typography](#typography)
|
||||
- [Layout](#layout)
|
||||
- [Spacing](#spacing)
|
||||
- [Global Styles](#global-styles)
|
||||
|
||||
## Bootstrap Flow
|
||||
|
||||
When WordPress loads a theme, it starts with `style.css` (for theme metadata) and `functions.php` (for logic). Here's exactly what happens in SoloFrame Evo:
|
||||
|
||||
```plain
|
||||
WordPress loads the theme
|
||||
│
|
||||
├─ style.css → Theme declaration (name, description, version)
|
||||
│
|
||||
└─ functions.php → Entry point
|
||||
│
|
||||
├─ namespace SoloFrameEvo
|
||||
│
|
||||
├─ glob(__DIR__ . '/lib/*.php') → Autoloads every PHP file in lib/
|
||||
│ ├─ activation.php → Theme activation handler (runs once)
|
||||
│ ├─ class-acf.php → ACF JSON sync paths
|
||||
│ ├─ class-breadcrumbs.php → Breadcrumb generation
|
||||
│ ├─ class-enqueue.php → Asset loading (CSS, JS, fonts)
|
||||
│ ├─ class-menuitems.php → Nav menu rendering
|
||||
│ ├─ class-resources.php → Custom post type
|
||||
│ ├─ extras.php → Sidebar, page header, Owner role, etc.
|
||||
│ ├─ helpers.php → Utility functions, globals, ACF options page
|
||||
│ ├─ hooks.php → WordPress hooks, cleanup, SVG support
|
||||
│ ├─ search-features.php → Enhanced search
|
||||
│ └─ show-template.php → Debug template path display
|
||||
│
|
||||
└─ regACFBlocks() → Registers ACF blocks (init hook, priority 5)
|
||||
└─ Scans views/blocks/*/block.json (skips 'boilerplate')
|
||||
```
|
||||
|
||||
The glob autoload means every file in `lib/` is loaded on every request. This is intentional — it keeps the architecture flat and predictable. If you add a new file to `lib/`, it's automatically available without modifying `functions.php`.
|
||||
|
||||
When the `init` hook fires (priority 1), `hooks.php::init()` runs its cleanup routine and adds theme supports. Then at priority 5, `regACFBlocks()` registers all ACF blocks. The `Enqueue` class constructor hooks into `wp_enqueue_scripts`, `admin_enqueue_scripts`, and `enqueue_block_editor_assets`.
|
||||
|
||||
## Architectural Layers
|
||||
|
||||
The theme is organized into seven distinct layers, each with a clear responsibility:
|
||||
|
||||
### 1. Entry Layer
|
||||
|
||||
The two files WordPress needs to recognize the theme:
|
||||
|
||||
| File | Purpose |
|
||||
| ------ | --------- |
|
||||
| `functions.php` | Autoloads `lib/*.php`, registers ACF blocks on `init` |
|
||||
| `style.css` | Theme declaration — name, description, version, author |
|
||||
|
||||
**Why it matters:** `functions.php` is the only file you should never edit directly. All logic lives in `lib/`.
|
||||
|
||||
### 2. Service Layer
|
||||
|
||||
PHP classes and utility functions in `lib/` that provide core functionality:
|
||||
|
||||
| File | Class/Function | Purpose |
|
||||
| ------ | --------------- | --------- |
|
||||
| `class-enqueue.php` | `Enqueue` | Loads all frontend CSS, JS, and fonts with cache-busting |
|
||||
| `class-menuitems.php` | `MenuItems` | Resolves WordPress nav menus into renderable item trees |
|
||||
| `class-breadcrumbs.php` | `Breadcrumbs` | Context-aware breadcrumb trails with Schema.org markup |
|
||||
| `class-acf.php` | `ACF` | Configures ACF JSON save/load paths for version control |
|
||||
| `class-resources.php` | `Resources` | Registers the "Resources" custom post type with URL rewriting |
|
||||
| `class-resources.php` | `ShowTemplate` | Adds HTML comment to footer showing active template (debug) |
|
||||
| `hooks.php` | `init()` | Aggressive WordPress cleanup, theme supports, SVG uploads |
|
||||
| `helpers.php` | Various | `getFieldValue()`, `blockWrapperAttributes()`, `blockCategories()`, etc. |
|
||||
| `extras.php` | Various | `createOwnerRole()`, `hasSidebar()`, `hasPageHeader()`, `divWrapper()` |
|
||||
| `search-features.php` | Various | `pageSearch()`, `dedupe()`, `postSort()`, `searchResultFilter()` |
|
||||
| `activation.php` | Various | Auto-installs plugins, creates pages, configures settings on theme activation |
|
||||
|
||||
### 3. UI Templates and Components
|
||||
|
||||
WordPress template hierarchy files, ACF blocks, reusable components, and icons:
|
||||
|
||||
**Template Hierarchy:**
|
||||
|
||||
| File | WordPress Template For |
|
||||
| ------ | ---------------------- |
|
||||
| `front-page.php` | The front page |
|
||||
| `index.php` | Blog posts listing (fallback for all) |
|
||||
| `single.php` | Individual posts |
|
||||
| `page.php` | Static pages (with optional sidebar) |
|
||||
| `search.php` | Search results |
|
||||
| `404.php` | Page not found |
|
||||
| `header.php` | Site header (included by other templates) |
|
||||
| `footer.php` | Site footer (included by other templates) |
|
||||
| `sidebar.php` | Primary sidebar |
|
||||
| `sidebar-page.php` | Page-specific sidebar |
|
||||
|
||||
**ACF Blocks (in `views/blocks/`):**
|
||||
|
||||
Each block follows a consistent three-file pattern: `block.json` (registration) + `{name}.php` (template) + `{name}.css` (styles).
|
||||
|
||||
| Block | Purpose |
|
||||
| ------- | --------- |
|
||||
| `accordion` | Collapsible content sections |
|
||||
| `boilerplate` | Starting template for new blocks (not registered) |
|
||||
| `button` | Single configurable button element |
|
||||
| `buttons` | Container that restricts children to `button` blocks |
|
||||
| `contact-info` | Contact information display |
|
||||
| `grid` | Flexible grid layout (restricts children to `grid-cell`) |
|
||||
| `grid-cell` | Individual grid item |
|
||||
| `homepage-hero` | Hero section for the front page |
|
||||
| `media-text` | Image/video with accompanying text |
|
||||
| `media-text-innerblocks` | Media-text with nested block support |
|
||||
| `page-children` | Displays child pages of the current page |
|
||||
| `section` | Container with background and width options |
|
||||
|
||||
**Components (in `views/components/`):**
|
||||
|
||||
| Component | Purpose |
|
||||
| ----------- | --------- |
|
||||
| `nav-aux.php` | Auxiliary navigation bar (social links + search) |
|
||||
| `nav-main.php` | Primary navigation menu |
|
||||
| `nav-main__toggle.php` | Mobile menu toggle button |
|
||||
| `menu-items/` | Recursive menu item rendering (index, has-children, single) |
|
||||
|
||||
**Partials (in `views/partials/`):**
|
||||
|
||||
| Partial | Purpose |
|
||||
| --------- | --------- |
|
||||
| `page-hero.php` | Page hero section |
|
||||
| `social-media.php` | Social media links |
|
||||
|
||||
**Icons (in `views/icons/`):**
|
||||
|
||||
SVG icon partials for Facebook, Instagram, LinkedIn, Pinterest, Twitter, YouTube, and others. Each is a minimal PHP file that outputs an SVG element.
|
||||
|
||||
### 4. Styling
|
||||
|
||||
CSS is organized as a layered cascade, managed through Tailwind CSS v4:
|
||||
|
||||
```plain
|
||||
styles/theme.css ← Entry point (imports everything below)
|
||||
├── @import "tailwindcss" ← Tailwind CSS v4 base
|
||||
├── @import "./base/index.css" ← Base styles barrel file
|
||||
│ ├── break-out.css ← Container break-out utilities
|
||||
│ ├── colors.css ← Color custom properties
|
||||
│ ├── forms.css ← Form element styles
|
||||
│ ├── global.css ← Global resets and base styles
|
||||
│ ├── misc.css ← Miscellaneous utilities
|
||||
│ ├── prose.css ← Prose/typography styles
|
||||
│ ├── skip-link.css ← Accessibility skip link
|
||||
│ └── typography.css ← Typography scale and fonts
|
||||
├── @import "./navigation/index.css" ← Navigation barrel file
|
||||
│ ├── nav-aux.css ← Auxiliary nav
|
||||
│ ├── nav-footer.css ← Footer nav
|
||||
│ ├── nav-functional.css ← Functional nav styles
|
||||
│ ├── nav-main-default.css ← Default main nav
|
||||
│ ├── nav-main-mega.css ← Mega menu nav
|
||||
│ ├── nav-mobile-accordion.css ← Accordion mobile nav
|
||||
│ └── nav-mobile-sliding.css ← Sliding mobile nav
|
||||
├── @import "./fonts/lineicons.css" ← Icon font (600+ glyphs)
|
||||
├── @import "./base/break-out.css" ← Break-out utilities (loaded after nav)
|
||||
├── @import "./components/index.css" ← Components barrel file
|
||||
│ ├── breadcrumbs.css ← Breadcrumbs
|
||||
│ ├── pagination.css ← Pagination
|
||||
│ ├── post-list.css ← Post listings
|
||||
│ ├── sidebar.css ← Sidebar
|
||||
│ ├── site-footer.css ← Footer
|
||||
│ └── site-header.css ← Header
|
||||
└── @import "./blocks/index.css" ← Block styles barrel file
|
||||
├── buttons.css ← Button styles with CSS custom properties
|
||||
└── core.css ← Core block overrides
|
||||
```
|
||||
|
||||
**Why this structure?** The barrel files (`index.css`) make it easy to add or remove stylesheets without modifying `theme.css`. Navigation styles are grouped because you typically only use one variant (default vs mega menu, accordion vs sliding mobile). Block-specific CSS lives alongside each block in `views/blocks/` and is auto-loaded by WordPress when the block renders.
|
||||
|
||||
**Important:** There is no `tailwind.config.js`. Tailwind CSS v4 uses CSS-first configuration via `@import "tailwindcss"` and `@plugin` directives in `theme.css`. Custom colors, fonts, and spacing are defined in `theme.json` and `styles/base/colors.css`.
|
||||
|
||||
### 5. Client Scripts
|
||||
|
||||
JavaScript modules loaded via WordPress's `wp_enqueue_script_module()` API (requires WordPress 6.5+):
|
||||
|
||||
```plain
|
||||
static/js/theme.js (entry point for frontend)
|
||||
├── Navigation.js → Mobile menu, sliding viewport, keyboard nav
|
||||
├── backToTop.js → BackToTopButton custom element
|
||||
├── button.js → ButtonComponent custom element (<x-button>)
|
||||
├── GetHeaderHeight.js → Sets --header-height CSS variable
|
||||
└── TagExternalLinks.js → Adds target="_blank" rel="noopener" to external links
|
||||
|
||||
static/js/admin.js (entry point for editor)
|
||||
└── button.js → ButtonComponent for editor context
|
||||
```
|
||||
|
||||
**How script modules work:** WordPress's `wp_enqueue_script_module()` creates proper ES module dependencies. The `Enqueue` class registers `sf-evo-theme` (theme.js) as a root module, and `sf-evo-button` (button.js) declares a dependency on it. This means button.js won't load until theme.js has loaded — no more manual script ordering.
|
||||
|
||||
**Passive event listener polyfill:** `theme.js` includes a polyfill that makes scroll, touch, and mouse event listeners passive by default. This improves scrolling performance without requiring `addEventListener(..., { passive: true })` on every listener.
|
||||
|
||||
**Custom elements:** The `ButtonComponent` (`<x-button>`) and `BackToTopButton` (`<back-to-top>`) are Web Components registered via `customElements.define()`. They accept attributes for styling, URL, target, and behavior.
|
||||
|
||||
### 6. Data Layer
|
||||
|
||||
SCF/ACF field group JSON files in the `acf/` directory:
|
||||
|
||||
| File | Block/Feature |
|
||||
| ------ | ------------- |
|
||||
| `group_5f7f85a2a3e13.json` | Accordion block fields |
|
||||
| `group_5fd3e006e5da5.json` | Global Fields (site-wide contact, social, footer settings) |
|
||||
| `group_600f5a9e242c3.json` | Grid block fields |
|
||||
| `group_60106ed700da3.json` | Button block fields |
|
||||
| `group_60bfb84ae973c.json` | Media Text block fields |
|
||||
| `group_60bfdb328901d.json` | Section block fields |
|
||||
| `group_6261bc658dd80.json` | Homepage Hero and Section fields |
|
||||
| `group_645e51f721207.json` | Grid Cell block fields |
|
||||
| `group_645e7cf448e66.json` | Contact Info block fields |
|
||||
|
||||
**Why JSON sync?** The `ACF` class in `class-acf.php` sets custom save/load paths so that field groups created in the WordPress admin are automatically saved as JSON files. This means:
|
||||
|
||||
- Field group configurations are version-controlled in Git
|
||||
- Field groups survive database resets
|
||||
- Multiple environments stay in sync
|
||||
- You can edit field groups in code or in the admin UI
|
||||
|
||||
### 7. Infrastructure
|
||||
|
||||
Build, CI/CD, and configuration files that support development and deployment:
|
||||
|
||||
| File | Purpose |
|
||||
| ------ | --------- |
|
||||
| `bin/.build.js` | Production build script — compiles Tailwind CSS with `--optimize` |
|
||||
| `bin/.watch.js` | Development server — BrowserSync with live reload |
|
||||
| `bin/.utils.js` | Shared utilities for build scripts (`tailwindToCSS`, debounce) |
|
||||
| `.github/workflows/wpengine.yml` | Deploys to WP Engine via rsync on manual trigger |
|
||||
| `.github/workflows/phpcs.yml` | Runs PHP CodeSniffer on pull requests |
|
||||
| `.github/workflows/todos.yml` | Syncs code TODOs to GitHub Issues |
|
||||
| `package.json` | Node dependencies and build/watch scripts |
|
||||
| `composer.json` | PHP dependencies (PHP_CodeSniffer, WordPress coding standards) |
|
||||
| `theme.json` | WordPress block editor configuration (colors, fonts, spacing) |
|
||||
| `.phpcs.xml` | PHP CodeSniffer ruleset |
|
||||
| `.env.example` | Environment variable template (`LOCALHOST_URL`, `BROWSERSYNC_PORT`) |
|
||||
| `playwright.config.js` | Playwright accessibility test configuration |
|
||||
| `tests/site-a11y.spec.js` | Accessibility test suite using @axe-core/playwright |
|
||||
|
||||
## Namespace Conventions
|
||||
|
||||
The project uses several naming conventions that can be confusing at first:
|
||||
|
||||
| Convention | Value | Where Used |
|
||||
| ----------- | ------- | ----------- |
|
||||
| PHP namespace | `SoloFrameEvo` | All PHP files use `namespace SoloFrameEvo;` |
|
||||
| Text domain | `sf-evo` | WordPress translation functions (`__()` , `_e()`) |
|
||||
| Block category | `sf-blocks` | Groups custom blocks in the editor (defined in `helpers.php::blockCategories()`) |
|
||||
| Script module IDs | `sf-evo-theme`, `sf-evo-button`, `sf-evo-admin` | JavaScript module registration in `class-enqueue.php` |
|
||||
| WP Engine folder | `soloframe-evo` | Deployment target in `.github/workflows/wpengine.yml` |
|
||||
| Git repo name | `SoloFrame-Evo` | The repository and theme directory name |
|
||||
|
||||
## Global Variables
|
||||
|
||||
Two global variables are defined in `helpers.php`:
|
||||
|
||||
```php
|
||||
global $theme, $views;
|
||||
$theme = get_template_directory(); // e.g., /var/www/wp-content/themes/SoloFrame Evo
|
||||
$views = $theme . '/views'; // e.g., /var/www/wp-content/themes/SoloFrame Evo/views
|
||||
```
|
||||
|
||||
**`$theme`** — Absolute path to the theme directory. Used when including files that need the full server path.
|
||||
|
||||
**`$views`** — Absolute path to the views directory. Used by `MenuItems::render()` to include navigation templates:
|
||||
|
||||
```php
|
||||
include $views . '/components/menu-items/index.php';
|
||||
```
|
||||
|
||||
These are available everywhere because `helpers.php` is loaded early via the glob autoload in `functions.php`.
|
||||
|
||||
## WordPress Hooks Cleanup
|
||||
|
||||
The `init()` function in `hooks.php` runs on every page load at priority 1. It performs aggressive cleanup of default WordPress output for performance and security reasons:
|
||||
|
||||
**What gets removed:**
|
||||
|
||||
| What | Why |
|
||||
| ------ | ----- |
|
||||
| Emoji detection scripts & styles | Most sites don't use WordPress emojis; they add ~10KB to every page |
|
||||
| `wp-block-library` styles | Theme provides its own block styles; core defaults add ~100KB |
|
||||
| `global-styles` & `core-block-styles` | Theme overrides these via `theme.json` and custom CSS |
|
||||
| `core-block-supports` | Duplicate of styling already handled by the theme |
|
||||
| WordPress generator meta tag | Security — hides WordPress version from page source |
|
||||
| RSD link | Rarely used XML-RPC discovery |
|
||||
| WLW manifest | Windows Live Writer support (deprecated) |
|
||||
| Shortlink | Removes `<link rel="shortlink">` from head |
|
||||
| REST API link in head | The API still works; only the discoverable link is removed |
|
||||
| oEmbed discovery links | Removes auto-embed discovery from head |
|
||||
| Canonical URL | Theme handles SEO via The SEO Framework plugin |
|
||||
| DNS prefetch hints | Removes unused `<link rel="dns-prefetch">` tags |
|
||||
| XML-RPC | Disabled via `xmlrpc_enabled` filter — prevents brute-force attacks |
|
||||
| Intrinsic image sizes | Prevents WordPress from adding width/height to img tags |
|
||||
| Auto sizes | Prevents automatic `sizes` attribute on images |
|
||||
|
||||
**What gets added:**
|
||||
|
||||
| Feature | Why |
|
||||
| --------- | ----- |
|
||||
| `post-thumbnails` | Featured image support |
|
||||
| `title-tag` | WordPress manages `<title>` tag |
|
||||
| `html5` (caption, comment-form, comment-list, gallery, search-form, script, style) | Modern HTML5 markup |
|
||||
| `align-wide` | Wide/full alignment for blocks |
|
||||
| `editor-styles` | Theme styles appear in the block editor |
|
||||
| `responsive-embeds` | Embeds respond to container width |
|
||||
| `customize-selective-refresh-widgets` | Widget changes update without full page reload |
|
||||
| SVG upload support | Allows SVG files in the media library |
|
||||
|
||||
**Important:** This cleanup is aggressive. If a plugin requires one of the removed features (like oEmbed discovery or XML-RPC), you'll need to comment out the corresponding `remove_action` line in `hooks.php`.
|
||||
|
||||
## Enqueue System
|
||||
|
||||
The `Enqueue` class (in `class-enqueue.php`) manages all asset loading:
|
||||
|
||||
### Frontend (`enqFEAssets()`)
|
||||
|
||||
| Asset | Method | Notes |
|
||||
| ------- | -------- | ------- |
|
||||
| `static/dist/theme.css` | `wp_enqueue_style()` | Compiled Tailwind CSS, cache-busted with `filemtime()` |
|
||||
| Raleway font | `wp_enqueue_style()` | Google Fonts with `preconnect` hint |
|
||||
| `sf-evo-theme` (theme.js) | `wp_enqueue_script_module()` | Frontend entry point |
|
||||
| `sf-evo-button` (button.js) | `wp_enqueue_script_module()` | Depends on `sf-evo-theme` |
|
||||
| jQuery | `wp_enqueue_script()` | Needed by downstream scripts; modules can't depend on classic scripts |
|
||||
|
||||
### Admin (`enqBEAssets()`)
|
||||
|
||||
| Asset | Method | Notes |
|
||||
| ------- | -------- | ------- |
|
||||
| Raleway font | `wp_enqueue_style()` | Same Google Fonts |
|
||||
| `styles/backend/admin.css` | `wp_enqueue_style()` | Admin-specific overrides |
|
||||
| `sf-evo-admin` (admin.js) | `wp_enqueue_script_module()` | Admin entry point |
|
||||
| `sf-evo-button` (button.js) | `wp_enqueue_script_module()` | Depends on `sf-evo-admin` |
|
||||
|
||||
### Block Editor (`enqEditorAssets()`)
|
||||
|
||||
| Asset | Method | Notes |
|
||||
| ------- | -------- | ------- |
|
||||
| Raleway font | `wp_enqueue_style()` | Same Google Fonts |
|
||||
| `styles/backend/editor.css` | `wp_enqueue_style()` | Editor-specific styles, scoped to block editor |
|
||||
|
||||
**Cache busting:** All enqueued files use `filemtime()` as the version number. This means the browser cache is automatically busted whenever a file changes — no manual version bumps needed.
|
||||
|
||||
**Script modules:** The theme uses `wp_enqueue_script_module()` (WordPress 6.5+) instead of traditional `wp_enqueue_script()` for frontend and admin JavaScript. This creates proper ES module dependencies where `sf-evo-button` won't load until `sf-evo-theme` has loaded.
|
||||
|
||||
## theme.json Design System
|
||||
|
||||
The `theme.json` file (WordPress block editor v3 schema) defines the design system for both the block editor and the frontend:
|
||||
|
||||
### Colors
|
||||
|
||||
Colors are defined as CSS custom properties and mapped to WordPress editor slugs:
|
||||
|
||||
| Editor Slug | CSS Variable | Purpose |
|
||||
| ------------ | ------------- | --------- |
|
||||
| `black` | `#000` | Pure black |
|
||||
| `white` | `#fff` | Pure white |
|
||||
| `theme-bg` | `var(--color-background)` | Page background |
|
||||
| `theme-text` | `var(--color-text)` | Body text |
|
||||
| `theme-primary` | `var(--color-primary)` | Primary brand color |
|
||||
| `theme-secondary` | `var(--color-secondary)` | Secondary brand color |
|
||||
| `theme-bodylinks` | `var(--color-bodylinks)` | Body link color |
|
||||
| `theme-footerlinks` | `var(--color-footlinks)` | Footer link color |
|
||||
| `theme-success` | `var(--color-success)` | Success/positive |
|
||||
| `theme-warning` | `var(--color-warning)` | Warning/caution |
|
||||
| `theme-danger` | `var(--color-danger)` | Danger/error |
|
||||
| `theme-info` | `var(--color-info)` | Informational |
|
||||
|
||||
The actual color values for the CSS variables are defined in `styles/base/colors.css`.
|
||||
|
||||
### Typography
|
||||
|
||||
One font family (`var(--font-sans)`) and 15 size presets:
|
||||
|
||||
| Slug | Variable | Typical Use |
|
||||
| ------ | ---------- | ------------- |
|
||||
| `base` | `var(--text-base)` | Body text |
|
||||
| `text-14px` | `var(--text-14px)` | Small text |
|
||||
| `text-16px` | `var(--text-16px)` | Standard text |
|
||||
| `text-18px` | `var(--text-18px)` | Large text |
|
||||
| `text-20px` | `var(--text-20px)` | Subheadings |
|
||||
| `text-22px` | `var(--text-22px)` | H4 |
|
||||
| `text-25px` | `var(--text-25px)` | H3 |
|
||||
| `text-30px` | `var(--text-30px)` | H2 |
|
||||
| `text-35px` | `var(--text-35px)` | Large heading |
|
||||
| `text-38px` | `var(--text-38px)` | H1 (small) |
|
||||
| `text-40px` | `var(--text-40px)` | H1 |
|
||||
| `text-45px` | `var(--text-45px)` | Hero heading |
|
||||
| `text-50px` | `var(--text-50px)` | Large hero |
|
||||
| `text-70px` | `var(--text-70px)` | Display size |
|
||||
| `text-75px` | `var(--text-75px)` | Maximum display |
|
||||
|
||||
### Layout
|
||||
|
||||
| Property | Value | Meaning |
|
||||
| ---------- | ------- | --------- |
|
||||
| `contentSize` | `100%` | Default content width (full-width by default) |
|
||||
| `wideSize` | `1536px` | Wide-alignment max width |
|
||||
|
||||
### Spacing
|
||||
|
||||
Available units: `px`, `em`, `rem`, `vh`, `vw`, `%`
|
||||
|
||||
### Global Styles
|
||||
|
||||
| Property | Value |
|
||||
| ---------- | ------- |
|
||||
| Background | `var(--wp--preset--color--background)` |
|
||||
| Text color | `var(--wp--preset--color--text)` |
|
||||
| Link color | `var(--wp--preset--color--theme-bodylinks)` |
|
||||
| Font family | `var(--wp--preset--font-family--theme-sans)` |
|
||||
| Line height | `1.5` |
|
||||
|
||||
**Why `theme.json` matters:** Changes to this file immediately affect the block editor UI — colors appear in the palette, font sizes in the typography controls, and spacing in the spacing panel. This is the single source of truth for the design system, and CSS custom properties cascade from here into the frontend styles.
|
||||
@@ -1,965 +0,0 @@
|
||||
# Creating Blocks in SoloFrame Evo
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Creating Blocks in SoloFrame Evo](#creating-blocks-in-soloframe-evo)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Overview](#overview)
|
||||
- [How Block Registration Works](#how-block-registration-works)
|
||||
- [Block Anatomy: The Three-File Pattern](#block-anatomy-the-three-file-pattern)
|
||||
- [block.json -- The Registration Manifest](#blockjson----the-registration-manifest)
|
||||
- [The PHP Template -- Rendering the Block](#the-php-template----rendering-the-block)
|
||||
- [The CSS File -- Scoped Styles](#the-css-file----scoped-styles)
|
||||
- [Helper Functions](#helper-functions)
|
||||
- [blockWrapperAttributes()](#blockwrapperattributes)
|
||||
- [getFieldValue()](#getfieldvalue)
|
||||
- [escEmbeds()](#escembeds)
|
||||
- [SCF/ACF Field Groups](#scfacf-field-groups)
|
||||
- [Creating a Field Group](#creating-a-field-group)
|
||||
- [JSON Sync](#json-sync)
|
||||
- [Parent-Child Block Patterns (InnerBlocks)](#parent-child-block-patterns-innerblocks)
|
||||
- [Basic InnerBlocks](#basic-innerblocks)
|
||||
- [Restricted InnerBlocks](#restricted-innerblocks)
|
||||
- [Enabling InnerBlocks in block.json](#enabling-innerblocks-in-blockjson)
|
||||
- [Adding Classes to InnerBlocks](#adding-classes-to-innerblocks)
|
||||
- [Tailwind CSS in Blocks](#tailwind-css-in-blocks)
|
||||
- [How Tailwind is Set Up](#how-tailwind-is-set-up)
|
||||
- [Using Tailwind Classes in Blocks](#using-tailwind-classes-in-blocks)
|
||||
- [Whitelisting Editor-Only Classes](#whitelisting-editor-only-classes)
|
||||
- [Block-Specific CSS Files](#block-specific-css-files)
|
||||
- [Step-by-Step: Creating a New Block](#step-by-step-creating-a-new-block)
|
||||
- [1. Create the Block Directory](#1-create-the-block-directory)
|
||||
- [2. Create block.json](#2-create-blockjson)
|
||||
- [3. Create the PHP Template](#3-create-the-php-template)
|
||||
- [4. Create the CSS File](#4-create-the-css-file)
|
||||
- [5. Create SCF/ACF Field Groups in WordPress Admin](#5-create-scfacf-field-groups-in-wordpress-admin)
|
||||
- [6. Build and Verify](#6-build-and-verify)
|
||||
- [Real-World Examples from This Theme](#real-world-examples-from-this-theme)
|
||||
- [Simple Block: Homepage Hero](#simple-block-homepage-hero)
|
||||
- [Parent Block with InnerBlocks: Section](#parent-block-with-innerblocks-section)
|
||||
- [Restricted Parent Block: Buttons](#restricted-parent-block-buttons)
|
||||
- [Dynamic Parent Block: Grid](#dynamic-parent-block-grid)
|
||||
- [Block Using Global Fields: Contact Info](#block-using-global-fields-contact-info)
|
||||
- [Common Pitfalls and Best Practices](#common-pitfalls-and-best-practices)
|
||||
- [Do](#do)
|
||||
- [Do Not](#do-not)
|
||||
- [Debugging Tips](#debugging-tips)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
SoloFrame Evo uses Secure Custom Fields (SCF) or Advanced Custom Fields (ACF) blocks to build page content. SCF/ACF blocks are a type of WordPress Gutenberg block where the editing interface comes from SCF/ACF field groups and the rendering is handled by a PHP template (instead of React). This approach lets you build rich, structured content blocks using familiar PHP templating and Tailwind CSS, without writing JavaScript.
|
||||
|
||||
Every block in this theme follows the same three-file pattern inside `views/blocks/{block-name}/`, and new blocks are automatically discovered and registered -- no manual registration required.
|
||||
|
||||
---
|
||||
|
||||
## How Block Registration Works
|
||||
|
||||
Block registration is handled by the `regACFBlocks()` function in `functions.php`. You never need to register a block manually; the function handles discovery for you.
|
||||
|
||||
```php
|
||||
// functions.php
|
||||
function regACFBlocks() {
|
||||
define( 'BLOCKS_DIR', get_stylesheet_directory() . '/views/blocks' );
|
||||
|
||||
if ( is_dir( BLOCKS_DIR ) ) {
|
||||
foreach ( scandir( BLOCKS_DIR ) as $folder ) {
|
||||
if ( ( '.' !== $folder && '..' !== $folder && 'boilerplate' !== $folder ) && is_dir( BLOCKS_DIR . '/' . $folder ) ) {
|
||||
register_block_type( BLOCKS_DIR . '/' . $folder );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
add_action( 'init', __NAMESPACE__ . '\\regACFBlocks', 5 );
|
||||
```
|
||||
|
||||
Here is what happens:
|
||||
|
||||
1. `BLOCKS_DIR` is defined as `get_stylesheet_directory() . '/views/blocks'`, pointing to the `views/blocks/` directory inside the theme.
|
||||
2. The function scans every subdirectory inside `views/blocks/`.
|
||||
3. It skips `.` (current dir), `..` (parent dir), and the `boilerplate` directory (the boilerplate is a template for creating new blocks, not a real block).
|
||||
4. For every remaining directory, it calls WordPress's `register_block_type()`, which reads the `block.json` file inside that directory and registers the block.
|
||||
5. The function runs on the `init` hook at priority 5, ensuring blocks are registered before the editor needs them.
|
||||
|
||||
**What this means for you:** To create a new block, you only need to add a new subdirectory under `views/blocks/` with a valid `block.json` file. WordPress discovers and registers it automatically the next time the theme loads.
|
||||
|
||||
---
|
||||
|
||||
## Block Anatomy: The Three-File Pattern
|
||||
|
||||
Every SCF/ACF block in this theme consists of exactly three files inside `views/blocks/{block-name}/`:
|
||||
|
||||
```plain
|
||||
views/blocks/
|
||||
boilerplate/ <-- Template for creating new blocks (not registered)
|
||||
block.json
|
||||
boilerplate.php
|
||||
boilerplate.css
|
||||
section/
|
||||
block.json
|
||||
section.php
|
||||
section.css
|
||||
homepage-hero/
|
||||
block.json
|
||||
homepage-hero.php
|
||||
homepage-hero.css
|
||||
...
|
||||
```
|
||||
|
||||
The naming convention is consistent: the directory name, the PHP file, and the CSS file all share the same slug (e.g., `homepage-hero`). The `block.json` file always uses the literal name `block.json`.
|
||||
|
||||
### block.json -- The Registration Manifest
|
||||
|
||||
The `block.json` file tells WordPress and SCF/ACF everything they need to know about the block. Here is the boilerplate version:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "acf/boilerplate",
|
||||
"title": "Block Boilerplate",
|
||||
"description": "Boilerplate code to create SCF/ACF blocks.",
|
||||
"style": ["file:./boilerplate.css"],
|
||||
"category": "sf-blocks",
|
||||
"icon": "block-default",
|
||||
"keywords": ["boilerplate"],
|
||||
"acf": {
|
||||
"mode": "preview",
|
||||
"renderTemplate": "boilerplate.php"
|
||||
},
|
||||
"supports": {
|
||||
"align": true,
|
||||
"anchor": true,
|
||||
"color": true,
|
||||
"html": false,
|
||||
"jsx": false,
|
||||
"mode": true,
|
||||
"multiple": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Field-by-field explanation:**
|
||||
|
||||
| Field | Purpose |
|
||||
| --- | --- |
|
||||
| `name` | The unique block identifier. **Must be prefixed with `acf/`** for ACF blocks. This becomes the machine name WordPress uses internally (e.g., `acf/testimonial`). |
|
||||
| `title` | The human-readable name shown in the block editor inserter (e.g., "Testimonial"). |
|
||||
| `description` | A short description shown in the block editor to help editors understand what the block does. |
|
||||
| `style` | An array of CSS files to load when this block renders. Use the `file:./` prefix for block-relative paths. WordPress only loads these stylesheets when the block is actually present on the page. |
|
||||
| `category` | Determines which section of the inserter the block appears under. **Always use `sf-blocks`** in this theme -- this is the custom category registered in `helpers.php` that groups all theme blocks together under "VDI Custom Blocks". |
|
||||
| `icon` | A Dashicon name (without the `dashicons-` prefix) shown next to the block in the inserter. Browse available icons at [DashIcons](https://developer.wordpress.org/resource/dashicons/). |
|
||||
| `keywords` | Additional search terms that help editors find the block in the inserter. For example, a "Testimonial" block might include `["testimonial", "quote", "review"]`. |
|
||||
| `acf.mode` | Controls how the block appears in the editor. `preview` shows the rendered block output; `edit` shows the ACF field inputs directly. Most blocks use `preview`. |
|
||||
| `acf.renderTemplate` | The PHP file that renders the block on the frontend and in preview mode. This filename must match the actual file in the directory. |
|
||||
| `supports.align` | Whether editors can choose alignment (left, center, right, wide, full). |
|
||||
| `supports.anchor` | Whether editors can set an HTML anchor (id attribute) for linking. |
|
||||
| `supports.color` | Whether editors can set text and background colors via the block editor. |
|
||||
| `supports.html` | Whether the block supports HTML editing mode in the editor. Set to `false` for ACF blocks since the template controls the markup. |
|
||||
| `supports.jsx` | Whether the block supports InnerBlocks (nesting other blocks inside it). Set to `true` if your block uses `<InnerBlocks />`. |
|
||||
| `supports.mode` | Whether editors can toggle between preview and edit mode in the editor. |
|
||||
| `supports.multiple` | Whether the block can be inserted more than once per post. Set to `false` for blocks that should be unique (e.g., a homepage hero). |
|
||||
|
||||
**Special field for parent blocks:** If your block restricts which blocks can be inserted as children, you can add an `allowedBlocks` key at the top level of `block.json` (not inside `supports`). The Buttons block does this:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "acf/buttons",
|
||||
"title": "Buttons",
|
||||
"description": "A button or group of buttons.",
|
||||
"allowedBlocks": ["acf/button"],
|
||||
"category": "sf-blocks",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### The PHP Template -- Rendering the Block
|
||||
|
||||
The PHP template is responsible for outputting the block's HTML. Every template follows the same structure:
|
||||
|
||||
```php
|
||||
<?php
|
||||
/**
|
||||
* Block Name: Boilerplate
|
||||
*
|
||||
* This is the template for building your own custom blocks.
|
||||
*
|
||||
* @package SoloFrameEvo
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
|
||||
$classes = 'boilerplate';
|
||||
|
||||
/**
|
||||
* NOTE: DO NOT remove this function call - it is required to avoid editor issues.
|
||||
* $is_preview is a WordPress global when in the editor.
|
||||
*/
|
||||
$wrapper = blockWrapperAttributes( $classes, $is_preview );
|
||||
?>
|
||||
|
||||
<section <?php echo wp_kses_post( $wrapper ); ?>>
|
||||
<!-- Your block code will go here -->
|
||||
</section>
|
||||
```
|
||||
|
||||
**Key elements explained:**
|
||||
|
||||
1. **`namespace SoloFrameEvo;`** -- Every block template must declare this namespace. It gives you access to the theme's helper functions (`blockWrapperAttributes`, `getFieldValue`, etc.) without needing fully-qualified class names.
|
||||
|
||||
2. **`$is_preview`** -- This is a WordPress global variable that is `true` when the block is being rendered inside the block editor, and `false` on the frontend. You can use it to conditionally show editor-only content or adjust markup for the editor.
|
||||
|
||||
3. **`blockWrapperAttributes()`** -- This helper function (defined in `lib/helpers.php`) generates the wrapper attributes for the block's root element. It handles the difference between editor preview mode and the frontend:
|
||||
- In preview mode (`$is_preview` is `true`): returns a simple `class="my-class"` string, which avoids rendering issues in the editor.
|
||||
- On the frontend (`$is_preview` is `false`): returns the full `get_block_wrapper_attributes()` output, which includes WordPress-generated classes and attributes for alignment, anchor, custom class names, etc.
|
||||
|
||||
**Always use `blockWrapperAttributes()` instead of calling `get_block_wrapper_attributes()` directly.** The direct call can cause rendering problems in the editor.
|
||||
|
||||
4. **`wp_kses_post()`** -- Always wrap the wrapper attributes output with `wp_kses_post()` for security. This sanitizes the output while preserving the HTML attributes that `blockWrapperAttributes()` generates.
|
||||
|
||||
5. **Semantic HTML wrapper** -- Use a semantic element like `<section>`, `<article>`, `<aside>`, or `<div>` as the outermost element. The block's wrapper attributes (classes, anchor, alignment) must go on this outermost element.
|
||||
|
||||
### The CSS File -- Scoped Styles
|
||||
|
||||
Each block has its own CSS file that is loaded automatically by WordPress when the block is present on the page. This means styles are only loaded when needed, keeping page weight minimal.
|
||||
|
||||
The CSS filename must match the block slug and be referenced in `block.json` using the `file:./` prefix:
|
||||
|
||||
```json
|
||||
"style": ["file:./testimonial.css"]
|
||||
```
|
||||
|
||||
You can use Tailwind utility classes directly in your PHP templates (e.g., `class="flex gap-4 p-6"`), and they will work as long as the Tailwind build process can detect them. For complex or block-specific styles that are not expressible as utility classes, write them in the block's CSS file. You can also use Tailwind's `@apply` directive in these CSS files to compose utility classes into reusable styles:
|
||||
|
||||
```css
|
||||
/* testimonial.css */
|
||||
|
||||
.testimonial {
|
||||
/* Block-level styles */
|
||||
}
|
||||
|
||||
.testimonial__quote {
|
||||
/* Element styles */
|
||||
}
|
||||
|
||||
.testimonial__quote--large {
|
||||
/* Modifier styles */
|
||||
}
|
||||
```
|
||||
|
||||
The file can be empty initially and filled in as needed.
|
||||
|
||||
---
|
||||
|
||||
## Helper Functions
|
||||
|
||||
The theme provides several helper functions in `lib/helpers.php`, all under the `SoloFrameEvo` namespace. Because every block template declares `namespace SoloFrameEvo;`, you can call these functions directly without any prefix.
|
||||
|
||||
### blockWrapperAttributes()
|
||||
|
||||
```php
|
||||
function blockWrapperAttributes( $classes, $is_preview )
|
||||
```
|
||||
|
||||
**Purpose:** Generates the HTML attributes string for a block's root element, handling the difference between the editor and the frontend.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `$classes` (string) -- A space-separated list of CSS class names to apply to the block wrapper.
|
||||
- `$is_preview` (bool) -- Whether the block is being rendered in the editor. Always pass the global `$is_preview` variable.
|
||||
|
||||
**Returns:** A string of HTML attributes ready to echo inside an HTML tag.
|
||||
|
||||
**How it works:**
|
||||
|
||||
- When `$is_preview` is `true` (in the editor), it returns `class="my-class"`. This is a simplified output that avoids rendering issues caused by WordPress's `get_block_wrapper_attributes()` in the editor context.
|
||||
- When `$is_preview` is `false` (on the frontend), it calls WordPress's `get_block_wrapper_attributes()` with your classes merged in, producing the full set of attributes including alignment classes, anchor IDs, custom class names from the editor, and more.
|
||||
|
||||
**Usage pattern:**
|
||||
|
||||
```php
|
||||
$classes = 'my-block some-tailwind-class';
|
||||
$wrapper = blockWrapperAttributes( $classes, $is_preview );
|
||||
?>
|
||||
<section <?php echo wp_kses_post( $wrapper ); ?>>
|
||||
<!-- block content -->
|
||||
</section>
|
||||
```
|
||||
|
||||
**Important:** Never call `get_block_wrapper_attributes()` directly. Always use `blockWrapperAttributes()` instead. Direct calls can cause the block to render incorrectly in the editor.
|
||||
|
||||
### getFieldValue()
|
||||
|
||||
```php
|
||||
function getFieldValue( $field_path )
|
||||
```
|
||||
|
||||
**Purpose:** Retrieves nested values from ACF option fields (Global Fields) using dot notation.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `$field_path` (string) -- A dot-notated path to the value. For example, `'contact_info.phone'` retrieves the `phone` subfield from the `contact_info` options page field.
|
||||
|
||||
**Returns:** The value at the specified path, or an empty string if the path does not exist.
|
||||
|
||||
**How it works:** The function splits the path on `.`, calls `get_field()` with the first segment and `'option'` as the second parameter (which tells ACF to look in the options table), then traverses the remaining segments through the nested array.
|
||||
|
||||
**Usage example:**
|
||||
|
||||
```php
|
||||
// Instead of:
|
||||
$phone = get_field( 'contact_info', 'option' )['phone'];
|
||||
|
||||
// You can write:
|
||||
$phone = getFieldValue( 'contact_info.phone' );
|
||||
```
|
||||
|
||||
This is cleaner and avoids "undefined index" errors when subfields are missing.
|
||||
|
||||
### escEmbeds()
|
||||
|
||||
```php
|
||||
function escEmbeds()
|
||||
```
|
||||
|
||||
**Purpose:** Returns an array of allowed HTML elements and attributes for safely outputting embed content (like YouTube or Vimeo iframes). Use this with `wp_kses()` when rendering embed blocks.
|
||||
|
||||
**Usage example:**
|
||||
|
||||
```php
|
||||
echo wp_kses( $video_embed_html, escEmbeds() );
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SCF/ACF Field Groups
|
||||
|
||||
After creating your block's three files, you need to create an ACF field group in the WordPress admin. This defines the fields that editors fill in when editing the block.
|
||||
|
||||
### Creating a Field Group
|
||||
|
||||
1. In WordPress admin, go to **Custom Fields > Add New**.
|
||||
2. Give the field group a descriptive name (e.g., "Testimonial Fields").
|
||||
3. Add your fields using the ACF interface. Common field types include:
|
||||
- **Text** -- Single-line text input (for headings, names, etc.)
|
||||
- **Textarea** -- Multi-line text (for body copy, quotes, etc.)
|
||||
- **Image** -- Image selector (returns an array with `url`, `alt`, `sizes`, etc.)
|
||||
- **Select** -- Dropdown menu for predefined options
|
||||
- **True/False** -- Checkbox toggle (useful for conditional display logic)
|
||||
- **Repeater** -- Repeatable groups of fields (for lists, slides, etc.)
|
||||
- **Link** -- URL + title + target picker
|
||||
- **WYSIWYG** -- Rich text editor
|
||||
4. Set the **location rule** to: **Block > is equal to > [Your Block Name]**. This tells SCF/ACF to show these fields when editing your block.
|
||||
5. Click **Save** or **Publish**.
|
||||
|
||||
### JSON Sync
|
||||
|
||||
The theme's `ACF` class (in `lib/class-acf.php`) configures custom save and load paths for SCF/ACF JSON:
|
||||
|
||||
```php
|
||||
class ACF {
|
||||
public $path;
|
||||
|
||||
public function __construct() {
|
||||
$this->path = get_stylesheet_directory() . '/acf';
|
||||
add_filter( 'acf/settings/load_json', array( $this, 'loadJson' ) );
|
||||
add_filter( 'acf/settings/save_json', array( $this, 'saveJson' ) );
|
||||
}
|
||||
|
||||
public function saveJson( $path ) {
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
public function loadJson( $paths ) {
|
||||
return array( $this->path );
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This means:
|
||||
|
||||
- When you save a field group in the admin, ACF writes a JSON file to the `acf/` directory in the theme root.
|
||||
- When ACF loads field groups, it reads from the same `acf/` directory.
|
||||
- These JSON files are version-controlled, so field group configurations travel with the codebase and sync across environments.
|
||||
|
||||
**Important:** After saving a field group, you will see a new JSON file appear in the `acf/` directory. Commit this file to version control so other environments receive the field group definition.
|
||||
|
||||
---
|
||||
|
||||
## Parent-Child Block Patterns (InnerBlocks)
|
||||
|
||||
Some blocks act as containers that hold other blocks. WordPress provides `<InnerBlocks />` for this purpose, and SCF/ACF blocks can use it too.
|
||||
|
||||
### Basic InnerBlocks
|
||||
|
||||
To allow any block to be inserted inside your block, simply add `<InnerBlocks />` to your template:
|
||||
|
||||
```php
|
||||
<section <?php echo wp_kses_post( $wrapper ); ?>>
|
||||
<InnerBlocks />
|
||||
</section>
|
||||
```
|
||||
|
||||
This is what the Section block does -- it wraps its children in a container div:
|
||||
|
||||
```php
|
||||
<section <?php echo wp_kses_post( $wrapper ); ?> style="<?php echo esc_attr( $styles ); ?>">
|
||||
<?php if ( $contentWidth === 'full' ) : ?>
|
||||
<InnerBlocks />
|
||||
<?php else : ?>
|
||||
<div class="container content-wrapper">
|
||||
<InnerBlocks />
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
```
|
||||
|
||||
### Restricted InnerBlocks
|
||||
|
||||
You can restrict which blocks are allowed inside your block using the `allowedBlocks` prop on `<InnerBlocks />`. This creates a parent-child relationship where only specific block types can be inserted.
|
||||
|
||||
The Buttons block only allows Button blocks:
|
||||
|
||||
```php
|
||||
<div id="<?php echo esc_attr( $block['id'] ); ?>" <?php echo esc_attr( $wrapper ); ?>>
|
||||
<InnerBlocks className="<?php echo esc_attr( $ibClasses ); ?>" />
|
||||
</div>
|
||||
```
|
||||
|
||||
And its `block.json` enforces this at the registration level:
|
||||
|
||||
```json
|
||||
{
|
||||
"allowedBlocks": ["acf/button"],
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
The Grid block restricts children to Grid Cell blocks:
|
||||
|
||||
```php
|
||||
$allowedBlocks = array( 'acf/grid-cell' );
|
||||
```
|
||||
|
||||
### Enabling InnerBlocks in block.json
|
||||
|
||||
For InnerBlocks to work, you must enable JSX support in your block's `supports` configuration:
|
||||
|
||||
```json
|
||||
"supports": {
|
||||
"jsx": true,
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Without `"jsx": true`, the block editor will not render the InnerBlocks area.
|
||||
|
||||
### Adding Classes to InnerBlocks
|
||||
|
||||
You can pass a `className` prop to `<InnerBlocks />` to style the inner block container:
|
||||
|
||||
```php
|
||||
<InnerBlocks className="<?php echo esc_attr( $ibClasses ); ?>" />
|
||||
```
|
||||
|
||||
Or with allowedBlocks:
|
||||
|
||||
```php
|
||||
<InnerBlocks allowedBlocks={['acf/button']} className="flex gap-4" />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tailwind CSS in Blocks
|
||||
|
||||
This theme uses Tailwind CSS v4 with the `@tailwindcss/cli` package. The configuration is handled entirely through CSS, not through a `tailwind.config.js` file.
|
||||
|
||||
### How Tailwind is Set Up
|
||||
|
||||
The entry point is `styles/theme.css`, which imports Tailwind and all the theme's stylesheets:
|
||||
|
||||
```css
|
||||
/* Tailwind setup */
|
||||
@import "tailwindcss";
|
||||
|
||||
/* Base styles */
|
||||
@import "./base/index.css";
|
||||
@import "./navigation/index.css";
|
||||
|
||||
/* ... more imports ... */
|
||||
|
||||
/* Blocks */
|
||||
@import "./blocks/index.css";
|
||||
|
||||
/* Import Tailwind typography plugin */
|
||||
@plugin "@tailwindcss/typography";
|
||||
```
|
||||
|
||||
### Using Tailwind Classes in Blocks
|
||||
|
||||
You can use Tailwind utility classes directly in your block PHP templates. The build process scans PHP files for class names and includes the corresponding CSS.
|
||||
|
||||
For example, the Homepage Hero block uses Tailwind classes extensively:
|
||||
|
||||
```php
|
||||
$classes = 'homepage-hero mx-break-out bg-black bg-cover bg-no-repeat text-light py-12 lg:py-16 overflow-hidden';
|
||||
```
|
||||
|
||||
And the Buttons block:
|
||||
|
||||
```php
|
||||
$ibClasses = 'flex flex-wrap gap-4 w-full justify-center sm:justify-start';
|
||||
```
|
||||
|
||||
### Whitelisting Editor-Only Classes
|
||||
|
||||
Some Tailwind classes are used only in the WordPress block editor (for example, classes applied through the editor's UI that do not appear anywhere in the theme's PHP or CSS source files). Because Tailwind's content scanning only finds classes in source files, these editor-applied classes would be purged from the final CSS.
|
||||
|
||||
To prevent this, add editor-only classes to `whitelist.php`. This file contains HTML `<span>` elements with the classes that Tailwind should always include:
|
||||
|
||||
```php
|
||||
<!-- whitelist.php -->
|
||||
<span class="grid"></span>
|
||||
<span class="grid-cols-1"></span>
|
||||
<span class="grid-cols-2"></span>
|
||||
<!-- ... more classes ... -->
|
||||
```
|
||||
|
||||
The whitelist is primarily used for grid and layout classes that the Grid block applies dynamically through SCF/ACF field values (since those class names are generated at runtime, not hardcoded in templates).
|
||||
|
||||
### Block-Specific CSS Files
|
||||
|
||||
Each block's CSS file (referenced in `block.json` via `"style": ["file:./block-name.css"]`) is loaded automatically by WordPress only when that block is present on the page. This keeps the CSS payload minimal. You can write both custom CSS and use Tailwind's `@apply` directive in these files.
|
||||
|
||||
Additionally, some blocks share styles that are imported globally. The `styles/blocks/index.css` file imports styles for blocks that need to be available more broadly (such as button styles that apply across multiple blocks).
|
||||
|
||||
---
|
||||
|
||||
## Step-by-Step: Creating a New Block
|
||||
|
||||
This walkthrough demonstrates creating a "Testimonial" block from scratch.
|
||||
|
||||
### 1. Create the Block Directory
|
||||
|
||||
Create a new folder under `views/blocks/` using a lowercase, hyphenated slug:
|
||||
|
||||
```plain
|
||||
views/blocks/testimonial/
|
||||
```
|
||||
|
||||
The directory name becomes the block's slug and must match the filenames of the PHP and CSS files inside it.
|
||||
|
||||
### 2. Create block.json
|
||||
|
||||
Create `views/blocks/testimonial/block.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "acf/testimonial",
|
||||
"title": "Testimonial",
|
||||
"description": "A customer testimonial with quote, name, image, and role.",
|
||||
"style": ["file:./testimonial.css"],
|
||||
"category": "sf-blocks",
|
||||
"icon": "format-quote",
|
||||
"keywords": ["testimonial", "quote", "review"],
|
||||
"acf": {
|
||||
"mode": "preview",
|
||||
"renderTemplate": "testimonial.php"
|
||||
},
|
||||
"supports": {
|
||||
"align": true,
|
||||
"anchor": true,
|
||||
"color": true,
|
||||
"html": false,
|
||||
"jsx": false,
|
||||
"mode": true,
|
||||
"multiple": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Checklist for `block.json`:**
|
||||
|
||||
- `name` starts with `acf/`
|
||||
- `category` is set to `sf-blocks`
|
||||
- `style` references the CSS file with the `file:./` prefix
|
||||
- `acf.renderTemplate` matches the PHP filename exactly
|
||||
- `supports.html` is `false` (ACF blocks should not support HTML editing)
|
||||
- `supports.jsx` is `false` unless the block uses InnerBlocks
|
||||
|
||||
### 3. Create the PHP Template
|
||||
|
||||
Create `views/blocks/testimonial/testimonial.php`:
|
||||
|
||||
```php
|
||||
<?php
|
||||
/**
|
||||
* Block Name: Testimonial
|
||||
*
|
||||
* A customer testimonial with quote, name, and role.
|
||||
*
|
||||
* @package SoloFrameEvo
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
|
||||
$classes = 'testimonial';
|
||||
$wrapper = blockWrapperAttributes( $classes, $is_preview );
|
||||
|
||||
// Retrieve ACF fields
|
||||
$quote = get_field( 'quote' );
|
||||
$name = get_field( 'name' );
|
||||
$role = get_field( 'role' );
|
||||
$image = get_field( 'image' );
|
||||
?>
|
||||
|
||||
<section <?php echo wp_kses_post( $wrapper ); ?>>
|
||||
<blockquote class="testimonial__quote">
|
||||
<?php echo wp_kses_post( $quote ); ?>
|
||||
</blockquote>
|
||||
|
||||
<div class="testimonial__author">
|
||||
<?php if ( $image ) : ?>
|
||||
<img
|
||||
src="<?php echo esc_url( $image['url'] ); ?>"
|
||||
alt="<?php echo esc_attr( $image['alt'] ); ?>"
|
||||
class="testimonial__image"
|
||||
>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="testimonial__info">
|
||||
<cite class="testimonial__name"><?php echo esc_html( $name ); ?></cite>
|
||||
<?php if ( $role ) : ?>
|
||||
<span class="testimonial__role"><?php echo esc_html( $role ); ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
**Checklist for the PHP template:**
|
||||
|
||||
- Always start with `namespace SoloFrameEvo;`
|
||||
- Always call `blockWrapperAttributes( $classes, $is_preview )` and assign it to `$wrapper`
|
||||
- Always echo `$wrapper` inside the root element with `wp_kses_post()`
|
||||
- Use `get_field()` to retrieve ACF field values
|
||||
- Use `getFieldValue()` for nested option fields
|
||||
- Escape all output: `wp_kses_post()` for HTML content, `esc_html()` for plain text, `esc_url()` for URLs, `esc_attr()` for HTML attributes
|
||||
- Use semantic HTML elements (`<section>`, `<blockquote>`, `<cite>`, etc.)
|
||||
|
||||
### 4. Create the CSS File
|
||||
|
||||
Create `views/blocks/testimonial/testimonial.css`. It can start empty or with basic structure:
|
||||
|
||||
```css
|
||||
/* Testimonial block styles */
|
||||
|
||||
.testimonial {
|
||||
/* Block-level styles */
|
||||
}
|
||||
|
||||
.testimonial__quote {
|
||||
/* Quote styles */
|
||||
}
|
||||
|
||||
.testimonial__author {
|
||||
/* Author layout */
|
||||
}
|
||||
|
||||
.testimonial__image {
|
||||
/* Avatar styles */
|
||||
}
|
||||
|
||||
.testimonial__info {
|
||||
/* Author info layout */
|
||||
}
|
||||
|
||||
.testimonial__name {
|
||||
/* Name styles */
|
||||
}
|
||||
|
||||
.testimonial__role {
|
||||
/* Role styles */
|
||||
}
|
||||
```
|
||||
|
||||
If you are using Tailwind utility classes in the PHP template, you may not need much custom CSS. The file still needs to exist and be referenced in `block.json` so WordPress can load it.
|
||||
|
||||
### 5. Create SCF/ACF Field Groups in WordPress Admin
|
||||
|
||||
1. Log in to the WordPress admin dashboard.
|
||||
2. Go to **Custom Fields > Add New**.
|
||||
3. Enter a title: "Testimonial Fields".
|
||||
4. Add the following fields:
|
||||
|
||||
| Field Label | Field Name | Field Type | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| Quote | `quote` | Textarea | The testimonial text |
|
||||
| Name | `name` | Text | The customer's name |
|
||||
| Role | `role` | Text | The customer's role or title (optional) |
|
||||
| Image | `image` | Image | The customer's photo (optional) |
|
||||
|
||||
5. Under **Location**, set the rule: **Block is equal to Testimonial**. ACF will auto-detect the block name from your `block.json`.
|
||||
6. Click **Save** or **Publish**.
|
||||
|
||||
After saving, ACF will write a JSON file to the `acf/` directory in the theme root. This file should be committed to version control.
|
||||
|
||||
### 6. Build and Verify
|
||||
|
||||
If you used Tailwind utility classes in your block template, run the build process:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Then verify the block appears in the editor:
|
||||
|
||||
1. Edit a page in the WordPress block editor.
|
||||
2. Open the inserter and look under **SoloFrame Custom Blocks**.
|
||||
3. You should see "Testimonial" with the quote icon.
|
||||
4. Insert the block and fill in the fields.
|
||||
5. Save and preview the page on the frontend to confirm rendering works correctly.
|
||||
|
||||
---
|
||||
|
||||
## Real-World Examples from This Theme
|
||||
|
||||
### Simple Block: Homepage Hero
|
||||
|
||||
The Homepage Hero is a straightforward block that retrieves ACF fields and renders them with Tailwind classes. It does not use InnerBlocks.
|
||||
|
||||
**Key patterns:**
|
||||
|
||||
- Retrieves multiple SCF/ACF fields with `get_field()`
|
||||
- Conditionally renders sections only when fields have values (`! empty( $heading )`)
|
||||
- Uses Tailwind classes extensively for layout and styling
|
||||
- Handles editor vs. frontend differences for link URLs
|
||||
|
||||
```php
|
||||
// Retrieve ACF fields
|
||||
$heading = get_field( 'heading' );
|
||||
$intro = get_field( 'intro' );
|
||||
$ctas = get_field( 'calls_to_action' );
|
||||
|
||||
$classes = 'homepage-hero mx-break-out bg-black bg-cover bg-no-repeat text-light py-12 lg:py-16 overflow-hidden';
|
||||
$wrapper = blockWrapperAttributes( $classes, $is_preview );
|
||||
?>
|
||||
|
||||
<section <?php echo wp_kses_post( $wrapper ); ?>>
|
||||
<div class="container content-wrapper">
|
||||
<div class="max-w-lg sm:text-center lg:text-left lg:items-center ml-0">
|
||||
<?php if ( ! empty( $heading ) ) : ?>
|
||||
<h1 class="text-4xl lg:text-5xl font-bold leading-tight mb-4">
|
||||
<?php echo esc_html( $heading ); ?>
|
||||
</h1>
|
||||
<?php endif; ?>
|
||||
<!-- ... more content ... -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
### Parent Block with InnerBlocks: Section
|
||||
|
||||
The Section block is a container that wraps its child blocks with optional background styling. It demonstrates conditional rendering based on SCF/ACF fields.
|
||||
|
||||
**Key patterns:**
|
||||
|
||||
- Builds CSS class strings dynamically based on field values
|
||||
- Builds inline `style` strings from field values
|
||||
- Conditionally renders an overlay div
|
||||
- Conditionally wraps InnerBlocks in a container div based on the `content_width` field
|
||||
- Uses `blockWrapperAttributes()` with dynamic classes
|
||||
|
||||
```php
|
||||
// Retrieve ACF fields
|
||||
$contentWidth = get_field( 'content_width' );
|
||||
$isDark = get_field( 'is_dark' );
|
||||
$bgColor = get_field( 'background_color' );
|
||||
$bgImage = get_field( 'background_image' );
|
||||
|
||||
// Build classes dynamically
|
||||
$classes = 'section';
|
||||
|
||||
if ( $contentWidth === 'full' ) {
|
||||
$classes .= ' mx-break-out';
|
||||
}
|
||||
if ( $isDark ) {
|
||||
$classes .= ' dark text-light';
|
||||
}
|
||||
if ( $bgColor || $bgImage ) {
|
||||
$classes .= ' has-background bg-no-repeat';
|
||||
}
|
||||
|
||||
// Build inline styles
|
||||
$styles = '';
|
||||
if ( $bgColor ) {
|
||||
$styles .= "background-color: $bgColor;";
|
||||
}
|
||||
if ( $bgImage ) {
|
||||
$styles .= ' background-image: url(' . esc_url( $bgImage['url'] ) . ');';
|
||||
}
|
||||
|
||||
$wrapper = blockWrapperAttributes( $classes, $is_preview );
|
||||
?>
|
||||
|
||||
<section <?php echo wp_kses_post( $wrapper ); ?> style="<?php echo esc_attr( $styles ); ?>">
|
||||
<?php if ( $ovlColor || $ovlImage ) : ?>
|
||||
<div aria-hidden="true" class="section-overlay absolute inset-0" style="<?php echo esc_attr( $overlayStyles ); ?>"></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ( $contentWidth === 'full' ) : ?>
|
||||
<InnerBlocks />
|
||||
<?php else : ?>
|
||||
<div class="container content-wrapper">
|
||||
<InnerBlocks />
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
```
|
||||
|
||||
### Restricted Parent Block: Buttons
|
||||
|
||||
The Buttons block is a container that only allows Button blocks as children. It enforces this restriction through both `block.json` and the template.
|
||||
|
||||
**Key patterns:**
|
||||
|
||||
- Uses `allowedBlocks` in `block.json` to restrict children to `acf/button`
|
||||
- Sets `"jsx": true` in `supports` to enable InnerBlocks
|
||||
- Passes Tailwind classes to InnerBlocks via the `className` prop
|
||||
- Sets `"align": false` and `"color": false` since styling comes from child Button blocks
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "acf/buttons",
|
||||
"allowedBlocks": ["acf/button"],
|
||||
"supports": {
|
||||
"align": false,
|
||||
"jsx": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```php
|
||||
$ibClasses = 'flex flex-wrap gap-4 w-full justify-center sm:justify-start';
|
||||
$classes = 'align-with-content my-[1.2em]';
|
||||
$wrapper = blockWrapperAttributes( $classes, $is_preview );
|
||||
?>
|
||||
|
||||
<div id="<?php echo esc_attr( $block['id'] ); ?>" <?php echo esc_attr( $wrapper ); ?>>
|
||||
<InnerBlocks className="<?php echo esc_attr( $ibClasses ); ?>" />
|
||||
</div>
|
||||
```
|
||||
|
||||
Note: The Buttons block uses `esc_attr()` instead of `wp_kses_post()` for the wrapper because the `<div>` tag is not inside a `<section>` -- both approaches are valid, but `wp_kses_post()` is preferred for the main block wrapper.
|
||||
|
||||
### Dynamic Parent Block: Grid
|
||||
|
||||
The Grid block builds CSS classes dynamically from ACF field values (columns, breakpoints, gaps). This is a case where runtime-generated class names need to be whitelisted.
|
||||
|
||||
**Key patterns:**
|
||||
|
||||
- Dynamically constructs Tailwind class names from field values (e.g., `'grid-cols-' . get_field( 'columns' )`)
|
||||
- Uses `$block['anchor']` and `$block['className']` for editor-set attributes
|
||||
- These dynamic class names are added to `whitelist.php` so Tailwind includes them in the build
|
||||
|
||||
```php
|
||||
$allowedBlocks = array( 'acf/grid-cell' );
|
||||
$gridClasses = 'grid grid-cols-' . get_field( 'columns' );
|
||||
|
||||
// Add breakpoint-specific column classes
|
||||
if ( $colBPs ) {
|
||||
foreach ( $colBPs as $bp ) {
|
||||
$gridClasses .= ' ' . $bp . ':grid-cols-' . get_field( 'columns_' . $bp );
|
||||
}
|
||||
}
|
||||
|
||||
// Add gap classes
|
||||
if ( $gapX ) {
|
||||
$gridClasses .= ' gap-x-' . $gapX;
|
||||
}
|
||||
if ( $gapY ) {
|
||||
$gridClasses .= ' gap-y-' . $gapY;
|
||||
}
|
||||
|
||||
$classes = trim( $className . ' ' . $gridClasses );
|
||||
?>
|
||||
|
||||
<div id="<?php echo esc_attr( $anchor ); ?>">
|
||||
<InnerBlocks className="<?php echo esc_attr( $classes ); ?>" />
|
||||
</div>
|
||||
```
|
||||
|
||||
Because the class names like `grid-cols-3` and `md:grid-cols-4` are generated at runtime from field values (not hardcoded in PHP templates), Tailwind cannot detect them through content scanning. That is why `whitelist.php` explicitly lists all possible grid column and gap classes.
|
||||
|
||||
### Block Using Global Fields: Contact Info
|
||||
|
||||
The Contact Info block demonstrates how to access ACF options page data (Global Fields) using `getFieldValue()`.
|
||||
|
||||
**Key patterns:**
|
||||
|
||||
- Uses `getFieldValue( 'contact_info.phone' )` to retrieve the options page field group, then accesses sub-fields with array syntax
|
||||
- Combines static content from Global Fields with dynamic InnerBlocks content (a contact form)
|
||||
|
||||
```php
|
||||
namespace SoloFrameEvo;
|
||||
|
||||
$classes = 'contact-info';
|
||||
$wrapper = blockWrapperAttributes( $classes, $is_preview );
|
||||
?>
|
||||
|
||||
<section <?php echo wp_kses_post( $wrapper ); ?>>
|
||||
<div class="flex flex-col lg:flex-row">
|
||||
<div class="w-full lg:w-1/2 p-6">
|
||||
<h2 class="text-2xl font-bold mb-4">Contact Information</h2>
|
||||
<p><?php echo wp_kses_post( getFieldValue( 'contact_info.address' ) ); ?></p>
|
||||
<p><a href="mailto:<?php echo esc_html( getFieldValue( 'contact_info.email' ) ); ?>">
|
||||
<?php echo esc_html( getFieldValue( 'contact_info.email' ) ); ?>
|
||||
</a></p>
|
||||
<p><a href="tel:<?php echo esc_html( getFieldValue( 'contact_info.phone' ) ); ?>">
|
||||
<?php echo esc_html( getFieldValue( 'contact_info.phone' ) ); ?>
|
||||
</a></p>
|
||||
</div>
|
||||
|
||||
<div class="w-full lg:w-1/2 p-6">
|
||||
<InnerBlocks />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls and Best Practices
|
||||
|
||||
### Do
|
||||
|
||||
- **Always use `namespace SoloFrameEvo;`** at the top of every block PHP template. Without it, helper functions like `blockWrapperAttributes()` and `getFieldValue()` will not be available.
|
||||
- **Always use `blockWrapperAttributes()`** for the root element's attributes. Never call `get_block_wrapper_attributes()` directly.
|
||||
- **Always escape output.** Use `wp_kses_post()` for HTML content, `esc_html()` for plain text, `esc_url()` for URLs, and `esc_attr()` for HTML attribute values.
|
||||
- **Always set `category` to `sf-blocks`** in `block.json` so your block appears under "VDI Custom Blocks" in the editor.
|
||||
- **Always prefix `name` with `acf/`** in `block.json` (e.g., `"acf/testimonial"`, not just `"testimonial"`).
|
||||
- **Always set `supports.html` to `false`** in `block.json` for SCF/ACF blocks. SCF/ACF blocks use PHP templates, not HTML editing.
|
||||
- **Always set `supports.jsx` to `true`** if your block uses `<InnerBlocks />`. Without this, the InnerBlocks area will not render.
|
||||
- **Commit ACF JSON files** from the `acf/` directory to version control after creating field groups.
|
||||
- **Use semantic HTML elements** as block wrappers (`<section>`, `<article>`, `<aside>`, `<nav>`, etc.) instead of generic `<div>` elements where appropriate.
|
||||
- **Use BEM-like naming** for custom CSS classes: `.block-name`, `.block-name__element`, `.block-name__element--modifier`.
|
||||
- **Add dynamic Tailwind classes to `whitelist.php`** if they are generated from ACF field values rather than hardcoded in templates.
|
||||
|
||||
### Do Not
|
||||
|
||||
- **Do not call `get_block_wrapper_attributes()` directly.** Always use `blockWrapperAttributes()` instead. Direct calls cause rendering issues in the editor.
|
||||
- **Do not edit the `boilerplate` directory.** It is excluded from registration and serves as a reference template. Copy its files to a new directory instead.
|
||||
- **Do not forget to create the CSS file** referenced in `block.json`. Even if the file is empty, WordPress needs it to exist. If the file is missing, WordPress may throw an error when loading the block.
|
||||
- **Do not use `'option'` directly with `get_field()` for nested values without null checking.** Prefer `getFieldValue()` which handles missing values gracefully.
|
||||
- **Do not set `supports.multiple` to `false`** unless the block truly must be unique per page (like a homepage hero). Most blocks should allow multiple instances.
|
||||
- **Do not hardcode editor-only Tailwind classes in PHP templates** without adding them to `whitelist.php`. If a class only appears in the editor's UI (like grid column classes set via SCF/ACF fields), Tailwind will not include it in the build.
|
||||
- **Do not use the `style` attribute on the block's root element alongside `blockWrapperAttributes()` for background colors** unless the block specifically needs it. WordPress's built-in color supports (enabled via `supports.color`) handle this automatically.
|
||||
|
||||
### Debugging Tips
|
||||
|
||||
- If a block does not appear in the editor, check that `block.json` is valid JSON and that the `name` field starts with `acf/`.
|
||||
- If ACF fields do not show up when editing a block, verify the field group's location rule is set to "Block is equal to [Your Block Name]".
|
||||
- If styles are not loading, confirm the `style` array in `block.json` uses the `file:./` prefix and the CSS filename matches exactly.
|
||||
- If Tailwind classes are not applying on the frontend, run `npm run build` and check that the classes are either in your templates or in `whitelist.php`.
|
||||
- If InnerBlocks are not rendering, confirm `"jsx": true` is set in the block's `supports` configuration.
|
||||
@@ -1,574 +0,0 @@
|
||||
# Getting Started with SoloFrame Evo
|
||||
|
||||
This guide walks you through setting up a local development environment for the SoloFrame Evo WordPress theme from scratch. It covers prerequisites, installation, configuration, and common development workflows.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Overview](#overview)
|
||||
2. [Prerequisites](#prerequisites)
|
||||
3. [Setting Up a Local WordPress Environment](#setting-up-a-local-wordpress-environment)
|
||||
4. [Installing the Theme](#installing-the-theme)
|
||||
5. [Environment Configuration](#environment-configuration)
|
||||
6. [Building Assets](#building-assets)
|
||||
7. [Activating the Theme](#activating-the-theme)
|
||||
8. [Development Workflow](#development-workflow)
|
||||
9. [Project Architecture](#project-architecture)
|
||||
10. [Creating Custom SCF/ACF Blocks](#creating-custom-scfacf-blocks)
|
||||
11. [Testing](#testing)
|
||||
12. [Code Quality](#code-quality)
|
||||
13. [Troubleshooting](#troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
SoloFrame Evo is a minimal WordPress theme designed as a starting point for custom theme development. It uses a modern stack:
|
||||
|
||||
- **Tailwind CSS v4** for utility-first styling, compiled via the Tailwind CLI
|
||||
- **ACF Pro** for custom field management and block registration
|
||||
- **WordPress Script Modules** (`wp_enqueue_script_module`) for JavaScript, requiring WordPress 6.5+
|
||||
- **BrowserSync** for live-reloading during development
|
||||
- **Playwright** for end-to-end accessibility testing
|
||||
- **PHP_CodeSniffer** with WordPress coding standards for linting
|
||||
|
||||
The theme intentionally avoids heavyweight frameworks. Every PHP file in `lib/` is loaded automatically via `functions.php`, which means you add a new file to `lib/` and it is included -- no manual `require` statements needed.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you begin, make sure the following tools are installed on your machine.
|
||||
|
||||
| Tool | Minimum Version | Why It Is Needed |
|
||||
| ------------- | -------------------------- | ---------------------------------------------------------------------------------------------------- |
|
||||
| Node.js | 22+ | Tailwind CSS v4 CLI requires a modern Node runtime. Older versions will fail during `npm run build`. |
|
||||
| npm | Latest (bundled with Node) | Package management for JavaScript dependencies and build scripts. |
|
||||
| PHP | 8.0+ | WordPress core requirement and theme compatibility. |
|
||||
| Composer | 2.x | Installs PHP_CodeSniffer with WordPress coding standards for linting. |
|
||||
| WordPress | 6.5+ | The theme uses `wp_enqueue_script_module()`, which was introduced in WordPress 6.5. |
|
||||
| ACF Pro | Latest | All custom blocks depend on ACF Pro. Blocks will not register without it. |
|
||||
|
||||
### Checking Your Versions
|
||||
|
||||
Run these commands to verify your environment:
|
||||
|
||||
```bash
|
||||
node --version # Should be v22 or higher
|
||||
npm --version # Any recent version is fine
|
||||
php --version # Should be 8.0 or higher
|
||||
composer --version # Should be 2.x
|
||||
```
|
||||
|
||||
If any of these commands fail, install the missing tool before proceeding.
|
||||
|
||||
---
|
||||
|
||||
## Setting Up a Local WordPress Environment
|
||||
|
||||
You need a running WordPress instance before you can activate or test the theme. Choose one of these options based on your preference and operating system.
|
||||
|
||||
### Option A: Local by Flywheel (Recommended for macOS/Windows)
|
||||
|
||||
Local by Flywheel (often just called "Local") is the easiest way to get a WordPress site running on macOS or Windows.
|
||||
|
||||
1. Download and install [Local](https://localwp.com/).
|
||||
2. Click **Create a new site** and follow the prompts.
|
||||
3. Choose **Preferred** environment (NGINX, PHP 8.x, MySQL 8.x).
|
||||
4. Once the site is created, note the local URL (e.g., `https://vdi-starter.local`).
|
||||
5. Click **WP Admin** to open the WordPress dashboard.
|
||||
|
||||
The site URL from Local is what you will put in your `.env` file as `LOCALHOST_URL`.
|
||||
|
||||
### Option B: DevKinsta (Windows/macOS/Linux)
|
||||
|
||||
DevKinsta is Kinsta's free local development tool. It is a good choice if your production site is hosted on Kinsta because you can push/pull databases directly.
|
||||
|
||||
1. Download and install [DevKinsta](https://kinsta.com/devkinsta/).
|
||||
2. Create a new custom WordPress site.
|
||||
3. Note the local URL (typically `http://localhost:xxxxx`).
|
||||
|
||||
### Option C: Docker (Any OS)
|
||||
|
||||
Docker gives you the most control and works on any operating system, but it requires more manual setup.
|
||||
|
||||
1. Install [Docker Desktop](https://www.docker.com/products/docker-desktop/).
|
||||
2. Use the official `wordpress` Docker image with a custom theme mount. A minimal `docker-compose.yml` might look like:
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
services:
|
||||
db:
|
||||
image: mysql:8.0
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: wordpress
|
||||
MYSQL_DATABASE: wordpress
|
||||
MYSQL_USER: wordpress
|
||||
MYSQL_PASSWORD: wordpress
|
||||
volumes:
|
||||
- db_data:/var/lib/mysql
|
||||
|
||||
wordpress:
|
||||
image: wordpress:latest
|
||||
ports:
|
||||
- "8080:80"
|
||||
environment:
|
||||
WORDPRESS_DB_HOST: db:3306
|
||||
WORDPRESS_DB_USER: wordpress
|
||||
WORDPRESS_DB_PASSWORD: wordpress
|
||||
WORDPRESS_DB_NAME: wordpress
|
||||
volumes:
|
||||
- ./themes/SoloFrame-Evo:/var/www/html/wp-content/themes/SoloFrame-Evo
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
volumes:
|
||||
db_data:
|
||||
```
|
||||
|
||||
3. Run `docker compose up -d` and visit `http://localhost:8080` to complete the WordPress installation wizard.
|
||||
|
||||
---
|
||||
|
||||
## Installing the Theme
|
||||
|
||||
### Step 1: Clone the Repository
|
||||
|
||||
```bash
|
||||
git clone https://github.com/ksolomon/SoloFrame-Evo.git
|
||||
cd SoloFrame-Evo
|
||||
```
|
||||
|
||||
If you are contributing to an existing project, clone it into your local WordPress `wp-content/themes/` directory so WordPress can detect it:
|
||||
|
||||
```bash
|
||||
cd /path/to/your/local-wp-site/wp-content/themes/
|
||||
git clone https://github.com/ksolomon/SoloFrame-Evo.git
|
||||
```
|
||||
|
||||
### Step 2: Install PHP Dependencies
|
||||
|
||||
```bash
|
||||
composer install
|
||||
```
|
||||
|
||||
This installs PHP_CodeSniffer and the WordPress Coding Standards (WPCS) ruleset. These are dev dependencies used for linting, not runtime dependencies, but they are required for `composer lint` and `composer fix` to work.
|
||||
|
||||
### Step 3: Install JavaScript Dependencies
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
This installs the frontend build toolchain: Tailwind CSS v4 and its CLI, BrowserSync, Playwright, dotenv, and other utilities. Tailwind v4 uses the `@tailwindcss/cli` package directly. No `tailwind.config.js` file is needed -- configuration lives in `styles/theme.css`.
|
||||
|
||||
## Environment Configuration
|
||||
|
||||
Copy the example environment file and edit it:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Open `.env` and set the two variables:
|
||||
|
||||
| Variable | Description | Example Value |
|
||||
| ------------------- | ----------------------------------------------------------------------------------- | ------------------------------ |
|
||||
| `LOCALHOST_URL` | The full URL of your local WordPress site, including the scheme (`http` or `https`) | `https://soloframe-evo.local` |
|
||||
| `BROWSERSYNC_PORT` | The port BrowserSync should listen on. Defaults to `5000` if not set. | `5000` |
|
||||
|
||||
The `LOCALHOST_URL` must match exactly what your local WordPress environment responds to. If you are using Local by Flywheel with SSL enabled, include `https://`. If you are using Docker on port 8080, use `http://localhost:8080`.
|
||||
|
||||
BrowserSync proxies this URL and injects a live-reload script, so any change you make to PHP templates, CSS, or JS files will automatically refresh the browser.
|
||||
|
||||
## Building Assets
|
||||
|
||||
Before activating the theme, compile the Tailwind CSS so the theme has its stylesheet:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
This runs:
|
||||
|
||||
```bash
|
||||
npx @tailwindcss/cli -i ./styles/theme.css -o ./static/dist/theme.css --optimize
|
||||
```
|
||||
|
||||
It reads `styles/theme.css` (the entry point that imports all sub-stylesheets and the Tailwind framework), compiles all utility classes, and writes the output to `static/dist/theme.css`. The `--optimize` flag minifies the output for production.
|
||||
|
||||
The `static/dist/` directory is where WordPress loads the compiled stylesheet from. The theme's `Enqueue` class (`lib/class-enqueue.php`) checks for `static/dist/theme.css` and enqueues it with a file-mtime version string for cache busting.
|
||||
|
||||
> **Why build before activating?** If the compiled CSS file does not exist, the theme will still activate, but the frontend will be completely unstyled. The `Enqueue` class only enqueues `theme.css` if the file exists on disk -- it does not fail gracefully with a fallback, it simply does not load any stylesheet.
|
||||
|
||||
---
|
||||
|
||||
## Activating the Theme
|
||||
|
||||
Log into your local WordPress admin dashboard and navigate to **Appearance > Themes**. You should see **VDI Starter v5** listed. Click **Activate**.
|
||||
|
||||
> **CRITICAL WARNING: What happens on activation**
|
||||
>
|
||||
> When this theme is activated on a fresh WordPress install, `lib/activation.php` automatically performs the following actions. Read this list carefully before activating on an existing site:
|
||||
>
|
||||
> - **Creates 4 default pages:** Home, News, Page Not Found (Error 404), and Contact Us.
|
||||
> - **Sets WordPress to use a static front page:** Home becomes the front page, News becomes the posts page. This overrides any existing "Reading Settings".
|
||||
> - **Deletes the default "Hello World" post** (ID 1) and the **sample page** (ID 2). These are trashed permanently (`wp_delete_post` with `$force_delete = true`).
|
||||
> - **Installs 4 plugins** from external URLs and selectively activates them:
|
||||
> 1. **Secure Custom Fields** -- installed and activated (from WordPress.org)
|
||||
> 2. **Simple History** -- installed and activated (from WordPress.org)
|
||||
> 3. **The SEO Framework** -- installed and activated (from WordPress.org)
|
||||
> 4. **Better Search Replace** -- installed and activated (from WordPress.org)
|
||||
> - **Writes an installation log** to `wp-content/mu-plugin-install.log`.
|
||||
>
|
||||
> **Do NOT activate this theme on an existing production site without reviewing `lib/activation.php` first.** The activation routine is designed for fresh installs and will modify pages, settings, and plugin state without confirmation.
|
||||
|
||||
### Optional: Import Sample Content
|
||||
|
||||
If you want test content (posts, pages, etc.) to work with, the repository includes a WordPress XML export file:
|
||||
|
||||
```bash
|
||||
# In WordPress admin: Tools > Import > WordPress > Run Importer
|
||||
# Upload: content/basic-wp-test-content.xml
|
||||
```
|
||||
|
||||
This gives you sample pages and posts to verify that templates and blocks render correctly.
|
||||
|
||||
---
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Starting the Dev Server
|
||||
|
||||
```bash
|
||||
npm run start
|
||||
# or equivalently:
|
||||
npm run watch
|
||||
```
|
||||
|
||||
This runs `bin/.watch.js`, which starts BrowserSync and watches for file changes. When you edit a `.php` or `.css` file, BrowserSync:
|
||||
|
||||
1. Detects the change.
|
||||
2. Recompiles Tailwind CSS (using the same `@tailwindcss/cli` command, but without `--optimize` so it is faster).
|
||||
3. Reloads the browser.
|
||||
|
||||
When you edit a `.js` file in `static/js/`, BrowserSync injects the updated script without a full page reload (hot injection).
|
||||
|
||||
The dev server is accessible at the `LOCALHOST_URL` you configured, proxied through the `BROWSERSYNC_PORT`. For example, if your `.env` has `LOCALHOST_URL=http://soloframe-evo.local` and `BROWSERSYNC_PORT=5000`, your dev URL is `http://soloframe-evo.local` with BrowserSync overlay on port 5000.
|
||||
|
||||
### Building for Production
|
||||
|
||||
Before deploying or pushing changes that affect styles, always run:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
This compiles Tailwind CSS with `--optimize` enabled, which minifies the output and removes unused styles. The result is written to `static/dist/theme.css`.
|
||||
|
||||
> **Why not use `npm run watch` output for production?** The watch mode compiles Tailwind without optimization for speed. Production builds are significantly smaller because `--optimize` removes unused utility classes and minifies the CSS.
|
||||
|
||||
---
|
||||
|
||||
## Project Architecture
|
||||
|
||||
Understanding the directory layout helps you know where to find things and where to put new files.
|
||||
|
||||
```plain
|
||||
SoloFrame-Evo/
|
||||
├── acf/ # SCF/ACF Pro field group JSON (auto-synced)
|
||||
│ └── group_*.json # One file per field group
|
||||
├── bin/
|
||||
│ ├── .watch.js # BrowserSync dev server script
|
||||
│ └── .utils.js # Shared build utilities (Tailwind compilation)
|
||||
├── content/
|
||||
│ └── basic-wp-test-content.xml # Sample content for testing
|
||||
├── docs/ # Documentation (this guide lives here)
|
||||
├── lib/ # PHP utility classes (auto-loaded)
|
||||
│ ├── activation.php # Theme activation routine (pages, plugins, settings)
|
||||
│ ├── class-acf.php # SCF/ACF JSON load/save path configuration
|
||||
│ ├── class-breadcrumbs.php # Breadcrumb navigation helper
|
||||
│ ├── class-enqueue.php # Frontend/backend/editor asset enqueueing
|
||||
│ ├── class-menuitems.php # Custom menu item handling
|
||||
│ ├── class-resources.php # Resource management
|
||||
│ ├── extras.php # Miscellaneous helper functions
|
||||
│ ├── helpers.php # Template helper functions
|
||||
│ ├── hooks.php # Theme hooks (menus, sidebars, cleanup, SVG uploads)
|
||||
│ ├── search-features.php # Enhanced search functionality
|
||||
│ └── show-template.php # Template debugging (shows which template is loaded)
|
||||
├── static/
|
||||
│ ├── dist/
|
||||
│ │ └── theme.css # Compiled Tailwind output (generated, do not edit)
|
||||
│ ├── img/ # Theme images
|
||||
│ └── js/
|
||||
│ ├── admin.js # Backend/editor JavaScript
|
||||
│ ├── theme.js # Frontend JavaScript (loaded as a script module)
|
||||
│ ├── components/ # JS components loaded as script modules
|
||||
│ │ ├── backToTop.js
|
||||
│ │ ├── button.js
|
||||
│ │ ├── GetHeaderHeight.js
|
||||
│ │ ├── Navigation.js
|
||||
│ │ └── TagExternalLinks.js
|
||||
│ └── modules/ # JS modules
|
||||
├── styles/
|
||||
│ ├── theme.css # Tailwind entry point (imports all sub-stylesheets)
|
||||
│ ├── base/ # Base/reset styles and break-out utilities
|
||||
│ ├── backend/ # Admin and editor styles (admin.css, editor.css)
|
||||
│ ├── blocks/ # Per-block styles
|
||||
│ ├── components/ # Reusable component styles
|
||||
│ ├── fonts/ # Icon font (Lineicons)
|
||||
│ └── navigation/ # Navigation styles
|
||||
├── views/
|
||||
│ └── blocks/ # ACF block definitions (block.json + template)
|
||||
│ ├── accordion/
|
||||
│ ├── boilerplate/ # Starter template for new blocks (excluded from registration)
|
||||
│ ├── button/
|
||||
│ ├── buttons/
|
||||
│ ├── contact-info/
|
||||
│ ├── grid/
|
||||
│ ├── grid-cell/
|
||||
│ ├── homepage-hero/
|
||||
│ ├── media-text/
|
||||
│ ├── media-text-innerblocks/
|
||||
│ ├── page-children/
|
||||
│ └── section/
|
||||
├── tests/
|
||||
│ └── site-a11y.spec.js # Playwright accessibility tests
|
||||
├── .env.example # Environment variable template
|
||||
├── composer.json # PHP dependencies (PHPCS + WPCS)
|
||||
├── functions.php # Theme bootstrap (loads all lib/*.php files, registers ACF blocks)
|
||||
├── package.json # Node dependencies and build scripts
|
||||
├── playwright.config.js # Playwright test configuration
|
||||
├── style.css # WordPress theme metadata header
|
||||
├── theme.json # WordPress theme.json (colors, typography, layout settings)
|
||||
└── whitelist.php # Playwright whitelist for testing
|
||||
```
|
||||
|
||||
### How Auto-Loading Works
|
||||
|
||||
`functions.php` loads every PHP file in `lib/` using a glob pattern:
|
||||
|
||||
```php
|
||||
foreach ( glob( __DIR__ . '/lib/*.php' ) as $filename ) {
|
||||
include_once $filename;
|
||||
}
|
||||
```
|
||||
|
||||
This means adding a new file to `lib/` automatically includes it. No manual `require` statements are needed. However, be aware that files are loaded in alphabetical order. If one file depends on something defined in another, you may need to rename files with numeric prefixes to control load order.
|
||||
|
||||
### How SCF/ACF Blocks Are Registered
|
||||
|
||||
The `regACFBlocks()` function in `functions.php` scans the `views/blocks/` directory at runtime:
|
||||
|
||||
```php
|
||||
function regACFBlocks() {
|
||||
define( 'BLOCKS_DIR', get_stylesheet_directory() . '/views/blocks' );
|
||||
if ( is_dir( BLOCKS_DIR ) ) {
|
||||
foreach ( scandir( BLOCKS_DIR ) as $folder ) {
|
||||
if ( ( '.' !== $folder && '..' !== $folder && 'boilerplate' !== $folder ) && is_dir( BLOCKS_DIR . '/' . $folder ) ) {
|
||||
register_block_type( BLOCKS_DIR . '/' . $folder );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
add_action( 'init', __NAMESPACE__ . '\\regACFBlocks', 5 );
|
||||
```
|
||||
|
||||
Each subdirectory that contains a `block.json` file is registered as a Gutenberg block. The `boilerplate` directory is explicitly excluded -- it is a starter template for creating new blocks, not a block itself.
|
||||
|
||||
### How Stylesheets Are Organized
|
||||
|
||||
The Tailwind entry point is `styles/theme.css`. It imports sub-stylesheets using CSS `@import` directives:
|
||||
|
||||
```css
|
||||
@import "tailwindcss"; /* Tailwind v4 framework */
|
||||
@import "./base/index.css"; /* Base styles */
|
||||
@import "./navigation/index.css"; /* Navigation styles */
|
||||
@import "./fonts/lineicons.css"; /* Icon font */
|
||||
@import "./base/break-out.css"; /* Break-out utility */
|
||||
@import "./components/index.css"; /* Component styles */
|
||||
@import "./blocks/index.css"; /* Block-specific styles */
|
||||
@plugin "@tailwindcss/typography"; /* Tailwind Typography plugin */
|
||||
```
|
||||
|
||||
When you create a new block or component, add its styles to the appropriate subdirectory and make sure it is imported through the corresponding `index.css` file.
|
||||
|
||||
---
|
||||
|
||||
## Creating Custom SCF/ACF Blocks
|
||||
|
||||
To create a new ACF block, use the `boilerplate` directory as a starting point:
|
||||
|
||||
1. Copy `views/blocks/boilerplate/` to a new directory with your block name (use lowercase, hyphen-separated):
|
||||
|
||||
```bash
|
||||
cp -r views/blocks/boilerplate views/blocks/my-block
|
||||
```
|
||||
|
||||
2. Edit `views/blocks/my-block/block.json`:
|
||||
- Change `"name"` to `"acf/my-block"`
|
||||
- Change `"title"` to a human-readable name like `"My Block"`
|
||||
- Update `"description"`, `"icon"`, and `"keywords"` as appropriate
|
||||
- If this block should only be nested inside another block, add a `"parent"` array (see `button/block.json` for an example)
|
||||
|
||||
3. Edit the PHP template file (`my-block.php`) to render your block's HTML. ACF fields are available via `get_fields()`.
|
||||
|
||||
4. If the block needs ACF field groups, create them in the WordPress admin under **Custom Fields > Field Groups** and associate them with the block. ACF will save the field group JSON to the `acf/` directory, which you should commit to version control.
|
||||
|
||||
5. Add block-specific styles in `my-block.css`.
|
||||
|
||||
The block will be automatically discovered and registered on the next page load because `regACFBlocks()` scans the directory on every `init` hook.
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### Playwright Accessibility Tests
|
||||
|
||||
The theme includes a Playwright configuration for end-to-end testing, with accessibility checks powered by `@axe-core/playwright`.
|
||||
|
||||
To set up Playwright for the first time:
|
||||
|
||||
```bash
|
||||
npx playwright install
|
||||
```
|
||||
|
||||
This downloads the browser binaries. Then run the tests:
|
||||
|
||||
```bash
|
||||
npx playwright test
|
||||
```
|
||||
|
||||
The test suite lives in `tests/site-a11y.spec.js`. By default, Playwright is configured to test against Chromium only (see `playwright.config.js`). You can uncomment other browser projects (Firefox, WebKit, mobile viewports) as needed.
|
||||
|
||||
If you want to initialize Playwright from scratch with all browsers:
|
||||
|
||||
```bash
|
||||
npm init playwright@latest --yes "--" . '--quiet' '--browser=chromium' '--browser=firefox' '--browser=webkit' '--lang=js'
|
||||
```
|
||||
|
||||
> **Note:** Playwright tests need a running WordPress instance to test against. Make sure your local environment is up and the theme is activated before running tests. You may also need to set a `baseURL` in `playwright.config.js` to point to your local site.
|
||||
|
||||
---
|
||||
|
||||
## Code Quality
|
||||
|
||||
### PHP Linting
|
||||
|
||||
Run PHP_CodeSniffer against the WordPress coding standards:
|
||||
|
||||
```bash
|
||||
composer lint
|
||||
```
|
||||
|
||||
This runs `phpcs` with the ruleset defined in `.phpcs.xml` and writes results to `phpcs-results.txt`. Fix violations manually, or use the auto-fixer:
|
||||
|
||||
```bash
|
||||
composer fix
|
||||
```
|
||||
|
||||
This runs `phpcbf` (PHP Code Beautifier and Fixer) to automatically correct fixable violations. Always run `composer lint` after `composer fix` to verify that remaining issues are intentional.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### CSS Is Not Compiling or Styles Are Missing
|
||||
|
||||
**Symptom:** The frontend loads but looks completely unstyled, or changes you made to Tailwind classes are not appearing.
|
||||
|
||||
**Solution:** Run `npm run build`. The theme loads CSS from `static/dist/theme.css`, which is a compiled file. If this file does not exist (e.g., after a fresh clone), nothing will be styled. If you added new utility classes and they are not appearing, the file may be stale -- rebuild it.
|
||||
|
||||
The compilation pipeline is: `styles/theme.css` (entry point with `@import` directives) --> Tailwind CLI --> `static/dist/theme.css` (output).
|
||||
|
||||
### ACF Blocks Are Not Appearing in the Editor
|
||||
|
||||
**Symptom:** The block inserter in the Gutenberg editor does not show custom blocks like "Homepage Hero" or "Section".
|
||||
|
||||
**Solution:** SCF (installed by default on theme activation) or ACF Pro must be installed and activated. Blocks are registered by scanning `views/blocks/*/block.json` on the `init` hook. Without SCF/ACF Pro, `register_block_type()` still runs, but blocks require the SCF/ACF plugin to provide field data and rendering.
|
||||
|
||||
Also check that:
|
||||
|
||||
- The block directory contains a valid `block.json` file.
|
||||
- The block directory is not named `boilerplate` (this is excluded by design).
|
||||
- SCF/ACF Pro is activated (not just installed).
|
||||
|
||||
### Menus Are Not Rendering
|
||||
|
||||
**Symptom:** Navigation menus appear empty or show a fallback message.
|
||||
|
||||
**Solution:** The theme registers three menu locations in `lib/hooks.php`:
|
||||
|
||||
- **Main Navigation** (`main_navigation`)
|
||||
- **Auxiliary Navigation** (`aux_navigation`)
|
||||
- **Footer Navigation** (`footer_navigation`)
|
||||
|
||||
You must assign menus to these locations manually. Go to **Appearance > Menus** in WordPress admin, create a menu, and check the appropriate "Display Location" checkbox.
|
||||
|
||||
### JavaScript Is Not Loading
|
||||
|
||||
**Symptom:** Interactive features like the mobile navigation toggle or back-to-top button do not work.
|
||||
|
||||
**Solution:** The theme uses `wp_enqueue_script_module()`, which was introduced in WordPress 6.5. If you are running an older version of WordPress, script modules will not be loaded. Verify your WordPress version is 6.5 or higher.
|
||||
|
||||
You can check by looking at the page source for `<script type="module">` tags. If they are absent, your WordPress version likely does not support script modules.
|
||||
|
||||
### BrowserSync Is Not Connecting
|
||||
|
||||
**Symptom:** Running `npm run watch` starts BrowserSync but the browser does not auto-refresh on file changes.
|
||||
|
||||
**Solution:** Check these common issues:
|
||||
|
||||
1. **Wrong `LOCALHOST_URL`:** The URL in `.env` must exactly match your local WordPress site URL, including the scheme (`http` vs `https`) and any port numbers. Open the URL directly in a browser first to confirm it loads.
|
||||
|
||||
2. **Port conflict:** If another service is using port 5000, BrowserSync will fail to start or behave unpredictably. Change `BROWSERSYNC_PORT` in `.env` to an available port (e.g., `3000` or `8080`).
|
||||
|
||||
3. **SSL certificates:** If your local site uses HTTPS with a self-signed certificate (common with Local by Flywheel), BrowserSync may reject the connection. The `bin/.watch.js` configuration does not currently set `https: true` or `cert`/`key` paths, so BrowserSync proxies over HTTP by default. If your WordPress site forces HTTPS, you may need to adjust the BrowserSync configuration in `bin/.watch.js`.
|
||||
|
||||
4. **Firewall:** Ensure your firewall allows connections on the BrowserSync port.
|
||||
|
||||
### Plugin Installation Failures on Theme Activation
|
||||
|
||||
**Symptom:** Some or all plugins fail to install when the theme is activated.
|
||||
|
||||
**Solution:** The activation routine in `lib/activation.php` downloads plugin ZIP files from external URLs. Failures can occur if:
|
||||
|
||||
- Your local environment does not have internet access.
|
||||
- The download URLs have changed (check the URLs in the source code).
|
||||
- The `WP_CONTENT_DIR` directory is not writable.
|
||||
- WordPress filesystem credentials are required but not available (the routine forces `direct` filesystem access, which may not work on all server configurations).
|
||||
|
||||
Check `wp-content/mu-plugin-install.log` for detailed error messages. Each plugin installation step logs success or failure with a timestamp.
|
||||
|
||||
### Tailwind Utility Classes Not Appearing in Output
|
||||
|
||||
**Symptom:** You added a Tailwind class to a template (e.g., `bg-blue-500`) but it does not appear in the compiled CSS.
|
||||
|
||||
**Solution:** Tailwind CSS v4 uses a content detection approach by default, scanning your template files for class names. Make sure the file containing the class is within the theme directory and uses a recognized extension (`.php`, `.html`, `.js`, etc.). If you are using dynamic class names (concatenating strings or storing classes in variables), Tailwind may not be able to detect them. In that case, use a Tailwind `@source` directive in your CSS or add the class to a safelist.
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Command | Purpose |
|
||||
| --------------------- | -------------------------------------------------------------------- |
|
||||
| `npm run start` | Start BrowserSync dev server with live reload (alias for `watch`) |
|
||||
| `npm run watch` | Start BrowserSync dev server with live reload |
|
||||
| `npm run build` | Compile Tailwind CSS for production (minified, optimized) |
|
||||
| `composer lint` | Run PHP_CodeSniffer against WordPress coding standards |
|
||||
| `composer fix` | Auto-fix PHP_CodeSniffer violations |
|
||||
| `npx playwright test` | Run Playwright end-to-end tests |
|
||||
|
||||
| File | Purpose |
|
||||
| ----------------------------------- | -------------------------------------------------------------- |
|
||||
| `.env` | Local environment config (`LOCALHOST_URL`, `BROWSERSYNC_PORT`) |
|
||||
| `styles/theme.css` | Tailwind CSS entry point (edit this to add imports) |
|
||||
| `static/dist/theme.css` | Compiled CSS output (generated, do not edit manually) |
|
||||
| `views/blocks/*/block.json` | ACF block registration files |
|
||||
| `lib/activation.php` | Theme activation routine (creates pages, installs plugins) |
|
||||
| `lib/class-enqueue.php` | Frontend/backend/editor asset loading |
|
||||
| `lib/hooks.php` | Menu registration, sidebars, theme support, cleanup |
|
||||
| `functions.php` | Theme bootstrap (auto-loads all `lib/*.php` files) |
|
||||
| `theme.json` | WordPress theme configuration (colors, typography, layout) |
|
||||
| `acf/group_*.json` | ACF field group definitions (version-controlled) |
|
||||
@@ -1,569 +0,0 @@
|
||||
# SoloFrame Evo Theme Reference
|
||||
|
||||
> Quick-lookup reference for the SoloFrame Evo WordPress theme. Covers hooks, filters, design tokens, CSS architecture, JS modules, helper functions, class APIs, CLI commands, deployment, and testing.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [SoloFrame Evo Theme Reference](#soloframe-evo-theme-reference)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Hooks and Filters](#hooks-and-filters)
|
||||
- [hooks.php (SoloFrameEvo Namespace)](#hooksphp-soloframeevo-namespace)
|
||||
- [extras.php](#extrasphp)
|
||||
- [helpers.php](#helpersphp)
|
||||
- [class-enqueue.php](#class-enqueuephp)
|
||||
- [class-breadcrumbs.php](#class-breadcrumbsphp)
|
||||
- [class-resources.php](#class-resourcesphp)
|
||||
- [theme.json Design Tokens](#themejson-design-tokens)
|
||||
- [Colors](#colors)
|
||||
- [Font Sizes](#font-sizes)
|
||||
- [Font Family](#font-family)
|
||||
- [Layout](#layout)
|
||||
- [Spacing Units](#spacing-units)
|
||||
- [CSS Architecture](#css-architecture)
|
||||
- [Import Order](#import-order)
|
||||
- [Adding a New Stylesheet](#adding-a-new-stylesheet)
|
||||
- [JS Module Dependency Graph](#js-module-dependency-graph)
|
||||
- [Script Module IDs](#script-module-ids)
|
||||
- [Navigation Class API](#navigation-class-api)
|
||||
- [Constructor](#constructor)
|
||||
- [Methods](#methods)
|
||||
- [Helper Functions](#helper-functions)
|
||||
- [Class Reference](#class-reference)
|
||||
- [Breadcrumbs Method Details](#breadcrumbs-method-details)
|
||||
- [CLI Commands](#cli-commands)
|
||||
- [Deployment (GitHub Actions)](#deployment-github-actions)
|
||||
- [Deployment Steps](#deployment-steps)
|
||||
- [rsync Flags](#rsync-flags)
|
||||
- [Testing](#testing)
|
||||
- [Accessibility Tests](#accessibility-tests)
|
||||
- [PHP Linting](#php-linting)
|
||||
- [Playwright Configuration](#playwright-configuration)
|
||||
|
||||
---
|
||||
|
||||
## Hooks and Filters
|
||||
|
||||
### hooks.php (SoloFrameEvo Namespace)
|
||||
|
||||
All hooks in this file live under the `SoloFrameEvo` namespace.
|
||||
|
||||
| Hook | Type | Priority | Args | Description |
|
||||
| ------ | ------ | ---------- | ------ | ------------- |
|
||||
| `wp_head` | Action | 0 | — | Adds Google Fonts `<link rel="preconnect">` tags |
|
||||
| `register_nav_menus()` | Direct call | — | — | Registers three menus: `main_navigation`, `aux_navigation`, `footer_navigation` |
|
||||
| `widgets_init` | Action | — | — | Registers four sidebars: `sidebar-primary`, `sidebar-page`, `footer-1`, `footer-2`, `footer-3` |
|
||||
| `wp_title` | Filter | — | — | Formats page titles as `Site Name: Page Title` |
|
||||
| `excerpt_more` | Filter | — | — | Replaces default excerpt ellipsis with `…` |
|
||||
| `include_page_title_in_hero` | Filter | — | — | Removes page title from hero section on single posts |
|
||||
| `init` | Action | 1 | — | Aggressive WP cleanup and theme support registration (see below) |
|
||||
| `wp_check_filetype_and_ext` | Filter | 10 | 4 | Allows SVG uploads (fix for WP 4.7.1 filetype check) |
|
||||
| `upload_mimes` | Filter | — | — | Adds `svg` mime type (`image/svg+xml`) |
|
||||
| `admin_head` | Action | — | — | Injects CSS to fix SVG display in the admin area |
|
||||
| `wp_mail_from` | Filter | — | — | Sets the `From` email address to the admin email |
|
||||
| `wp_mail_from_name` | Filter | — | — | Sets the `From` name to the blog name |
|
||||
|
||||
**The `init` (priority 1) hook performs aggressive cleanup:**
|
||||
|
||||
Removes:
|
||||
|
||||
- Emoji detection and styles (`remove_action` on `wp_head`)
|
||||
- Block library styles (`wp-block-library`)
|
||||
- Global styles (`global-styles`)
|
||||
- REST API link tag from `wp_head`
|
||||
- REST API link tag from `template_redirect`
|
||||
- XML-RPC link (`rsd_link`)
|
||||
- WP generator tag (`wp_generator`)
|
||||
- WLW manifest link (`wlwmanifest_link`)
|
||||
|
||||
Adds theme supports:
|
||||
|
||||
- `post-thumbnails`
|
||||
- `title-tag`
|
||||
- `html5` (search-form, comment-form, comment-list, gallery, caption, style, script)
|
||||
- `align-wide`
|
||||
- `editor-styles`
|
||||
- `responsive-embeds`
|
||||
- `customize-selective-refresh-widgets`
|
||||
|
||||
---
|
||||
|
||||
### extras.php
|
||||
|
||||
| Hook / Filter | Type | Description |
|
||||
| --------------- | ------ | ------------- |
|
||||
| `hasSidebar` | Filter | Controls sidebar display based on an ACF true/false field on the current page |
|
||||
| `body_class` | Filter | Appends `has-sidebar` class to `<body>` when a sidebar is present |
|
||||
| `the_content` | Filter | `divWrapper()` wraps `<iframe>` and embed elements in `<div class="embed">` |
|
||||
| `acf/include_fields` | Action | Registers "Page Sidebar" ACF field group (a true/false toggle for pages) |
|
||||
| `init` | Action | `createOwnerRole()` creates the Owner role on every init |
|
||||
|
||||
**Owner role details:** The Owner role is equivalent to Administrator minus plugin management, theme management, and core update capabilities.
|
||||
|
||||
---
|
||||
|
||||
### helpers.php
|
||||
|
||||
| Hook / Filter | Type | Priority | Args | Description |
|
||||
| --------------- | ------ | ---------- | ------ | ------------- |
|
||||
| `custom_menu_order` | Filter | 10 | 1 | Enables custom admin menu ordering |
|
||||
| `menu_order` | Filter | 10 | 1 | Defines the custom admin menu order |
|
||||
| `block_categories_all` | Filter | 10 | — | Adds the `sf-blocks` category to the block editor |
|
||||
| `init` | Action | — | — | Registers the ACF "Global Fields" options page |
|
||||
|
||||
---
|
||||
|
||||
### class-enqueue.php
|
||||
|
||||
| Hook | Type | Method | Description |
|
||||
| ------ | ------ | -------- | ------------- |
|
||||
| `wp_enqueue_scripts` | Action | `enqFEAssets()` | Loads frontend CSS and JS |
|
||||
| `admin_enqueue_scripts` | Action | `enqBEAssets()` | Loads admin CSS and JS |
|
||||
| `enqueue_block_editor_assets` | Action | `enqEditorAssets()` | Loads block editor CSS |
|
||||
|
||||
---
|
||||
|
||||
### class-breadcrumbs.php
|
||||
|
||||
The `Breadcrumbs` class generates Schema.org-compatible breadcrumb markup. Context-specific methods:
|
||||
|
||||
| Method | Context |
|
||||
| -------- | --------- |
|
||||
| `getHomeBreadcrumb()` | Site front page |
|
||||
| `getBlogPostsIndexBreadcrumb()` | Blog posts index |
|
||||
| `getSinglePostBreadcrumbs()` | Single post |
|
||||
| `getCustomPostTypeBreadcrumbs()` | Custom post type single |
|
||||
| `getStaticPageBreadcrumbs()` | Static page |
|
||||
| `getTaxonomyArchiveBreadcrumb()` | Taxonomy archive |
|
||||
| `getPostTypeArchiveBreadcrumb()` | Post type archive |
|
||||
| `getDateArchiveBreadcrumbs()` | Date archive (day/month/year) |
|
||||
| `getSearchBreadcrumb()` | Search results |
|
||||
| `get404Breadcrumb()` | 404 page |
|
||||
|
||||
---
|
||||
|
||||
### class-resources.php
|
||||
|
||||
| Hook | Type | Description |
|
||||
| ------ | ------ | ------------- |
|
||||
| `init` | Action | Registers the `resources` custom post type |
|
||||
| `post_type_link` | Filter | Customizes resource permalinks to `/resources/{term-slug}/{post-name}` |
|
||||
|
||||
The `resources` CPT uses a URL rewrite pattern that incorporates the first taxonomy term slug into the permalink path.
|
||||
|
||||
---
|
||||
|
||||
## theme.json Design Tokens
|
||||
|
||||
### Colors
|
||||
|
||||
All theme colors map to CSS custom properties. Use the CSS variable in your stylesheets or reference the slug in the block editor.
|
||||
|
||||
| Slug | CSS Variable | Name | Value |
|
||||
| ------ | ------------- | ------ | ------- |
|
||||
| `black` | — | Black | `#000` |
|
||||
| `white` | — | White | `#fff` |
|
||||
| `theme-bg` | `var(--color-background)` | Theme Background | Dynamic |
|
||||
| `theme-text` | `var(--color-text)` | Theme Text | Dynamic |
|
||||
| `theme-primary` | `var(--color-primary)` | Theme Primary | Dynamic |
|
||||
| `theme-secondary` | `var(--color-secondary)` | Theme Secondary | Dynamic |
|
||||
| `theme-bodylinks` | `var(--color-bodylinks)` | Theme Body Links | Dynamic |
|
||||
| `theme-footerlinks` | `var(--color-footlinks)` | Theme Footer Links | Dynamic |
|
||||
| `theme-success` | `var(--color-success)` | Theme Success | Dynamic |
|
||||
| `theme-warning` | `var(--color-warning)` | Theme Warning | Dynamic |
|
||||
| `theme-danger` | `var(--color-danger)` | Theme Danger | Dynamic |
|
||||
| `theme-info` | `var(--color-info)` | Theme Info | Dynamic |
|
||||
|
||||
The dynamic colors (`theme-*`) resolve to CSS custom properties that can be overridden per-site in the customizer or ACF options pages. The static colors (`black`, `white`) are literal hex values.
|
||||
|
||||
**Usage example in CSS:**
|
||||
|
||||
```css
|
||||
.my-element {
|
||||
color: var(--color-primary);
|
||||
background-color: var(--color-background);
|
||||
}
|
||||
```
|
||||
|
||||
**Usage in a block template:**
|
||||
|
||||
```html
|
||||
<div class="has-theme-primary-color has-theme-bg-background-color">
|
||||
...
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Font Sizes
|
||||
|
||||
| Slug | CSS Variable | Name |
|
||||
| ------ | ------------- | ------ |
|
||||
| `base` | `var(--text-base)` | Base |
|
||||
| `text-14px` | `var(--text-14px)` | Text 14px |
|
||||
| `text-16px` | `var(--text-16px)` | Text 16px |
|
||||
| `text-18px` | `var(--text-18px)` | Text 18px |
|
||||
| `text-20px` | `var(--text-20px)` | Text 20px |
|
||||
| `text-22px` | `var(--text-22px)` | Text 22px |
|
||||
| `text-25px` | `var(--text-25px)` | Text 25px |
|
||||
| `text-30px` | `var(--text-30px)` | Text 30px |
|
||||
| `text-35px` | `var(--text-35px)` | Text 35px |
|
||||
| `text-38px` | `var(--text-38px)` | Text 38px |
|
||||
| `text-40px` | `var(--text-40px)` | Text 40px |
|
||||
| `text-45px` | `var(--text-45px)` | Text 45px |
|
||||
| `text-50px` | `var(--text-50px)` | Text 50px |
|
||||
| `text-70px` | `var(--text-70px)` | Text 70px |
|
||||
| `text-75px` | `var(--text-75px)` | Text 75px |
|
||||
|
||||
**Usage example in CSS:**
|
||||
|
||||
```css
|
||||
.hero-title {
|
||||
font-size: var(--text-50px);
|
||||
}
|
||||
```
|
||||
|
||||
**Usage in a block template:**
|
||||
|
||||
```html
|
||||
<p class="has-text-50px-font-size">Big headline text</p>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Font Family
|
||||
|
||||
| Slug | CSS Variable | Name |
|
||||
| ------ | ------------- | ------ |
|
||||
| `theme-sans` | `var(--font-sans)` | Theme Sans |
|
||||
|
||||
```css
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Layout
|
||||
|
||||
| Setting | Value |
|
||||
| --------- | ------- |
|
||||
| `contentSize` | `100%` |
|
||||
| `wideSize` | `1536px` |
|
||||
|
||||
These control the WordPress block editor content width and wide-alignment width.
|
||||
|
||||
---
|
||||
|
||||
### Spacing Units
|
||||
|
||||
Available spacing units for the block editor spacing scale:
|
||||
|
||||
`px`, `em`, `rem`, `vh`, `vw`, `%`
|
||||
|
||||
---
|
||||
|
||||
## CSS Architecture
|
||||
|
||||
The entry point is `styles/theme.css`. All imports use the CSS `@import` syntax processed by Tailwind CSS v4.
|
||||
|
||||
### Import Order
|
||||
|
||||
```plain
|
||||
styles/theme.css
|
||||
|
|
||||
+-- @import "tailwindcss" # Tailwind CSS v4 base
|
||||
|
|
||||
+-- @import "./base/index.css" # Base styles
|
||||
| +-- break-out.css
|
||||
| +-- colors.css
|
||||
| +-- forms.css
|
||||
| +-- global.css
|
||||
| +-- misc.css
|
||||
| +-- prose.css
|
||||
| +-- skip-link.css
|
||||
| +-- typography.css
|
||||
|
|
||||
+-- @import "./navigation/index.css" # Navigation styles
|
||||
| +-- nav-aux.css
|
||||
| +-- nav-footer.css
|
||||
| +-- nav-functional.css
|
||||
| +-- nav-main-default.css
|
||||
| +-- nav-main-mega.css
|
||||
| +-- nav-mobile-accordion.css
|
||||
| +-- nav-mobile-sliding.css
|
||||
|
|
||||
+-- @import "./fonts/lineicons.css" # Icon font
|
||||
|
|
||||
+-- @import "./base/break-out.css" # Break-out utilities (repeated for cascade)
|
||||
|
|
||||
+-- @import "./components/index.css" # Component styles
|
||||
| +-- breadcrumbs.css
|
||||
| +-- pagination.css
|
||||
| +-- post-list.css
|
||||
| +-- sidebar.css
|
||||
| +-- site-footer.css
|
||||
| +-- site-header.css
|
||||
|
|
||||
+-- @import "./blocks/index.css" # Block styles
|
||||
| +-- buttons.css
|
||||
| +-- core.css
|
||||
|
|
||||
+-- @plugin "@tailwindcss/typography" # Tailwind prose plugin
|
||||
```
|
||||
|
||||
### Adding a New Stylesheet
|
||||
|
||||
1. Create the `.css` file in the appropriate directory (`base/`, `navigation/`, `components/`, or `blocks/`).
|
||||
2. Add an `@import` line to that directory's `index.css`.
|
||||
|
||||
Example -- adding a new `cards.css` component:
|
||||
|
||||
```css
|
||||
/* styles/components/index.css */
|
||||
@import "cards.css";
|
||||
```
|
||||
|
||||
```css
|
||||
/* styles/components/cards.css */
|
||||
.card {
|
||||
/* styles here */
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## JS Module Dependency Graph
|
||||
|
||||
```plain
|
||||
theme.js (entry point)
|
||||
├── Navigation.js # Mobile menu, sliding viewport, keyboard nav
|
||||
├── backToTop.js # BackToTopButton custom element
|
||||
├── button.js # ButtonComponent custom element, registerButtonComponent
|
||||
├── GetHeaderHeight.js # Sets --header-height CSS variable
|
||||
└── TagExternalLinks.js # Adds target="_blank" rel="noopener" to external links
|
||||
|
||||
admin.js
|
||||
└── button.js # ButtonComponent for editor context
|
||||
```
|
||||
|
||||
### Script Module IDs
|
||||
|
||||
WordPress registers these script modules via `wp_register_script_module()`:
|
||||
|
||||
| Module ID | Source | Dependencies |
|
||||
| ----------- | -------- | -------------- |
|
||||
| `sf-evo-theme` | `theme.js` | None |
|
||||
| `sf-evo-button` | `button.js` | `sf-evo-theme` |
|
||||
| `sf-evo-admin` | `admin.js` | `sf-evo-button` |
|
||||
|
||||
**Loading in a template:**
|
||||
|
||||
```php
|
||||
wp_enqueue_script_module('sf-evo-theme');
|
||||
wp_enqueue_script_module('sf-evo-button');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Navigation Class API
|
||||
|
||||
The `Navigation` class is located at `static/js/modules/Navigation.js`.
|
||||
|
||||
### Constructor
|
||||
|
||||
```js
|
||||
const nav = new Navigation(toggleId, menuSelector);
|
||||
// toggleId: ID of the hamburger toggle button (e.g., 'menu-toggle')
|
||||
// menuSelector: CSS selector for the nav menu container (e.g., '.nav-main')
|
||||
```
|
||||
|
||||
### Methods
|
||||
|
||||
| Method | Description |
|
||||
| -------- | ------------- |
|
||||
| `desktopMenuDropdowns()` | Enables dropdown menus for desktop navigation |
|
||||
| `mobileMenuToggle()` | Toggles the mobile hamburger menu open/closed |
|
||||
| `initializeSlidingViewport()` | Sets up the sliding mobile menu structure |
|
||||
| `navigateToLevel(level)` | Navigates the sliding menu to a specific depth level |
|
||||
| `navigateBack()` | Goes back one level in the sliding menu |
|
||||
| `animateToLevel(level)` | Animates the sliding transition to a target depth level |
|
||||
| `resetSlidingNavigation()` | Resets sliding nav to the root level |
|
||||
| `cleanupSlidingStructure()` | Removes sliding nav DOM elements (cleanup/teardown) |
|
||||
| `createBackButton(label)` | Creates a back button element for the sliding menu |
|
||||
| `setupSlidingClickHandlers()` | Attaches click handlers for sliding menu items |
|
||||
| `shouldEnableSlidingViewport()` | Returns `true` if the current viewport width warrants the sliding menu |
|
||||
|
||||
**Usage example:**
|
||||
|
||||
```js
|
||||
import Navigation from './modules/Navigation.js';
|
||||
|
||||
const nav = new Navigation('menu-toggle', '.nav-main');
|
||||
nav.desktopMenuDropdowns();
|
||||
nav.mobileMenuToggle();
|
||||
nav.initializeSlidingViewport();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Helper Functions
|
||||
|
||||
| Function | File | Signature | Description |
|
||||
| ---------- | ------ | ----------- | ------------- |
|
||||
| `getFieldValue` | helpers.php | `getFieldValue($field_path)` | Retrieves nested ACF values using dot notation. E.g., `getFieldValue('contact_info.phone')` resolves `get_field('contact_info', 'option')['phone']`. Uses `'option'` for Global Fields. |
|
||||
| `blockWrapperAttributes` | helpers.php | `blockWrapperAttributes($classes, $is_preview)` | Returns block wrapper attributes. In preview mode returns `class="..."`; on frontend returns `get_block_wrapper_attributes()`. |
|
||||
| `customMenuOrder` | helpers.php | `customMenuOrder($menu_ord)` | Customizes WordPress admin menu order. |
|
||||
| `blockCategories` | helpers.php | `blockCategories($categories)` | Adds the `sf-blocks` category to the block editor. |
|
||||
| `consoleLog` | helpers.php | `consoleLog($data)` | Outputs data to the browser console via `<script>console.log()</script>`. |
|
||||
| `customExcerpt` | helpers.php | `customExcerpt($text, $number_of_words, $more)` | Generates custom excerpts that end at sentence boundaries instead of mid-sentence. |
|
||||
| `escEmbeds` | helpers.php | `escEmbeds()` | Returns an allowed HTML array for iframe/embed content (used with `wp_kses`). |
|
||||
| `strposArray` | helpers.php | `strposArray($haystack, $needles, $offset)` | Finds the position of the first occurrence of any needle from an array. |
|
||||
| `getChildrenPages` | extras.php | `getChildrenPages()` | Gets child pages of the current page, sorted by `menu_order`. |
|
||||
| `hasSidebar` | extras.php | `hasSidebar()` | Checks if the current page should render a sidebar (controlled by ACF field). |
|
||||
| `hasPageHeader` | extras.php | `hasPageHeader()` | Checks if the page should render a page header (based on `hero_style` ACF field). |
|
||||
| `createOwnerRole` | extras.php | `createOwnerRole()` | Creates the Owner role (admin minus plugin/theme/core management). Runs on every `init`. |
|
||||
| `getTheTitle` | extras.php | `getTheTitle()` | Gets the appropriate title for the current context (home, single, archive, search, 404). |
|
||||
| `divWrapper` | extras.php | `divWrapper($content)` | Wraps iframes and embeds in `<div class="embed">`. |
|
||||
|
||||
**Usage examples:**
|
||||
|
||||
```php
|
||||
// Get a nested ACF option field
|
||||
$phone = getFieldValue('contact_info.phone');
|
||||
|
||||
// Block wrapper attributes (works in both editor and frontend)
|
||||
$attrs = blockWrapperAttributes('my-block-class', $is_preview);
|
||||
echo '<div ' . $attrs . '>';
|
||||
|
||||
// Custom excerpt ending at sentence boundaries
|
||||
$excerpt = customExcerpt(get_the_content(), 30, '...');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Class Reference
|
||||
|
||||
| Class | File | Key Methods | Description |
|
||||
| ------- | ------ | ------------- | ------------- |
|
||||
| `Enqueue` | `class-enqueue.php` | `enqFEAssets()`, `enqBEAssets()`, `enqEditorAssets()` | Manages all asset loading: frontend, admin, and editor |
|
||||
| `ACF` | `class-acf.php` | `saveJson($path)`, `loadJson($paths)` | Sets ACF JSON save/load paths for field group synchronization |
|
||||
| `Breadcrumbs` | `class-breadcrumbs.php` | `generate()`, `render()`, plus per-context methods (see below) | Generates Schema.org-compatible breadcrumb markup |
|
||||
| `MenuItems` | `class-menuitems.php` | `render()` | Renders nav menu items using `$views . '/components/menu-items/index.php'` |
|
||||
| `Resources` | `class-resources.php` | CPT registration, `postTypeLink` filter | Registers the `resources` CPT with custom permalink structure |
|
||||
| `ShowTemplate` | `class-show-template.php` | HTML comment in footer | Adds an HTML comment to the footer showing the active template path (debugging) |
|
||||
|
||||
### Breadcrumbs Method Details
|
||||
|
||||
| Method | Returns | Description |
|
||||
| -------- | --------- | ------------- |
|
||||
| `generate()` | `array` | Builds breadcrumb data array for the current context |
|
||||
| `render()` | `string` | Outputs breadcrumb HTML with Schema.org markup |
|
||||
| `getHomeBreadcrumb()` | `array` | Breadcrumb for the front page |
|
||||
| `getBlogPostsIndexBreadcrumb()` | `array` | Breadcrumb for the blog posts index |
|
||||
| `getSinglePostBreadcrumbs()` | `array` | Breadcrumbs for a single post (includes category) |
|
||||
| `getCustomPostTypeBreadcrumbs()` | `array` | Breadcrumbs for a custom post type single |
|
||||
| `getStaticPageBreadcrumbs()` | `array` | Breadcrumbs for a static page (includes parent pages) |
|
||||
| `getTaxonomyArchiveBreadcrumb()` | `array` | Breadcrumb for a taxonomy archive |
|
||||
| `getPostTypeArchiveBreadcrumb()` | `array` | Breadcrumb for a post type archive |
|
||||
| `getDateArchiveBreadcrumbs()` | `array` | Breadcrumbs for date archives (day/month/year) |
|
||||
| `getSearchBreadcrumb()` | `array` | Breadcrumb for search results |
|
||||
| `get404Breadcrumb()` | `array` | Breadcrumb for 404 pages |
|
||||
|
||||
**Usage example:**
|
||||
|
||||
```php
|
||||
$breadcrumbs = new Breadcrumbs();
|
||||
echo $breadcrumbs->render();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CLI Commands
|
||||
|
||||
| Command | Description |
|
||||
| --------- | ------------- |
|
||||
| `npm run build` | Compiles Tailwind CSS v4 from `styles/theme.css` to `static/dist/theme.css` with `--optimize` |
|
||||
| `npm run start` | Starts BrowserSync dev server with live reloading (alias for `npm run watch`) |
|
||||
| `npm run watch` | Runs `.watch.js` -- BrowserSync with CSS injection on changes |
|
||||
| `composer lint` | Runs PHP_CodeSniffer against WordPress coding standards; outputs to `phpcs-results.txt` |
|
||||
| `composer fix` | Auto-fixes PHPCS violations |
|
||||
| `npx playwright test` | Runs Playwright accessibility tests |
|
||||
| `npx playwright test --ui` | Opens Playwright interactive UI |
|
||||
|
||||
---
|
||||
|
||||
## Deployment (GitHub Actions)
|
||||
|
||||
The deployment workflow is defined in `.github/workflows/wpengine.yml`.
|
||||
|
||||
| Setting | Value |
|
||||
| --------- | ------- |
|
||||
| Trigger | `workflow_dispatch` (manual). Push to `main` trigger is commented out. |
|
||||
| Skip condition | Commits containing `#skipGA` in the message are skipped |
|
||||
| Target path | `wp-content/themes/ soloframe-evo` |
|
||||
| WP Engine environment | `soloframe-evo` |
|
||||
| SSH key secret | `WPE_SSHG_KEY_PRIVATE` |
|
||||
|
||||
### Deployment Steps
|
||||
|
||||
1. **Checkout** the repository
|
||||
2. **Composer install** -- `composer install`
|
||||
3. **npm install** -- `npm install`
|
||||
4. **Build** -- `npm run build`
|
||||
5. **Remove node_modules** -- deleted before deploy
|
||||
6. **rsync** to WP Engine
|
||||
|
||||
### rsync Flags
|
||||
|
||||
```plain
|
||||
-azvr --inplace --delete --exclude=".*"
|
||||
```
|
||||
|
||||
| Flag | Meaning |
|
||||
| ------ | --------- |
|
||||
| `-a` | Archive mode (preserve permissions, timestamps, etc.) |
|
||||
| `-z` | Compress during transfer |
|
||||
| `-v` | Verbose output |
|
||||
| `-r` | Recursive |
|
||||
| `--inplace` | Update files in-place on the target |
|
||||
| `--delete` | Remove files on target that no longer exist in source |
|
||||
| `--exclude=".*"` | Exclude dotfiles (e.g., `.git`, `.env`) |
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### Accessibility Tests
|
||||
|
||||
```bash
|
||||
npx playwright test
|
||||
```
|
||||
|
||||
Runs `tests/site-a11y.spec.js` using `@axe-core/playwright`. Tests scan pages for WCAG violations.
|
||||
|
||||
```bash
|
||||
npx playwright test --ui
|
||||
```
|
||||
|
||||
Opens the Playwright interactive UI for step-by-step test debugging.
|
||||
|
||||
### PHP Linting
|
||||
|
||||
```bash
|
||||
composer lint
|
||||
```
|
||||
|
||||
Runs PHP_CodeSniffer against WordPress coding standards. Results are written to `phpcs-results.txt`.
|
||||
|
||||
```bash
|
||||
composer fix
|
||||
```
|
||||
|
||||
Auto-fixes PHPCS violations where possible.
|
||||
|
||||
### Playwright Configuration
|
||||
|
||||
The Playwright config (`playwright.config.js`) is currently set to run on **Chromium only**. Firefox and WebKit browsers are commented out but available for enabling.
|
||||
+72
-54
@@ -2,11 +2,11 @@
|
||||
/**
|
||||
* Theme footer template
|
||||
*
|
||||
* @package SoloFrameEvo
|
||||
* @package KsPortfolio
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
$footerLogo = getFieldValue( 'footer.footer_logo.url' ) ? getFieldValue( 'footer.footer_logo.url' ) : '';
|
||||
$footerDesc = getFieldValue( 'footer.footer_description' ) ? getFieldValue( 'footer.footer_description' ) : '';
|
||||
@@ -23,86 +23,104 @@ $footerNav = ! empty( $locations['footer_navigation'] )
|
||||
|
||||
</main>
|
||||
|
||||
<footer role="contentinfo" class="site-footer bg-gray-800 text-white text-base">
|
||||
<div class="pt-16 pb-12 text-base">
|
||||
<div class="container mx-auto">
|
||||
<div class="grid grid-cols-4 gap-6 md:grid-cols-8 lg:grid-cols-12">
|
||||
<div class="col-span-4 md:col-span-8 lg:col-span-4 max-w-[40ch] prose-p:text-14px prose-p:mb-4 text-balance" aria-labelledby="footer-header">
|
||||
<h2 id="footer-header" class="max-w-64 h-auto">
|
||||
<a href="<?php echo esc_url( home_url( '/' ) ); ?>" class="site-footer__logo-link h-full w-full">
|
||||
<?php if ( $footerLogo ) { ?>
|
||||
<img src="<?php echo esc_url( $footerLogo ); ?>" alt="<?php echo esc_attr( get_bloginfo( 'name' ) ); ?>" class="site-footer__logo-image" />
|
||||
<?php
|
||||
} else {
|
||||
echo esc_html( get_bloginfo( 'name' ) );
|
||||
}
|
||||
?>
|
||||
<footer role="contentinfo" class="site-footer">
|
||||
<div class="container mx-auto py-12">
|
||||
<div class="flex flex-col-reverse lg:flex-row justify-between gap-8">
|
||||
<div class="site-footer__brand max-w-[40ch]">
|
||||
<h2 class="site-footer__title">
|
||||
<a href="<?php echo esc_url( home_url( '/' ) ); ?>">
|
||||
<?php echo esc_html( get_bloginfo( 'name' ) ); ?>
|
||||
</a>
|
||||
</h2>
|
||||
|
||||
<div id="footer-description" aria-label="Footer description">
|
||||
<?php echo wp_kses_post( $footerDesc ); ?>
|
||||
</div>
|
||||
|
||||
<div class="social-links mt-8">
|
||||
<div class="social-links">
|
||||
<?php
|
||||
get_template_part(
|
||||
'views/partials/social-media',
|
||||
null,
|
||||
array(
|
||||
'circle' => false,
|
||||
'classes' => 'social-icons p-0 mr-2 text-30px text-gray-300! hover:text-info!',
|
||||
'classes' => 'social-icons p-0 mr-1',
|
||||
)
|
||||
);
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="footRight" class="col-span-4 md:col-span-8 grid md:grid-cols-4 gap-10 lg:justify-end">
|
||||
<div aria-labelledby="footer-area-1" class="prose-p:text-balance">
|
||||
<h3 class="mb-4 pb-2 border-b border-b-secondary font-bold text-white" id="footer-area-1">Footer Area 1</h3>
|
||||
<?php dynamic_sidebar( 'footer-1' ); ?>
|
||||
<?php
|
||||
for ( $i = 1; $i <= 3; $i++ ) :
|
||||
$area_id = 'footer-' . $i;
|
||||
if ( ! is_active_sidebar( $area_id ) ) {
|
||||
continue;
|
||||
}
|
||||
?>
|
||||
<div class="site-footer__area">
|
||||
<?php dynamic_sidebar( $area_id ); ?>
|
||||
</div>
|
||||
<?php endfor; ?>
|
||||
|
||||
<div aria-labelledby="footer-area-2" class="prose-p:text-balance">
|
||||
<h3 class="mb-4 pb-2 border-b border-b-secondary font-bold text-white" id="footer-area-2">Footer Area 2</h3>
|
||||
<?php dynamic_sidebar( 'footer-2' ); ?>
|
||||
</div>
|
||||
|
||||
<div aria-labelledby="footer-area-3" class="prose-p:text-balance">
|
||||
<h3 class="mb-4 pb-2 border-b border-b-secondary font-bold text-white" id="footer-area-3">Footer Area 3</h3>
|
||||
<?php dynamic_sidebar( 'footer-3' ); ?>
|
||||
</div>
|
||||
|
||||
<div id="footer-nav" aria-labelledby="footer-navigation">
|
||||
<h3 class="mb-4 pb-2 border-b border-b-secondary font-bold text-white" id="footer-navigation"><?php echo esc_html__( 'Navigation' ); ?></h3>
|
||||
<?php if ( $footerNav ) : ?>
|
||||
<nav class="site-footer__nav" aria-label="Footer navigation">
|
||||
<ul class="site-footer__nav-list">
|
||||
<?php foreach ( $footerNav as $item ) : ?>
|
||||
<li class="site-footer__nav-item list-none text-left <?php echo esc_attr( implode( ' ', $item->classes ) ); ?>">
|
||||
<a href="<?php echo esc_url( $item->url ); ?>" class="site-footer__nav-link">
|
||||
<nav class="site-footer__nav" aria-label="<?php echo esc_attr__( 'Footer navigation', 'ks-portfolio' ); ?>">
|
||||
<h3><?php echo esc_html__( 'Quick Links', 'ks-portfolio' ); ?></h3>
|
||||
<ul>
|
||||
<?php
|
||||
// Build a tree of parent -> children relationships
|
||||
$menu_items_by_parent = array();
|
||||
foreach ( $footerNav as $item ) {
|
||||
$parent_id = (int) $item->menu_item_parent;
|
||||
if ( ! isset( $menu_items_by_parent[ $parent_id ] ) ) {
|
||||
$menu_items_by_parent[ $parent_id ] = array();
|
||||
}
|
||||
$menu_items_by_parent[ $parent_id ][] = $item;
|
||||
}
|
||||
|
||||
// Display top-level items (parent = 0)
|
||||
if ( isset( $menu_items_by_parent[0] ) ) {
|
||||
foreach ( $menu_items_by_parent[0] as $item ) :
|
||||
// Check if this is the contact link
|
||||
$is_contact = false;
|
||||
if ( function_exists( 'get_field' ) ) {
|
||||
$contact_link = get_field( 'contact_info.email', 'option' );
|
||||
if ( $contact_link && strpos( $item->url, 'contact' ) !== false ) {
|
||||
$is_contact = true;
|
||||
}
|
||||
}
|
||||
?>
|
||||
<li<?php echo $is_contact ? ' class="site-footer__nav-contact"' : ''; ?>>
|
||||
<a href="<?php echo esc_url( $item->url ); ?>" class="<?php echo $is_contact ? 'button button-primary' : ''; ?>">
|
||||
<?php echo esc_html( $item->title ); ?>
|
||||
</a>
|
||||
|
||||
<?php
|
||||
// Check for child items
|
||||
$item_id = (int) $item->ID;
|
||||
if ( isset( $menu_items_by_parent[ $item_id ] ) ) {
|
||||
?>
|
||||
<ul class="site-footer__nav-submenu">
|
||||
<?php foreach ( $menu_items_by_parent[ $item_id ] as $child_item ) : ?>
|
||||
<li class="site-footer__nav-submenu-item">
|
||||
<a href="<?php echo esc_url( $child_item->url ); ?>">
|
||||
<?php echo esc_html( $child_item->title ); ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
</li>
|
||||
<?php
|
||||
endforeach;
|
||||
}
|
||||
?>
|
||||
</ul>
|
||||
</nav>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="copyright bg-primary text-white text-center py-3 text-14px">
|
||||
<?php
|
||||
if ( $copyright ) {
|
||||
echo wp_kses_post( $copyright );
|
||||
} else {
|
||||
echo esc_html( '© ' . gmdate( 'Y' ) . ' ' . get_bloginfo( 'name' ) . '.' );
|
||||
}
|
||||
?>
|
||||
<div class="site-footer__copyright">
|
||||
<?php echo wp_kses_post( $copyright ); ?>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
+2
-2
@@ -2,10 +2,10 @@
|
||||
/**
|
||||
* Front Page Template
|
||||
*
|
||||
* @package SoloFrameEvo
|
||||
* @package KsPortfolio
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
get_header();
|
||||
?>
|
||||
|
||||
+9
-3
@@ -1,21 +1,27 @@
|
||||
<?php
|
||||
/**
|
||||
* Functions file for the SoloFrameEvo theme.
|
||||
* Functions file for the KsPortfolio theme.
|
||||
*
|
||||
* This file initializes the theme by including necessary dependencies
|
||||
* and loading additional function files.
|
||||
*
|
||||
* @package SoloFrameEvo
|
||||
* @package KsPortfolio
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
// Load functions.
|
||||
foreach ( glob( __DIR__ . '/lib/*.php' ) as $filename ) {
|
||||
include_once $filename;
|
||||
}
|
||||
|
||||
// Project helper (thin wrapper around the projects-portfolio plugin).
|
||||
$projects_helper = __DIR__ . '/lib/class-projects.php';
|
||||
if ( file_exists( $projects_helper ) ) {
|
||||
require_once $projects_helper;
|
||||
}
|
||||
|
||||
/** Registers custom ACF (Advanced Custom Fields) blocks for use in the WordPress theme.
|
||||
*
|
||||
* This function is intended to define and register custom Gutenberg blocks
|
||||
|
||||
+14
-9
@@ -2,15 +2,16 @@
|
||||
/**
|
||||
* Theme header template
|
||||
*
|
||||
* @package SoloFrameEvo
|
||||
* @package KsPortfolio
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
global $views;
|
||||
|
||||
$headerLogo = getFieldValue( 'header.header_logo.url' ) ? getFieldValue( 'header.header_logo.url' ) : get_theme_file_uri( '/static/img/logo.svg' );
|
||||
$linkResume = getFieldValue( 'header.resume_link' ) ? getFieldValue( 'header.resume_link' ) : '';
|
||||
|
||||
// Check conditions for displaying the hero section
|
||||
$showHero = in_array(
|
||||
@@ -25,14 +26,12 @@ $showHero = in_array(
|
||||
),
|
||||
true
|
||||
);
|
||||
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html <?php language_attributes(); ?>>
|
||||
|
||||
<head>
|
||||
<title><?php wp_title( '' ); ?></title>
|
||||
<meta charset="<?php bloginfo( 'charset' ); ?>">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
@@ -44,15 +43,21 @@ $showHero = in_array(
|
||||
<?php echo esc_html__( 'Skip to main content' ); ?>
|
||||
</a>
|
||||
|
||||
<header role="banner" class="site-header bg-secondary flex flex-col items-center justify-start">
|
||||
<?php get_template_part( 'views/components/nav-aux' ); ?>
|
||||
<?php get_template_part( 'views/partials/aura-bg' ); ?>
|
||||
|
||||
<div class="header__nav-main container py-4 items-center grid gap-x-8 gap-y-0 grid-cols-[83px_auto] grid-rows-[1fr] justify-between">
|
||||
<a href="<?php bloginfo( 'url' ); ?>" class="site-header__logo block size-20">
|
||||
<img class="size-full" src="<?php echo esc_url( $headerLogo ); ?>" alt="<?php bloginfo( 'name' ); ?> logo"/>
|
||||
<header role="banner" class="site-header bg-secondary flex flex-col items-center justify-start">
|
||||
<div class="header__nav-main container py-4 items-center grid gap-x-8 gap-y-0 grid-cols-[auto_1fr_auto] grid-rows-[1fr] justify-between">
|
||||
<a href="<?php echo esc_url( home_url( '/' ) ); ?>" class="site-header__logo m-0 p-0 flex gap-x-4 items-center justify-start text-white! text-45px font-bold leading-none hover:underline">
|
||||
<img class="site-header__logo-img aspect-square h-full w-full max-h-12 max-w-12" src="<?php echo esc_url( $headerLogo ); ?>" alt="<?php echo esc_attr( get_bloginfo( 'name' ) ); ?> logo" /> Keith Solomon
|
||||
</a>
|
||||
|
||||
<?php get_template_part( 'views/components/nav-main' ); ?>
|
||||
|
||||
<div class="site-header__aux">
|
||||
<a class="button button--outline" href="<?php echo esc_url( $linkResume ); ?>">
|
||||
<?php echo esc_html__( 'Resume', 'ks-portfolio' ); ?>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
/**
|
||||
* Blog posts list
|
||||
*
|
||||
* @package SoloFrameEvo
|
||||
* @package KsPortfolio
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
// Determine classes based on sidebar presence
|
||||
if ( hasSidebar() ) {
|
||||
@@ -29,17 +29,16 @@ get_header();
|
||||
the_post();
|
||||
?>
|
||||
<div class="post-list__post flex flex-col border border-secondary rounded-md shadow-lg hover:prose-img:scale-110 hover:prose-img:origin-center hover:prose-img:duration-500">
|
||||
<figure class="post-list__img aspect-video border-b border-secondary rounded-t-md block h-auto w-full overflow-hidden m-0 p-0">
|
||||
<?php if ( has_post_thumbnail() ) : ?>
|
||||
<figure class="post-list__img aspect-video border-b border-secondary rounded-t-md block h-auto w-full overflow-hidden m-0 p-0">
|
||||
<?php
|
||||
$featImg = get_the_post_thumbnail_url();
|
||||
$postImg = $featImg ? $featImg : 'https://picsum.photos/600/400?random=' . get_the_ID();
|
||||
$postImg = get_the_post_thumbnail_url();
|
||||
$imgAlt = get_post_meta( get_post_thumbnail_id(), '_wp_attachment_image_alt', true ) ?? get_the_title();
|
||||
?>
|
||||
|
||||
<img class="block h-full object-cover transition-transform duration-300 ease-linear w-full will-change-transform" src="<?php echo esc_url( $postImg ); ?>" alt="<?php echo esc_attr( $imgAlt ); ?>">
|
||||
<?php endif; ?>
|
||||
</figure>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="post-list__details px-4 py-8 flex flex-col grow">
|
||||
<div class="post-list__cats">
|
||||
|
||||
+2
-2
@@ -2,11 +2,11 @@
|
||||
/**
|
||||
* ACF (Advanced Custom Fields) support class & functions
|
||||
*
|
||||
* @package SoloFrameEvo
|
||||
* @package KsPortfolio
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
/**
|
||||
* Class ACF
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
/**
|
||||
* Class Breadcrumbs
|
||||
@@ -46,7 +46,11 @@ class Breadcrumbs {
|
||||
// Static Page - add the parent pages if any, and the page title
|
||||
$breadcrumbs = array_merge( $breadcrumbs, self::getStaticPageBreadcrumbs( $post ) );
|
||||
} elseif ( is_category() || is_tag() || is_tax() ) {
|
||||
// Taxonomy Archive - add the taxonomy term name
|
||||
// Taxonomy Archive - if this is a project-type term, surface the
|
||||
// Projects archive as a clickable parent in the trail.
|
||||
if ( is_tax( 'project-type' ) ) {
|
||||
$breadcrumbs[] = self::getProjectsArchiveLinkBreadcrumb();
|
||||
}
|
||||
$breadcrumbs[] = self::getTaxonomyArchiveBreadcrumb();
|
||||
} elseif ( is_post_type_archive() ) {
|
||||
// Post Type Archive - add the post type name
|
||||
@@ -205,6 +209,24 @@ class Breadcrumbs {
|
||||
return $postType ? array( 'label' => $postType->labels->name ) : array();
|
||||
}
|
||||
|
||||
/** Generates a clickable Projects archive breadcrumb.
|
||||
*
|
||||
* Used in the taxonomy archive trail so users have a working "back to
|
||||
* all projects" link in the breadcrumb. Distinct from
|
||||
* getPostTypeArchiveBreadcrumb(), which omits the URL because on the
|
||||
* post-type archive itself that crumb is the active leaf and shouldn't
|
||||
* link to itself.
|
||||
*
|
||||
* @return array Breadcrumb data with both label and url.
|
||||
*/
|
||||
private static function getProjectsArchiveLinkBreadcrumb() {
|
||||
$url = get_post_type_archive_link( 'projects' );
|
||||
return array(
|
||||
'url' => $url ? $url : '',
|
||||
'label' => __( 'Projects', 'ks-portfolio' ),
|
||||
);
|
||||
}
|
||||
|
||||
/** Generates breadcrumbs for date-based archives.
|
||||
*
|
||||
* This method is responsible for creating breadcrumb navigation
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<?php
|
||||
/**
|
||||
* SoloFrameEvo Theme Enqueue Class
|
||||
* KsPortfolio Theme Enqueue Class
|
||||
*
|
||||
* @package SoloFrameEvo
|
||||
* @package KsPortfolio
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
/**
|
||||
* Class Enqueue
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<?php
|
||||
/**
|
||||
* SoloFrameEvo MenuItems Class
|
||||
* KsPortfolio MenuItems Class
|
||||
*
|
||||
* @package SoloFrameEvo
|
||||
* @package KsPortfolio
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
/**
|
||||
* Class MenuItems
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
/**
|
||||
* Project helper for the ks-portfolio theme.
|
||||
*
|
||||
* Thin wrapper around the projects-portfolio plugin's provider-aware
|
||||
* helpers. Templates should call these methods instead of branching on
|
||||
* provider, so the theme stays decoupled from GitHub/Gitea implementation
|
||||
* details.
|
||||
*
|
||||
* Every accessor is safe to call whether the plugin is active or not: when
|
||||
* the underlying function is missing or repo data is unavailable, defaults
|
||||
* that match the documented data contract are returned.
|
||||
*
|
||||
* @package KsPortfolio
|
||||
*/
|
||||
|
||||
namespace KsPortfolio;
|
||||
|
||||
/**
|
||||
* Provider-agnostic helper wrapping the projects-portfolio plugin.
|
||||
*/
|
||||
class Projects {
|
||||
|
||||
/**
|
||||
* Whether the projects-portfolio plugin is active.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_plugin_active(): bool {
|
||||
return function_exists( 'projects_portfolio_get_provider' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider label for a project (e.g. 'GitHub', 'Gitea', or 'Unknown').
|
||||
*
|
||||
* @param int $post_id Project post ID.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_provider_label( int $post_id ): string {
|
||||
if ( ! self::is_plugin_active() ) {
|
||||
return 'Unknown';
|
||||
}
|
||||
$provider = projects_portfolio_get_provider( $post_id );
|
||||
return method_exists( $provider, 'get_label' ) ? (string) $provider->get_label() : 'Unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Release ZIP URL (or empty string).
|
||||
*
|
||||
* @param int $post_id Project post ID.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_release_url( int $post_id ): string {
|
||||
if ( ! self::is_plugin_active() ) {
|
||||
return '';
|
||||
}
|
||||
return (string) projects_portfolio_get_release_url( $post_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Public repository browse URL (or empty string).
|
||||
*
|
||||
* @param int $post_id Project post ID.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_repo_browse_url( int $post_id ): string {
|
||||
if ( ! self::is_plugin_active() ) {
|
||||
return '';
|
||||
}
|
||||
$provider = projects_portfolio_get_provider( $post_id );
|
||||
return method_exists( $provider, 'get_repo_browse_url' ) ? (string) $provider->get_repo_browse_url() : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalized repository data array (or null).
|
||||
*
|
||||
* @param int $post_id Project post ID.
|
||||
* @return array<string,mixed>|null
|
||||
*/
|
||||
public static function get_repo_data( int $post_id ) {
|
||||
if ( ! self::is_plugin_active() ) {
|
||||
return null;
|
||||
}
|
||||
$data = projects_portfolio_get_repo_data( $post_id );
|
||||
return is_array( $data ) ? $data : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Owner data (avatar/login/html_url) or null.
|
||||
*
|
||||
* @param int $post_id Project post ID.
|
||||
* @return array<string,mixed>|null
|
||||
*/
|
||||
public static function get_owner_data( int $post_id ) {
|
||||
$data = self::get_repo_data( $post_id );
|
||||
if ( ! $data || empty( $data['owner']['login'] ) ) {
|
||||
return null;
|
||||
}
|
||||
return $data['owner'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Latest release tag, or 'Unknown'.
|
||||
*
|
||||
* @param int $post_id Project post ID.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_latest_version( int $post_id ): string {
|
||||
if ( ! self::is_plugin_active() ) {
|
||||
return 'Unknown';
|
||||
}
|
||||
$provider = projects_portfolio_get_provider( $post_id );
|
||||
return method_exists( $provider, 'get_latest_version' ) ? (string) $provider->get_latest_version() : 'Unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Local download endpoint URL for the project.
|
||||
*
|
||||
* @param int $post_id Project post ID.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_download_url( int $post_id ): string {
|
||||
return home_url( '/download/' . $post_id . '/' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a stat-card-safe integer with optional formatting.
|
||||
*
|
||||
* @param mixed $value Raw repo stat value.
|
||||
* @return string Display-safe string ('1.2k', '12,345', etc.) or '—' when empty.
|
||||
*/
|
||||
public static function format_stat( $value ): string {
|
||||
if ( null === $value || '' === $value ) {
|
||||
return '—';
|
||||
}
|
||||
$value = (int) $value;
|
||||
if ( $value >= 10000 ) {
|
||||
return number_format( $value / 1000, 1 ) . 'k';
|
||||
}
|
||||
return number_format( $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the project is configured as a website rather than a git repo.
|
||||
*
|
||||
* @param int $post_id Project post ID.
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_website( int $post_id ): bool {
|
||||
if ( ! self::is_plugin_active() ) {
|
||||
return false;
|
||||
}
|
||||
return (bool) projects_portfolio_is_website( $post_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Public URL for the project's website (or empty string).
|
||||
*
|
||||
* @param int $post_id Project post ID.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_site_url( int $post_id ): string {
|
||||
if ( ! self::is_plugin_active() ) {
|
||||
return '';
|
||||
}
|
||||
return (string) projects_portfolio_get_site_url( $post_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Platform label for the project's website (or empty string).
|
||||
*
|
||||
* @param int $post_id Project post ID.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_site_platform( int $post_id ): string {
|
||||
if ( ! self::is_plugin_active() ) {
|
||||
return '';
|
||||
}
|
||||
return (string) projects_portfolio_get_site_platform( $post_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Screenshot attachment URL for the project's website (or empty string).
|
||||
*
|
||||
* @param int $post_id Project post ID.
|
||||
* @param string $size Registered image size.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_site_screenshot_url( int $post_id, string $size = 'large' ): string {
|
||||
if ( ! self::is_plugin_active() ) {
|
||||
return '';
|
||||
}
|
||||
return (string) projects_portfolio_get_site_screenshot_url( $post_id, $size );
|
||||
}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Resources custom post type & taxonomies
|
||||
*
|
||||
* @package SoloFrameEvo
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
|
||||
/**
|
||||
* Class Resources
|
||||
*
|
||||
* This class is responsible for setting up the Resources post type and taxonomies.
|
||||
*
|
||||
* @package Basic-WP
|
||||
*/
|
||||
class Resources {
|
||||
/**
|
||||
* Constructor for the class.
|
||||
*
|
||||
* Initializes the class and sets up any necessary properties or methods.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'init', array( $this, 'registerPostType' ) );
|
||||
add_action( 'init', array( $this, 'registerTaxonomy' ) );
|
||||
add_filter( 'post_type_link', array( $this, 'postTypeLink' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a custom post type.
|
||||
*
|
||||
* This method is responsible for defining and registering a custom post type
|
||||
* with WordPress. It should include all necessary arguments and labels
|
||||
* required for the post type to function correctly.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function registerPostType() {
|
||||
register_post_type(
|
||||
'resources',
|
||||
array(
|
||||
'labels' => array(
|
||||
'name' => 'Resources',
|
||||
'singular_name' => 'Resource',
|
||||
'menu_name' => 'Resources',
|
||||
'name_admin_bar' => 'Resource',
|
||||
'add_new' => 'Add New Resource',
|
||||
'add_new_item' => 'Add New Resource',
|
||||
'edit_item' => 'Edit Resource',
|
||||
'new_item' => 'New Resource',
|
||||
'view_item' => 'View Resource',
|
||||
'search_items' => 'Search Resources',
|
||||
'not_found' => 'No resources found',
|
||||
'not_found_in_trash' => 'No resources found in Trash',
|
||||
),
|
||||
'public' => true,
|
||||
'has_archive' => true,
|
||||
'rewrite' => array( 'slug' => 'resources' ),
|
||||
'supports' => array( 'title', 'editor', 'excerpt', 'thumbnail', 'revisions', 'custom-fields' ),
|
||||
'menu_position' => 20,
|
||||
'menu_icon' => 'dashicons-hammer',
|
||||
'show_in_rest' => true,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a custom taxonomy.
|
||||
*
|
||||
* This method is responsible for defining and registering a custom taxonomy
|
||||
* within the WordPress environment. It should include the necessary arguments
|
||||
* and settings for the taxonomy to function as intended.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function registerTaxonomy() {
|
||||
register_taxonomy(
|
||||
'resource_type',
|
||||
array( 'resources' ),
|
||||
array(
|
||||
'labels' => array(
|
||||
'name' => 'Resource Types',
|
||||
'singular_name' => 'Resource Type',
|
||||
'search_items' => 'Search Resource Types',
|
||||
'all_items' => 'All Resource Types',
|
||||
'parent_item' => 'Parent Resource Type',
|
||||
'parent_item_colon' => 'Parent Resource Type:',
|
||||
'edit_item' => 'Edit Resource Type',
|
||||
'update_item' => 'Update Resource Type',
|
||||
'add_new_item' => 'Add New Resource Type',
|
||||
'new_item_name' => 'New Resource Type Name',
|
||||
'menu_name' => 'Resource Types',
|
||||
),
|
||||
'public' => true,
|
||||
'hierarchical' => true,
|
||||
'show_admin_column' => true,
|
||||
'rewrite' => array(
|
||||
'slug' => 'resources',
|
||||
'with_front' => false,
|
||||
'hierarchical' => true,
|
||||
),
|
||||
'show_in_rest' => true,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the permalink for a post of a specific post type.
|
||||
*
|
||||
* @param string $post_link The post's permalink.
|
||||
* @param WP_Post $post The post object.
|
||||
* @return string The filtered post permalink.
|
||||
*/
|
||||
public function postTypeLink( $post_link, $post ) {
|
||||
if ( 'resources' === $post->post_type ) {
|
||||
$terms = get_the_terms( $post->ID, 'resource_type' );
|
||||
if ( $terms && ! is_wp_error( $terms ) ) {
|
||||
$term_slug = $terms[0]->slug;
|
||||
|
||||
return home_url( "resources/{$term_slug}/{$post->post_name}" );
|
||||
}
|
||||
}
|
||||
|
||||
return $post_link;
|
||||
}
|
||||
}
|
||||
|
||||
new Resources();
|
||||
+51
-5
@@ -2,11 +2,11 @@
|
||||
/**
|
||||
* Filters. etc
|
||||
*
|
||||
* @package SoloFrameEvo
|
||||
* @package KsPortfolio
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
/** Get child pages of the current page, sorted by menu order.
|
||||
*
|
||||
@@ -65,16 +65,20 @@ add_filter(
|
||||
/**
|
||||
* Checks if the page should render a page header.
|
||||
*
|
||||
* Returns true only when the page heading ACF field is explicitly set to
|
||||
* 'default'. When 'none' is selected, no page header should render (the
|
||||
* page instead relies on its own hero block / template hero).
|
||||
*
|
||||
* @return bool true if page header should be rendered, false otherwise
|
||||
*/
|
||||
function hasPageHeader() {
|
||||
global $post;
|
||||
|
||||
if ( get_field( 'hero_style' ) !== 'none' ) {
|
||||
return false;
|
||||
if ( get_field( 'hero_style' ) === 'default' ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Create the Owner role.
|
||||
@@ -150,6 +154,8 @@ function getTheTitle() {
|
||||
if ( is_home() || is_single() ) {
|
||||
$title = get_the_title( get_option( 'page_for_posts', true ) );
|
||||
} elseif ( is_archive() ) {
|
||||
// get_the_archive_title() is filtered below to drop the
|
||||
// "Project Type:" / "Category:" prefix — see stripArchiveTitlePrefix().
|
||||
$title = get_the_archive_title();
|
||||
} elseif ( is_search() ) {
|
||||
$title = sprintf(
|
||||
@@ -166,6 +172,26 @@ function getTheTitle() {
|
||||
return $title;
|
||||
}
|
||||
|
||||
/** Strip the "Project Type:" / "Category:" / "Tag:" prefix from archive titles.
|
||||
*
|
||||
* WordPress's get_the_archive_title() returns plain text with the taxonomy
|
||||
* or post-type label concatenated before a colon (e.g. "Project Type: Plugins",
|
||||
* "Category: WordPress", "Tag: php"). The site's hero partial uses the raw
|
||||
* archive title as its <h1>, and templates render their own context-appropriate
|
||||
* headers, so the prefix is redundant and visually noisy.
|
||||
*
|
||||
* @param string $title The full archive title, including prefix.
|
||||
* @return string The title with the prefix removed.
|
||||
*/
|
||||
function stripArchiveTitlePrefix( $title ) {
|
||||
// Drop the "<Label>: " prefix WordPress prepends. The label may be wrapped
|
||||
// in a <span> on some archive types (date archives) — strip that too.
|
||||
$title = (string) $title;
|
||||
$title = preg_replace( '/^\s*(?:<[^>]+>\s*)*[^:<]+:\s*/u', '', $title );
|
||||
return $title;
|
||||
}
|
||||
add_filter( 'get_the_archive_title', __NAMESPACE__ . '\\stripArchiveTitlePrefix' );
|
||||
|
||||
/** Wraps iframes and embed elements in a div with a specific class.
|
||||
*
|
||||
* This function searches for iframe and embed elements within the provided
|
||||
@@ -194,6 +220,26 @@ function divWrapper( $content ) {
|
||||
|
||||
add_filter( 'the_content', __NAMESPACE__ . '\\divWrapper' );
|
||||
|
||||
/** Strip the "<Label>: " prefix from the document title.
|
||||
*
|
||||
* The SEO Framework wipes out all `pre_get_document_title` filters at
|
||||
* `template_redirect` priority 20 and composes the title itself. To strip
|
||||
* the archive prefix consistently across <title>, og:title, and twitter:title
|
||||
* (which all derive from TSF's generated archive title list), hook the
|
||||
* archive-title-items filter and return the unprefixed title.
|
||||
*
|
||||
* @param array $items [title, prefix, title_without_prefix].
|
||||
* @return array The same items with the prefix removed from the title.
|
||||
*/
|
||||
function stripArchiveTitleItemsPrefix( $items ) {
|
||||
if ( ! is_array( $items ) || empty( $items[2] ) ) {
|
||||
return $items;
|
||||
}
|
||||
$items[0] = $items[2];
|
||||
return $items;
|
||||
}
|
||||
add_filter( 'the_seo_framework_generated_archive_title_items', __NAMESPACE__ . '\\stripArchiveTitleItemsPrefix' );
|
||||
|
||||
/** Selectively add sidebar to page.
|
||||
*
|
||||
* This function adds a custom field group to the WordPress editor for
|
||||
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
<?php
|
||||
/**
|
||||
* SoloFrameEvo Theme Helpers
|
||||
* KsPortfolio Theme Helpers
|
||||
*
|
||||
* @package SoloFrameEvo
|
||||
* @package KsPortfolio
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
// Define global variables for theme and views folder paths.
|
||||
global $theme, $views;
|
||||
|
||||
+6
-6
@@ -1,12 +1,12 @@
|
||||
<?php
|
||||
/**
|
||||
* SoloFrameEvo Theme Hooks
|
||||
* KsPortfolio Theme Hooks
|
||||
*
|
||||
* @package SoloFrameEvo
|
||||
* @package KsPortfolio
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
/**
|
||||
* Add preconnect for Google fonts to head
|
||||
@@ -189,7 +189,7 @@ function init() {
|
||||
add_theme_support( 'customize-selective-refresh-widgets' );
|
||||
}
|
||||
|
||||
add_action( 'init', __NAMESPACE__ . '\\init', 1 );
|
||||
add_action( 'init', '\\KsPortfolio\\init', 1 );
|
||||
|
||||
/**
|
||||
* Allow SVG uploads
|
||||
@@ -268,5 +268,5 @@ function new_mail_from_name( $old ) {
|
||||
return get_option( 'blogname' );
|
||||
}
|
||||
|
||||
add_filter( 'wp_mail_from', __NAMESPACE__ . '\\new_mail_from' );
|
||||
add_filter( 'wp_mail_from_name', __NAMESPACE__ . '\\new_mail_from_name' );
|
||||
add_filter( 'wp_mail_from', '\\KsPortfolio\\new_mail_from' );
|
||||
add_filter( 'wp_mail_from_name', '\\KsPortfolio\\new_mail_from_name' );
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<?php
|
||||
/**
|
||||
* Search features for SoloFrameEvo theme.
|
||||
* Search features for KsPortfolio theme.
|
||||
*
|
||||
* @package SoloFrameEvo
|
||||
* @package KsPortfolio
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
/**
|
||||
* Modifies the WordPress query object for page search functionality.
|
||||
|
||||
+12
-10
@@ -2,11 +2,11 @@
|
||||
/**
|
||||
* Add a comment to show which template is being used on the current page.
|
||||
*
|
||||
* @package SoloFrameEvo
|
||||
* @package KsPortfolio
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
/**
|
||||
* Class ShowTemplate
|
||||
@@ -14,7 +14,7 @@ namespace SoloFrameEvo;
|
||||
* Displays the active WordPress template in the footer for debugging purposes.
|
||||
* Determines which template WordPress has chosen to use and outputs it as an HTML comment.
|
||||
*
|
||||
* @package SoloFrameEvo
|
||||
* @package KsPortfolio
|
||||
* @since 1.0.0
|
||||
*/
|
||||
class ShowTemplate {
|
||||
@@ -160,13 +160,15 @@ class ShowTemplate {
|
||||
if ( $fudge === $this->template || $fudge === false ) {
|
||||
echo wp_kses_post( "<!-- Active Template: {$this->template} -->\n" );
|
||||
} else {
|
||||
echo esc_html( "<!--\n" );
|
||||
echo esc_html( "The template loader logic has chosen a different template than what was used.\n\n" );
|
||||
echo esc_html( "Chosen Template: {$this->template}\n" );
|
||||
echo esc_html( "Actual Template: $fudge\n\n" );
|
||||
echo esc_html( "This will usually occur if the template file was overriden using an action on template_redirect.\n" );
|
||||
echo esc_html( "This is a best effort guess to catch such scenarios as mentioned above but can be incorrect.\n" );
|
||||
echo esc_html( "-->\n" );
|
||||
// phpcs:disable
|
||||
echo "<!--\n";
|
||||
echo "The template loader logic has chosen a different template than what was used.\n\n";
|
||||
echo "Chosen Template: {$this->template}\n";
|
||||
echo "Actual Template: $fudge\n\n";
|
||||
echo "This will usually occur if the template file was overriden using an action on template_redirect.\n";
|
||||
echo "This is a best effort guess to catch such scenarios as mentioned above but can be incorrect.\n";
|
||||
echo "-->\n";
|
||||
// phpcs:enable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
/**
|
||||
* Single Pages
|
||||
*
|
||||
* @package SoloFrameEvo
|
||||
* @package KsPortfolio
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
get_header();
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ export default defineConfig({
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
// baseURL: 'http://127.0.0.1:3000',
|
||||
baseURL: 'http://portfolio.test',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
@@ -38,7 +38,12 @@ export default defineConfig({
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
launchOptions: {
|
||||
args: [ '--host-resolver-rules=MAP portfolio.test 127.0.0.1' ],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 2.8 MiB |
+5
-4
@@ -2,11 +2,11 @@
|
||||
/**
|
||||
* Blog search template
|
||||
*
|
||||
* @package SoloFrameEvo
|
||||
* @package KsPortfolio
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
// Determine classes based on sidebar presence
|
||||
if ( hasSidebar() ) {
|
||||
@@ -29,15 +29,16 @@ get_header();
|
||||
the_post();
|
||||
?>
|
||||
<div class="post-list__post flex flex-col border border-secondary rounded-md shadow-lg hover:prose-img:scale-110 hover:prose-img:origin-center hover:prose-img:duration-500">
|
||||
<?php if ( has_post_thumbnail() ) : ?>
|
||||
<figure class="post-list__img aspect-video border-b border-secondary rounded-t-md block h-auto w-full overflow-hidden m-0 p-0">
|
||||
<?php
|
||||
$featImg = get_the_post_thumbnail_url();
|
||||
$postImg = $featimg ? $featImg : 'https://picsum.photos/600/400?random=' . get_the_ID();
|
||||
$postImg = get_the_post_thumbnail_url();
|
||||
$imgAlt = get_post_meta( get_post_thumbnail_id(), '_wp_attachment_image_alt', true ) ?? get_the_title();
|
||||
?>
|
||||
|
||||
<img class="block h-full object-cover transition-transform duration-300 ease-linear w-full will-change-transform" src="<?php echo esc_url( $postImg ); ?>" alt="<?php echo esc_attr( $imgAlt ); ?>">
|
||||
</figure>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="post-list__details px-4 py-8 flex flex-col flex-grow">
|
||||
<?php
|
||||
|
||||
+2
-2
@@ -2,10 +2,10 @@
|
||||
/**
|
||||
* Page Sidebar
|
||||
*
|
||||
* @package SoloFrameEvo
|
||||
* @package KsPortfolio
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
?>
|
||||
|
||||
|
||||
+2
-2
@@ -2,10 +2,10 @@
|
||||
/**
|
||||
* Sidebar
|
||||
*
|
||||
* @package SoloFrameEvo
|
||||
* @package KsPortfolio
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
?>
|
||||
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
/**
|
||||
* Single Project Template
|
||||
*
|
||||
* Renders the project detail page: Carbon Blue hero with provider,
|
||||
* title, and version; Download / View Repo CTAs; two-column body with
|
||||
* main content and a sidebar of owner, repository details, and stat
|
||||
* cards.
|
||||
*
|
||||
* @package KsPortfolio
|
||||
*/
|
||||
|
||||
namespace KsPortfolio;
|
||||
|
||||
get_header();
|
||||
|
||||
while ( have_posts() ) :
|
||||
the_post();
|
||||
$project_id = (int) get_the_ID();
|
||||
$is_website = Projects::is_website( $project_id );
|
||||
$site_url = Projects::get_site_url( $project_id );
|
||||
$site_platform = Projects::get_site_platform( $project_id );
|
||||
$site_shot_url = Projects::get_site_screenshot_url( $project_id, 'large' );
|
||||
$provider = $is_website ? __( 'Website', 'ks-portfolio' ) : Projects::get_provider_label( $project_id );
|
||||
$version = Projects::get_latest_version( $project_id );
|
||||
$release = Projects::get_release_url( $project_id );
|
||||
$repo = Projects::get_repo_browse_url( $project_id );
|
||||
$repo_data = Projects::get_repo_data( $project_id );
|
||||
$owner = Projects::get_owner_data( $project_id );
|
||||
$type_terms = get_the_terms( $project_id, 'project-type' );
|
||||
$type_label = ! empty( $type_terms ) && ! is_wp_error( $type_terms ) ? strtoupper( $type_terms[0]->name ) : '';
|
||||
$excerpt = has_excerpt( $project_id ) ? get_the_excerpt( $project_id ) : '';
|
||||
?>
|
||||
<article class="single-project">
|
||||
<div class="container mx-auto my-section">
|
||||
|
||||
<header class="single-project__hero">
|
||||
<p class="single-project__meta">
|
||||
<span class="single-project__provider"><?php echo esc_html( $provider ); ?></span>
|
||||
<?php if ( $type_label ) : ?>
|
||||
<span class="single-project__type"><?php echo esc_html( $type_label ); ?></span>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
<h1 class="single-project__title">
|
||||
<?php echo esc_html( get_the_title( $project_id ) ); ?>
|
||||
<?php if ( ! $is_website && 'Unknown' !== $version ) : ?>
|
||||
<span class="single-project__version">v<?php echo esc_html( $version ); ?></span>
|
||||
<?php endif; ?>
|
||||
</h1>
|
||||
<?php if ( $excerpt ) : ?>
|
||||
<p class="single-project__excerpt"><?php echo esc_html( $excerpt ); ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="single-project__actions">
|
||||
<?php if ( $is_website ) : ?>
|
||||
<?php if ( $site_url ) : ?>
|
||||
<a class="button button--primary" href="<?php echo esc_url( $site_url ); ?>" target="_blank" rel="noopener noreferrer">
|
||||
<?php echo esc_html__( 'Visit Site', 'ks-portfolio' ); ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php else : ?>
|
||||
<?php if ( $release ) : ?>
|
||||
<a class="button button--primary" href="<?php echo esc_url( Projects::get_download_url( $project_id ) ); ?>" rel="nofollow">
|
||||
<?php echo esc_html__( 'Download', 'ks-portfolio' ); ?>
|
||||
<?php if ( 'Unknown' !== $version ) : ?>
|
||||
v<?php echo esc_html( $version ); ?>
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php if ( $repo ) : ?>
|
||||
<a class="button button--outline" href="<?php echo esc_url( $repo ); ?>" target="_blank" rel="noopener noreferrer">
|
||||
<?php echo esc_html__( 'View Repo', 'ks-portfolio' ); ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="single-project__layout">
|
||||
<div class="single-project__body">
|
||||
<?php if ( $is_website && $site_shot_url ) : ?>
|
||||
<figure class="single-project__screenshot">
|
||||
<a href="<?php echo esc_url( $site_url ); ?>" target="_blank" rel="noopener noreferrer">
|
||||
<img src="<?php echo esc_url( $site_shot_url ); ?>" alt="<?php echo esc_attr( get_the_title( $project_id ) ); ?>" />
|
||||
</a>
|
||||
</figure>
|
||||
<?php elseif ( has_post_thumbnail( $project_id ) ) : ?>
|
||||
<figure class="single-project__featured">
|
||||
<?php echo get_the_post_thumbnail( $project_id, 'large' ); ?>
|
||||
</figure>
|
||||
<?php endif; ?>
|
||||
|
||||
<section class="single-project__content" aria-label="<?php echo esc_attr__( 'Architecture and Implementation', 'ks-portfolio' ); ?>">
|
||||
<h2><?php echo esc_html__( 'Architecture & Implementation', 'ks-portfolio' ); ?></h2>
|
||||
<?php the_content(); ?>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<aside class="single-project__sidebar" aria-label="<?php echo esc_attr__( $is_website ? __( 'Website details', 'ks-portfolio' ) : __( 'Repository details', 'ks-portfolio' ) ); ?>">
|
||||
<?php if ( $is_website ) : ?>
|
||||
<?php if ( $site_platform ) : ?>
|
||||
<section class="single-project__details">
|
||||
<h3><?php echo esc_html__( 'Website Details', 'ks-portfolio' ); ?></h3>
|
||||
<?php
|
||||
set_query_var( 'label', __( 'Platform', 'ks-portfolio' ) );
|
||||
set_query_var( 'value', $site_platform );
|
||||
get_template_part( 'views/components/project-meta-row' );
|
||||
?>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
<?php else : ?>
|
||||
<?php if ( $owner ) : ?>
|
||||
<section class="single-project__maintainer">
|
||||
<h3><?php echo esc_html__( 'Maintainer', 'ks-portfolio' ); ?></h3>
|
||||
<div class="single-project__owner">
|
||||
<?php if ( ! empty( $owner['avatar_url'] ) ) : ?>
|
||||
<span class="single-project__owner-avatar" style="background-image: url('<?php echo esc_url( $owner['avatar_url'] ); ?>');" aria-hidden="true"></span>
|
||||
<?php endif; ?>
|
||||
<a class="single-project__owner-link" href="<?php echo esc_url( ! empty( $owner['html_url'] ) ? $owner['html_url'] : ( 'https://github.com/' . $owner['login'] ) ); ?>" target="_blank" rel="noopener noreferrer">
|
||||
<?php echo esc_html( $owner['login'] ); ?>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<section class="single-project__details">
|
||||
<h3><?php echo esc_html__( 'Repository Details', 'ks-portfolio' ); ?></h3>
|
||||
<?php
|
||||
$updated_at = is_array( $repo_data ) && ! empty( $repo_data['updated_at'] )
|
||||
? human_time_diff( strtotime( $repo_data['updated_at'] ) ) . ' ' . __( 'ago', 'ks-portfolio' )
|
||||
: '—';
|
||||
$license = is_array( $repo_data ) && ! empty( $repo_data['license']['name'] ) ? $repo_data['license']['name'] : __( 'None', 'ks-portfolio' );
|
||||
$language = is_array( $repo_data ) && ! empty( $repo_data['language'] ) ? $repo_data['language'] : '—';
|
||||
|
||||
$rows = array();
|
||||
$rows[] = array(
|
||||
'label' => __( 'Version', 'ks-portfolio' ),
|
||||
'value' => 'Unknown' !== $version ? 'v' . $version : __( 'Unknown', 'ks-portfolio' ),
|
||||
);
|
||||
$rows[] = array(
|
||||
'label' => __( 'Last Updated', 'ks-portfolio' ),
|
||||
'value' => $updated_at,
|
||||
);
|
||||
$rows[] = array(
|
||||
'label' => __( 'License', 'ks-portfolio' ),
|
||||
'value' => $license,
|
||||
);
|
||||
$rows[] = array(
|
||||
'label' => __( 'Language', 'ks-portfolio' ),
|
||||
'value' => $language,
|
||||
);
|
||||
|
||||
foreach ( $rows as $row ) :
|
||||
set_query_var( 'label', $row['label'] );
|
||||
set_query_var( 'value', $row['value'] );
|
||||
get_template_part( 'views/components/project-meta-row' );
|
||||
endforeach;
|
||||
?>
|
||||
</section>
|
||||
|
||||
<section class="single-project__stats" aria-label="<?php echo esc_attr__( 'Repository statistics', 'ks-portfolio' ); ?>">
|
||||
<?php
|
||||
$stats = array();
|
||||
$stats[] = array(
|
||||
'label' => __( 'Downloads', 'ks-portfolio' ),
|
||||
'value' => Projects::format_stat( get_post_meta( $project_id, '_projects_portfolio_download_count', true ) ),
|
||||
);
|
||||
$stats[] = array(
|
||||
'label' => __( 'Stars', 'ks-portfolio' ),
|
||||
'value' => Projects::format_stat( is_array( $repo_data ) ? ( $repo_data['stargazers_count'] ?? 0 ) : 0 ),
|
||||
);
|
||||
$stats[] = array(
|
||||
'label' => __( 'Forks', 'ks-portfolio' ),
|
||||
'value' => Projects::format_stat( is_array( $repo_data ) ? ( $repo_data['forks_count'] ?? 0 ) : 0 ),
|
||||
);
|
||||
$stats[] = array(
|
||||
'label' => __( 'Issues', 'ks-portfolio' ),
|
||||
'value' => Projects::format_stat( is_array( $repo_data ) ? ( $repo_data['open_issues_count'] ?? 0 ) : 0 ),
|
||||
);
|
||||
|
||||
foreach ( $stats as $stat ) :
|
||||
set_query_var( 'label', $stat['label'] );
|
||||
set_query_var( 'value', $stat['value'] );
|
||||
get_template_part( 'views/components/project-stat-card' );
|
||||
endforeach;
|
||||
?>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<?php
|
||||
endwhile;
|
||||
|
||||
get_footer();
|
||||
+2
-2
@@ -2,11 +2,11 @@
|
||||
/**
|
||||
* Single Posts
|
||||
*
|
||||
* @package SoloFrameEvo
|
||||
* @package KsPortfolio
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
get_header();
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Theme Name: Portfolio 2026
|
||||
* Description: A clean and modern portfolio theme for showcasing your work.
|
||||
* Description: A clean and modern portfolio theme for showcasing my work.
|
||||
* Version: 1.0
|
||||
* Author: Keith Solomon
|
||||
* Text Domain: sf-evo
|
||||
* Text Domain: ks-portfolio
|
||||
*/
|
||||
|
||||
+35
-7
@@ -1,6 +1,6 @@
|
||||
/* Theme color definitions */
|
||||
|
||||
@theme {
|
||||
@theme static {
|
||||
--color-black: oklch(0% 0 0);
|
||||
--color-white: oklch(100% 0 0);
|
||||
|
||||
@@ -29,13 +29,41 @@
|
||||
--color-secondary-800: color-mix(in oklch, var(--color-secondary) 80%, white);
|
||||
--color-secondary-900: color-mix(in oklch, var(--color-secondary) 90%, white);
|
||||
|
||||
--color-bodylinks: oklch(0.48 0.0789 211.58);
|
||||
--color-footlinks: oklch(0.65 0.1104 212.2);
|
||||
/* WCAG AA on --color-page-base (#100e0b): ~7:1 contrast, also visually
|
||||
* distinct from surrounding --color-on-surface (#e3e1e9) for inline links. */
|
||||
--color-bodylinks: oklch(0.68 0.16 250);
|
||||
--color-footlinks: oklch(0.68 0.16 250);
|
||||
|
||||
--color-success: oklch(64.01% 0.1751 146.7);
|
||||
--color-info: oklch(0.55 0.0922 211.57);
|
||||
--color-warning: oklch(84.42% 0.1722 84.93);
|
||||
--color-danger: oklch(0.5126 0.1865 22.61);
|
||||
--color-light: oklch(98.16% 0.0017 247.8);
|
||||
--color-dark: oklch(34.51% 0.0133 248.2);
|
||||
|
||||
/* Carbon Blue / dark surface tokens (Portfolio 2026) */
|
||||
--color-surface: oklch(18.79% 0.0103 276.4);
|
||||
--color-surface-dim: oklch(18.79% 0.0103 276.4);
|
||||
--color-surface-bright: oklch(34.59% 0.0105 278.3);
|
||||
--color-surface-container-lowest: oklch(16.51% 0.0106 276.3);
|
||||
--color-surface-container-low: oklch(22.37% 0.0117 277.9);
|
||||
--color-surface-container: oklch(24.08% 0.0115 278);
|
||||
--color-surface-container-high: oklch(28.6% 0.0092 276.8);
|
||||
--color-surface-container-highest: oklch(32.74% 0.0105 285.8);
|
||||
--color-on-surface: oklch(91.36% 0.011 297.6);
|
||||
--color-on-surface-variant: oklch(82.77% 0.0194 286);
|
||||
--color-outline: oklch(65.68% 0.0192 282.3);
|
||||
--color-outline-variant: oklch(39.65% 0.0187 277.4);
|
||||
--color-page-base: oklch(16.5% 0.007 78.1);
|
||||
--color-backlight: oklch(71.37% 0.1434 254.6 / 0.2);
|
||||
|
||||
--color-primary-fixed: oklch(91.52% 0.0412 278.1);
|
||||
--color-primary-fixed-dim: oklch(83.03% 0.0845 274.1);
|
||||
--color-on-primary: oklch(32.02% 0.1432 262.8);
|
||||
--color-on-primary-fixed: oklch(23.06% 0.1072 262.1);
|
||||
--color-on-primary-fixed-variant: oklch(40.35% 0.1363 266.3);
|
||||
|
||||
--color-primary-12: oklch(83.03% 0.0845 274.1 / 0.12);
|
||||
--color-primary-87: oklch(91.52% 0.0412 278.1);
|
||||
|
||||
--color-success: oklch(77.17% 0.1386 157.2);
|
||||
--color-warning: oklch(91.37% 0.1234 97.2);
|
||||
--color-danger: oklch(54.12% 0.1456 29.8);
|
||||
--color-info: oklch(45.67% 0.1345 210.3);
|
||||
}
|
||||
|
||||
+50
-2
@@ -23,8 +23,8 @@
|
||||
|
||||
/* Basic layout styles */
|
||||
main#maincontent {
|
||||
background-color: var(--color-background);
|
||||
color: var(--color-text);
|
||||
background-color: transparent;
|
||||
color: var(--color-on-surface, #e3e1e9);
|
||||
margin: 0;
|
||||
padding: 0 0 4rem;
|
||||
}
|
||||
@@ -83,3 +83,51 @@ main#maincontent {
|
||||
/* Responsive embeds */
|
||||
.embed { position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; }
|
||||
.embed iframe, .embed object, .embed embed, .embed video { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
|
||||
|
||||
/* Carbon Blue atmospheric field — rendered once globally (in header.php)
|
||||
* as a fixed-position layer behind all content. Decorative only; content
|
||||
* stays clickable thanks to pointer-events: none. The body carries
|
||||
* `background-color: #100e0b` so the gradient's mix-blend-mode layers
|
||||
* have something to composite against. */
|
||||
.aura-bg {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.aura-layer-1 {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(138deg, #05070c 0%, #0f172a 40%, #172554 70%, #1e3a8a 100%);
|
||||
mix-blend-mode: normal;
|
||||
pointer-events: none;
|
||||
transform: translateZ(0);
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.aura-layer-2 {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(ellipse 45% 50% at 72% 40%, rgba(96, 165, 250, 0.20) 0%, transparent 68%);
|
||||
mix-blend-mode: screen;
|
||||
pointer-events: none;
|
||||
transform: translateZ(0);
|
||||
will-change: transform;
|
||||
filter: blur(105px);
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.aura-layer-2 { filter: blur(151px); }
|
||||
}
|
||||
|
||||
.aura-grain {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
mix-blend-mode: overlay;
|
||||
opacity: 0.85;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.aura-grain svg { display: block; width: 100%; height: 100%; }
|
||||
|
||||
@@ -39,8 +39,9 @@
|
||||
* text-75px | 32.00 | 39.46 | 43.25 | 50.30 | 57.36 | 61.77 | 75.00
|
||||
*/
|
||||
|
||||
@theme {
|
||||
@theme static {
|
||||
--font-sans: "Raleway", sans-serif;
|
||||
--font-headings: var(--font-sans);
|
||||
--line-height: 1.6;
|
||||
|
||||
--text-base: 1rem;
|
||||
@@ -68,8 +69,8 @@
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: white;
|
||||
color: black;
|
||||
background-color: #100e0b;
|
||||
color: var(--color-on-surface, #e3e1e9);
|
||||
font-family: var(--font-sans);
|
||||
font-size: var(--text-base);
|
||||
line-height: var(--line-height);
|
||||
@@ -119,6 +120,14 @@ a, .link {
|
||||
&:hover { color: var(--color-primary); }
|
||||
}
|
||||
|
||||
/* Inline links inside flowing text need an underline so they remain
|
||||
* distinguishable from surrounding text without relying solely on color
|
||||
* (WCAG 2.4.4 / axe link-in-text-block). */
|
||||
p a, li a, dd a, td a, blockquote a {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
h1 a, .h1 a,
|
||||
h2 a, .h2 a,
|
||||
h3 a, .h3 a {
|
||||
@@ -131,10 +140,15 @@ p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ul, ol { margin: 0 0 1rem 1.5rem; padding: 0; }
|
||||
|
||||
ul { list-style-type: disc; }
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
ol { list-style-type: decimal; }
|
||||
ol {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
li ul, li ol { margin: 0 1rem; }
|
||||
|
||||
@@ -160,9 +174,7 @@ pre code {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
code {
|
||||
@apply bg-black/30 px-[3px] py-0.5 font-mono text-black text-xs rounded-sm;
|
||||
}
|
||||
code { @apply bg-white/30 px-0.25 py-0.25 font-mono text-black rounded-sm; }
|
||||
|
||||
hr {
|
||||
background-color: black;
|
||||
|
||||
+93
-32
@@ -1,6 +1,6 @@
|
||||
/* Button styles */
|
||||
|
||||
@theme {
|
||||
@theme inline {
|
||||
/* Configuration */
|
||||
|
||||
/**
|
||||
@@ -18,14 +18,14 @@
|
||||
* --button-outline-color (fallback is --button-border-color)
|
||||
*/
|
||||
|
||||
--button-bg: var(--color-primary);
|
||||
--button-color: var(--color-white);
|
||||
--button-bg: var(--color-on-primary-fixed-variant);
|
||||
--button-color: var(--color-primary-fixed-dim);
|
||||
--button-hover-bg: var(--color-info);
|
||||
--button-hover-border-color: var(--color-info);
|
||||
--button-hover-color: var(--color-white);
|
||||
--button-border-width: 3px;
|
||||
--button-border-style: solid;
|
||||
--button-border-color: var(--button-bg);
|
||||
--button-border-color: var(--color-primary-fixed-dim);
|
||||
--button-radius: 0.5rem;
|
||||
}
|
||||
|
||||
@@ -50,11 +50,14 @@
|
||||
|
||||
&[data-button-variant="outline"] {
|
||||
background: transparent;
|
||||
&:hover { background: transparent; }
|
||||
color: var(--button-border-color);
|
||||
border-color: var(--button-bg);
|
||||
|
||||
--button-color: var(--button-outline-color, var(--button-bg));
|
||||
--button-hover-border-color: var(--button-hover-bg);
|
||||
--button-hover-color: var(--button-hover-bg);
|
||||
&:hover {
|
||||
background: transparent;
|
||||
border-color: var(--button-hover-bg);
|
||||
color: var(--button-hover-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,20 +79,39 @@
|
||||
|
||||
/* Back To Top Button */
|
||||
#backToTopBtn {
|
||||
display:none;
|
||||
position:fixed;
|
||||
bottom:2rem;
|
||||
right:2rem;
|
||||
z-index:1000;
|
||||
padding:0.75em 1.5em;
|
||||
font-size:1.1rem;
|
||||
border-radius:2em;
|
||||
background: var(--color-primary,#3857BC);
|
||||
color:#fff;
|
||||
border: none;
|
||||
border-radius: 2em;
|
||||
bottom: 2rem;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
font-size: 1.1rem;
|
||||
padding: 0.75em 1.5em;
|
||||
position: fixed;
|
||||
right: 2rem;
|
||||
transition: opacity 0.2s;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.back-to-top {
|
||||
background: var(--color-primary, #3857BC);
|
||||
border: none;
|
||||
border-radius: 2em;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.85;
|
||||
padding: 0.75em 1.5em;
|
||||
transition: opacity 0.2s, background 0.2s;
|
||||
|
||||
&:hover, &:focus {
|
||||
background: var(--color-info, #233a7a);
|
||||
opacity: 1;
|
||||
outline: 2px solid var(--color-info, #233a7a);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -157,20 +179,59 @@ x-button:has(.button[data-button-width="full"]) { @apply w-full; }
|
||||
--button-hover-color: var(--color-dark);
|
||||
}
|
||||
|
||||
.back-to-top {
|
||||
background: var(--color-primary, #3857BC);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 2em;
|
||||
padding: 0.75em 1.5em;
|
||||
font-size: 1.1rem;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s, background 0.2s;
|
||||
opacity: 0.85;
|
||||
/* Dark-surface button variants (WCAG-AA against #100e0b) */
|
||||
.button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
line-height: 1.1;
|
||||
padding: 0.5rem 2rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 3px solid transparent;
|
||||
text-decoration: none;
|
||||
transition: color 200ms ease, background-color 200ms ease, border-color 200ms ease;
|
||||
}
|
||||
|
||||
.button:active { transform: scale(0.99); }
|
||||
|
||||
@media (prefers-reduced-motion: reduce) { .button:active { transform: none; } }
|
||||
|
||||
.button:focus-visible { outline: 2px solid var(--color-primary, #b6c4ff); outline-offset: 3px; }
|
||||
|
||||
.button--primary {
|
||||
background: var(--color-primary, #b6c4ff);
|
||||
color: var(--color-on-primary, #05297a);
|
||||
border-color: var(--color-primary, #b6c4ff);
|
||||
|
||||
&:hover { background: var(--color-primary-87, #dce1ff); border-color: var(--color-primary-87, #dce1ff); }
|
||||
}
|
||||
|
||||
.button--outline {
|
||||
background: transparent;
|
||||
border-color: var(--color-outline, #8f909d);
|
||||
color: var(--color-on-surface, #e3e1e9);
|
||||
|
||||
&:hover { border-color: var(--color-primary, #b6c4ff); color: var(--color-primary, #b6c4ff); }
|
||||
}
|
||||
|
||||
.button--secondary {
|
||||
background: var(--color-surface-container-high, #292a2f);
|
||||
border-color: var(--color-outline-variant, #444651);
|
||||
color: var(--color-on-surface, #e3e1e9);
|
||||
}
|
||||
|
||||
/* Outline button over the site-header's bg-secondary surface needs lighter text
|
||||
* because --color-on-surface (#e3e1e9) on bg-secondary (#6e757f) is only 3.59:1.
|
||||
* Pure white clears 4.5:1 against #6e757f; --color-on-primary (#05297a) is too
|
||||
* dark and drops to 2.81:1, so we go to --color-white instead. */
|
||||
.site-header .button--outline {
|
||||
border-color: var(--color-white, #ffffff);
|
||||
color: var(--color-white, #ffffff);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-primary-87, #dce1ff);
|
||||
color : var(--color-primary-87, #dce1ff);
|
||||
}
|
||||
.back-to-top:hover, .back-to-top:focus {
|
||||
background: var(--color-info, #233a7a);
|
||||
opacity: 1;
|
||||
outline: 2px solid var(--color-info, #233a7a);
|
||||
}
|
||||
|
||||
@@ -3,3 +3,10 @@
|
||||
margin-right: calc(50% - 50vw);
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
/* WP block buttons authored with vivid-cyan-blue backgrounds carry inline
|
||||
* white text — that combo is only 3.33:1 and fails WCAG AA. Force dark text.
|
||||
* !important needed because WP core ships .has-white-color with !important. */
|
||||
.wp-block-button__link.has-vivid-cyan-blue-background-color {
|
||||
color: var(--color-page-base, #100e0b) !important;
|
||||
}
|
||||
|
||||
@@ -6,3 +6,5 @@
|
||||
@import "./sidebar.css";
|
||||
@import "./pagination.css";
|
||||
@import "./site-footer.css";
|
||||
@import "./project-card.css";
|
||||
@import "./page-hero.css";
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/* Page Hero partial styles */
|
||||
|
||||
.page-hero {
|
||||
background: var(--color-surface-container-low, #1a1b21);
|
||||
color: var(--color-on-surface, #e3e1e9);
|
||||
border-bottom: 1px solid var(--color-outline-variant, #444651);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.page-hero__title { color: var(--color-on-surface, #e3e1e9); font-family: var(--font-sans); font-weight: 700; }
|
||||
.page-hero__intro { color: var(--color-on-surface-variant, #c5c5d3); }
|
||||
@@ -1,5 +1,11 @@
|
||||
/* Blog/post index listing styles */
|
||||
|
||||
/* Category pill on dark page surface — needs higher contrast than the default
|
||||
* --color-bodylinks (~3:1 against --color-page-base) for WCAG AA. */
|
||||
.post-list__category {
|
||||
color: var(--color-primary-87, #dce1ff);
|
||||
}
|
||||
|
||||
.post-list {
|
||||
/* Original styles, if needed
|
||||
* .post-list__posts {
|
||||
|
||||
@@ -0,0 +1,357 @@
|
||||
/* Project card — Carbon Blue surface */
|
||||
.project-card {
|
||||
background: var(--color-surface-container, #1e1f25);
|
||||
border: 1px solid var(--color-outline-variant, #444651);
|
||||
border-radius: 1rem;
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
transition: border-color 200ms ease, transform 200ms ease;
|
||||
}
|
||||
|
||||
.project-card:hover,
|
||||
.project-card:focus-within {
|
||||
border-color: var(--color-primary, #b6c4ff);
|
||||
}
|
||||
|
||||
.project-card:focus-within {
|
||||
outline: 2px solid var(--color-primary, #b6c4ff);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.project-card { transition: none; }
|
||||
}
|
||||
|
||||
.project-card__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.project-card__type {
|
||||
background: var(--color-primary-12);
|
||||
color: var(--color-primary, #b6c4ff);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.project-card__provider { color: var(--color-on-surface-variant, #c5c5d3); }
|
||||
|
||||
.project-card__title {
|
||||
font-family: var(--font-sans);
|
||||
font-weight: 700;
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
color: var(--color-on-surface, #e3e1e9);
|
||||
}
|
||||
|
||||
.project-card__title-link {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.project-card__title-link:hover { text-decoration: underline; }
|
||||
|
||||
.project-card__excerpt {
|
||||
color: var(--color-on-surface-variant, #c5c5d3);
|
||||
font-size: 0.9375rem;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 4;
|
||||
line-clamp: 4;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.project-card__foot {
|
||||
margin-top: auto;
|
||||
padding-top: 0.75rem;
|
||||
border-top: 1px solid var(--color-outline-variant, #444651);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.project-card__meta { display: inline-flex; gap: 0.75rem; align-items: center; color: var(--color-on-surface-variant, #c5c5d3); font-size: 0.875rem; }
|
||||
|
||||
.project-card__actions { display: inline-flex; gap: 0.5rem; }
|
||||
|
||||
.project-card__action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.1;
|
||||
text-decoration: none;
|
||||
border: 2px solid transparent;
|
||||
transition: color 200ms ease, background-color 200ms ease, border-color 200ms ease;
|
||||
|
||||
&:hover { background: var(--color-primary-87); border-color: var(--color-primary-87); }
|
||||
}
|
||||
|
||||
.project-card__action--primary {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border-color: var(--color-primary, #b6c4ff);
|
||||
}
|
||||
|
||||
|
||||
.project-card__action--secondary {
|
||||
color: var(--color-on-surface, #e3e1e9);
|
||||
border-color: var(--color-outline, #8f909d);
|
||||
}
|
||||
|
||||
.project-card__action:focus-visible {
|
||||
outline: 2px solid var(--color-primary, #b6c4ff);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--color-surface-container, #1e1f25);
|
||||
border: 1px solid var(--color-outline-variant, #444651);
|
||||
border-radius: 1rem;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-card__icon { color: var(--color-on-surface-variant, #c5c5d3); }
|
||||
|
||||
.stat-card__value {
|
||||
font-weight: 700;
|
||||
font-size: 1.5rem;
|
||||
color: var(--color-on-surface, #e3e1e9);
|
||||
}
|
||||
|
||||
.stat-card__label {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-on-surface-variant, #c5c5d3);
|
||||
}
|
||||
|
||||
.meta-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 0;
|
||||
border-bottom: 1px solid var(--color-outline-variant, #444651);
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
.meta-row:last-child { border-bottom: 0; }
|
||||
|
||||
.meta-row__label { color: var(--color-on-surface-variant, #c5c5d3); display: inline-flex; align-items: center; gap: 0.5rem; }
|
||||
.meta-row__value { color: var(--color-on-surface, #e3e1e9); font-weight: 600; }
|
||||
.meta-row__icon { color: var(--color-on-surface-variant, #c5c5d3); }
|
||||
|
||||
.archive-projects { padding-top: 2rem; padding-bottom: 4rem; }
|
||||
|
||||
.archive-projects__head {
|
||||
padding-left: 1rem;
|
||||
border-left: 4px solid var(--color-primary, #b6c4ff);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.archive-projects__title {
|
||||
font-family: var(--font-sans);
|
||||
font-weight: 700;
|
||||
font-size: 2.25rem;
|
||||
line-height: 1.2;
|
||||
margin: 0 0 0.5rem;
|
||||
color: var(--color-on-surface, #e3e1e9);
|
||||
}
|
||||
|
||||
.archive-projects__intro {
|
||||
color: var(--color-on-surface-variant, #c5c5d3);
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
max-width: 70ch;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.archive-projects__empty {
|
||||
text-align: center;
|
||||
padding: 4rem 1rem;
|
||||
color: var(--color-on-surface-variant, #c5c5d3);
|
||||
}
|
||||
|
||||
.archive-projects__empty-title {
|
||||
font-weight: 700;
|
||||
font-size: 1.5rem;
|
||||
margin: 0 0 0.5rem;
|
||||
color: var(--color-on-surface, #e3e1e9);
|
||||
}
|
||||
|
||||
/* Pagination links on dark surface */
|
||||
.archive-projects__pagination a,
|
||||
.archive-projects__pagination span {
|
||||
background: var(--color-surface-container, #1e1f25);
|
||||
color: var(--color-on-surface, #e3e1e9);
|
||||
border: 1px solid var(--color-outline-variant, #444651);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Project-type taxonomy archive — the page hero renders the heading,
|
||||
* breadcrumbs handle navigation. */
|
||||
.tax-project { padding-top: 2rem; padding-bottom: 4rem; }
|
||||
|
||||
.tax-project__pagination {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.tax-project__empty {
|
||||
text-align: center;
|
||||
padding: 4rem 1rem;
|
||||
color: var(--color-on-surface-variant, #c5c5d3);
|
||||
}
|
||||
|
||||
.tax-project__empty-title {
|
||||
font-weight: 700;
|
||||
font-size: 1.5rem;
|
||||
margin: 0 0 0.5rem;
|
||||
color: var(--color-on-surface, #e3e1e9);
|
||||
}
|
||||
|
||||
.tax-project__pagination a,
|
||||
.tax-project__pagination span {
|
||||
background: var(--color-surface-container, #1e1f25);
|
||||
color: var(--color-on-surface, #e3e1e9);
|
||||
border: 1px solid var(--color-outline-variant, #444651);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.single-project__hero {
|
||||
background: var(--color-surface-container-low, #1a1b21);
|
||||
border: 1px solid var(--color-outline-variant, #444651);
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.single-project__meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin: 0 0 0.5rem;
|
||||
color: var(--color-on-surface-variant, #c5c5d3);
|
||||
font-size: 0.875rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.single-project__title {
|
||||
font-family: var(--font-sans);
|
||||
font-weight: 700;
|
||||
font-size: 2.25rem;
|
||||
line-height: 1.2;
|
||||
color: var(--color-on-surface, #e3e1e9);
|
||||
margin: 0 0 0.75rem;
|
||||
}
|
||||
|
||||
.single-project__version { color: var(--color-on-surface-variant, #c5c5d3); font-weight: 400; font-size: 1.5rem; margin-left: 0.5rem; }
|
||||
|
||||
.single-project__excerpt {
|
||||
color: var(--color-on-surface-variant, #c5c5d3);
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
max-width: 70ch;
|
||||
margin: 0 0 1.5rem;
|
||||
}
|
||||
|
||||
.single-project__actions { display: flex; flex-wrap: wrap; gap: 0.75rem; }
|
||||
|
||||
.single-project__layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.single-project__layout {
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.single-project__body h2,
|
||||
.single-project__sidebar h3 { color: var(--color-on-surface, #e3e1e9); font-family: var(--font-sans); font-weight: 700; }
|
||||
|
||||
.single-project__maintainer,
|
||||
.single-project__details,
|
||||
.single-project__stats {
|
||||
background: var(--color-surface-container, #1e1f25);
|
||||
border: 1px solid var(--color-outline-variant, #444651);
|
||||
border-radius: 1rem;
|
||||
padding: 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.single-project__owner { display: flex; align-items: center; gap: 0.75rem; }
|
||||
.single-project__owner-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background-size: 40px 40px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--color-surface-container-high, #292a2f);
|
||||
}
|
||||
|
||||
.single-project__owner-link { color: var(--color-on-surface, #e3e1e9); font-weight: 600; text-decoration: none; }
|
||||
.single-project__owner-link:hover { color: var(--color-primary, #b6c4ff); }
|
||||
|
||||
.single-project__stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.home-recent__head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.home-recent__title {
|
||||
font-family: var(--font-sans);
|
||||
font-weight: 700;
|
||||
font-size: 1.75rem;
|
||||
color: var(--color-on-surface, #e3e1e9);
|
||||
margin: 0;
|
||||
}
|
||||
.home-recent__view-all {
|
||||
color: var(--color-primary-87, #dce1ff);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
.home-recent__view-all:hover { text-decoration: underline; }
|
||||
|
||||
.home-recent__divider {
|
||||
border: 0;
|
||||
border-top: 1px solid var(--color-outline-variant, #444651);
|
||||
margin: 0 0 1.5rem;
|
||||
}
|
||||
|
||||
.home-recent__empty { text-align: center; padding: 4rem 1rem; color: var(--color-on-surface-variant, #c5c5d3); }
|
||||
|
||||
/* Website-mode project card + single-project screenshot/platform */
|
||||
.single-project__screenshot { margin: 0 0 1.5rem; }
|
||||
.single-project__screenshot img { width: 100%; height: auto; border-radius: 0.75rem; display: block; border: 1px solid var(--color-outline-variant, #444651); }
|
||||
.project-card__platform { color: var(--color-on-surface-variant, #c5c5d3); font-size: 0.875rem; }
|
||||
|
||||
@@ -24,5 +24,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Links inside sidebar widgets on dark page surface — bumped to --color-primary-87
|
||||
* to clear WCAG AA (the inherited global --color-bodylinks is only ~3:1). */
|
||||
a[data-type],
|
||||
a[data-id] {
|
||||
color: var(--color-primary-87, #dce1ff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,153 @@
|
||||
/* Footer styles */
|
||||
|
||||
.site-footer {
|
||||
#footRight {
|
||||
div {
|
||||
@apply col-span-1 md:col-span-4 lg:col-span-1;
|
||||
background: var(--color-surface, #121318);
|
||||
color: var(--color-on-surface, #e3e1e9);
|
||||
border-top: 1px solid var(--color-outline-variant, #444651);
|
||||
|
||||
h3 {
|
||||
@apply font-bold text-secondary-300 text-20px mb-4 pb-2 border-b border-b-secondary-300
|
||||
.site-footer__title a { color: #e3e1e9; text-decoration: none; }
|
||||
|
||||
.site-footer__area h3,
|
||||
.site-footer__nav h3 {
|
||||
color: var(--color-on-surface, #e3e1e9);
|
||||
font-family: var(--font-sans);
|
||||
font-weight: 700;
|
||||
margin: 0 0 0.75rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--color-outline-variant, #444651);
|
||||
}
|
||||
|
||||
a {
|
||||
@apply text-footlinks hover:opacity-60;
|
||||
}
|
||||
.site-footer a { color: #b6c4ff; }
|
||||
|
||||
.site-footer a:hover { text-decoration: underline; }
|
||||
|
||||
.site-footer__copyright {
|
||||
background: var(--color-surface-container-lowest, #0d0e13);
|
||||
color: var(--color-on-surface-variant, #c5c5d3);
|
||||
text-align: center;
|
||||
padding: 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.widget {
|
||||
li {
|
||||
@apply text-16px my-1 mb-2 leading-4;
|
||||
/* Social media links styling */
|
||||
.social-links {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
h4 {
|
||||
@apply font-bold text-18px text-secondary mb-2;
|
||||
.social-links a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
@apply transition-colors duration-300 hover:text-success focus-visible:text-success;
|
||||
}
|
||||
/* SVG icon alignment */
|
||||
.site-footer svg {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.menu-footer-menu-container {
|
||||
@apply grow;
|
||||
}
|
||||
/* Quick Links styling */
|
||||
.site-footer__nav ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.copyright {
|
||||
p { @apply leading-none m-0 p-0; }
|
||||
.site-footer__nav > ul > li {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
a { @apply text-white hover:text-primary-500 underline underline-offset-2; }
|
||||
.site-footer__nav > ul > li:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Main menu items with SVG marker */
|
||||
.site-footer__nav > ul > li > a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.site-footer__nav > ul > li > a::before {
|
||||
content: '';
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='24' height='25' viewBox='0 0 24 25' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M7.15533 5.59469C6.94083 5.38019 6.61824 5.31603 6.33799 5.43211C6.05773 5.5482 5.875 5.82168 5.875 6.12502V18.625C5.875 18.9284 6.05773 19.2018 6.33799 19.3179C6.61824 19.434 6.94083 19.3699 7.15533 19.1554L13.4053 12.9054C13.6982 12.6125 13.6982 12.1376 13.4053 11.8447L7.15533 5.59469Z' fill='%23b6c4ff'/%3E%3Cpath opacity='0.4' d='M11.6553 5.59467C11.3624 5.30178 10.8876 5.30178 10.5947 5.59467C10.3018 5.88756 10.3018 6.36244 10.5947 6.65533L16.3143 12.375L10.5947 18.0947C10.3018 18.3876 10.3018 18.8624 10.5947 19.1553C10.8876 19.4482 11.3624 19.4482 11.6553 19.1553L17.9053 12.9053C18.1982 12.6124 18.1982 12.1376 17.9053 11.8447L11.6553 5.59467Z' fill='%23b6c4ff'/%3E%3C/svg%3E");
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
/* Submenu styling */
|
||||
.site-footer__nav-submenu {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0.5rem 0 0 4rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.site-footer__nav-submenu-item {
|
||||
margin: 0;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.site-footer__nav-submenu-item a {
|
||||
color: #b6c4ff;
|
||||
font-size: 0.95rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.site-footer__nav-submenu-item a::before {
|
||||
content: '';
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='24' height='25' viewBox='0 0 24 25' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M7.15533 5.59469C6.94083 5.38019 6.61824 5.31603 6.33799 5.43211C6.05773 5.5482 5.875 5.82168 5.875 6.12502V18.625C5.875 18.9284 6.05773 19.2018 6.33799 19.3179C6.61824 19.434 6.94083 19.3699 7.15533 19.1554L13.4053 12.9054C13.6982 12.6125 13.6982 12.1376 13.4053 11.8447L7.15533 5.59469Z' fill='%23b6c4ff'/%3E%3Cpath opacity='0.4' d='M11.6553 5.59467C11.3624 5.30178 10.8876 5.30178 10.5947 5.59467C10.3018 5.88756 10.3018 6.36244 10.5947 6.65533L16.3143 12.375L10.5947 18.0947C10.3018 18.3876 10.3018 18.8624 10.5947 19.1553C10.8876 19.4482 11.3624 19.4482 11.6553 19.1553L17.9053 12.9053C18.1982 12.6124 18.1982 12.1376 17.9053 11.8447L11.6553 5.59467Z' fill='%23b6c4ff'/%3E%3C/svg%3E");
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.site-footer__nav-submenu a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Contact button styling */
|
||||
.site-footer__nav-contact > a.button {
|
||||
display: inline-block;
|
||||
padding: 0.5rem 1rem;
|
||||
background-color: #b6c4ff;
|
||||
color: #121318;
|
||||
border-radius: 0.25rem;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.site-footer__nav-contact > a.button::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.site-footer__nav-contact > a.button:hover {
|
||||
background-color: #e3e1e9;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.site-footer__nav-contact {
|
||||
margin-top: 1.5rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid var(--color-outline-variant, #444651);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,39 +29,40 @@
|
||||
|
||||
@media screen and (min-width: 62.5rem) {
|
||||
.menu-vdi {
|
||||
.menu-vdi__toggle {
|
||||
@apply flex items-center gap-2;
|
||||
}
|
||||
.menu-vdi__toggle { @apply flex items-center gap-2; }
|
||||
|
||||
.menu-vdi__item--parent {
|
||||
@apply relative;
|
||||
}
|
||||
.menu-vdi__item--parent { @apply relative; }
|
||||
|
||||
.menu-vdi__submenu {
|
||||
@apply bg-white shadow-lg left-4 w-64 flex-col;
|
||||
top: calc(100% + 1rem);
|
||||
|
||||
>li {
|
||||
@apply w-full;
|
||||
}
|
||||
>li { @apply w-full; }
|
||||
|
||||
.menu-vdi__item {
|
||||
/* text */
|
||||
@apply font-bold text-18px text-black hover:text-light no-underline leading-snug;
|
||||
@apply font-bold text-18px text-black! hover:text-light! no-underline leading-snug;
|
||||
/* spacing & display */
|
||||
@apply block w-full;
|
||||
/* interaction */
|
||||
@apply focus-visible:bg-secondary-200 hover:bg-secondary-200;
|
||||
|
||||
a {
|
||||
@apply block w-full;
|
||||
}
|
||||
a { @apply block w-full; }
|
||||
}
|
||||
|
||||
a.menu-vdi__item,
|
||||
.menu-vdi__item a {
|
||||
@apply p-4;
|
||||
.menu-vdi__item a { @apply p-4; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-main a,
|
||||
.nav-aux a {
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.nav-main a:hover,
|
||||
.nav-aux a:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const AxeBuilder = require('@axe-core/playwright').default;
|
||||
|
||||
test.use({ viewport: { width: 1920, height: 1080 } });
|
||||
|
||||
test.describe('archive-projects', () => {
|
||||
test('project archive renders grid or empty + zero axe violations', async ({ page }, testInfo) => {
|
||||
await page.goto('/projects/');
|
||||
await page.screenshot({ path: 'test-results/archive.png', fullPage: true });
|
||||
|
||||
await expect(page.locator('.archive-projects__head h1')).toBeVisible();
|
||||
const grid = page.locator('.archive-projects__grid');
|
||||
const empty = page.locator('.archive-projects__empty');
|
||||
await expect(grid.or(empty)).toBeVisible();
|
||||
|
||||
const results = await new AxeBuilder({ page })
|
||||
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22a', 'wcag22aa'])
|
||||
.analyze();
|
||||
await testInfo.attach('axe', {
|
||||
body: JSON.stringify(results, null, 2),
|
||||
contentType: 'application/json',
|
||||
});
|
||||
expect(results.violations).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
test.use({ viewport: { width: 1920, height: 1080 } });
|
||||
|
||||
test.describe('aura-bg Carbon Blue gradient (global, fixed background)', () => {
|
||||
test('body carries the dark base color that the aura layers blend against', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
// Without the body color, mix-blend-mode on the aura layers has nothing
|
||||
// to composite against and the gradient disappears.
|
||||
const bodyBg = await page.evaluate(() => getComputedStyle(document.body).backgroundColor);
|
||||
// #100e0b = rgb(16, 14, 11)
|
||||
expect(bodyBg).toBe('rgb(16, 14, 11)');
|
||||
});
|
||||
|
||||
test('.aura-bg renders globally as a fixed viewport layer (not a per-template wrapper)', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
// aura-bg is rendered once in header.php and must be position:fixed so
|
||||
// it covers the entire viewport behind all content regardless of scroll.
|
||||
const auraBg = page.locator('.aura-bg').first();
|
||||
await expect(auraBg).toBeAttached();
|
||||
|
||||
const position = await auraBg.evaluate((el) => getComputedStyle(el).position);
|
||||
expect(position).toBe('fixed');
|
||||
|
||||
// z-index: -1 puts the gradient behind page content. Content must sit
|
||||
// on top via its own stacking context (which the body and main do not
|
||||
// create, so the negative z-index is safe).
|
||||
const zIndex = await auraBg.evaluate((el) => getComputedStyle(el).zIndex);
|
||||
expect(zIndex).toBe('-1');
|
||||
|
||||
// pointer-events: none so clicks pass through to content underneath.
|
||||
const pointerEvents = await auraBg.evaluate((el) => getComputedStyle(el).pointerEvents);
|
||||
expect(pointerEvents).toBe('none');
|
||||
});
|
||||
|
||||
test('.aura-layer-1 renders the Carbon Blue linear gradient', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
const layer1 = page.locator('.aura-layer-1').first();
|
||||
await expect(layer1).toBeAttached();
|
||||
|
||||
const bgImage = await layer1.evaluate((el) => getComputedStyle(el).backgroundImage);
|
||||
expect(bgImage).toMatch(/linear-gradient/);
|
||||
});
|
||||
|
||||
test('.aura-bg is not isolated (no stacking context traps blend modes)', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
// isolation: isolate on the .aura-bg parent would trap mix-blend-mode
|
||||
// children inside an empty container, preventing them from blending
|
||||
// with the body's #100e0b beneath. The global fixed layer must let
|
||||
// blend modes reach through to the body.
|
||||
const isolation = await page.locator('.aura-bg').first().evaluate((el) => getComputedStyle(el).isolation);
|
||||
expect(isolation).not.toBe('isolate');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const AxeBuilder = require('@axe-core/playwright').default;
|
||||
|
||||
test.use({ viewport: { width: 1920, height: 1080 } });
|
||||
|
||||
test.describe('home', () => {
|
||||
test('home renders Carbon Blue hero + recent projects + zero axe violations', async ({ page }, testInfo) => {
|
||||
await page.goto('/');
|
||||
await page.screenshot({ path: 'test-results/home.png', fullPage: true });
|
||||
|
||||
// Hero region must be present.
|
||||
await expect(page.locator('.homepage-hero')).toBeVisible();
|
||||
|
||||
// Either the grid OR the empty state must be present.
|
||||
const grid = page.locator('.home-recent__grid');
|
||||
const empty = page.locator('.home-recent__empty');
|
||||
await expect(grid.or(empty)).toBeVisible();
|
||||
|
||||
const results = await new AxeBuilder({ page })
|
||||
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22a', 'wcag22aa'])
|
||||
.analyze();
|
||||
await testInfo.attach('axe', {
|
||||
body: JSON.stringify(results, null, 2),
|
||||
contentType: 'application/json',
|
||||
});
|
||||
expect(results.violations).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const AxeBuilder = require('@axe-core/playwright').default;
|
||||
|
||||
test.use({ viewport: { width: 1920, height: 1080 } });
|
||||
|
||||
test.describe('single-project', () => {
|
||||
test('project detail renders hero, sidebar, no axe violations', async ({ page }, testInfo) => {
|
||||
// Navigate to the projects archive and pick the first card. The test
|
||||
// fails (not skips) when no projects exist — silently skipping masks
|
||||
// regression of the single-project template itself.
|
||||
const response = await page.goto('/projects/');
|
||||
expect(response && response.status()).toBeLessThan(400);
|
||||
|
||||
const firstLink = page.locator('.archive-projects__grid a.project-card__title-link').first();
|
||||
const linkCount = await firstLink.count();
|
||||
expect(linkCount, 'archive must contain at least one project card to exercise the single-project template').toBeGreaterThan(0);
|
||||
|
||||
await Promise.all([
|
||||
page.waitForLoadState('networkidle'),
|
||||
firstLink.click(),
|
||||
]);
|
||||
await page.screenshot({ path: 'test-results/single.png', fullPage: true });
|
||||
|
||||
await expect(page.locator('.single-project__title')).toBeVisible();
|
||||
await expect(page.locator('.single-project__sidebar')).toBeVisible();
|
||||
|
||||
const results = await new AxeBuilder({ page })
|
||||
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22a', 'wcag22aa'])
|
||||
.analyze();
|
||||
await testInfo.attach('axe', {
|
||||
body: JSON.stringify(results, null, 2),
|
||||
contentType: 'application/json',
|
||||
});
|
||||
expect(results.violations).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -1,14 +1,14 @@
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const AxeBuilder = require('@axe-core/playwright').default;
|
||||
|
||||
const domain = 'http://domain.local/';
|
||||
const domain = ''; // uses playwright.config.js baseURL
|
||||
test.use({
|
||||
viewport: { width: 1920, height: 1080 },
|
||||
});
|
||||
|
||||
test.describe('site-test', () => {
|
||||
test('Homepage Test', async ({ page }, testInfo) => {
|
||||
await page.goto(domain);
|
||||
await page.goto('/');
|
||||
|
||||
await page.screenshot({ path: 'test-results/homepage.png', fullPage: true });
|
||||
|
||||
@@ -25,7 +25,7 @@ test.describe('site-test', () => {
|
||||
});
|
||||
|
||||
test('Blog Index Page Test', async ({ page }, testInfo) => {
|
||||
await page.goto(`${domain}news/`);
|
||||
await page.goto(`/news/`);
|
||||
|
||||
await page.screenshot({ path: 'test-results/blog-index.png', fullPage: true });
|
||||
|
||||
@@ -42,7 +42,7 @@ test.describe('site-test', () => {
|
||||
});
|
||||
|
||||
test('404 Page Test', async ({ page }, testInfo) => {
|
||||
await page.goto(`${domain}yaya/`);
|
||||
await page.goto(`/yaya/`);
|
||||
|
||||
await page.screenshot({ path: 'test-results/404.png', fullPage: true });
|
||||
|
||||
|
||||
+27
-2
@@ -67,6 +67,31 @@
|
||||
"slug": "theme-info",
|
||||
"color": "var(--color-info)",
|
||||
"name": "Theme Info"
|
||||
},
|
||||
{
|
||||
"slug": "theme-surface",
|
||||
"color": "#121318",
|
||||
"name": "Theme Surface"
|
||||
},
|
||||
{
|
||||
"slug": "theme-on-surface",
|
||||
"color": "#e3e1e9",
|
||||
"name": "Theme On Surface"
|
||||
},
|
||||
{
|
||||
"slug": "theme-outline",
|
||||
"color": "#8f909d",
|
||||
"name": "Theme Outline"
|
||||
},
|
||||
{
|
||||
"slug": "theme-outline-variant",
|
||||
"color": "#444651",
|
||||
"name": "Theme Outline Variant"
|
||||
},
|
||||
{
|
||||
"slug": "theme-page-base",
|
||||
"color": "#100e0b",
|
||||
"name": "Theme Page Base"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -165,8 +190,8 @@
|
||||
},
|
||||
"styles": {
|
||||
"color": {
|
||||
"background": "var(--wp--preset--color--background)",
|
||||
"text": "var(--wp--preset--color--text)"
|
||||
"background": "var(--wp--preset--color--theme-bg, #100e0b)",
|
||||
"text": "var(--wp--preset--color--theme-text, #e3e1e9)"
|
||||
},
|
||||
"elements": {
|
||||
"link": {
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
*
|
||||
* This is the template that renders the Accordion block.
|
||||
*
|
||||
* @package SoloFrameEvo
|
||||
* @package KsPortfolio
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
$open = get_field( 'open' );
|
||||
$group = get_field( 'group_items' );
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
*
|
||||
* This is the template for building your own custom blocks.
|
||||
*
|
||||
* @package SoloFrameEvo
|
||||
* @package KsPortfolio
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
$classes = 'boilerplate';
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
/**
|
||||
* Button block
|
||||
*
|
||||
* @package SoloFrameEvo
|
||||
* @package KsPortfolio
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
// Retrieve ACF fields
|
||||
$element = get_field( 'element' ) ? get_field( 'element' ) : 'a';
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
*
|
||||
* This is the template that renders the Buttons block.
|
||||
*
|
||||
* @package SoloFrameEvo
|
||||
* @package KsPortfolio
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
$ibClasses = 'flex flex-wrap gap-4 w-full justify-center sm:justify-start';
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
*
|
||||
* Display contact information from global fields with icons and optional form.
|
||||
*
|
||||
* @package SoloFrameEvo
|
||||
* @package KsPortfolio
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
$classes = 'contact-info';
|
||||
$wrapper = blockWrapperAttributes( $classes, $is_preview );
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
*
|
||||
* This is the template that displays the Grid Cell block.
|
||||
*
|
||||
* @package SoloFrameEvo
|
||||
* @package KsPortfolio
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
// Initialize variables
|
||||
$className = ! empty( $block['className'] ) ? $block['className'] : '';
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
*
|
||||
* This is the template that displays the grid block.
|
||||
*
|
||||
* @package SoloFrameEvo
|
||||
* @package KsPortfolio
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
$allowedBlocks = array( 'acf/grid-cell' );
|
||||
$default_blocks = array(
|
||||
|
||||
@@ -2,31 +2,32 @@
|
||||
/**
|
||||
* Block Name: Homepage Hero
|
||||
*
|
||||
* @package SoloFrameEvo
|
||||
* @package KsPortfolio
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
// Retrieve ACF fields
|
||||
$imgHero = get_field( 'hero_image' );
|
||||
$heading = get_field( 'heading' );
|
||||
$intro = get_field( 'intro' );
|
||||
$ctas = get_field( 'calls_to_action' );
|
||||
|
||||
$classes = 'homepage-hero mx-break-out bg-black bg-cover bg-no-repeat text-light py-12 lg:py-16 overflow-hidden';
|
||||
$classes = 'homepage-hero mx-break-out text-light py-12 lg:py-16 overflow-hidden';
|
||||
$wrapper = blockWrapperAttributes( $classes, $is_preview );
|
||||
?>
|
||||
|
||||
<section <?php echo wp_kses_post( $wrapper ); ?>>
|
||||
<div class="container content-wrapper">
|
||||
<div class="max-w-lg sm:text-center lg:text-left lg:items-center ml-0">
|
||||
<div class="container content-wrapper flex flex-col lg:flex-row items-start justify-between">
|
||||
<div class="max-w-2xl sm:text-center lg:text-left lg:items-center ml-0">
|
||||
<?php if ( ! empty( $heading ) ) : ?>
|
||||
<h1 class="text-4xl lg:text-5xl font-bold leading-tight mb-4">
|
||||
<h1 class="text-45px font-bold leading-tight mb-4">
|
||||
<?php echo esc_html( $heading ); ?>
|
||||
</h1>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ( ! empty( $intro ) ) : ?>
|
||||
<div class="mt-3 text-base text-light sm:mt-5 sm:text-xl lg:text-lg xl:text-xl with:max-w-full">
|
||||
<div class="mt-3 text-20p text-light sm:mt-5 sm:text-xl lg:text-lg xl:text-xl with:max-w-full">
|
||||
<?php echo wp_kses_post( $intro ); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
@@ -62,5 +63,17 @@ $wrapper = blockWrapperAttributes( $classes, $is_preview );
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 max-w-2xl">
|
||||
<?php if ( ! empty( $imgHero ) ) : ?>
|
||||
<div class="">
|
||||
<img
|
||||
src="<?php echo esc_url( $imgHero['url'] ); ?>"
|
||||
alt="<?php echo esc_attr( $imgHero['alt'] ); ?>"
|
||||
class="w-full h-full object-cover rounded-lg shadow-lg"
|
||||
/>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
*
|
||||
* This is the template that displays the media text inner blocks.
|
||||
*
|
||||
* @package SoloFrameEvo
|
||||
* @package KsPortfolio
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
// Retrieve ACF fields
|
||||
$bgColor = get_field( 'background_color' ) ? get_field( 'background_color' ) : '#c5c5c5';
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
*
|
||||
* This is the template that displays the Media With Text block.
|
||||
*
|
||||
* @package SoloFrameEvo
|
||||
* @package KsPortfolio
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
// Retrieve ACF fields
|
||||
$bgColor = get_field( 'background_color' ) ? get_field( 'background_color' ) : '#c5c5c5';
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
/**
|
||||
* Page Children block
|
||||
*
|
||||
* @package SoloFrameEvo
|
||||
* @package KsPortfolio
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
// Retrieve the current page ID and its children
|
||||
$parentId = get_queried_object_id();
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "acf/recent-projects",
|
||||
"title": "Recent Projects",
|
||||
"description": "Block to display recent projects on the home page.",
|
||||
"style": [
|
||||
"file:./recent-projects.css"
|
||||
],
|
||||
"category": "sf-blocks",
|
||||
"icon": "block-default",
|
||||
"keywords": [
|
||||
"recent-projects"
|
||||
],
|
||||
"acf": {
|
||||
"mode": "preview",
|
||||
"renderTemplate": "recent-projects.php"
|
||||
},
|
||||
"supports": {
|
||||
"align": true,
|
||||
"anchor": true,
|
||||
"color": true,
|
||||
"html": false,
|
||||
"jsx": false,
|
||||
"mode": true,
|
||||
"multiple": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
/**
|
||||
* Block Name: Recent Projects
|
||||
|
||||
* Block to display recent projects on the home page.
|
||||
*
|
||||
* @package KsPortfolio
|
||||
*/
|
||||
|
||||
namespace KsPortfolio;
|
||||
|
||||
$classes = 'home-recent container mx-auto my-section';
|
||||
|
||||
/**
|
||||
* NOTE: DO NOT remove this function call - it is required to avoid editor issues.
|
||||
* $is_preview is a WordPress global when in the editor.
|
||||
*/
|
||||
$wrapper = blockWrapperAttributes( $classes, $is_preview );
|
||||
?>
|
||||
|
||||
<section <?php echo wp_kses_post( $wrapper ); ?>>
|
||||
<header class="home-recent__head">
|
||||
<h2 class="home-recent__title"><?php echo esc_html__( 'Recent Projects', 'ks-portfolio' ); ?></h2>
|
||||
|
||||
<a class="home-recent__view-all" href="<?php echo esc_url( get_post_type_archive_link( 'projects' ) ); ?>">
|
||||
<?php echo esc_html__( 'View All', 'ks-portfolio' ); ?> →
|
||||
</a>
|
||||
</header>
|
||||
|
||||
<hr class="home-recent__divider" />
|
||||
|
||||
<?php
|
||||
$recent = new \WP_Query(
|
||||
array(
|
||||
'post_type' => 'projects',
|
||||
'posts_per_page' => 3,
|
||||
'no_found_rows' => true,
|
||||
)
|
||||
);
|
||||
|
||||
if ( $recent->have_posts() ) :
|
||||
?>
|
||||
<div class="home-recent__grid grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<?php
|
||||
while ( $recent->have_posts() ) :
|
||||
$recent->the_post();
|
||||
|
||||
get_template_part( 'views/components/project-card' );
|
||||
endwhile;
|
||||
|
||||
wp_reset_postdata();
|
||||
?>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<div class="home-recent__empty">
|
||||
<h3><?php echo esc_html__( 'Nothing here yet…', 'ks-portfolio' ); ?></h3>
|
||||
<p><?php echo esc_html__( 'No published projects found.', 'ks-portfolio' ); ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
@@ -4,10 +4,10 @@
|
||||
*
|
||||
* This is the template that displays the section block.
|
||||
*
|
||||
* @package SoloFrameEvo
|
||||
* @package KsPortfolio
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
// Retrieve ACF fields
|
||||
$contentWidth = get_field( 'content_width' );
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* https://docs.vincentdevelopment.ca/docs/starter-v3-enhancements/navigation/
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
// Variables available:
|
||||
// $item from parent template
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* https://docs.vincentdevelopment.ca/docs/starter-v3-enhancements/navigation/
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
// Variables available from MenuItems component:
|
||||
// $topLevelNavItems, $hasChildren, $nestedNavItems, $currentPage, $location
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* https://docs.vincentdevelopment.ca/docs/starter-v3-enhancements/navigation/
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
// Variables available:
|
||||
// $item from parent template
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* https://docs.vincentdevelopment.ca/docs/starter-v3-enhancements/navigation-aux.html
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
// Init Variables
|
||||
global $wp, $views;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* https://docs.vincentdevelopment.ca/docs/starter-v3-enhancements/navigation/
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
global $views;
|
||||
?>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* https://docs.vincentdevelopment.ca/docs/starter-v3-enhancements/navigation/
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
// Init Variables
|
||||
$navIcon = isset( get_field( 'header', 'option' )['nav_icon'] ) ? get_field( 'header', 'option' )['nav_icon'] : '';
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
/**
|
||||
* Project Card Component
|
||||
*
|
||||
* Renders one project card. Used by front-page (Recent Projects) and
|
||||
* archive-projects. Expects $post (WP_Post) in scope.
|
||||
*
|
||||
* @package KsPortfolio
|
||||
*/
|
||||
|
||||
namespace KsPortfolio;
|
||||
|
||||
if ( ! isset( $post ) || ! ( $post instanceof \WP_Post ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$project_id = (int) $post->ID;
|
||||
$type_label = '';
|
||||
$type_terms = get_the_terms( $project_id, 'project-type' );
|
||||
|
||||
if ( ! empty( $type_terms ) && ! is_wp_error( $type_terms ) ) {
|
||||
$type_label = strtoupper( $type_terms[0]->name );
|
||||
}
|
||||
|
||||
$is_website = Projects::is_website( $project_id );
|
||||
$site_url = Projects::get_site_url( $project_id );
|
||||
$site_platform = Projects::get_site_platform( $project_id );
|
||||
$release_url = Projects::get_release_url( $project_id );
|
||||
$repo_url = Projects::get_repo_browse_url( $project_id );
|
||||
$repo_data = Projects::get_repo_data( $project_id );
|
||||
$language = is_array( $repo_data ) && isset( $repo_data['stargazers_count'] ) ? (int) $repo_data['stargazers_count'] : 0;
|
||||
$excerpt = has_excerpt( $project_id ) ? get_the_excerpt( $project_id ) : wp_trim_words( wp_strip_all_tags( $post->post_content ), 30, '…' );
|
||||
$card_classes = 'project-card group';
|
||||
?>
|
||||
|
||||
<article class="<?php echo esc_attr( $card_classes ); ?>" aria-labelledby="project-<?php echo esc_attr( (string) $project_id ); ?>-title">
|
||||
<header class="project-card__head">
|
||||
<?php if ( $type_label ) : ?>
|
||||
<span class="project-card__type"><?php echo esc_html( $type_label ); ?></span>
|
||||
<?php endif; ?>
|
||||
|
||||
<span class="project-card__provider" aria-hidden="true">
|
||||
<?php echo esc_html( $is_website ? __( 'Website', 'ks-portfolio' ) : Projects::get_provider_label( $project_id ) ); ?>
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<h3 id="project-<?php echo esc_attr( (string) $project_id ); ?>-title" class="project-card__title">
|
||||
<a class="project-card__title-link" href="<?php echo esc_url( get_permalink( $project_id ) ); ?>">
|
||||
<?php echo esc_html( get_the_title( $project_id ) ); ?>
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
<?php if ( $excerpt ) : ?>
|
||||
<p class="project-card__excerpt"><?php echo esc_html( $excerpt ); ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<footer class="project-card__foot">
|
||||
<?php if ( ! $is_website && ( $language || $stars > 0 ) ) : ?>
|
||||
<div class="project-card__meta">
|
||||
<?php if ( $language ) : ?>
|
||||
<span class="project-card__lang"><?php echo esc_html( $language ); ?></span>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ( $stars > 0 ) : ?>
|
||||
<span class="project-card__stars" aria-label="<?php echo esc_attr( $stars . ' stars' ); ?>">★ <?php echo esc_html( (string) $stars ); ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php elseif ( $is_website && $site_platform ) : ?>
|
||||
<div class="project-card__meta">
|
||||
<span class="project-card__platform"><?php echo esc_html( $site_platform ); ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="project-card__actions">
|
||||
<?php if ( $is_website ) : ?>
|
||||
<?php if ( $site_url ) : ?>
|
||||
<a class="project-card__action project-card__action--primary" href="<?php echo esc_url( $site_url ); ?>" target="_blank" rel="noopener noreferrer">
|
||||
<?php echo esc_html__( 'Visit Site', 'ks-portfolio' ); ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php else : ?>
|
||||
<?php if ( $release_url ) : ?>
|
||||
<a class="project-card__action project-card__action--primary" href="<?php echo esc_url( Projects::get_download_url( $project_id ) ); ?>" rel="nofollow">
|
||||
<?php echo esc_html__( 'Download', 'ks-portfolio' ); ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ( $repo_url ) : ?>
|
||||
<a class="project-card__action project-card__action--secondary" href="<?php echo esc_url( $repo_url ); ?>" target="_blank" rel="noopener noreferrer">
|
||||
<?php echo esc_html__( 'View Repo', 'ks-portfolio' ); ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</footer>
|
||||
</article>
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
/**
|
||||
* Project Meta Row Component
|
||||
*
|
||||
* Renders one row of the Repository Details table on the project detail
|
||||
* page. Reads $label, $value, and optional $icon from get_query_var(),
|
||||
* because get_template_part() does not forward its $args parameter into
|
||||
* the partial's local scope.
|
||||
*
|
||||
* @package KsPortfolio
|
||||
*/
|
||||
|
||||
namespace KsPortfolio;
|
||||
|
||||
$row_label = get_query_var( 'label' );
|
||||
$row_value = get_query_var( 'value' );
|
||||
$icon = get_query_var( 'icon' );
|
||||
|
||||
if ( ! isset( $row_label ) || ! isset( $row_value ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$row_label = (string) $row_label;
|
||||
$row_value = (string) $row_value;
|
||||
?>
|
||||
<div class="meta-row">
|
||||
<span class="meta-row__label">
|
||||
<?php if ( ! empty( $icon ) ) : ?>
|
||||
<span class="meta-row__icon" aria-hidden="true"><?php echo $icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- icon is hardcoded decorative SVG in callers. ?></span>
|
||||
<?php endif; ?>
|
||||
<?php echo esc_html( $row_label ); ?>
|
||||
</span>
|
||||
<span class="meta-row__value"><?php echo esc_html( $row_value ); ?></span>
|
||||
</div>
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
/**
|
||||
* Project Stat Card Component
|
||||
*
|
||||
* Renders one stat tile. Reads $label, $value, and optional $icon from
|
||||
* get_query_var(), because get_template_part() does not forward its $args
|
||||
* parameter into the partial's local scope. $icon MUST be aria-hidden
|
||||
* decorative markup; the label is the accessible name.
|
||||
*
|
||||
* @package KsPortfolio
|
||||
*/
|
||||
|
||||
namespace KsPortfolio;
|
||||
|
||||
$stat_label = get_query_var( 'label' );
|
||||
$stat_value = get_query_var( 'value' );
|
||||
$icon = get_query_var( 'icon' );
|
||||
|
||||
if ( ! isset( $stat_label ) || ! isset( $stat_value ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$stat_label = (string) $stat_label;
|
||||
$stat_value = (string) $stat_value;
|
||||
?>
|
||||
|
||||
<div class="stat-card" role="group" aria-label="<?php echo esc_attr( $stat_label ); ?>">
|
||||
<?php if ( ! empty( $icon ) ) : ?>
|
||||
<span class="stat-card__icon" aria-hidden="true"><?php echo $icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- icon is hardcoded decorative SVG in callers. ?></span>
|
||||
<?php endif; ?>
|
||||
|
||||
<span class="stat-card__value"><?php echo esc_html( $stat_value ); ?></span>
|
||||
<span class="stat-card__label"><?php echo esc_html( $stat_label ); ?></span>
|
||||
</div>
|
||||
@@ -6,7 +6,7 @@
|
||||
* https://docs.vincentdevelopment.ca/docs/starter-v3-enhancements/search-global.html
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
?>
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace SoloFrameEvo;
|
||||
<form method="get" action="<?php echo esc_url( home_url( '/' ) ); ?>" class="global-search-form relative flex justify-start max-w-full w-full">
|
||||
<label for="globalSearch">
|
||||
<svg role="img" aria-labelledby="globalSearchLabel" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" class="absolute w-4 top-2 left-2 fill-primary-700">
|
||||
<title id="globalSearchLabel"><?php echo esc_attr_x( 'Search', 'search-label', 'sf-evo' ); ?></title>
|
||||
<title id="globalSearchLabel"><?php echo esc_attr_x( 'Search', 'search-label', 'ks-portfolio' ); ?></title>
|
||||
|
||||
<path d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z"/>
|
||||
</svg>
|
||||
@@ -23,7 +23,7 @@ namespace SoloFrameEvo;
|
||||
<input id="globalSearch" type="search" placeholder="" value="<?php echo esc_attr( get_search_query() ); ?>" name="s" class="md:min-w-[25ch] w-full md:w-[18em] max-w-full text-sm p-1 pl-7 border-2 border-primary-700 rounded-l focus-visible:ring-2 ring-primary-700 outline-default!">
|
||||
|
||||
<button type="submit" class="bg-primary rounded-r py-0 px-3 height-full text-sm text-white focus-visible:ring-2 ring-primary-700 outline-default! cursor-default">
|
||||
<?php echo esc_attr_x( 'Search', 'search-submit', 'sf-evo' ); ?>
|
||||
<?php echo esc_attr_x( 'Search', 'search-submit', 'ks-portfolio' ); ?>
|
||||
</button>
|
||||
</form>
|
||||
</search>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" width="1.5rem" height="1.5rem" role="img" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path d="M4.209 4.603c-.247 0-.525.02-.84.088-.333.07-1.28.283-2.054 1.027C-.403 7.25.035 9.685.089 10.052c.065.446.263 1.687 1.21 2.768 1.749 2.141 5.513 2.092 5.513 2.092s.462 1.103 1.168 2.119c.955 1.263 1.936 2.248 2.89 2.367 2.406 0 7.212-.004 7.212-.004s.458.004 1.08-.394c.535-.324 1.013-.893 1.013-.893s.492-.527 1.18-1.73c.21-.37.385-.729.538-1.068 0 0 2.107-4.471 2.107-8.823-.042-1.318-.367-1.55-.443-1.627-.156-.156-.366-.153-.366-.153s-4.475.252-6.792.306c-.508.011-1.012.023-1.512.027v4.474l-.634-.301c0-1.39-.004-4.17-.004-4.17-1.107.016-3.405-.084-3.405-.084s-5.399-.27-5.987-.324c-.187-.011-.401-.032-.648-.032zm.354 1.832h.111s.271 2.269.6 3.597C5.549 11.147 6.22 13 6.22 13s-.996-.119-1.641-.348c-.99-.324-1.409-.714-1.409-.714s-.73-.511-1.096-1.52C1.444 8.73 2.021 7.7 2.021 7.7s.32-.859 1.47-1.145c.395-.106.863-.12 1.072-.12zm8.33 2.554c.26.003.509.127.509.127l.868.422-.529 1.075a.686.686 0 0 0-.614.359.685.685 0 0 0 .072.756l-.939 1.924a.69.69 0 0 0-.66.527.687.687 0 0 0 .347.763.686.686 0 0 0 .867-.206.688.688 0 0 0-.069-.882l.916-1.874a.667.667 0 0 0 .237-.02.657.657 0 0 0 .271-.137 8.826 8.826 0 0 1 1.016.512.761.761 0 0 1 .286.282c.073.21-.073.569-.073.569-.087.29-.702 1.55-.702 1.55a.692.692 0 0 0-.676.477.681.681 0 1 0 1.157-.252c.073-.141.141-.282.214-.431.19-.397.515-1.16.515-1.16.035-.066.218-.394.103-.814-.095-.435-.48-.638-.48-.638-.467-.301-1.116-.58-1.116-.58s0-.156-.042-.27a.688.688 0 0 0-.148-.241l.516-1.062 2.89 1.401s.48.218.583.619c.073.282-.019.534-.069.657-.24.587-2.1 4.317-2.1 4.317s-.232.554-.748.588a1.065 1.065 0 0 1-.393-.045l-.202-.08-4.31-2.1s-.417-.218-.49-.596c-.083-.31.104-.691.104-.691l2.073-4.272s.183-.37.466-.497a.855.855 0 0 1 .35-.077z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1 @@
|
||||
<i class="icon-github"></i>
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
/**
|
||||
* Aura Background Partial — Carbon Blue
|
||||
*
|
||||
* Renders three decorative layers (linear gradient, radial backlight, SVG
|
||||
* grain) over a transparent container. The body carries `background-color:
|
||||
* #100e0b`; do NOT set a background on this container or the blend modes
|
||||
* will composite against the container instead of the page.
|
||||
*
|
||||
* All layers are aria-hidden and ignore pointer events. Content must sit
|
||||
* inside a wrapper with `position: relative; z-index: 1`.
|
||||
*
|
||||
* @package KsPortfolio
|
||||
*/
|
||||
|
||||
namespace KsPortfolio;
|
||||
|
||||
?>
|
||||
<div class="aura-bg" aria-hidden="true">
|
||||
<div class="aura-layer-1"></div>
|
||||
<div class="aura-layer-2"></div>
|
||||
<div class="aura-grain">
|
||||
<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<filter id="aura-grain-filter">
|
||||
<feTurbulence type="fractalNoise" baseFrequency="0.7" numOctaves="4" stitchTiles="stitch" />
|
||||
<feColorMatrix type="matrix"
|
||||
values="0.181 0.608 0.061 0 0.075
|
||||
0.181 0.608 0.061 0 0.075
|
||||
0.181 0.608 0.061 0 0.075
|
||||
0 0 0 1 0" />
|
||||
</filter>
|
||||
<rect width="100%" height="100%" filter="url(#aura-grain-filter)" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2,10 +2,10 @@
|
||||
/**
|
||||
* Page Hero Partial
|
||||
*
|
||||
* @package SoloFrameEvo
|
||||
* @package KsPortfolio
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
// Set variables
|
||||
$bgColor = get_field( 'background_color' );
|
||||
@@ -24,7 +24,7 @@ if ( is_home() || is_single() || is_archive() || is_search() || is_404() ) {
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="bg-cover bg-no-repeat mb-12 py-12 lg:py-16 bg-dark text-light overflow-hidden <?php echo $isDark ? 'dark' : ''; ?>" <?php echo $bgColor ? 'style="background-color: ' . esc_attr( $bgColor ) . '"' : ''; ?>>
|
||||
<section class="page-hero py-12 lg:py-16 <?php echo $isDark ? 'dark' : ''; ?>" <?php echo $bgColor ? 'style="background-color: ' . esc_attr( $bgColor ) . '"' : ''; ?>>
|
||||
|
||||
<div class="container mx-auto">
|
||||
<div id="breadcrumbs">
|
||||
@@ -35,22 +35,22 @@ if ( is_home() || is_single() || is_archive() || is_search() || is_404() ) {
|
||||
<?php
|
||||
// Heading
|
||||
if ( apply_filters( 'include_page_title_in_hero', true ) ) {
|
||||
echo '<h1 class="mx-auto text-center text-light font-normal text-4xl sm:text-5xl lg:text-6xl xl:text-7xl">';
|
||||
echo '<h1 class="mx-auto text-center page-hero__title font-normal text-4xl sm:text-5xl lg:text-6xl xl:text-7xl">';
|
||||
echo wp_kses_post( $heading );
|
||||
echo '</h1>';
|
||||
} else {
|
||||
echo '<span class="mx-auto block text-center text-light font-normal text-4xl sm:text-5xl lg:text-6xl xl:text-7xl">';
|
||||
echo '<span class="mx-auto block text-center page-hero__title font-normal text-4xl sm:text-5xl lg:text-6xl xl:text-7xl">';
|
||||
echo wp_kses_post( $heading );
|
||||
echo '</span>';
|
||||
}
|
||||
|
||||
// Intro
|
||||
if ( $intro ) {
|
||||
echo '<p class="mt-3 text-base text-light sm:mt-5 sm:text-xl lg:text-lg xl:text-xl text-center">';
|
||||
echo '<p class="mt-3 text-base page-hero__intro sm:mt-5 sm:text-xl lg:text-lg xl:text-xl text-center">';
|
||||
echo wp_kses_post( $intro );
|
||||
echo '</p>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -2,38 +2,73 @@
|
||||
/**
|
||||
* Social Media Links Partial
|
||||
*
|
||||
* @package SoloFrameEvo
|
||||
* @package KsPortfolio
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
$args = $args ?? array();
|
||||
$classes = $args['classes'] ?? '';
|
||||
$circle = $args['circle'] ?? '';
|
||||
|
||||
// Define social media sites and their URLs
|
||||
$sites = array(
|
||||
'facebook' => getFieldValue( 'social_media.facebook' ) ? getFieldValue( 'social_media.facebook' ) : '',
|
||||
'twitter' => getFieldValue( 'social_media.twitter' ) ? getFieldValue( 'social_media.twitter' ) : '',
|
||||
'pinterest' => getFieldValue( 'social_media.pinterest' ) ? getFieldValue( 'social_media.pinterest' ) : '',
|
||||
'instagram' => getFieldValue( 'social_media.instagram' ) ? getFieldValue( 'social_media.instagram' ) : '',
|
||||
'youtube' => getFieldValue( 'social_media.youtube' ) ? getFieldValue( 'social_media.youtube' ) : '',
|
||||
'linkedin' => getFieldValue( 'social_media.linkedin' ) ? getFieldValue( 'social_media.linkedin' ) : '',
|
||||
);
|
||||
// Get social media repeater data
|
||||
$social_media_items = get_field( 'social_media', 'option' );
|
||||
|
||||
// Add circle class if the circle option is enabled
|
||||
if ( $circle ) {
|
||||
$classes .= ' circular-icon';
|
||||
}
|
||||
|
||||
// Loop through the social media sites and output links
|
||||
foreach ( $sites as $name => $url ) {
|
||||
/**
|
||||
* Detect social media service from URL
|
||||
*
|
||||
* @param string $url The social media URL.
|
||||
* @return string The service name
|
||||
*/
|
||||
function detect_social_service( $url ) {
|
||||
$domain = wp_parse_url( $url, PHP_URL_HOST );
|
||||
$domain = strtolower( $domain );
|
||||
|
||||
// Define domain patterns for each service
|
||||
$services = array(
|
||||
'facebook' => array( 'facebook.com', 'fb.com' ),
|
||||
'instagram' => array( 'instagram.com' ),
|
||||
'twitter' => array( 'twitter.com', 'x.com' ),
|
||||
'linkedin' => array( 'linkedin.com' ),
|
||||
'github' => array( 'github.com' ),
|
||||
'youtube' => array( 'youtube.com', 'youtu.be' ),
|
||||
'pinterest' => array( 'pinterest.com' ),
|
||||
'gitea' => array( 'gitea.com', 'keithsolomon.net', 'codeberg.org' ),
|
||||
);
|
||||
|
||||
foreach ( $services as $service => $domains ) {
|
||||
foreach ( $domains as $service_domain ) {
|
||||
if ( strpos( $domain, $service_domain ) !== false ) {
|
||||
return $service;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
// Loop through the social media repeater items and output links
|
||||
if ( $social_media_items && is_array( $social_media_items ) ) {
|
||||
foreach ( $social_media_items as $item ) {
|
||||
$url = $item['url'] ?? '';
|
||||
|
||||
if ( $url ) {
|
||||
$service = detect_social_service( $url );
|
||||
|
||||
if ( $service ) {
|
||||
?>
|
||||
<a href="<?php echo esc_url( $url ); ?>" class="<?php echo esc_attr( $classes ); ?>">
|
||||
<?php get_template_part( 'views/icons/' . $name ); ?>
|
||||
<span class="sr-only bg-white text-black">Visit our <?php echo esc_html( $name ); ?> page</span>
|
||||
<?php get_template_part( 'views/icons/' . $service ); ?>
|
||||
<span class="sr-only bg-white text-black">Visit my <?php echo esc_html( $service ); ?> page</span>
|
||||
</a>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
+2
-2
@@ -6,11 +6,11 @@
|
||||
* These are for use in the editor and do not appear in the theme codebase, so
|
||||
* Tailwind does not know to include them without this list.
|
||||
*
|
||||
* @package SoloFrameEvo
|
||||
* @package KsPortfolio
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
namespace SoloFrameEvo;
|
||||
namespace KsPortfolio;
|
||||
|
||||
?>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user