Compare commits

..
57 Commits
Author SHA1 Message Date
Keith Solomon 7859d24c71 feature: Update project archive title and enhance header layout
Deploy to Dreamhost / build (push) Successful in 36s
Sync TODOs with Issues / sync_todos (push) Successful in 7s
2026-08-29 10:58:12 -05:00
Keith Solomon 9ad4763f8b feature: Enhance navigation components with scroll control and styling updates
Deploy to Dreamhost / build (push) Successful in 36s
Sync TODOs with Issues / sync_todos (push) Successful in 6s
2026-08-29 10:46:03 -05:00
Keith Solomon b9bbf615c9 🐞 fix: Fix mobile issues, enablke deploy on push #skipGA
Deploy to Dreamhost / build (push) Skipped
Sync TODOs with Issues / sync_todos (push) Successful in 6s
2026-08-29 08:52:57 -05:00
Keith Solomon 21db4f31a3 🔵 other: Rename action
Sync TODOs with Issues / sync_todos (push) Successful in 7s
2026-08-28 21:05:23 -05:00
Keith Solomon d0cb20ce59 feature: Add Dreamhost deployment action
Sync TODOs with Issues / sync_todos (push) Successful in 6s
2026-08-28 21:04:17 -05:00
Keith Solomon 44cf74b6c4 🔵 other: lint fixes
Sync TODOs with Issues / sync_todos (push) Successful in 7s
2026-08-28 20:07:44 -05:00
Keith Solomon 935dd74194 feature: Implement project-type taxonomy archive template and enhance breadcrumb navigation
Sync TODOs with Issues / sync_todos (push) Successful in 8s
2026-08-28 20:06:54 -05:00
Keith Solomon e31ed29962 feature: Enhance footer with social media links and styling updates
Sync TODOs with Issues / sync_todos (push) Successful in 6s
2026-08-23 16:05:31 -05:00
Keith Solomon 0613e33411 Enhance project handling for websites in portfolio
Sync TODOs with Issues / sync_todos (push) Successful in 6s
- Added methods in Projects class to determine if a project is a website, retrieve its URL, platform, and screenshot.
- Updated single-project template to display website-specific information and actions.
- Modified project card component to show website details and adjust action buttons accordingly.
- Improved CSS for project cards and buttons to align with new website features.
2026-08-22 15:14:28 -05:00
Keith Solomon d0dd9fa98a feature: Set up homepage hero
Sync TODOs with Issues / sync_todos (push) Successful in 6s
2026-08-18 21:21:14 -05:00
Keith Solomon 2538a07028 🐞 fix: Remove hero bg
Sync TODOs with Issues / sync_todos (push) Successful in 7s
2026-08-18 16:40:46 -05:00
Keith Solomon 868c2cc70a feature: Add Recent Projects block
Sync TODOs with Issues / sync_todos (push) Successful in 6s
2026-08-18 16:39:26 -05:00
Keith Solomon d4df6d8678 🐞 fix: Update styles for proper background
Sync TODOs with Issues / sync_todos (push) Successful in 6s
2026-08-18 16:37:55 -05:00
Keith Solomon 81547254fc 🔵 other: Remove dist folder from repo
Sync TODOs with Issues / sync_todos (push) Successful in 6s
2026-08-18 10:05:17 -05:00
Keith Solomon fe52e51bd9 🐞 fix: Update escaping for template id script
Sync TODOs with Issues / sync_todos (push) Successful in 6s
2026-08-18 10:03:23 -05:00
Keith Solomon cc463c6131 chore: add .gitattributes for consistent line endings
Sync TODOs with Issues / sync_todos (push) Successful in 7s
2026-08-18 09:57:22 -05:00
Keith Solomon 22c35d0f56 🔵 other: Remove unneeded files
Sync TODOs with Issues / sync_todos (push) Successful in 8s
2026-08-18 09:53:12 -05:00
Keith Solomon afa594aa0a fix(theme): complete namespace swap + page-hero/aura-bg placement + single.spec gating 2026-08-16 14:23:04 -05:00
Keith Solomon ca9c5f215c docs: add Portfolio 2026 testing report 2026-08-16 14:13:58 -05:00
Keith Solomon c6bf4d0ebf chore: replace Basic WP screenshot with Carbon Blue home capture 2026-08-16 14:12:34 -05:00
Keith Solomon a50ea9f0ae fix(a11y): address axe color-contrast violations on dark surface 2026-08-16 14:08:22 -05:00
Keith Solomon ba5e6fcd86 fix(playwright): resolve portfolio.test via Chromium --host-resolver-rules; fix site-a11y baseURL 2026-08-16 13:54:15 -05:00
Keith Solomon 1905a0cfd0 fix(theme): namespace swap on views/* files missed by Tasks 5/16 2026-08-16 13:51:28 -05:00
Keith Solomon 7d381e2d2e test: add Playwright + axe specs for home, archive, single 2026-08-16 13:48:53 -05:00
Keith Solomon 6dcd1233be style: PHPCS clean across theme PHP 2026-08-16 13:37:49 -05:00
Keith Solomon aaef1ee4f8 feat(buttons): dark-surface button variants with WCAG-AA contrast 2026-08-16 13:24:04 -05:00
Keith Solomon 8125d1c818 feat(templates): namespace swap + dark surface + remove picsum fallbacks 2026-08-16 13:20:06 -05:00
Keith Solomon 2d9f92618b feat(footer): Carbon Blue surface + cleaner widget rendering 2026-08-16 13:12:01 -05:00
Keith Solomon bb8c35a0a3 feat(header): three-region layout + dark nav colors 2026-08-16 13:09:48 -05:00
Keith Solomon 9005aeed1a refactor: rename namespace SoloFrameEvo to KsPortfolio 2026-08-16 13:06:38 -05:00
Keith Solomon 664e227f15 feat(home): rebuild front-page template to match Carbon Blue mockup 2026-08-16 13:00:32 -05:00
Keith Solomon 353030d2dc feat(projects): add single-projects detail template 2026-08-16 12:54:38 -05:00
Keith Solomon 5ab42f02ab feat(projects): add taxonomy-project-type template 2026-08-16 12:47:03 -05:00
Keith Solomon b0ed0d7a5a fix(aura): drive blur via @media instead of broken argument 2026-08-16 12:44:15 -05:00
Keith Solomon 1caef627a0 feat(projects): add archive-projects template 2026-08-16 12:41:50 -05:00
Keith Solomon 5619522bf6 feat(projects): add project meta-row component 2026-08-16 12:39:20 -05:00
Keith Solomon 3fae5e54ac feat(projects): add stat-card partial 2026-08-16 12:36:21 -05:00
Keith Solomon 248179ac58 fix(projects): replace raw color literals with tokens in project-card.css 2026-08-16 12:33:58 -05:00
Keith Solomon 631ff4e1d0 feat(projects): add reusable project-card component + styles 2026-08-16 12:30:50 -05:00
Keith Solomon 83d4540a07 fix(projects): add class-level docblock to satisfy WPCS 2026-08-16 12:28:02 -05:00
Keith Solomon 0e502b64cb feat(projects): add provider-agnostic helper for templates 2026-08-16 12:25:26 -05:00
Keith Solomon 8c4e6d8d08 feat(aura): add Carbon Blue atmospheric background partial 2026-08-16 12:21:29 -05:00
Keith Solomon 1fef4c22a4 feat(tokens): add Carbon Blue dark-surface tokens; fix theme.json presets 2026-08-16 12:19:21 -05:00
Keith Solomon ccccd87544 feat(theme): add --font-headings; flip body to Carbon Blue dark 2026-08-16 12:16:20 -05:00
Keith Solomon 969cdb14a0 chore: set Playwright baseURL to portfolio.test 2026-08-16 12:13:08 -05:00
Keith Solomon af8e13d3a8 Add Portfolio 2026 implementation plan 2026-08-16 12:04:21 -05:00
Keith Solomon 6f828ad3d8 Add Portfolio 2026 design spec 2026-08-16 11:58:22 -05:00
keith bf4c2cf2e3 Set up new portfolio theme (#2)
Sync TODOs with Issues / sync_todos (push) Successful in 5s
2026-08-16 02:28:47 +00:00
Keith Solomon d27240226b Merge pull request #3 from ksolomon/SoloFrame-Evo
Style.css update
2026-07-29 16:02:14 -05:00
Keith Solomon cb3afb7453 Merge branch 'main' into SoloFrame-Evo 2026-07-29 16:00:32 -05:00
Keith Solomon c5250c594b 🔵 other: Update style.css 2026-07-29 15:59:24 -05:00
Keith SolomonandClaude Opus 4.7 e2f6270717 Release as SoloFrame Evo (#2)
* docs: Add onboarding documentation for new developers

Add four comprehensive guides to help new developers get started
with the VDI-Starter-v5 WordPress theme:

- docs/getting-started.md: Setup from zero, local WordPress options,
  env config, theme activation warnings, troubleshooting
- docs/architecture.md: Bootstrap flow, 7 architectural layers,
  namespace conventions, WP hooks cleanup, enqueue system, theme.json
- docs/creating-blocks.md: Step-by-step ACF block creation tutorial,
  helper functions, parent-child patterns, Tailwind integration
- docs/reference.md: Hooks/filters tables, design tokens, CSS import
  tree, JS module graph, class reference, CLI commands, deployment

Update README.md: trim deep-dive API docs (moved to reference),
add documentation links section, fix CSS paths (views/styles/ →
styles/), add missing contact-info block, fix deployment filename
typo (wpengine,yml → wpengine.yml), add backToTop.js and
enqEditorAssets(), add namespace conventions table.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs: language graph

* docs: Update readme

* 🔵 other: Rename project and publish

* 🔵 other: Update .gitignore
2026-07-29 15:52:21 -05:00
Keith Solomon de48640b57 🔵 other: Update .gitignore 2026-07-29 15:49:53 -05:00
Keith Solomon 7dbb991fb9 🔵 other: Rename project and publish 2026-07-29 14:25:19 -05:00
Keith Solomon 5512918b0f docs: Update readme 2026-05-03 17:43:37 -05:00
Keith Solomon 61307fc970 docs: language graph 2026-05-03 17:43:04 -05:00
Keith SolomonandClaude Opus 4.7 b3cc21a860 docs: Add onboarding documentation for new developers
Add four comprehensive guides to help new developers get started
with the VDI-Starter-v5 WordPress theme:

- docs/getting-started.md: Setup from zero, local WordPress options,
  env config, theme activation warnings, troubleshooting
- docs/architecture.md: Bootstrap flow, 7 architectural layers,
  namespace conventions, WP hooks cleanup, enqueue system, theme.json
- docs/creating-blocks.md: Step-by-step ACF block creation tutorial,
  helper functions, parent-child patterns, Tailwind integration
- docs/reference.md: Hooks/filters tables, design tokens, CSS import
  tree, JS module graph, class reference, CLI commands, deployment

Update README.md: trim deep-dive API docs (moved to reference),
add documentation links section, fix CSS paths (views/styles/ →
styles/), add missing contact-info block, fix deployment filename
typo (wpengine,yml → wpengine.yml), add backToTop.js and
enqEditorAssets(), add namespace conventions table.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-03 17:41:32 -05:00
100 changed files with 2677 additions and 1549 deletions
+1
View File
@@ -0,0 +1 @@
* text=auto eol=lf
+46
View File
@@ -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/
-64
View File
@@ -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: vdiv5
FLAGS: '-azvr --inplace --delete --exclude=".*"'
REMOTE_PATH: "wp-content/themes/vdi-v5"
+4 -2
View File
@@ -5,9 +5,11 @@ node_modules
static/dist/
.env
bak/
testimonials.class.php
resource-filter/
notes/
docs/
phpcs-results.txt
.claude/
.superpowers/
# Playwright
/test-results/
+4 -4
View File
@@ -2,11 +2,11 @@
/**
* Page Not Found
*
* @package BasicWP
* @package KsPortfolio
* @since 1.0.0
*/
namespace BasicWP;
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 &hellip;', '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 &hellip;', '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"
+108 -429
View File
@@ -1,488 +1,167 @@
# VDI WordPress Theme Starter v5
# SoloFrame Evo
VDI WordPress Theme Starter v5 is a minimal WordPress theme designed as a starting point for custom theme development. It focuses on modern development approaches with a lean architecture that avoids the overhead of theme frameworks.
SoloFrame Evo is a minimal WordPress theme designed as a starting point for custom theme development. It focuses on modern development approaches with a lean architecture that avoids the overhead of theme frameworks.
Repo: [https://github.com/Vincent-Design-Inc/VDI-Starter-v5](https://github.com/Vincent-Design-Inc/VDI-Starter-v5)
AC Bug/Issue Tracking: [https://next-app.activecollab.com/119590/projects/4553?modal=Task-98192-4553](https://next-app.activecollab.com/119590/projects/4553?modal=Task-98192-4553)
Repo: [https://github.com/ksolomon/SoloFrame-Evo](https://github.com/ksolomon/SoloFrame-Evo)
## Key Features
- Tailwind CSS
- Namespaced PHP for isolation
- Built-in support for ACF blocks
- Fast development workflow with watch and compile functions
- Tailwind CSS v4 (CSS-first configuration, no `tailwind.config.js`)
- Namespaced PHP (`SoloFrameEvo` namespace) for isolation
- Built-in support for ACF blocks with auto-registration
- Fast development workflow with BrowserSync live reload
- WordPress 6.5+ script modules for modern JS loading
- Automated theme activation (plugin installs, page creation, settings)
## Documentation
| Guide | Description |
| ------- | ------------- |
| [Getting Started](docs/getting-started.md) | Setup from zero, local WordPress, environment config, troubleshooting |
| [Architecture](docs/architecture.md) | Bootstrap flow, layers, namespace conventions, hooks, enqueue system, theme.json |
| [Creating Blocks](docs/creating-blocks.md) | Step-by-step ACF block creation tutorial with examples |
| [Reference](docs/reference.md) | Hooks/filters, design tokens, CSS architecture, JS modules, CLI commands, deployment |
## Project Structure
```plaintext
VDI-Starter-v5/
├── acf/ # ACF field group JSON definitions
├── bin/ # Build scripts
```plain
SoloFrame-Evo/
├── acf/ # ACF/SCF field group JSON definitions
├── bin/ # Build scripts (.build.js, .watch.js, .utils.js)
├── content/ # Sample page and post content for testing
├── docs/ # Documentation guides
├── lib/ # PHP library files
│ ├── class-breadcrumbs.php # Breadcrumb generation
│ ├── class-acf.php # ACF integration
│ ├── class-enqueue.php # Assets enqueuing
│ ├── activation.php # Theme activation handler (plugins, pages, settings)
│ ├── class-acf.php # ACF/SCF integration (JSON sync paths)
│ ├── class-breadcrumbs.php # Breadcrumb generation with Schema.org markup
│ ├── class-enqueue.php # Assets enqueuing (CSS, JS, fonts)
│ ├── class-menuitems.php # Navigation menu builder
│ ├── class-resources.php # Custom Resources post type
│ ├── extras.php # Miscellaneous theme functions
│ ├── helpers.php # Helper functions
│ ├── hooks.php # WordPress hooks and filters
│ ├── extras.php # Sidebar, page header, Owner role, embed wrapper
│ ├── helpers.php # Utility functions and globals ($theme, $views)
│ ├── hooks.php # WordPress hooks, filters, and cleanup
│ ├── search-features.php # Enhanced search functionality
│ └── show-template.php # Script to show which template is used for the current page
├── static/ # Static assets
│ ├── dist/ # Compiled assets
│ ├── img/ # Static theme images
│ └── show-template.php # Debug: shows active template in HTML comment
├── static/
│ ├── dist/ # Compiled assets (theme.css)
│ └── js/ # JavaScript files
│ ├── components/ # JS components
│ └── button.js # Button custom component
│ ├── modules/ # JS theme modules
│ │ ├── GetHeaderHeight.js # Calculate header height and set it as a CSS variable
│ │ ├── Navigation.js # Script controlling navigation behavior
│ │ └── TagExternalLinks.js # Tags external links with appropriate attributes
│ ├── components/ # JS custom elements (button, backToTop)
├── modules/ # JS theme modules (Navigation, GetHeaderHeight, TagExternalLinks)
│ ├── admin.js # Admin-specific JS
│ └── theme.js # Main theme JS
│ └── theme.js # Main theme JS (entry point)
├── styles/ # CSS styles
│ ├── backend/ # Admin styles
│ ├── base/ # Base styles
│ ├── blocks/ # Block styles
│ ├── components/ # Component styles
│ ├── fonts/ # Font styles
── theme.css # Main CSS entry point
│ ├── backend/ # Admin and editor styles
│ ├── base/ # Base styles (typography, colors, forms, etc.)
│ ├── blocks/ # Block-specific styles
│ ├── components/ # Component styles (breadcrumbs, pagination, etc.)
│ ├── fonts/ # Icon font styles (Lineicons)
── navigation/ # Navigation styles (default, mega, accordion, sliding)
│ └── theme.css # Main CSS entry point (Tailwind + imports)
├── tests/ # Automated Playwright tests
│ └── site-a11y.spec.js # Site Accessibility tests
├── views/ # Template views
│ ├── blocks/ # Custom ACF blocks
│ ├── icons/ # SVG icons
│ ├── forms/ # Form templates
│ └── partials/ # Reusable template parts
│ ├── blocks/ # Custom ACF/SCF blocks (accordion, button, grid, section, etc.)
│ ├── icons/ # SVG icon templates
│ ├── forms/ # Form templates (search)
│ └── partials/ # Reusable template parts (page-hero, social-media)
├── 404.php # 404 error template
├── footer.php # Footer template
├── front-page.php # Front page template
├── functions.php # Main functions file
├── functions.php # Main functions file (autoloads lib/)
├── header.php # Header template
├── index.php # Main template
├── page.php # Page template
├── search.php # Search results template
├── sidebar.php # Main sidebar template
├── sidebar.php # Primary sidebar template
├── sidebar-page.php # Page sidebar template
├── single.php # Single post template
├── style.css # Theme metadata
── whitelist.php # Tailwind class whitelist
── theme.json # Block editor design system (colors, fonts, spacing)
└── whitelist.php # Tailwind class whitelist for editor-only classes
```
## Core Files and Templates
### Theme Core Files
- **style.css**: Contains theme metadata and registration information
- **functions.php**: Initializes the theme and loads necessary dependencies
- **class-acf.php**: Handles Advanced Custom Fields (ACF) functionality
- **class-breadcrumbs.php**: Generates breadcrumb navigation for the site
- **class-enqueue.php**: Manages script and style enqueuing for frontend and backend
- **class-menuitems.php**: Builder for the navigation menus
- **class-resources.php**: Custom post type management for "Resources", included as an example of CPT usage
- **extras.php**: Miscellaneous theme functions, including sidebar and page header checks
- **helpers.php**: Utility functions for common tasks
- **hooks.php**: Contains WordPress hooks and filters for theme functionality
- **search-features.php**: Enhances search functionality with custom queries and filters
- **show-template.php**: Outputs the current template file being used for debugging
- **whitelist.php**: Contains Tailwind CSS class whitelist for styles used in WordPress editor
### Template Files
- **header.php**: Defines the site header with primary navigation
- **footer.php**: Defines the site footer with widget areas and copyright
- **front-page.php**: Template for the front page of the site
- **page.php**: Displays individual static pages with optional sidebar
- **index.php**: Main template file that displays the blog posts list
- **single.php**: Displays individual posts with author and category information
- **search.php**: Displays search results with pagination
- **404.php**: Custom "Page Not Found" template with search form
- **sidebar.php**: Primary sidebar template
- **sidebar-page.php**: Page-specific sidebar template
- **views/forms/search.php** - Search form template for use in various locations
### Included Blocks
- **Boilerplate**: Example block for custom development
- **Accordion**: Collapsible content sections
- **Buttons**: Group of buttons
- **Button**: Configurable button element
- **Grid**: Flexible grid layout system
- **Grid Cell**: Individual grid item
- **Homepage Hero**: Hero section typically used on homepage
- **Media Text**: Image or video with accompanying text
- **Media Text with Inner Blocks**: Media-text with nested block support
- **Page Children**: Displays child pages of current page
- **Section**: Container block with background and width (contained and full-screen width) options
## Library Functions and Classes
### helpers.php
```php
getFieldValue($field_path)
```
- Retrieves a nested value from an ACF field using dot notation
- **Parameters**: `$field_path` - The dot-notated path to the value (e.g., `contact_info.phone`)
- **Returns**: The value at the specified path
```php
customMenuOrder($menu_ord)
```
- Customizes the order of admin menu items in WordPress dashboard
- Add other admin menu URLs to the array to change their order
- **Parameters**: `$menu_ord` - The existing menu order array
- **Returns**: Array specifying the custom menu order, or true for default order
```php
blockCategories($categories)
```
- Adds custom block category for theme blocks
- **Parameters**: `$categories` - The existing block categories
- **Returns**: Modified array of categories
```php
consoleLog($data)
```
- Utility function to print a variable to the console for debugging
- **Parameters**: `$data` - The data to print to the console
### search-features.php
```php
pageSearch($query)
```
- Modifies the WordPress query for page search functionality
- **Parameters**: `$query` - The WordPress query object
- **Returns**: void
```php
postSort($key)
```
- Returns a sorting function for WP_Post objects in reverse order
- **Parameters**: `$key` - WP_Post object property used for sorting
- **Returns**: Sorting function
```php
dedupe($posts, $key)
```
- Removes duplicate posts based on a specified key
- **Parameters**:
- `$posts` - Array of posts
- `$key` - Key to check for duplicates
- **Returns**: Filtered array of posts
```php
searchResultFilter($posts, $query)
```
- Filters and processes search results
- **Parameters**:
- `$posts` - Array of posts
- `$query` - The search query
- **Returns**: Filtered array of posts
### extras.php
```php
getChildrenPages()
```
- Gets child pages of the current page
- **Returns**: Array of child pages or empty array
```php
hasSidebar()
```
- Checks if sidebar should be rendered
- **Returns**: Boolean indicating sidebar presence
```php
hasPageHeader()
```
- Checks if the page should render a page header
- **Returns**: Boolean indicating page header presence
```php
createOwnerRole()
```
- Creates a new WordPress role named "Owner" with admin capabilities minus plugin/theme management
- **Returns**: void
```php
getTheTitle()
```
- Gets the title based on the current context (archive, search, etc.)
- **Returns**: Appropriate title string
```php
divWrapper($content)
```
- Wraps iframes and embeds in a div with the class "embed"
- **Parameters**: `$content` - The content to process
- **Returns**: Modified content
### Class: Resources (class-resources.php)
Custom post type and taxonomy management for "Resources"
### Class: Enqueue (class-enqueue.php)
Manages script and style enqueueing
```php
enqFEAssets()
```
- Enqueues frontend CSS and JS files
- **Returns**: void
```php
enqBEAssets()
```
- Enqueues backend (admin/editor) CSS and JS files
- **Returns**: void
### Class: ACF (class-acf.php)
Handles ACF (Advanced Custom Fields) functionality
```php
saveJson($path)
```
- Sets the path for saving ACF field groups as JSON
- **Parameters**: `$path` - The default path
- **Returns**: Modified path
```php
loadJson($paths)
```
- Sets the path for loading ACF field groups from JSON
- **Parameters**: `$paths` - The default paths
- **Returns**: Modified paths array
### Class: Breadcrumbs (class-breadcrumbs.php)
Generates and displays breadcrumb navigation
```php
generate()
```
- Generates breadcrumb data based on the current page
- **Returns**: Array of breadcrumb items
```php
render()
```
- Renders breadcrumb HTML with schema.org markup
- **Returns**: void (echoes HTML)
The class also includes other helper methods for different page types:
- `getHomeBreadcrumb()`
- `getBlogPostsIndexBreadcrumb()`
- `getSinglePostBreadcrumbs()`
- `getCustomPostTypeBreadcrumbs()`
- `getStaticPageBreadcrumbs($post)`
- `getTaxonomyArchiveBreadcrumb()`
- `getPostTypeArchiveBreadcrumb()`
- `getDateArchiveBreadcrumbs()`
- `getMonthArchiveBreadcrumb()`
- `getYearArchiveBreadcrumb()`
- `getAuthorArchiveBreadcrumb()`
- `getSearchBreadcrumb()`
- `get404Breadcrumb()`
## ACF Blocks System
The theme implements a custom block system using Advanced Custom Fields. Blocks are registered automatically in functions.php via the `regACFBlocks()` function.
See `views/blocks/boilerplate/` for a starting point for custom blocks.
### Block Structure
Each block consists of:
- PHP template file (block rendering)
- CSS file (block-specific styles)
- block.json file (block configuration)
See the [ACF Blocks Documentation](https://www.advancedcustomfields.com/resources/#acf-blocks) for more details on creating custom blocks.
## JavaScript Components
The theme utilizes custom web components for enhanced functionality.
### ButtonComponent
A custom HTML element for creating buttons with various configurations:
```javascript
class ButtonComponent extends HTMLElement {
// Attributes:
// - btnClasses: Additional CSS classes
// - element: The element to use (`a` or `button`)
// - type: Button type (for non-anchor elements)
// - url: The URL for links
// - target: Target attribute for links
// - title: Button text
// - color: Button color
// - variant: Button style variant
// - size: Button size
// - width: Button width
}
```
Usage:
```html
<x-button
btnClasses="button text-center"
element="a"
url="https://example.com"
target="_blank"
title="Click Me"
color="primary"
variant="default"
size="medium"
width="auto"
></x-button>
```
## Asset Management
### CSS
The theme uses Tailwind CSS for styling with custom utilities:
- Base styles in `views/styles/base`
- Component styles in `views/styles/components`
- Block-specific styles in individual block folders and `views/styles/blocks`
- Admin/editor styles in `views/styles/backend`
### JavaScript
- **theme.js**: Main frontend JavaScript
- **admin.js**: Admin-specific JavaScript
- **components**: JavaScript components
- **modules**: JavaScript modules for specific functionality (e.g., navigation, focus styling)
### Build System
The theme includes a simple build system in the bin directory:
- **.build.js**: Production build script that compiles Tailwind CSS
- **.watch.js**: Development watch script with BrowserSync for live reloading
- **.utils.js**: Shared utility functions for the build process
## Development Workflow
## Namespace Conventions
The project uses several naming conventions. Here's how they relate:
| Convention | Value | Where Used |
| ----------- | ------- | ----------- |
| PHP namespace | `SoloFrameEvo` | All PHP files |
| Text domain | `sf-evo` | WordPress translations |
| Block category | `sf-blocks` | Groups blocks in the editor |
| Script module IDs | `sf-evo-theme`, `sf-evo-button`, `sf-evo-admin` | JS module registration |
| WP Engine folder | `<change-me>>` | Deployment target |
| Theme directory | `SoloFrame-Evo` | Git repo name |
## Included Blocks
| Block | Purpose |
| ------- | --------- |
| Accordion | Collapsible content sections |
| Buttons | Container for Button blocks |
| Button | Configurable button element (`<x-button>` custom element) |
| 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 or video with accompanying text |
| Media Text with Inner Blocks | Media-text with nested block support |
| Page Children | Displays child pages of current page |
| Section | Container with background and width options |
## Development
### Setup
1. Create a new repo using this one as a template.
2. Clone the repo to your local dev folder.
3. Install dependencies (if asked to overwrite, choose "no"):
3. Install dependencies:
```bash
# Install theme dependencies
composer install
npm i
npm install
```
# Configure playwright for testing
npm init playwright@latest --yes "--" . '--quiet' '--browser=chromium' '--browser=firefox' '--browser=webkit' '--lang=js'
4. Copy `.env.example` to `.env` and set:
- `LOCALHOST_URL`: Your local development URL (e.g., `http://site-dev.local`)
- `BROWSERSYNC_PORT`: Port for BrowserSync (default: `5000`)
# Perform initial build
5. Run the initial build:
```bash
npm run build
```
4. Create .env file from .env.example and set:
6. Activate the theme in WordPress admin.
- `LOCALHOST_URL`: Local development URL.
- `BROWSERSYNC_PORT`: Port for BrowserSync (default: 5000).
> **Warning:** Theme activation automatically installs plugins, creates pages, and modifies WordPress settings. See [Getting Started](docs/getting-started.md#theme-activation-warning) for details.
5. Start building your project.
### Development Server
- Run `npm run start` to start the development server with live reloading.
- Activate the theme in your WordPress admin.
- (Optional) Import the sample content from `content`.
- Run `npm run start` or `npm run watch` to start the development server with live reloading.
- Changes to PHP, CSS, and JS files trigger automatic reload.
### Sart Development Server
### Production Build
- Run `npm run watch` or `npm run start` to start development server with live reloading.
- Changes to PHP, CSS, and JS files will trigger automatic reload.
### Production
- Run `npm run build` to compile and optimize CSS for production (normally handled by GitHub Actions).
- Run `npm run build` to compile and optimize CSS for production.
- This is normally handled by GitHub Actions during deployment.
### Deployment
- Deployment is handled via GitHub Actions.
- Ensure you update `.github/workflows/wpengine,yml` with the correct WP Engine deployment configuration (environment and theme folder).
- Deployment to WP Engine is handled via GitHub Actions.
- Update `.github/workflows/wpengine.yml` with the correct WP Engine deployment configuration.
## Testing
The theme includes a suite of testing tools to ensure code quality and functionality.
### Accessibility
Accessibility tests using Playwright and Axe:
Tests use Playwright and Axe:
- Test files located in `tests`.
- Run via the [VSCode Playwright extension](https://marketplace.visualstudio.com/items/?itemName=ms-playwright.playwright) or with `npx playwright test --ui` in your terminal for the dedicated Playwright window.
- Tests include:
- `site-a11y.spec.js`: Accessibility tests for the site.
- Generates reports in `playwright-report` and screenshots in `test-results`.
- Run via the [VSCode Playwright extension](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright) or `npx playwright test --ui`
- Test file: `tests/site-a11y.spec.js`
Code linting for modified WordPress coding standards using phpcs:
### PHP Code Standards
- Run `composer lint` in your terminal to check PHP files for coding standards violations
- Test results are saved in `phpcs-results.txt`. Review the file for error details.
- Automated fixes noted in the results can be done using `composer fix` in your terminal.
### Files
- `404.php` - Displays a custom "Page Not Found" message when a visitor tries to access a page that doesn't exist.
- `archive.php` - Displays a list of posts from a particular archive, such as a category, date, or tag archive.
- `author.php` - Displays a list of posts by a specific author, along with the author's information.
- `category.php` - Displays a list of posts from a specific category.
- `footer.php` - Contains the footer section of the theme, typically including closing HTML tags and widgets or navigation in the footer area.
- `front-page.php` - The template used for the front page of the site, either static or a blog, depending on the site settings.
- `functions.php` - Imports custom functionality for the theme. **Should not be edited**, all changes/additions should live in the `lib` directory.
- `header.php` - Contains the header section of the theme, typically including the site's title, meta tags, and navigation menu.
- `index.php` - The fallback template for all WordPress pages, used if no other more specific template (like `category.php` or `single.php`) is available.
- `page.php` - Displays individual static pages, such as "About" or "Contact" pages.
- `screenshot.png` - An image of the themes design, shown in the WordPress theme selector to give users a preview of the theme's appearance.
- `search.php` - Displays the results of a search query, showing posts or pages that match the search terms entered by the user.
- `single.php` - Displays individual posts, often used for blog posts or custom post types.
- `tag.php` - Displays a list of posts associated with a specific tag.
### Folders
- `acf` - Storage for ACF field group JSON files. You shouldn't edit anything here manually.
- `bin` - Helper files for the dev and build processes.
- `lib` - Main library for the theme, including the Twig engine and other helper functions.
- `static` - Static assets for the theme, including CSS, JS, and images. The rendered stylesheet is stored here.
- `styles` - The raw CSS files compiled by Tailwind into the final CSS file.
- `views` - Twig view templates for the theme. This is where you will do most of your work.
- `blocks` - ACF block templates.
- `boilerplate` - Example ACF block template. Used to build your own blocks (more on this below).
- `icons` - Icon templates for SVG icons.
- `partials` - Partial templates for the page hero, breadcrumb, and social media sections.
- Run `composer lint` to check PHP files against WordPress coding standards.
- Results are saved to `phpcs-results.txt`.
- Auto-fix with `composer fix`.
+32 -179
View File
@@ -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
}
+30 -1
View File
@@ -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
}
+53
View File
@@ -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' => '&laquo; ' . esc_html__( 'Previous', 'ks-portfolio' ),
'next_text' => esc_html__( 'Next', 'ks-portfolio' ) . ' &raquo;',
)
);
?>
</div>
<?php else : ?>
<div class="tax-project__empty">
<h2 class="tax-project__empty-title"><?php echo esc_html__( 'Nothing here yet&hellip;', '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();
+57
View File
@@ -0,0 +1,57 @@
<?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__( 'Projects', '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' => '&laquo; ' . esc_html__( 'Previous', 'ks-portfolio' ),
'next_text' => esc_html__( 'Next', 'ks-portfolio' ) . ' &raquo;',
)
);
?>
</div>
<?php else : ?>
<div class="archive-projects__empty">
<h2 class="archive-projects__empty-title"><?php echo esc_html__( 'Nothing here yet&hellip;', '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,130 +0,0 @@
# Onboarding Documentation Design
## Goal
Write thorough documentation that enables a new developer (team member or external contributor) to set up, understand, and work with the VDI-Starter-v5 WordPress theme with minimal hand-holding.
## Audience
Both internal team members and external contributors, served with layered docs — quick-start up top, deep dives below.
## Approach
Keep the existing README.md as a project overview (trimmed of deep-dive content that moves to `docs/`). Add a `docs/` directory with four focused guides. The README links to each guide.
## Doc Structure
```
docs/
├── getting-started.md # Setup from zero, local WP, env config, first build
├── architecture.md # Bootstrap flow, layers, conventions, namespacing, globals
├── creating-blocks.md # Step-by-step ACF block creation with block.json, fields, CSS
└── reference.md # Hooks/filters, theme.json tokens, CSS architecture, JS modules, deployment
```
Plus updates to README.md: trim deep-dive content, add Documentation section with links, fix known discrepancies.
## Writing Style
Detailed and explanatory — include context and rationale ("why"), not just "what" and "how". Short paragraphs with code examples and tables where appropriate. Designed to be read end-to-end by a newcomer, but also scannable for quick lookup.
## Guide Contents
### 1. getting-started.md
**Purpose:** Get a developer from zero to a running local environment.
- Prerequisites (Node 22+, pnpm/npm, PHP 8+, Composer, Local by Flywheel or equivalent)
- Local WordPress setup options (Local by Flywheel, DevKinsta, Docker) with pros/cons
- Clone, install, build, run steps (npm install, composer install, npm run build, npm run start/watch)
- .env configuration (LOCALHOST_URL, BROWSERSYNC_PORT)
- BrowserSync behavior (what triggers reload, which port, how to configure)
- What happens on theme activation — **important warning**:
- Plugin auto-installation (7 plugins downloaded from URLs)
- Default page creation (Home, News, 404, Contact Us)
- WordPress reading settings configuration
- Owner role creation
- Warning: do not activate on existing sites without reviewing `lib/activation.php`
- Troubleshooting common issues:
- CSS not compiling → run `npm run build`
- Blocks not appearing → ACF Pro must be active
- Menu not rendering → must assign menu to location in WP admin
- JavaScript not loading → check script module support (WP 6.5+)
### 2. architecture.md
**Purpose:** Explain how the theme is organized and how it boots.
- Bootstrap flow: `functions.php` → glob autoload of `lib/*.php``init` hook → class instantiation
- The 7 architectural layers (from knowledge graph):
1. **Entry**`functions.php` and `style.css` (theme declaration)
2. **Service Layer** — PHP classes in `lib/` (Enqueue, MenuItems, Breadcrumbs, ACF, Resources)
3. **UI Templates** — WordPress template hierarchy, ACF blocks, components, partials, icons
4. **Styling** — Layered CSS with Tailwind v4 (base → components → blocks → navigation)
5. **Client Scripts** — JS modules (Navigation, BackToTop, Button, GetHeaderHeight, TagExternalLinks)
6. **Data Layer** — ACF field group JSON schemas in `acf/`
7. **Infrastructure** — Build scripts, CI/CD pipelines, project configs, tests
- Namespace conventions:
- PHP namespace: `BasicWP`
- Text domain: `basicwp`
- Block category: `vdi-blocks`
- WP Engine folder: `vdi-v5`
- Explain the relationship between these naming choices
- Global variables: `$views` (path to views/ directory) and `$theme` (theme directory path)
- WordPress hooks cleanup in `hooks.php`: what gets removed (emojis, block library styles, REST links, XML-RPC) and why — performance and security rationale
- Enqueue system: `wp_enqueue_script_module()` for JS (dependency graph: theme → button, backToTop, Navigation, etc.) vs traditional `wp_enqueue_script` for admin
- theme.json design system: color palette (12 colors via CSS custom properties), font sizes (15 presets), spacing, layout
### 3. creating-blocks.md
**Purpose:** Teach a developer how to create a new ACF block from scratch.
- Anatomy of a block: `block.json` (registration) + PHP template (rendering) + CSS file (scoped styles)
- Step-by-step tutorial creating a new "Testimonial" block:
1. Create directory `views/blocks/testimonial/`
2. Create `block.json` with required fields (name, title, description, category, icon, supports)
3. Create `testimonial.php` template using `blockWrapperAttributes()` and `getFieldValue()`
4. Create `testimonial.css` for block-specific styles
5. Create ACF field group in WP admin → exported to `acf/` as JSON
- How blocks are auto-registered: `regACFBlocks()` in `functions.php` scans `views/blocks/*/block.json`
- Helper functions: `blockWrapperAttributes()` for class/id passthrough, `getFieldValue()` for dot-notation ACF access
- Parent-child block patterns: buttons→button, grid→grid-cell (using `allowed_blocks` or `InnerBlocks`)
- Tailwind in blocks: how the whitelist system works (`whitelist.php`), how to add editor-only classes
- Block CSS loading: each block's CSS file is auto-enqueued by the block registration
### 4. reference.md
**Purpose:** Complete API and configuration reference for quick lookup.
- **Hooks and Filters** — Complete table of all `add_action`/`add_filter` calls from `hooks.php`, `extras.php`, `search-features.php`, and class files, with priority, callback, and purpose
- **theme.json Design Tokens** — Color palette (12 colors with CSS variable names), font sizes (15 presets with rem values), spacing scale, layout sizes
- **CSS Architecture** — Import tree from `theme.css`, explanation of each layer (base, components, blocks, navigation), how to add new stylesheets
- **JS Module Dependency Graph** — Visual/text tree showing `theme.js` → Navigation, BackToTop, Button, GetHeaderHeight, TagExternalLinks; `admin.js` → Button
- **Navigation Class API** — Methods for the sliding viewport feature (initializeSlidingViewport, navigateToLevel, navigateBack, etc.), configuration options
- **Helper Functions** — `getFieldValue()`, `blockWrapperAttributes()`, `customMenuOrder()`, `blockCategories()`, `consoleLog()`, `customExcerpt()`, `escEmbeds()`
- **Class Reference** — Enqueue (enqFEAssets, enqBEAssets, enqEditorAssets), ACF (saveJson, loadJson), Breadcrumbs (generate, render, and per-page-type methods), MenuItems (render), Resources (CPT + permalink rewrite), ShowTemplate (debug template path)
- **CLI Commands** — `npm run build`, `npm run start`/`npm run watch`, `composer lint`, `composer fix`, Playwright test commands
- **Deployment Workflow** — GitHub Actions `wpengine.yml` pipeline: install deps → build Tailwind → rsync to WP Engine
- **Testing** — Playwright a11y test setup and commands, PHPCS configuration and usage
## README.md Changes
- Trim: Remove the detailed function-level API docs (they move to `reference.md`)
- Trim: Remove the detailed project structure tree (keep a shorter version, point to `architecture.md` for the full picture)
- Add: "Documentation" section with links to all four guides
- Fix: Wrong CSS paths (`views/styles/``styles/`)
- Fix: Missing `contact-info` block from the block list
- Fix: Deployment filename typo (`wpengine,yml``wpengine.yml`)
- Fix: Note that `class-enqueue.php` also has `enqEditorAssets()`
- Add: Mention `backToTop.js` custom element (missing from current README)
- Add: Clarify namespace conventions (BasicWP, basicwp, vdi-blocks, vdi-v5)
## Implementation Plan
This is a documentation-only change — no code modifications. Implementation consists of:
1. Create `docs/` directory with 4 markdown files
2. Update `README.md` with trimmed content and links
3. Fix known discrepancies in README
Estimated scope: ~4 documentation files of ~200-400 lines each, plus README edits.
+72 -54
View File
@@ -2,11 +2,11 @@
/**
* Theme footer template
*
* @package BasicWP
* @package KsPortfolio
* @since 1.0.0
*/
namespace BasicWP;
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( '&copy; ' . gmdate( 'Y' ) . ' ' . get_bloginfo( 'name' ) . '.' );
}
?>
<div class="site-footer__copyright">
<?php echo wp_kses_post( $copyright ); ?>
</div>
</footer>
+2 -2
View File
@@ -2,10 +2,10 @@
/**
* Front Page Template
*
* @package BasicWP
* @package KsPortfolio
*/
namespace BasicWP;
namespace KsPortfolio;
get_header();
?>
+9 -3
View File
@@ -1,21 +1,27 @@
<?php
/**
* Functions file for the BasicWP theme.
* Functions file for the KsPortfolio theme.
*
* This file initializes the theme by including necessary dependencies
* and loading additional function files.
*
* @package BasicWP
* @package KsPortfolio
* @since 1.0.0
*/
namespace BasicWP;
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
+19 -9
View File
@@ -2,15 +2,19 @@
/**
* Theme header template
*
* @package BasicWP
* @package KsPortfolio
* @since 1.0.0
*/
namespace BasicWP;
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' ) : '';
// Expose resume link to the main nav component (used by the mobile menu's footer area).
$GLOBALS['ks_portfolio_link_resume'] = $linkResume;
// Check conditions for displaying the hero section
$showHero = in_array(
@@ -25,14 +29,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 +46,23 @@ $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 lg:container py-4 items-center flex gap-x-8 gap-y-0 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>
<div class="headRight lg:flex lg:items-center lg:gap-x-4 lg:gap-y-0 lg:justify-end">
<?php get_template_part( 'views/components/nav-main' ); ?>
<div class="site-header__aux hidden lg:flex">
<a class="button button--outline" href="<?php echo esc_url( $linkResume ); ?>">
<?php echo esc_html__( 'Resume', 'ks-portfolio' ); ?>
</a>
</div>
</div>
</div>
</header>
+5 -6
View File
@@ -2,11 +2,11 @@
/**
* Blog posts list
*
* @package BasicWP
* @package KsPortfolio
* @since 1.0.0
*/
namespace BasicWP;
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">
+5 -17
View File
@@ -18,11 +18,11 @@ if ( isset( $_GET['activated'] ) && is_admin() ) {
* Sample nested structure
*
* 'Parent Page' => array(
* 'Subpage 1' => array(
* 'Sub-subpage 1',
* 'Sub-subpage 2'
* 'Child page 1' => array(
* 'Grandchild page 1',
* 'Grandchild page 2'
* ),
* 'Subpage 2.2' => array()
* 'Child page 2' => array()
* ),
*/
);
@@ -105,17 +105,9 @@ if ( isset( $_GET['activated'] ) && is_admin() ) {
$muPlugins = array(
array(
'url' => 'https://docs.vincentdevelopment.ca/files/advanced-custom-fields-pro.zip',
'url' => 'https://downloads.wordpress.org/plugin/secure-custom-fields.6.9.3.zip',
'active' => true,
),
array(
'url' => 'https://docs.vincentdevelopment.ca/files/gravity-forms.zip',
'active' => true,
),
array(
'url' => 'https://updraftplus.com/wp-content/uploads/updraftplus.zip',
'active' => false,
),
array(
'url' => 'https://downloads.wordpress.org/plugin/simple-history.5.11.0.zip',
'active' => true,
@@ -128,10 +120,6 @@ if ( isset( $_GET['activated'] ) && is_admin() ) {
'url' => 'https://downloads.wordpress.org/plugin/better-search-replace.1.4.10.zip',
'active' => true,
),
array(
'url' => 'https://downloads.wordpress.org/plugin/google-site-kit.1.153.0.zip',
'active' => false,
),
);
// Custom log file
+2 -2
View File
@@ -2,11 +2,11 @@
/**
* ACF (Advanced Custom Fields) support class & functions
*
* @package BasicWP
* @package KsPortfolio
* @since 1.0.0
*/
namespace BasicWP;
namespace KsPortfolio;
/**
* Class ACF
+24 -2
View File
@@ -1,5 +1,5 @@
<?php
namespace BasicWP;
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
+10 -10
View File
@@ -1,12 +1,12 @@
<?php
/**
* BasicWP Theme Enqueue Class
* KsPortfolio Theme Enqueue Class
*
* @package BasicWP
* @package KsPortfolio
* @since 1.0.0
*/
namespace BasicWP;
namespace KsPortfolio;
/**
* Class Enqueue
@@ -39,7 +39,7 @@ class Enqueue {
$css_path = '/static/dist/theme.css';
if ( file_exists( $theme_dir . $css_path ) ) {
$version = filemtime( $theme_dir . $css_path );
wp_enqueue_style( 'basicwp-theme', $theme_uri . $css_path, array(), $version );
wp_enqueue_style( 'sf-evo-theme', $theme_uri . $css_path, array(), $version );
}
$font_ver = gmdate( 'U' );
@@ -53,8 +53,8 @@ class Enqueue {
if ( file_exists( $theme_dir . $js_path ) ) {
$version = filemtime( $theme_dir . $js_path );
wp_enqueue_script( 'jquery' ); // Needed by downstream scripts; modules can't depend on classic scripts.
wp_enqueue_script_module( 'basicwp-theme', $theme_uri . $js_path, array(), $version );
wp_enqueue_script_module( 'basicwp-button', $theme_uri . '/static/js/components/button.js', array( 'basicwp-theme' ), $version );
wp_enqueue_script_module( 'sf-evo-theme', $theme_uri . $js_path, array(), $version );
wp_enqueue_script_module( 'sf-evo-button', $theme_uri . '/static/js/components/button.js', array( 'sf-evo-theme' ), $version );
}
}
@@ -74,7 +74,7 @@ class Enqueue {
$admin_css_path = '/styles/backend/admin.css';
if ( file_exists( $theme_dir . $admin_css_path ) ) {
$version = filemtime( $theme_dir . $admin_css_path );
wp_enqueue_style( 'basicwp-admin', $theme_uri . $admin_css_path, array(), $version );
wp_enqueue_style( 'sf-evo-admin', $theme_uri . $admin_css_path, array(), $version );
}
/**
@@ -84,8 +84,8 @@ class Enqueue {
if ( file_exists( $theme_dir . $admin_js_path ) ) {
$version = filemtime( $theme_dir . $admin_js_path );
wp_enqueue_script( 'jquery' ); // Needed by downstream scripts; modules can't depend on classic scripts.
wp_enqueue_script_module( 'basicwp-admin', $theme_uri . $admin_js_path, array(), $version );
wp_enqueue_script_module( 'basicwp-button', $theme_uri . '/static/js/components/button.js', array( 'basicwp-admin' ), $version );
wp_enqueue_script_module( 'sf-evo-admin', $theme_uri . $admin_js_path, array(), $version );
wp_enqueue_script_module( 'sf-evo-button', $theme_uri . '/static/js/components/button.js', array( 'sf-evo-admin' ), $version );
}
}
@@ -103,7 +103,7 @@ class Enqueue {
if ( file_exists( $theme_dir . $editor_css_path ) ) {
$version = filemtime( $theme_dir . $editor_css_path );
wp_enqueue_style( 'basicwp-editor', $theme_uri . $editor_css_path, array(), $version );
wp_enqueue_style( 'sf-evo-editor', $theme_uri . $editor_css_path, array(), $version );
}
}
}
+3 -3
View File
@@ -1,12 +1,12 @@
<?php
/**
* BasicWP MenuItems Class
* KsPortfolio MenuItems Class
*
* @package BasicWP
* @package KsPortfolio
* @since 1.0.0
*/
namespace BasicWP;
namespace KsPortfolio;
/**
* Class MenuItems
+195
View File
@@ -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 );
}
}
-129
View File
@@ -1,129 +0,0 @@
<?php
/**
* Resources custom post type & taxonomies
*
* @package BasicWP
* @since 1.0.0
*/
namespace BasicWP;
/**
* 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();
+52 -6
View File
@@ -2,11 +2,11 @@
/**
* Filters. etc
*
* @package BasicWP
* @package KsPortfolio
* @since 1.0.0
*/
namespace BasicWP;
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,11 +154,13 @@ 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(
/* translators: %s is replaced with the search query */
__( 'Search Results for "%s"', 'basicwp' ),
__( 'Search Results for "%s"', 'sf-evo' ),
get_search_query()
);
} elseif ( is_404() ) {
@@ -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
+5 -5
View File
@@ -1,12 +1,12 @@
<?php
/**
* BasicWP Theme Helpers
* KsPortfolio Theme Helpers
*
* @package BasicWP
* @package KsPortfolio
* @since 1.0.0
*/
namespace BasicWP;
namespace KsPortfolio;
// Define global variables for theme and views folder paths.
global $theme, $views;
@@ -122,8 +122,8 @@ add_filter( 'menu_order', __NAMESPACE__ . '\\customMenuOrder', 10, 1 );
*/
function blockCategories( $categories ) {
$vdi_cat = array(
'slug' => 'vdi-blocks',
'title' => 'VDI Custom Blocks',
'slug' => 'sf-blocks',
'title' => 'SoloFrame Custom Blocks',
'icon' => 'dashicons-admin-customizer',
);
+6 -6
View File
@@ -1,12 +1,12 @@
<?php
/**
* BasicWP Theme Hooks
* KsPortfolio Theme Hooks
*
* @package BasicWP
* @package KsPortfolio
* @since 1.0.0
*/
namespace BasicWP;
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' );
+3 -3
View File
@@ -1,12 +1,12 @@
<?php
/**
* Search features for BasicWP theme.
* Search features for KsPortfolio theme.
*
* @package BasicWP
* @package KsPortfolio
* @since 1.0.0
*/
namespace BasicWP;
namespace KsPortfolio;
/**
* Modifies the WordPress query object for page search functionality.
+12 -10
View File
@@ -2,11 +2,11 @@
/**
* Add a comment to show which template is being used on the current page.
*
* @package BasicWP
* @package KsPortfolio
* @since 1.0.0
*/
namespace BasicWP;
namespace KsPortfolio;
/**
* Class ShowTemplate
@@ -14,7 +14,7 @@ namespace BasicWP;
* 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 BasicWP
* @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 -2
View File
@@ -1,11 +1,11 @@
{
"name": "vdi-starter-v5",
"name": "soloframe-evo",
"version": "5.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "vdi-starter-v5",
"name": "soloframe-evo",
"version": "5.0",
"license": "MIT",
"dependencies": {
+5 -5
View File
@@ -1,7 +1,7 @@
{
"name": "vdi-starter-v5",
"name": "soloframe-evo",
"version": "5.0",
"description": "VDI-Starter-v5 is a minimal WordPress theme designed as a starting point for custom theme development. It focuses on modern development approaches with a lean architecture that avoids the overhead of theme frameworks.",
"description": "SoloFrame Evo is a minimal WordPress theme designed as a starting point for custom theme development. It focuses on modern development approaches with a lean architecture that avoids the overhead of theme frameworks.",
"scripts": {
"start": "npm run watch",
"watch": "node bin/.watch.js",
@@ -9,7 +9,7 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/Vincent-Design-Inc/VDI-Starter-v5.git"
"url": "git+https://github.com/ksolomon/SoloFrame Evo.git"
},
"keywords": [
"wordpress",
@@ -21,9 +21,9 @@
"author": "Keith Solomon <keith@vincentdesign.ca>",
"license": "MIT",
"bugs": {
"url": "https://github.com/Vincent-Design-Inc/VDI-Starter-v5/issues"
"url": "https://github.com/ksolomon/SoloFrame Evo/issues"
},
"homepage": "https://github.com/Vincent-Design-Inc/VDI-Starter-v5#readme",
"homepage": "https://github.com/ksolomon/SoloFrame Evo#readme",
"devDependencies": {
"@axe-core/playwright": "^4.10.1",
"@playwright/test": "^1.52.0",
+2 -2
View File
@@ -2,11 +2,11 @@
/**
* Single Pages
*
* @package BasicWP
* @package KsPortfolio
* @since 1.0.0
*/
namespace BasicWP;
namespace KsPortfolio;
get_header();
+7 -2
View File
@@ -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' ],
},
},
},
// {
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 2.8 MiB

+5 -4
View File
@@ -2,11 +2,11 @@
/**
* Blog search template
*
* @package BasicWP
* @package KsPortfolio
* @since 1.0.0
*/
namespace BasicWP;
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
+3 -3
View File
@@ -2,14 +2,14 @@
/**
* Page Sidebar
*
* @package BasicWP
* @package KsPortfolio
*/
namespace BasicWP;
namespace KsPortfolio;
?>
<aside class="sidebar sidebar-page">
<h2><?php esc_html_e( 'Page Sidebar', 'basicwp' ); ?></h2>
<h2><?php esc_html_e( 'Page Sidebar', 'sf-evo' ); ?></h2>
<?php dynamic_sidebar( 'sidebar-page' ); ?>
</aside>
+2 -2
View File
@@ -2,10 +2,10 @@
/**
* Sidebar
*
* @package BasicWP
* @package KsPortfolio
*/
namespace BasicWP;
namespace KsPortfolio;
?>
+196
View File
@@ -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
View File
@@ -2,11 +2,11 @@
/**
* Single Posts
*
* @package BasicWP
* @package KsPortfolio
* @since 1.0.0
*/
namespace BasicWP;
namespace KsPortfolio;
get_header();
+68 -2
View File
@@ -54,6 +54,12 @@ class Navigation {
*/
#slidingViewportEnabled = false;
/**
* Function reference for preventing scroll (needed to remove listener)
* @type {Function}
*/
#preventScrollHandler = (e) => e.preventDefault();
constructor(mobileMenuButtonId, dropDownClass, subMenuLinkClass = "sub-menu-item") {
@@ -118,7 +124,14 @@ class Navigation {
const isExpanded = button.getAttribute("aria-expanded") === "true";
if (!isExpanded) {
if (isExpanded) {
// Prevent scroll when menu is open
document.documentElement.style.overflow = "hidden";
document.body.style.overflow = "hidden";
} else {
// Re-enable scroll when menu is closed
document.documentElement.style.overflow = "";
document.body.style.overflow = "";
// Reset sliding navigation when menu is closed
if (this.#slidingViewportEnabled) {
this.resetSlidingNavigation();
@@ -172,12 +185,23 @@ class Navigation {
&& !event.relatedTarget.classList.contains("menu-vdi__link")
&& !event.relatedTarget.classList.contains("menu-vdi__back"); // Don't close on back button click
if (isNull || isNotMenuItem)
if (isNull || isNotMenuItem) {
button.setAttribute("aria-expanded", false);
// Re-enable scroll when menu is closed
document.documentElement.style.overflow = "";
document.body.style.overflow = "";
}
}, true)
}
}
/**
* Handles escape behaviour
* Closes all dropdown when user hits
* escape key
*
* @link {https://developer.mozilla.org/en-US/docs/Web/API/Element/keyup_event}
*/
/**
* Handles escape behaviour
* Closes all dropdown when user hits
@@ -191,6 +215,10 @@ class Navigation {
this.#mobileMenuButton.setAttribute("aria-expanded", false);
this.closeAllDropDowns();
// Re-enable scroll when menu is closed
document.documentElement.style.overflow = "";
document.body.style.overflow = "";
// Reset sliding navigation on escape
if (this.#slidingViewportEnabled) {
this.resetSlidingNavigation();
@@ -571,6 +599,44 @@ class Navigation {
console.log('Sliding structure cleaned up');
}
/**
* Disable scroll on the page
* Prevents scrolling in all directions
*/
disableScroll() {
try {
// Apply overflow hidden to both html and body for better compatibility
document.documentElement.style.overflow = "hidden";
document.body.style.overflow = "hidden";
// Prevent touch scroll on mobile devices
if (this.#preventScrollHandler) {
document.addEventListener("touchmove", this.#preventScrollHandler, { passive: false });
}
} catch (error) {
console.warn("Error disabling scroll:", error);
}
}
/**
* Enable scroll on the page
* Restores normal scrolling behavior
*/
enableScroll() {
try {
// Restore overflow behavior
document.documentElement.style.overflow = "";
document.body.style.overflow = "";
// Re-enable touch scroll on mobile devices
if (this.#preventScrollHandler) {
document.removeEventListener("touchmove", this.#preventScrollHandler);
}
} catch (error) {
console.warn("Error enabling scroll:", error);
}
}
}
export default Navigation;
+5 -5
View File
@@ -1,7 +1,7 @@
/*
* Theme Name:VDI Starter v5
* Description: Custom WordPress theme starter for VDI Projects
* Version: 5.0
* Author: Vincent Design Inc.
* Text Domain: basicwp
* Theme Name: Portfolio 2026
* Description: A clean and modern portfolio theme for showcasing my work.
* Version: 1.0
* Author: Keith Solomon
* Text Domain: ks-portfolio
*/
+35 -7
View File
@@ -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
View File
@@ -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%; }
+20 -8
View File
@@ -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;
+98 -37
View File
@@ -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;
box-shadow:0 2px 8px rgba(0,0,0,0.15);
cursor:pointer;
transition:opacity 0.2s;
background: var(--color-primary,#3857BC);
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;
}
.back-to-top:hover, .back-to-top:focus {
background: var(--color-info, #233a7a);
opacity: 1;
outline: 2px solid var(--color-info, #233a7a);
.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);
}
}
+7
View File
@@ -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;
}
+2
View File
@@ -6,3 +6,5 @@
@import "./sidebar.css";
@import "./pagination.css";
@import "./site-footer.css";
@import "./project-card.css";
@import "./page-hero.css";
+10
View File
@@ -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); }
+6
View File
@@ -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 {
+357
View File
@@ -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; }
+7
View File
@@ -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);
}
}
}
+134 -22
View File
@@ -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);
}
}
+17 -16
View File
@@ -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);
}
+25
View File
@@ -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([]);
});
});
+58
View File
@@ -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');
});
});
+28
View File
@@ -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([]);
});
});
+36
View File
@@ -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([]);
});
});
+4 -4
View File
@@ -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
View File
@@ -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": {
+2 -2
View File
@@ -4,10 +4,10 @@
*
* This is the template that renders the Accordion block.
*
* @package BasicWP
* @package KsPortfolio
*/
namespace BasicWP;
namespace KsPortfolio;
$open = get_field( 'open' );
$group = get_field( 'group_items' );
+1 -1
View File
@@ -5,7 +5,7 @@
"style": [
"file:./boilerplate.css"
],
"category": "vdi-blocks",
"category": "sf-blocks",
"icon": "block-default",
"keywords": [
"boilerplate"
+2 -2
View File
@@ -4,10 +4,10 @@
*
* This is the template for building your own custom blocks.
*
* @package BasicWP
* @package KsPortfolio
*/
namespace BasicWP;
namespace KsPortfolio;
$classes = 'boilerplate';
+2 -2
View File
@@ -1,8 +1,8 @@
{
"name": "acf/button",
"title": "Button (VDI)",
"title": "Button (SFE)",
"description": "A button.",
"category": "vdi-blocks",
"category": "sf-blocks",
"icon": "button",
"keywords": [
"button"
+2 -2
View File
@@ -2,10 +2,10 @@
/**
* Button block
*
* @package BasicWP
* @package KsPortfolio
*/
namespace BasicWP;
namespace KsPortfolio;
// Retrieve ACF fields
$element = get_field( 'element' ) ? get_field( 'element' ) : 'a';
+2 -2
View File
@@ -1,9 +1,9 @@
{
"name": "acf/buttons",
"title": "Buttons (VDI)",
"title": "Buttons (SFE)",
"description": "A button or group of buttons.",
"allowedBlocks": [ "acf/button" ],
"category": "vdi-blocks",
"category": "sf-blocks",
"icon": "button",
"keywords": [
"buttons"
+2 -2
View File
@@ -4,10 +4,10 @@
*
* This is the template that renders the Buttons block.
*
* @package BasicWP
* @package KsPortfolio
*/
namespace BasicWP;
namespace KsPortfolio;
$ibClasses = 'flex flex-wrap gap-4 w-full justify-center sm:justify-start';
+1 -1
View File
@@ -5,7 +5,7 @@
"style": [
"file:./contact-info.css"
],
"category": "vdi-blocks",
"category": "sf-blocks",
"icon": "feedback",
"keywords": [
"contact info",
+5 -5
View File
@@ -4,10 +4,10 @@
*
* Display contact information from global fields with icons and optional form.
*
* @package BasicWP
* @package KsPortfolio
*/
namespace BasicWP;
namespace KsPortfolio;
$classes = 'contact-info';
$wrapper = blockWrapperAttributes( $classes, $is_preview );
@@ -19,13 +19,13 @@ $wrapper = blockWrapperAttributes( $classes, $is_preview );
<h2 class="text-2xl font-bold mb-4">Contact Information</h2>
<div class="not-prose text-black my-4">
<h3 class="mb-0"><?php echo esc_html__( 'Mailing Address', 'basicwp' ); ?></h3>
<h3 class="mb-0"><?php echo esc_html__( 'Mailing Address', 'sf-evo' ); ?></h3>
<p class="my-0.5"><?php echo wp_kses_post( get_field( 'contact_info', 'option' )['address'] ); ?></p>
<h3 class="mb-0"><?php echo esc_html__( 'Email', 'basicwp' ); ?></h3>
<h3 class="mb-0"><?php echo esc_html__( 'Email', 'sf-evo' ); ?></h3>
<p class="my-0.5"><a class="hover:opacity-80 transition-colors duration-100" href="mailto:<?php echo esc_html( get_field( 'contact_info', 'option' )['email'] ); ?>"><?php echo esc_html( get_field( 'contact_info', 'option' )['email'] ); ?></a></p>
<h3 class="mb-0"><?php echo esc_html__( 'Phone', 'basicwp' ); ?></h3>
<h3 class="mb-0"><?php echo esc_html__( 'Phone', 'sf-evo' ); ?></h3>
<p class="my-0.5"><a class="hover:opacity-80 transition-colors duration-100" href="tel:<?php echo esc_html( get_field( 'contact_info', 'option' )['phone'] ); ?>"><?php echo esc_html( get_field( 'contact_info', 'option' )['phone'] ); ?></a></p>
</div>
</div>
+2 -2
View File
@@ -1,11 +1,11 @@
{
"name": "acf/grid-cell",
"title": "Grid Cell (VDI)",
"title": "Grid Cell (SFE)",
"description": "Custom grid cell block.",
"style": [
"file:./grid-cell.css"
],
"category": "vdi-blocks",
"category": "sf-blocks",
"icon": "grid-view",
"keywords": [
"grid"
+2 -2
View File
@@ -4,10 +4,10 @@
*
* This is the template that displays the Grid Cell block.
*
* @package BasicWP
* @package KsPortfolio
*/
namespace BasicWP;
namespace KsPortfolio;
// Initialize variables
$className = ! empty( $block['className'] ) ? $block['className'] : '';
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "acf/grid",
"title": "Grid Block (VDI)",
"title": "Grid Block (SFE)",
"description": "Custom grid block.",
"style": [
"file:./grid.css"
],
"allowedBlocks": [ "acf/grid-cell" ],
"category": "vdi-blocks",
"category": "sf-blocks",
"icon": "grid-view",
"keywords": [
"grid"
+2 -2
View File
@@ -4,10 +4,10 @@
*
* This is the template that displays the grid block.
*
* @package BasicWP
* @package KsPortfolio
*/
namespace BasicWP;
namespace KsPortfolio;
$allowedBlocks = array( 'acf/grid-cell' );
$default_blocks = array(
+1 -1
View File
@@ -5,7 +5,7 @@
"style": [
"file:./homepage-hero.css"
],
"category": "vdi-blocks",
"category": "sf-blocks",
"icon": "block-default",
"keywords": [
"homepage-hero",
+20 -7
View File
@@ -2,31 +2,32 @@
/**
* Block Name: Homepage Hero
*
* @package BasicWP
* @package KsPortfolio
*/
namespace BasicWP;
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>
@@ -5,7 +5,7 @@
"style": [
"file:./media-text-innerblocks.css"
],
"category": "vdi-blocks",
"category": "sf-blocks",
"icon": "screenoptions",
"keywords": [
"media",
@@ -4,10 +4,10 @@
*
* This is the template that displays the media text inner blocks.
*
* @package BasicWP
* @package KsPortfolio
*/
namespace BasicWP;
namespace KsPortfolio;
// Retrieve ACF fields
$bgColor = get_field( 'background_color' ) ? get_field( 'background_color' ) : '#c5c5c5';
+1 -1
View File
@@ -5,7 +5,7 @@
"style": [
"file:./media-text.css"
],
"category": "vdi-blocks",
"category": "sf-blocks",
"icon": "screenoptions",
"keywords": [
"media",
+2 -2
View File
@@ -4,10 +4,10 @@
*
* This is the template that displays the Media With Text block.
*
* @package BasicWP
* @package KsPortfolio
*/
namespace BasicWP;
namespace KsPortfolio;
// Retrieve ACF fields
$bgColor = get_field( 'background_color' ) ? get_field( 'background_color' ) : '#c5c5c5';
+1 -1
View File
@@ -5,7 +5,7 @@
"style": [
"file:./page-children.css"
],
"category": "vdi-blocks",
"category": "sf-blocks",
"icon": "block-default",
"keywords": [
"page-children"
+3 -3
View File
@@ -2,10 +2,10 @@
/**
* Page Children block
*
* @package BasicWP
* @package KsPortfolio
*/
namespace BasicWP;
namespace KsPortfolio;
// Retrieve the current page ID and its children
$parentId = get_queried_object_id();
@@ -89,7 +89,7 @@ $wrapper = blockWrapperAttributes( $classes, $is_preview );
</h3>
</div>
<div class="my-0 grow-1 text-18px text-gray-600 <?php echo esc_attr( $gcPad ); ?>">
<div class="my-0 grow text-18px text-gray-600 <?php echo esc_attr( $gcPad ); ?>">
<?php
if ( ! empty( $grandchildren ) ) {
?>
+26
View File
@@ -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' ); ?> &rarr;
</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&hellip;', 'ks-portfolio' ); ?></h3>
<p><?php echo esc_html__( 'No published projects found.', 'ks-portfolio' ); ?></p>
</div>
<?php endif; ?>
</section>
+1 -1
View File
@@ -5,7 +5,7 @@
"style": [
"file:./section.css"
],
"category": "vdi-blocks",
"category": "sf-blocks",
"icon": "align-wide",
"keywords": [
"setion",
+2 -2
View File
@@ -4,10 +4,10 @@
*
* This is the template that displays the section block.
*
* @package BasicWP
* @package KsPortfolio
*/
namespace BasicWP;
namespace KsPortfolio;
// Retrieve ACF fields
$contentWidth = get_field( 'content_width' );
+1 -1
View File
@@ -6,7 +6,7 @@
* https://docs.vincentdevelopment.ca/docs/starter-v3-enhancements/navigation/
*/
namespace BasicWP;
namespace KsPortfolio;
// Variables available:
// $item from parent template
+14 -1
View File
@@ -6,7 +6,7 @@
* https://docs.vincentdevelopment.ca/docs/starter-v3-enhancements/navigation/
*/
namespace BasicWP;
namespace KsPortfolio;
// Variables available from MenuItems component:
// $topLevelNavItems, $hasChildren, $nestedNavItems, $currentPage, $location
@@ -16,6 +16,11 @@ if ( $location === 'main_navigation' ) {
} else {
$menuID = 'menu-container-aux';
}
// Resume link is exposed by header.php via $GLOBALS['ks_portfolio_link_resume']
// and read here so it can be appended as a mobile-only CTA at the bottom of
// the menu panel (hidden on desktop via lg:hidden).
$linkResume = isset( $GLOBALS['ks_portfolio_link_resume'] ) ? $GLOBALS['ks_portfolio_link_resume'] : '';
?>
<ul id="<?php echo esc_attr( $menuID ); ?>" class="menu-vdi">
@@ -30,4 +35,12 @@ if ( $location === 'main_navigation' ) {
<?php include __DIR__ . '/single.php'; ?>
<?php endif; ?>
<?php endforeach; ?>
<?php if ( ! empty( $linkResume ) && $location === 'main_navigation' ) : ?>
<li class="menu-vdi__item menu-item--resume lg:hidden border-t border-secondary-200 mt-2 pt-2">
<a href="<?php echo esc_url( $linkResume ); ?>" class="menu-vdi__link button button--primary mx-4 inline-block">
<?php echo esc_html__( 'Resume', 'ks-portfolio' ); ?>
</a>
</li>
<?php endif; ?>
</ul>
+1 -1
View File
@@ -6,7 +6,7 @@
* https://docs.vincentdevelopment.ca/docs/starter-v3-enhancements/navigation/
*/
namespace BasicWP;
namespace KsPortfolio;
// Variables available:
// $item from parent template
+1 -1
View File
@@ -6,7 +6,7 @@
* https://docs.vincentdevelopment.ca/docs/starter-v3-enhancements/navigation-aux.html
*/
namespace BasicWP;
namespace KsPortfolio;
// Init Variables
global $wp, $views;
+5 -1
View File
@@ -6,9 +6,13 @@
* https://docs.vincentdevelopment.ca/docs/starter-v3-enhancements/navigation/
*/
namespace BasicWP;
namespace KsPortfolio;
global $views;
// Expose resume link to the menu items renderer so the CTA can be appended
// as the final <li> in the mobile panel (hidden on desktop via lg:hidden).
$GLOBALS['ks_portfolio_link_resume'] = isset( $GLOBALS['ks_portfolio_link_resume'] ) ? $GLOBALS['ks_portfolio_link_resume'] : '';
?>
<nav class="nav-main" role="navigation" aria-label="Main">
+7 -5
View File
@@ -6,13 +6,14 @@
* https://docs.vincentdevelopment.ca/docs/starter-v3-enhancements/navigation/
*/
namespace BasicWP;
namespace KsPortfolio;
// Init Variables
$navIcon = isset( get_field( 'header', 'option' )['nav_icon'] ) ? get_field( 'header', 'option' )['nav_icon'] : '';
// Generate toggle classes
$toggleClasses = array( 'nav-main__toggle' );
if ( $navIcon ) {
$toggleClasses[] = 'custom-icon';
}
@@ -29,17 +30,18 @@ if ( $navIcon ) {
<?php else : ?>
<!-- Hamburger Icon -->
<span class="nav-toggle-icon nav-toggle-hamburger">
<svg class="text-black fill-black" viewBox="0 0 100 80" width="40" height="40">
<svg class="text-current fill-[#1A2A45]" viewBox="0 0 100 60" width="40" height="40">
<rect y="0" width="100" height="15"></rect>
<rect y="30" width="100" height="15"></rect>
<rect y="60" width="100" height="15"></rect>
</svg>
</span>
<!-- X Icon -->
<span class="nav-toggle-icon nav-toggle-x">
<svg class="text-black fill-black" viewBox="0 0 100 80" width="40" height="40">
<line x1="20" y1="20" x2="80" y2="60" stroke="black" stroke-width="15" stroke-linecap="round" />
<line x1="80" y1="20" x2="20" y2="60" stroke="black" stroke-width="15" stroke-linecap="round" />
<svg class="text-current stroke-[#1A2A45]" viewBox="0 0 100 100" width="40" height="40">
<line x1="15" y1="15" x2="85" y2="85" stroke-width="15" stroke-linecap="round" />
<line x1="85" y1="15" x2="15" y2="85" stroke-width="15" stroke-linecap="round" />
</svg>
</span>
<?php endif; ?>
+96
View File
@@ -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>
+34
View File
@@ -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>
+34
View File
@@ -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>
+5 -5
View File
@@ -6,7 +6,7 @@
* https://docs.vincentdevelopment.ca/docs/starter-v3-enhancements/search-global.html
*/
namespace BasicWP;
namespace KsPortfolio;
?>
@@ -14,16 +14,16 @@ namespace BasicWP;
<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', 'basicwp' ); ?></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>
</label>
<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">
<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', 'basicwp' ); ?>
<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', 'ks-portfolio' ); ?>
</button>
</form>
</search>
+1
View File
@@ -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

+1
View File
@@ -0,0 +1 @@
<i class="icon-github"></i>
+35
View File
@@ -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>
+7 -7
View File
@@ -2,10 +2,10 @@
/**
* Page Hero Partial
*
* @package BasicWP
* @package KsPortfolio
*/
namespace BasicWP;
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>
+50 -15
View File
@@ -2,38 +2,73 @@
/**
* Social Media Links Partial
*
* @package BasicWP
* @package KsPortfolio
*/
namespace BasicWP;
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
View File
@@ -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 BasicWP
* @package KsPortfolio
* @since 1.0.0
*/
namespace BasicWP;
namespace KsPortfolio;
?>