2373 lines
77 KiB
Markdown
2373 lines
77 KiB
Markdown
# Portfolio 2026 Theme Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** Rebuild the surface of the `ks-portfolio` WordPress theme to match the Portfolio 2026 identity (Carbon Blue dark atmosphere + Raleway typography + project catalog focused on GitHub/Gitea-backed Projects), resolve the documented source conflicts, and ship a working tested theme.
|
||
|
||
**Architecture:** SoloFrame Evo base (Tailwind 4 + ACF blocks + namespaced PHP classes), namespaced from `SoloFrameEvo` → `KsPortfolio`. The theme is a pure presentation layer over the installed `projects-portfolio` plugin — no new CPT registration. Tokens drive every color/typography decision; templates pull data from WordPress content and the plugin's `projects_portfolio_get_*()` helpers with empty-state fallbacks.
|
||
|
||
**Tech Stack:** PHP 8.1+ (WordPress 6.x), Tailwind CSS 4, ACF Pro (Secure Custom Fields), Playwright + axe-core, PHPCS + WPCS, Raleway (Google Fonts).
|
||
|
||
## Global Constraints
|
||
|
||
- PHP namespace: `KsPortfolio` (was `SoloFrameEvo`). Every PHP file in the theme uses this namespace. Composer text-domain: `ks-portfolio`.
|
||
- Text domain: `ks-portfolio` for every `__()`, `_e()`, `esc_html__()`, etc. call. Theme slug in `style.css` header matches.
|
||
- Tokens only: no raw hex outside `theme.json` editor palette, `styles/base/colors.css`, or the Carbon Blue partial. Reference via `var(--color-*)` / `var(--font-*)` / `var(--text-*)`.
|
||
- `--font-headings: var(--font-sans)` is defined in `styles/base/typography.css`.
|
||
- Body surface: `background-color: #100e0b; color: var(--color-on-surface);`. The light `main#maincontent { background: var(--color-background); }` rule is removed.
|
||
- Carbon Blue atmospheric field lives in `views/partials/aura-bg.php` and is loaded on `front-page.php` and `archive-projects.php`. The container must NOT carry `background-color` (blend-mode architecture).
|
||
- Decorative layers: `position: absolute; inset: 0; pointer-events: none; aria-hidden="true"`.
|
||
- Accessibility target: WCAG 2.1 AA. Every template has exactly one `<h1>`. Skip link preserved. `prefers-reduced-motion` honored.
|
||
- Security: all forms use `wp_nonce_field`; state-changing requests use `wp_verify_nonce`; dynamic output uses the right `esc_*` for context; external URLs `esc_url` + `rel="noopener noreferrer"` when `target="_blank"`.
|
||
- No `picsum.photos` URLs anywhere in shipped theme.
|
||
- No invented project copy, statistics, or testimonials. Empty states use the documented copy: `Nothing here yet...`, `No published projects found.`, `Page Not Found`, `Sorry, the page you're looking for doesn't exist.`
|
||
- Test scope: Chromium + axe only. Firefox / WebKit / mobile are out of scope. OWASP ZAP out of scope — security audit is PHPCS + grep + manual review.
|
||
- Local base URL: `http://portfolio.test` (matches `.env`).
|
||
|
||
---
|
||
|
||
## File Structure
|
||
|
||
### Modified
|
||
- `style.css` — already correct; verify only.
|
||
- `functions.php` — namespace swap, include new `class-projects.php`, include new aura-bg partial bootstrap.
|
||
- `header.php` — namespace swap, dark surface, three-region responsive header, aura-bg on home/archive.
|
||
- `footer.php` — namespace swap, dark surface, footer links match Carbon Blue.
|
||
- `front-page.php` — full home template replacing 8-line stub.
|
||
- `index.php` — namespace swap, dark surface, remove `picsum.photos`, empty-state copy.
|
||
- `single.php` — namespace swap, dark surface, remove `picsum.photos`.
|
||
- `page.php` — namespace swap, dark surface.
|
||
- `search.php` — namespace swap, dark surface, remove `picsum.photos`.
|
||
- `404.php` — namespace swap, dark surface, documented empty-state copy.
|
||
- `lib/activation.php` — namespace swap.
|
||
- `lib/class-acf.php` — namespace swap.
|
||
- `lib/class-breadcrumbs.php` — namespace swap.
|
||
- `lib/class-enqueue.php` — namespace swap, verify Raleway + theme.css enqueue.
|
||
- `lib/class-menuitems.php` — namespace swap.
|
||
- `lib/class-resources.php` — namespace swap (keep CPT registration as-is).
|
||
- `lib/extras.php` — namespace swap.
|
||
- `lib/helpers.php` — namespace swap, add `getTheTitle()`, `hasSidebar()`, `customExcerpt()`, `getFieldValue()`.
|
||
- `lib/hooks.php` — namespace swap.
|
||
- `lib/search-features.php` — namespace swap.
|
||
- `lib/show-template.php` — namespace swap.
|
||
- `theme.json` — fix `styles` references to existing slugs, add dark surface palette.
|
||
- `styles/base/colors.css` — add Carbon Blue tokens.
|
||
- `styles/base/typography.css` — define `--font-headings`.
|
||
- `styles/base/global.css` — dark body, remove light `main` rule, dark container / section tweaks.
|
||
- `styles/base/forms.css` — dark form surfaces.
|
||
- `styles/navigation/nav-*.css` — dark link colors.
|
||
- `styles/blocks/buttons.css` — verify dark-surface contrast for primary/secondary buttons.
|
||
- `views/components/nav-aux.php`, `nav-main.php`, `nav-main__toggle.php` — namespace swap.
|
||
- `views/partials/page-hero.php` — namespace swap, dark surface defaults.
|
||
- `views/partials/social-media.php` — namespace swap, dark color overrides.
|
||
- `views/forms/search.php` — escape audit only.
|
||
- `playwright.config.js` — set base URL, keep chromium only.
|
||
|
||
### New
|
||
- `archive-projects.php` — project archive template.
|
||
- `single-projects.php` — project detail template.
|
||
- `taxonomy-project-type.php` — project type archive template.
|
||
- `views/partials/aura-bg.php` — Carbon Blue atmospheric field.
|
||
- `views/components/project-card.php` — reusable project card.
|
||
- `views/components/project-stat-card.php` — Downloads/Stars/Forks/Issues tile.
|
||
- `views/components/project-meta-row.php` — repository metadata table row.
|
||
- `lib/class-projects.php` — thin wrapper around plugin helpers + provider-aware defaults.
|
||
- `tests/home.spec.js` — Playwright + axe on home.
|
||
- `tests/archive.spec.js` — Playwright + axe on project archive.
|
||
- `tests/single.spec.js` — Playwright + axe on project detail.
|
||
- `docs/superpowers/specs/2026-08-16-portfolio-2026-design.md` — already committed.
|
||
- `docs/superpowers/specs/2026-08-16-portfolio-2026-testing-report.md` — final report (delivered at end).
|
||
|
||
### Removed
|
||
- `screenshot.png` (Basic WP) — replaced at end.
|
||
- `picsum.photos` URLs in `index.php`, `search.php`.
|
||
- Legacy hex values in pagination CSS.
|
||
|
||
---
|
||
|
||
## Task 1: Verify environment + capture baseline screenshots
|
||
|
||
**Files:**
|
||
- Inspect: `playwright.config.js`
|
||
- Inspect: `.env`
|
||
- Inspect: `static/dist/theme.css`
|
||
- Inspect: `wp-content/plugins/projects-portfolio/` (installed)
|
||
|
||
**Goal:** Confirm the local site at `http://portfolio.test` is reachable, the existing Playwright suite runs, and capture a baseline of each mockup reference page so we can diff later.
|
||
|
||
- [ ] **Step 1: Confirm base URL and Playwright config**
|
||
|
||
Open `playwright.config.js`. Confirm chromium is enabled and `baseURL` is set to `http://portfolio.test`. If baseURL is commented out, set:
|
||
```js
|
||
use: {
|
||
baseURL: 'http://portfolio.test',
|
||
trace: 'on-first-retry',
|
||
ignoreHTTPSErrors: true,
|
||
},
|
||
```
|
||
|
||
- [ ] **Step 2: Confirm Playwright + axe dependencies are installed**
|
||
|
||
Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npx playwright --version`
|
||
Expected: prints a version (e.g. `Version 1.52.x`).
|
||
|
||
Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm ls @axe-core/playwright @playwright/test --depth=0`
|
||
Expected: both packages installed.
|
||
|
||
- [ ] **Step 3: Confirm the projects-portfolio plugin is present**
|
||
|
||
Run (PowerShell): `Get-ChildItem "C:\Users\ksolo\Herd\portfolio\wp-content\plugins\projects-portfolio" | Select-Object Name`
|
||
Expected: shows `projects-portfolio.php`, `README.md`, `includes/`, `templates/`, etc.
|
||
|
||
- [ ] **Step 4: Probe local site reachability with a 3-second timeout**
|
||
|
||
Run (PowerShell):
|
||
```powershell
|
||
try { (Invoke-WebRequest -Uri "http://portfolio.test" -UseBasicParsing -TimeoutSec 3).StatusCode } catch { "unreachable: $($_.Exception.Message)" }
|
||
```
|
||
Expected: prints `200`. If `unreachable: ...`, stop and ask the user — we cannot run browser tests without the local site.
|
||
|
||
- [ ] **Step 5: Run the existing Playwright suite as baseline**
|
||
|
||
Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npx playwright test tests/site-a11y.spec.js --reporter=list`
|
||
Expected: may fail (axe violations expected on current light theme) — that's fine, we capture baseline.
|
||
|
||
- [ ] **Step 6: Commit no changes (this task is verification only)**
|
||
|
||
If you modified `playwright.config.js`, run:
|
||
```bash
|
||
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
|
||
git add playwright.config.js
|
||
git commit -m "chore: set Playwright baseURL to portfolio.test"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 2: Define `--font-headings` + dark body surface
|
||
|
||
**Files:**
|
||
- Modify: `styles/base/typography.css` (add `--font-headings`)
|
||
- Modify: `styles/base/global.css` (dark body, remove light main rule)
|
||
|
||
**Goal:** Resolve the unresolved token from `notes/DESIGN.md` §12 and flip the body surface to Carbon Blue dark.
|
||
|
||
- [ ] **Step 1: Add `--font-headings` to typography.css**
|
||
|
||
In `styles/base/typography.css`, inside the `@theme { ... }` block (after `--font-sans`), add:
|
||
```css
|
||
--font-headings: var(--font-sans);
|
||
```
|
||
|
||
- [ ] **Step 2: Darken body + remove light main rule in global.css**
|
||
|
||
In `styles/base/global.css`, find the existing rule:
|
||
```css
|
||
main#maincontent {
|
||
background-color: var(--color-background);
|
||
color: var(--color-text);
|
||
margin: 0;
|
||
padding: 0 0 4rem;
|
||
}
|
||
```
|
||
|
||
Replace with:
|
||
```css
|
||
main#maincontent {
|
||
background-color: transparent;
|
||
color: var(--color-on-surface, #e3e1e9);
|
||
margin: 0;
|
||
padding: 0 0 4rem;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: Set dark body in typography.css**
|
||
|
||
In `styles/base/typography.css`, find the existing `body { ... }` block and replace with:
|
||
```css
|
||
body {
|
||
background-color: #100e0b;
|
||
color: var(--color-on-surface, #e3e1e9);
|
||
font-family: var(--font-sans);
|
||
font-size: var(--text-base);
|
||
line-height: var(--line-height);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: Build the CSS bundle**
|
||
|
||
Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build`
|
||
Expected: `static/dist/theme.css` rebuilt without errors.
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
|
||
git add styles/base/typography.css styles/base/global.css static/dist/theme.css
|
||
git commit -m "feat(theme): add --font-headings; flip body to Carbon Blue dark"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 3: Add Carbon Blue tokens to colors.css + theme.json
|
||
|
||
**Files:**
|
||
- Modify: `styles/base/colors.css` (add Carbon Blue + Material-derived tokens)
|
||
- Modify: `theme.json` (add dark palette; fix `styles` preset references)
|
||
|
||
**Goal:** Expose Carbon Blue / dark-surface tokens through both CSS custom properties and the WordPress block-editor palette. Fix the `background` / `text` preset reference mismatch from `notes/DESIGN.md` §12.
|
||
|
||
- [ ] **Step 1: Append Carbon Blue tokens to colors.css**
|
||
|
||
Open `styles/base/colors.css`. Inside the `@theme { ... }` block, append:
|
||
```css
|
||
/* Carbon Blue / dark surface tokens (Portfolio 2026) */
|
||
--color-surface: #121318;
|
||
--color-surface-dim: #121318;
|
||
--color-surface-bright: #38393f;
|
||
--color-surface-container-lowest: #0d0e13;
|
||
--color-surface-container-low: #1a1b21;
|
||
--color-surface-container: #1e1f25;
|
||
--color-surface-container-high: #292a2f;
|
||
--color-surface-container-highest:#34343a;
|
||
--color-on-surface: #e3e1e9;
|
||
--color-on-surface-variant:#c5c5d3;
|
||
--color-outline: #8f909d;
|
||
--color-outline-variant: #444651;
|
||
--color-page-base: #100e0b;
|
||
--color-backlight: rgba(96, 165, 250, 0.20);
|
||
|
||
--color-primary-fixed: #dce1ff;
|
||
--color-primary-fixed-dim: #b6c4ff;
|
||
--color-on-primary-fixed: #00164e;
|
||
--color-on-primary-fixed-variant: #264191;
|
||
|
||
--color-success: #5ccf91;
|
||
--color-warning: #f4e04d;
|
||
--color-danger: #cc3b3b;
|
||
--color-info: #3d7ab0;
|
||
```
|
||
|
||
(If `--color-success` / `--color-warning` / `--color-danger` / `--color-info` already exist in oklch, REMOVE the existing lines first so the new hex values are the only definitions.)
|
||
|
||
- [ ] **Step 2: Update theme.json palette**
|
||
|
||
Open `theme.json`. In the `settings.color.palette` array, ADD these entries (don't remove existing ones):
|
||
```json
|
||
{
|
||
"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"
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: Fix theme.json styles preset references**
|
||
|
||
In `theme.json`, find the top-level `styles` block:
|
||
```json
|
||
"styles": {
|
||
"color": {
|
||
"background": "var(--wp--preset--color--background)",
|
||
"text": "var(--wp--preset--color--text)"
|
||
},
|
||
...
|
||
}
|
||
```
|
||
|
||
Replace with:
|
||
```json
|
||
"styles": {
|
||
"color": {
|
||
"background": "var(--wp--preset--color--theme-bg, #100e0b)",
|
||
"text": "var(--wp--preset--color--theme-text, #e3e1e9)"
|
||
},
|
||
"elements": {
|
||
"link": {
|
||
"color": {
|
||
"text": "var(--wp--preset--color--theme-bodylinks)"
|
||
}
|
||
}
|
||
},
|
||
"typography": {
|
||
"fontSize": "var(--wp--preset--font-size--base)",
|
||
"fontFamily": "var(--wp--preset--font-family--theme-sans)",
|
||
"lineHeight": "1.5"
|
||
}
|
||
}
|
||
```
|
||
|
||
(The `elements.link` is moved up here so it actually applies; previously it was nested under `styles` rather than `styles.elements`, which makes WordPress ignore it.)
|
||
|
||
- [ ] **Step 4: Build the CSS bundle**
|
||
|
||
Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build`
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
|
||
git add styles/base/colors.css theme.json static/dist/theme.css
|
||
git commit -m "feat(tokens): add Carbon Blue dark-surface tokens; fix theme.json presets"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 4: Carbon Blue atmospheric partial
|
||
|
||
**Files:**
|
||
- Create: `views/partials/aura-bg.php`
|
||
|
||
**Goal:** Provide a reusable partial that renders the Carbon Blue atmospheric field per `notes/Background Gradient.md`. Used by `front-page.php` and `archive-projects.php` only.
|
||
|
||
- [ ] **Step 1: Write the partial**
|
||
|
||
Create `views/partials/aura-bg.php`:
|
||
```php
|
||
<?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;
|
||
|
||
$blur = isset( $blur ) && 'desktop' === $blur ? 'aura-layer-2--desktop' : 'aura-layer-2--mobile';
|
||
?>
|
||
<div class="aura-bg" aria-hidden="true">
|
||
<div class="aura-layer-1"></div>
|
||
<div class="aura-layer-2 <?php echo esc_attr( $blur ); ?>"></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>
|
||
```
|
||
|
||
- [ ] **Step 2: Add CSS for the partial**
|
||
|
||
Append to `styles/base/global.css` (or create `styles/components/aura.css` and `@import` it from `styles/theme.css`):
|
||
```css
|
||
/* Carbon Blue atmospheric field (decorative — content must sit in z-index: 1) */
|
||
.aura-bg {
|
||
position: relative;
|
||
overflow: hidden;
|
||
min-height: 100vh;
|
||
isolation: isolate;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.aura-layer-2--mobile { filter: blur(105px); }
|
||
.aura-layer-2--desktop { 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%; }
|
||
```
|
||
|
||
- [ ] **Step 3: Build the CSS bundle**
|
||
|
||
Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build`
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
|
||
git add views/partials/aura-bg.php styles/base/global.css static/dist/theme.css
|
||
git commit -m "feat(aura): add Carbon Blue atmospheric background partial"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 5: PHP namespace swap — header.php, footer.php, all lib/*.php
|
||
|
||
**Files:**
|
||
- Modify: `functions.php`
|
||
- Modify: `header.php`
|
||
- Modify: `footer.php`
|
||
- Modify: `lib/activation.php`
|
||
- Modify: `lib/class-acf.php`
|
||
- Modify: `lib/class-breadcrumbs.php`
|
||
- Modify: `lib/class-enqueue.php`
|
||
- Modify: `lib/class-menuitems.php`
|
||
- Modify: `lib/class-resources.php`
|
||
- Modify: `lib/extras.php`
|
||
- Modify: `lib/helpers.php`
|
||
- Modify: `lib/hooks.php`
|
||
- Modify: `lib/search-features.php`
|
||
- Modify: `lib/show-template.php`
|
||
|
||
**Goal:** Replace `namespace SoloFrameEvo;` with `namespace KsPortfolio;` and update internal `__NAMESPACE__ . '\\…'` references.
|
||
|
||
- [ ] **Step 1: Replace namespace declaration in each PHP file**
|
||
|
||
For each file listed above, find `namespace SoloFrameEvo;` and replace with `namespace KsPortfolio;`.
|
||
|
||
- [ ] **Step 2: Replace internal namespace references in header.php**
|
||
|
||
In `header.php`, the only reference is `namespace SoloFrameEvo;` (already handled in step 1). Verify no `__NAMESPACE__ . '\\…'` calls remain.
|
||
|
||
- [ ] **Step 3: Replace internal namespace references in lib/class-breadcrumbs.php**
|
||
|
||
In `lib/class-breadcrumbs.php`, find every `__NAMESPACE__ . '\\…'` and replace `'\\KsPortfolio\\…'` (literal). Example: `__NAMESPACE__ . '\\Breadcrumbs'` becomes `'\\KsPortfolio\\Breadcrumbs'`.
|
||
|
||
- [ ] **Step 4: Replace internal namespace references in lib/hooks.php**
|
||
|
||
In `lib/hooks.php`, find every `__NAMESPACE__ . '\\…'` and replace with `'\\KsPortfolio\\…'`.
|
||
|
||
- [ ] **Step 5: Update functions.php to include the new project helper**
|
||
|
||
In `functions.php`, after the existing `foreach ( glob( __DIR__ . '/lib/*.php' ) ... )`, add the project helper include (we will create the file in Task 6 — guard with `file_exists` for now):
|
||
```php
|
||
// 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;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 6: Run PHPCS**
|
||
|
||
Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && composer run lint 2>&1 | tail -40`
|
||
Expected: lists current violations (we expect namespace-related errors fixed; other violations are expected at this stage).
|
||
|
||
- [ ] **Step 7: Commit**
|
||
|
||
```bash
|
||
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
|
||
git add functions.php header.php footer.php lib/
|
||
git commit -m "refactor: rename namespace SoloFrameEvo to KsPortfolio"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 6: `lib/class-projects.php` — provider-agnostic helper
|
||
|
||
**Files:**
|
||
- Create: `lib/class-projects.php`
|
||
|
||
**Goal:** Wrap the `projects-portfolio` plugin's helpers so templates never branch on provider. Provide safe defaults when the plugin is missing or repo data is unavailable.
|
||
|
||
- [ ] **Step 1: Write the helper**
|
||
|
||
Create `lib/class-projects.php`:
|
||
```php
|
||
<?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;
|
||
|
||
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 );
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run PHPCS**
|
||
|
||
Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && composer run lint 2>&1 | tail -30`
|
||
Expected: no new violations introduced by this file.
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
|
||
git add lib/class-projects.php
|
||
git commit -m "feat(projects): add provider-agnostic helper for templates"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 7: Reusable project-card partial
|
||
|
||
**Files:**
|
||
- Create: `views/components/project-card.php`
|
||
|
||
**Goal:** One shared card used by home and archive templates. Provider-agnostic. Empty-state safe.
|
||
|
||
- [ ] **Step 1: Write the partial**
|
||
|
||
Create `views/components/project-card.php`:
|
||
```php
|
||
<?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;
|
||
}
|
||
|
||
$post_id = (int) $post->ID;
|
||
$type_label = '';
|
||
$type_terms = get_the_terms( $post_id, 'project-type' );
|
||
if ( ! empty( $type_terms ) && ! is_wp_error( $type_terms ) ) {
|
||
$type_label = strtoupper( $type_terms[0]->name );
|
||
}
|
||
$release_url = Projects::get_release_url( $post_id );
|
||
$repo_url = Projects::get_repo_browse_url( $post_id );
|
||
$repo_data = Projects::get_repo_data( $post_id );
|
||
$language = is_array( $repo_data ) && ! empty( $repo_data['language'] ) ? (string) $repo_data['language'] : '';
|
||
$stars = is_array( $repo_data ) && isset( $repo_data['stargazers_count'] ) ? (int) $repo_data['stargazers_count'] : 0;
|
||
$excerpt = has_excerpt( $post_id ) ? get_the_excerpt( $post_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) $post_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( Projects::get_provider_label( $post_id ) ); ?>
|
||
</span>
|
||
</header>
|
||
|
||
<h3 id="project-<?php echo esc_attr( (string) $post_id ); ?>-title" class="project-card__title">
|
||
<a class="project-card__title-link" href="<?php echo esc_url( get_permalink( $post_id ) ); ?>">
|
||
<?php echo esc_html( get_the_title( $post_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 ( $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 endif; ?>
|
||
|
||
<div class="project-card__actions">
|
||
<?php if ( $release_url ) : ?>
|
||
<a class="project-card__action project-card__action--primary" href="<?php echo esc_url( Projects::get_download_url( $post_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; ?>
|
||
</div>
|
||
</footer>
|
||
</article>
|
||
```
|
||
|
||
- [ ] **Step 2: Add CSS for the card**
|
||
|
||
Append to `styles/components/project-card.css` (create new file) and import it from `styles/theme.css`:
|
||
```css
|
||
/* 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: rgba(182, 196, 255, 0.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;
|
||
-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;
|
||
}
|
||
|
||
.project-card__action--primary {
|
||
background: var(--color-primary, #b6c4ff);
|
||
color: var(--color-on-primary, #05297a);
|
||
border-color: var(--color-primary, #b6c4ff);
|
||
}
|
||
|
||
.project-card__action--primary:hover { background: #dce1ff; border-color: #dce1ff; }
|
||
|
||
.project-card__action--secondary {
|
||
color: var(--color-on-surface, #e3e1e9);
|
||
border-color: var(--color-outline, #8f909d);
|
||
}
|
||
|
||
.project-card__action--secondary:hover { border-color: var(--color-primary, #b6c4ff); color: var(--color-primary, #b6c4ff); }
|
||
|
||
.project-card__action:focus-visible {
|
||
outline: 2px solid var(--color-primary, #b6c4ff);
|
||
outline-offset: 3px;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: Register the import**
|
||
|
||
In `styles/theme.css`, after `@import "./blocks/index.css";`, add:
|
||
```css
|
||
@import "./components/project-card.css";
|
||
```
|
||
|
||
(If a `styles/components/index.css` already exists, append `@import "./project-card.css";` to it instead and import the index from theme.css.)
|
||
|
||
- [ ] **Step 4: Build the CSS bundle**
|
||
|
||
Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build`
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
|
||
git add views/components/project-card.php styles/components/project-card.css styles/theme.css static/dist/theme.css
|
||
git commit -m "feat(projects): add reusable project-card component + styles"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 8: Project stat-card partial
|
||
|
||
**Files:**
|
||
- Create: `views/components/project-stat-card.php`
|
||
|
||
**Goal:** A single stat tile (Downloads / Stars / Forks / Issues) used by the sidebar in `single-projects.php`.
|
||
|
||
- [ ] **Step 1: Write the partial**
|
||
|
||
Create `views/components/project-stat-card.php`:
|
||
```php
|
||
<?php
|
||
/**
|
||
* Project Stat Card Component
|
||
*
|
||
* Renders one stat tile. Expects $label, $value, and $icon (optional SVG
|
||
* markup) in scope. $icon MUST be aria-hidden decorative markup; the label
|
||
* is the accessible name.
|
||
*
|
||
* @package KsPortfolio
|
||
*/
|
||
|
||
namespace KsPortfolio;
|
||
|
||
if ( ! isset( $label ) || ! isset( $value ) ) {
|
||
return;
|
||
}
|
||
$stat_label = (string) $label;
|
||
$stat_value = (string) $value;
|
||
?>
|
||
<div class="stat-card" role="group" aria-label="<?php echo esc_attr( $stat_label ); ?>">
|
||
<?php if ( isset( $icon ) && $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>
|
||
```
|
||
|
||
- [ ] **Step 2: Add CSS for the stat card**
|
||
|
||
Append to `styles/components/project-card.css` (or create `styles/components/stat-card.css` and import it):
|
||
```css
|
||
.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);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: Build the CSS bundle**
|
||
|
||
Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build`
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
|
||
git add views/components/project-stat-card.php styles/components/project-card.css static/dist/theme.css
|
||
git commit -m "feat(projects): add stat-card partial"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 9: Project meta row partial
|
||
|
||
**Files:**
|
||
- Create: `views/components/project-meta-row.php`
|
||
|
||
**Goal:** One row in the Repository Details table (Version, Last Updated, License, Language). Provider-agnostic.
|
||
|
||
- [ ] **Step 1: Write the partial**
|
||
|
||
Create `views/components/project-meta-row.php`:
|
||
```php
|
||
<?php
|
||
/**
|
||
* Project Meta Row Component
|
||
*
|
||
* Renders one row of the Repository Details table on the project detail
|
||
* page. Expects $label, $value, and $icon (optional decorative SVG) in
|
||
* scope.
|
||
*
|
||
* @package KsPortfolio
|
||
*/
|
||
|
||
namespace KsPortfolio;
|
||
|
||
if ( ! isset( $label ) || ! isset( $value ) ) {
|
||
return;
|
||
}
|
||
$row_label = (string) $label;
|
||
$row_value = (string) $value;
|
||
?>
|
||
<div class="meta-row">
|
||
<span class="meta-row__label">
|
||
<?php if ( isset( $icon ) && $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>
|
||
```
|
||
|
||
- [ ] **Step 2: Add CSS for the meta row**
|
||
|
||
Append to `styles/components/project-card.css`:
|
||
```css
|
||
.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); }
|
||
```
|
||
|
||
- [ ] **Step 3: Build the CSS bundle**
|
||
|
||
Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build`
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
|
||
git add views/components/project-meta-row.php styles/components/project-card.css static/dist/theme.css
|
||
git commit -m "feat(projects): add project meta-row component"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 10: `archive-projects.php` — project archive template
|
||
|
||
**Files:**
|
||
- Create: `archive-projects.php`
|
||
|
||
**Goal:** Render the project archive to match `notes/archive-screen.png` — hero + grid of project cards + pagination.
|
||
|
||
- [ ] **Step 1: Write the template**
|
||
|
||
Create `archive-projects.php`:
|
||
```php
|
||
<?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 aura-bg">
|
||
<div class="aura-bg__content">
|
||
<div class="container mx-auto my-section">
|
||
<header class="archive-projects__head">
|
||
<h1 class="archive-projects__title"><?php echo esc_html__( 'Project Archive', 'ks-portfolio' ); ?></h1>
|
||
<p class="archive-projects__intro"><?php echo esc_html( $hero_intro ); ?></p>
|
||
</header>
|
||
|
||
<?php if ( have_posts() ) : ?>
|
||
<div class="archive-projects__grid grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||
<?php
|
||
while ( have_posts() ) :
|
||
the_post();
|
||
get_template_part( 'views/components/project-card' );
|
||
endwhile;
|
||
?>
|
||
</div>
|
||
|
||
<div class="archive-projects__pagination">
|
||
<?php
|
||
the_posts_pagination(
|
||
array(
|
||
'mid_size' => 2,
|
||
'prev_text' => '« ' . esc_html__( 'Previous', 'ks-portfolio' ),
|
||
'next_text' => esc_html__( 'Next', 'ks-portfolio' ) . ' »',
|
||
)
|
||
);
|
||
?>
|
||
</div>
|
||
<?php else : ?>
|
||
<div class="archive-projects__empty">
|
||
<h2 class="archive-projects__empty-title"><?php echo esc_html__( 'Nothing here yet…', 'ks-portfolio' ); ?></h2>
|
||
<p class="archive-projects__empty-desc"><?php echo esc_html__( 'No published projects found.', 'ks-portfolio' ); ?></p>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<?php
|
||
get_template_part( 'views/partials/aura-bg', null, array( 'blur' => 'desktop' ) );
|
||
get_footer();
|
||
```
|
||
|
||
- [ ] **Step 2: Add hero + grid styles**
|
||
|
||
Append to `styles/components/project-card.css`:
|
||
```css
|
||
.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;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: Build the CSS bundle**
|
||
|
||
Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build`
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
|
||
git add archive-projects.php styles/components/project-card.css static/dist/theme.css
|
||
git commit -m "feat(projects): add archive-projects template"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 11: `taxonomy-project-type.php` — taxonomy archive
|
||
|
||
**Files:**
|
||
- Create: `taxonomy-project-type.php`
|
||
|
||
**Goal:** Same shell as archive-projects but with the taxonomy label as the title.
|
||
|
||
- [ ] **Step 1: Write the template**
|
||
|
||
Create `taxonomy-project-type.php`:
|
||
```php
|
||
<?php
|
||
/**
|
||
* Project Type Taxonomy Archive Template
|
||
*
|
||
* @package KsPortfolio
|
||
*/
|
||
|
||
namespace KsPortfolio;
|
||
|
||
get_header();
|
||
|
||
$queried = get_queried_object();
|
||
$title = $queried && ! is_wp_error( $queried ) ? $queried->name : __( 'Projects', 'ks-portfolio' );
|
||
?>
|
||
<section class="archive-projects aura-bg">
|
||
<div class="aura-bg__content">
|
||
<div class="container mx-auto my-section">
|
||
<header class="archive-projects__head">
|
||
<h1 class="archive-projects__title"><?php echo esc_html( $title ); ?></h1>
|
||
<p class="archive-projects__intro">
|
||
<a class="archive-projects__back-link" href="<?php echo esc_url( get_post_type_archive_link( 'projects' ) ); ?>">
|
||
<?php echo esc_html__( '← All Projects', 'ks-portfolio' ); ?>
|
||
</a>
|
||
</p>
|
||
</header>
|
||
|
||
<?php if ( have_posts() ) : ?>
|
||
<div class="archive-projects__grid grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||
<?php
|
||
while ( have_posts() ) :
|
||
the_post();
|
||
get_template_part( 'views/components/project-card' );
|
||
endwhile;
|
||
?>
|
||
</div>
|
||
|
||
<div class="archive-projects__pagination">
|
||
<?php
|
||
the_posts_pagination(
|
||
array(
|
||
'mid_size' => 2,
|
||
'prev_text' => '« ' . esc_html__( 'Previous', 'ks-portfolio' ),
|
||
'next_text' => esc_html__( 'Next', 'ks-portfolio' ) . ' »',
|
||
)
|
||
);
|
||
?>
|
||
</div>
|
||
<?php else : ?>
|
||
<div class="archive-projects__empty">
|
||
<h2 class="archive-projects__empty-title"><?php echo esc_html__( 'Nothing here yet…', 'ks-portfolio' ); ?></h2>
|
||
<p class="archive-projects__empty-desc"><?php echo esc_html__( 'No published projects found in this category.', 'ks-portfolio' ); ?></p>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<?php
|
||
get_template_part( 'views/partials/aura-bg', null, array( 'blur' => 'desktop' ) );
|
||
get_footer();
|
||
```
|
||
|
||
- [ ] **Step 2: Add back-link style**
|
||
|
||
Append to `styles/components/project-card.css`:
|
||
```css
|
||
.archive-projects__back-link {
|
||
color: var(--color-primary, #b6c4ff);
|
||
text-decoration: none;
|
||
font-weight: 600;
|
||
}
|
||
.archive-projects__back-link:hover { text-decoration: underline; }
|
||
```
|
||
|
||
- [ ] **Step 3: Build + commit**
|
||
|
||
Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build`
|
||
|
||
```bash
|
||
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
|
||
git add taxonomy-project-type.php styles/components/project-card.css static/dist/theme.css
|
||
git commit -m "feat(projects): add taxonomy-project-type template"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 12: `single-projects.php` — project detail
|
||
|
||
**Files:**
|
||
- Create: `single-projects.php`
|
||
|
||
**Goal:** Render the project detail page to match `notes/single-screen.png` — hero with provider + title + version, Download/View Repo CTAs, two-column body, owner, repository details, stat cards.
|
||
|
||
- [ ] **Step 1: Write the template**
|
||
|
||
Create `single-projects.php`:
|
||
```php
|
||
<?php
|
||
/**
|
||
* Single Project Template
|
||
*
|
||
* @package KsPortfolio
|
||
*/
|
||
|
||
namespace KsPortfolio;
|
||
|
||
get_header();
|
||
|
||
while ( have_posts() ) :
|
||
the_post();
|
||
$post_id = (int) get_the_ID();
|
||
$provider = Projects::get_provider_label( $post_id );
|
||
$version = Projects::get_latest_version( $post_id );
|
||
$release = Projects::get_release_url( $post_id );
|
||
$repo = Projects::get_repo_browse_url( $post_id );
|
||
$repo_data = Projects::get_repo_data( $post_id );
|
||
$owner = Projects::get_owner_data( $post_id );
|
||
$type_terms = get_the_terms( $post_id, 'project-type' );
|
||
$type_label = ! empty( $type_terms ) && ! is_wp_error( $type_terms ) ? strtoupper( $type_terms[0]->name ) : '';
|
||
$excerpt = has_excerpt( $post_id ) ? get_the_excerpt( $post_id ) : '';
|
||
?>
|
||
<article class="single-project aura-bg">
|
||
<div class="aura-bg__content">
|
||
<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( $post_id ) ); ?>
|
||
<?php if ( '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 ( $release ) : ?>
|
||
<a class="button button--primary" href="<?php echo esc_url( Projects::get_download_url( $post_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; ?>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="single-project__layout">
|
||
<div class="single-project__body">
|
||
<?php if ( has_post_thumbnail( $post_id ) ) : ?>
|
||
<figure class="single-project__featured">
|
||
<?php echo get_the_post_thumbnail( $post_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__( 'Repository details', 'ks-portfolio' ); ?>">
|
||
<?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'] ), current_time( 'timestamp' ) ) . ' ' . __( '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(
|
||
array( 'label' => __( 'Version', 'ks-portfolio' ), 'value' => 'Unknown' !== $version ? 'v' . $version : __( 'Unknown', 'ks-portfolio' ) ),
|
||
array( 'label' => __( 'Last Updated', 'ks-portfolio' ), 'value' => $updated_at ),
|
||
array( 'label' => __( 'License', 'ks-portfolio' ), 'value' => $license ),
|
||
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(
|
||
array( 'label' => __( 'Downloads', 'ks-portfolio' ), 'value' => Projects::format_stat( get_post_meta( $post_id, '_projects_portfolio_download_count', true ) ) ),
|
||
array( 'label' => __( 'Stars', 'ks-portfolio' ), 'value' => Projects::format_stat( is_array( $repo_data ) ? ( $repo_data['stargazers_count'] ?? 0 ) : 0 ) ),
|
||
array( 'label' => __( 'Forks', 'ks-portfolio' ), 'value' => Projects::format_stat( is_array( $repo_data ) ? ( $repo_data['forks_count'] ?? 0 ) : 0 ) ),
|
||
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>
|
||
</aside>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
<?php
|
||
endwhile;
|
||
|
||
get_template_part( 'views/partials/aura-bg', null, array( 'blur' => 'desktop' ) );
|
||
get_footer();
|
||
```
|
||
|
||
- [ ] **Step 2: Add single-project CSS**
|
||
|
||
Append to `styles/components/project-card.css`:
|
||
```css
|
||
.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;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: Build + commit**
|
||
|
||
Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build`
|
||
|
||
```bash
|
||
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
|
||
git add single-projects.php styles/components/project-card.css static/dist/theme.css
|
||
git commit -m "feat(projects): add single-projects detail template"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 13: `front-page.php` — full home template
|
||
|
||
**Files:**
|
||
- Modify: `front-page.php`
|
||
|
||
**Goal:** Replace the 8-line stub with a home page matching `notes/home-screen.png`: Carbon Blue hero, Recent Projects grid, footer.
|
||
|
||
- [ ] **Step 1: Write the template**
|
||
|
||
Replace `front-page.php` with:
|
||
```php
|
||
<?php
|
||
/**
|
||
* Front Page Template — Portfolio 2026 home
|
||
*
|
||
* @package KsPortfolio
|
||
*/
|
||
|
||
namespace KsPortfolio;
|
||
|
||
get_header();
|
||
|
||
$hero_heading = get_field( 'homepage_hero_heading' );
|
||
$hero_intro = get_field( 'homepage_hero_intro' );
|
||
$hero_cta_1 = get_field( 'homepage_hero_cta_1' ); // array( url, title, target )
|
||
$hero_cta_2 = get_field( 'homepage_hero_cta_2' );
|
||
?>
|
||
<section class="home-hero aura-bg">
|
||
<div class="aura-bg__content">
|
||
<div class="container mx-auto my-section">
|
||
<div class="home-hero__inner">
|
||
<?php if ( $hero_heading ) : ?>
|
||
<h1 class="home-hero__heading"><?php echo esc_html( $hero_heading ); ?></h1>
|
||
<?php endif; ?>
|
||
<?php if ( $hero_intro ) : ?>
|
||
<p class="home-hero__intro"><?php echo esc_html( $hero_intro ); ?></p>
|
||
<?php endif; ?>
|
||
|
||
<div class="home-hero__actions">
|
||
<?php if ( ! empty( $hero_cta_1['url'] ) && ! empty( $hero_cta_1['title'] ) ) : ?>
|
||
<a class="button button--primary" href="<?php echo esc_url( $hero_cta_1['url'] ); ?>"<?php echo ! empty( $hero_cta_1['target'] ) ? ' target="_blank" rel="noopener noreferrer"' : ''; ?>>
|
||
<?php echo esc_html( $hero_cta_1['title'] ); ?>
|
||
</a>
|
||
<?php endif; ?>
|
||
<?php if ( ! empty( $hero_cta_2['url'] ) && ! empty( $hero_cta_2['title'] ) ) : ?>
|
||
<a class="button button--outline" href="<?php echo esc_url( $hero_cta_2['url'] ); ?>"<?php echo ! empty( $hero_cta_2['target'] ) ? ' target="_blank" rel="noopener noreferrer"' : ''; ?>>
|
||
<?php echo esc_html( $hero_cta_2['title'] ); ?>
|
||
</a>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="home-recent container mx-auto my-section">
|
||
<header class="home-recent__head">
|
||
<h2 class="home-recent__title"><?php echo esc_html__( 'Recent Projects', 'ks-portfolio' ); ?></h2>
|
||
<a class="home-recent__view-all" href="<?php echo esc_url( get_post_type_archive_link( 'projects' ) ); ?>">
|
||
<?php echo esc_html__( 'View All', 'ks-portfolio' ); ?> →
|
||
</a>
|
||
</header>
|
||
<hr class="home-recent__divider" />
|
||
|
||
<?php
|
||
$recent = new \WP_Query(
|
||
array(
|
||
'post_type' => 'projects',
|
||
'posts_per_page' => 3,
|
||
'no_found_rows' => true,
|
||
)
|
||
);
|
||
|
||
if ( $recent->have_posts() ) : ?>
|
||
<div class="home-recent__grid grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||
<?php
|
||
while ( $recent->have_posts() ) :
|
||
$recent->the_post();
|
||
get_template_part( 'views/components/project-card' );
|
||
endwhile;
|
||
wp_reset_postdata();
|
||
?>
|
||
</div>
|
||
<?php else : ?>
|
||
<div class="home-recent__empty">
|
||
<h3><?php echo esc_html__( 'Nothing here yet…', 'ks-portfolio' ); ?></h3>
|
||
<p><?php echo esc_html__( 'No published projects found.', 'ks-portfolio' ); ?></p>
|
||
</div>
|
||
<?php endif; ?>
|
||
</section>
|
||
|
||
<?php
|
||
get_template_part( 'views/partials/aura-bg', null, array( 'blur' => 'desktop' ) );
|
||
get_footer();
|
||
```
|
||
|
||
- [ ] **Step 2: Add home-hero + home-recent CSS**
|
||
|
||
Append to `styles/components/project-card.css`:
|
||
```css
|
||
.home-hero { min-height: 60vh; display: flex; align-items: center; padding: 4rem 0; }
|
||
.home-hero__inner { max-width: 50rem; }
|
||
.home-hero__heading {
|
||
font-family: var(--font-sans);
|
||
font-weight: 700;
|
||
font-size: clamp(2rem, 4vw, 3.5rem);
|
||
line-height: 1.1;
|
||
color: var(--color-on-surface, #e3e1e9);
|
||
margin: 0 0 1rem;
|
||
}
|
||
.home-hero__intro {
|
||
color: var(--color-on-surface-variant, #c5c5d3);
|
||
font-size: clamp(1rem, 1.5vw, 1.25rem);
|
||
line-height: 1.6;
|
||
max-width: 50ch;
|
||
margin: 0 0 1.5rem;
|
||
}
|
||
.home-hero__actions { display: flex; flex-wrap: wrap; 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, #b6c4ff);
|
||
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); }
|
||
```
|
||
|
||
- [ ] **Step 3: Build + commit**
|
||
|
||
Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build`
|
||
|
||
```bash
|
||
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
|
||
git add front-page.php styles/components/project-card.css static/dist/theme.css
|
||
git commit -m "feat(home): rebuild front-page template to match Carbon Blue mockup"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 14: Header — three-region responsive layout + dark nav
|
||
|
||
**Files:**
|
||
- Modify: `header.php`
|
||
- Modify: `styles/navigation/nav-aux.css`
|
||
- Modify: `styles/navigation/nav-main-default.css`
|
||
|
||
**Goal:** Switch from `grid-cols-[83px_auto]` to a three-region header (logo | nav | aux button), recolor nav to Carbon Blue.
|
||
|
||
- [ ] **Step 1: Update header.php markup**
|
||
|
||
Replace the existing `.header__nav-main` block in `header.php` with:
|
||
```php
|
||
<div class="header__nav-main container py-4 items-center grid gap-x-8 gap-y-0 grid-cols-[auto_1fr_auto] grid-rows-[1fr] justify-between">
|
||
<a href="<?php echo esc_url( home_url( '/' ) ); ?>" class="site-header__logo block">
|
||
<img class="site-header__logo-img" src="<?php echo esc_url( $headerLogo ); ?>" alt="<?php echo esc_attr( get_bloginfo( 'name' ) ); ?> logo" />
|
||
</a>
|
||
|
||
<?php get_template_part( 'views/components/nav-main' ); ?>
|
||
|
||
<div class="site-header__aux">
|
||
<a class="button button--outline" href="<?php echo esc_url( home_url( '/cv/' ) ); ?>">
|
||
<?php echo esc_html__( 'Download CV', 'ks-portfolio' ); ?>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
```
|
||
|
||
- [ ] **Step 2: Recolor nav for dark surface**
|
||
|
||
Append to `styles/navigation/nav-main-default.css`:
|
||
```css
|
||
.nav-main a,
|
||
.nav-aux a {
|
||
color: var(--color-on-surface, #e3e1e9);
|
||
}
|
||
|
||
.nav-main a:hover,
|
||
.nav-aux a:hover {
|
||
color: var(--color-primary, #b6c4ff);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: Build + commit**
|
||
|
||
Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build`
|
||
|
||
```bash
|
||
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
|
||
git add header.php styles/navigation/ static/dist/theme.css
|
||
git commit -m "feat(header): three-region layout + dark nav colors"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 15: Footer — Carbon Blue surface
|
||
|
||
**Files:**
|
||
- Modify: `footer.php`
|
||
|
||
**Goal:** Recolor footer links, swap "Footer Area 1/2/3" headings for widget titles, and update copyright to match Carbon Blue palette.
|
||
|
||
- [ ] **Step 1: Update footer.php**
|
||
|
||
Find the existing `bg-gray-800 text-white` block on `<footer>` and replace with:
|
||
```php
|
||
<footer role="contentinfo" class="site-footer">
|
||
<div class="container mx-auto py-12">
|
||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||
<div class="site-footer__brand max-w-[40ch]">
|
||
<h2 class="site-footer__title">
|
||
<a href="<?php echo esc_url( home_url( '/' ) ); ?>">
|
||
<?php if ( $footerLogo ) { ?>
|
||
<img src="<?php echo esc_url( $footerLogo ); ?>" alt="<?php echo esc_attr( get_bloginfo( 'name' ) ); ?>" />
|
||
<?php } else {
|
||
echo esc_html( get_bloginfo( 'name' ) );
|
||
} ?>
|
||
</a>
|
||
</h2>
|
||
<?php if ( $footerDesc ) : ?>
|
||
<p class="site-footer__desc"><?php echo wp_kses_post( $footerDesc ); ?></p>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<?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; ?>
|
||
|
||
<?php if ( $footerNav ) : ?>
|
||
<nav class="site-footer__nav" aria-label="<?php echo esc_attr__( 'Footer navigation', 'ks-portfolio' ); ?>">
|
||
<h3><?php echo esc_html__( 'Navigation', 'ks-portfolio' ); ?></h3>
|
||
<ul>
|
||
<?php foreach ( $footerNav as $item ) : ?>
|
||
<li>
|
||
<a href="<?php echo esc_url( $item->url ); ?>">
|
||
<?php echo esc_html( $item->title ); ?>
|
||
</a>
|
||
</li>
|
||
<?php endforeach; ?>
|
||
</ul>
|
||
</nav>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="site-footer__copyright">
|
||
<?php echo wp_kses_post( $copyright ); ?>
|
||
</div>
|
||
</footer>
|
||
```
|
||
|
||
- [ ] **Step 2: Add footer CSS**
|
||
|
||
Append to `styles/navigation/nav-footer.css` (or create `styles/components/footer.css` and import from `styles/theme.css`):
|
||
```css
|
||
.site-footer {
|
||
background: var(--color-surface, #121318);
|
||
color: var(--color-on-surface, #e3e1e9);
|
||
border-top: 1px solid var(--color-outline-variant, #444651);
|
||
}
|
||
.site-footer__title a { color: var(--color-on-surface, #e3e1e9); text-decoration: none; }
|
||
.site-footer__desc { color: var(--color-on-surface-variant, #c5c5d3); }
|
||
.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);
|
||
}
|
||
.site-footer a { color: var(--color-primary, #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;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: Build + commit**
|
||
|
||
Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build`
|
||
|
||
```bash
|
||
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
|
||
git add footer.php styles/ static/dist/theme.css
|
||
git commit -m "feat(footer): Carbon Blue surface + cleaner widget rendering"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 16: Other templates — namespace swap + dark surface + remove picsum
|
||
|
||
**Files:**
|
||
- Modify: `index.php`
|
||
- Modify: `single.php`
|
||
- Modify: `page.php`
|
||
- Modify: `search.php`
|
||
- Modify: `404.php`
|
||
- Modify: `views/partials/page-hero.php`
|
||
|
||
**Goal:** Apply namespace swap, remove `picsum.photos` fallbacks, ensure empty-state copy is the documented strings.
|
||
|
||
- [ ] **Step 1: Namespace swap + picsum removal in index.php**
|
||
|
||
In `index.php`:
|
||
1. Replace `namespace SoloFrameEvo;` with `namespace KsPortfolio;`.
|
||
2. Find the line:
|
||
```php
|
||
$postImg = $featImg ? $featImg : 'https://picsum.photos/600/400?random=' . get_the_ID();
|
||
```
|
||
Replace with:
|
||
```php
|
||
$postImg = $featImg;
|
||
```
|
||
3. Find the `<img>` tag and wrap the whole `<figure>` in:
|
||
```php
|
||
<?php if ( has_post_thumbnail() ) : ?>
|
||
<figure>...existing markup...</figure>
|
||
<?php endif; ?>
|
||
```
|
||
(Don't render an empty figure.)
|
||
|
||
- [ ] **Step 2: Namespace swap + picsum removal in search.php**
|
||
|
||
Same edits as Step 1.
|
||
|
||
- [ ] **Step 3: Namespace swap in single.php, page.php, 404.php, page-hero.php**
|
||
|
||
In each file, replace `namespace SoloFrameEvo;` with `namespace KsPortfolio;`. In `404.php`, also update:
|
||
```php
|
||
<?php echo esc_html__( 'Search for:', 'label' ); ?>
|
||
```
|
||
to:
|
||
```php
|
||
<?php echo esc_html__( 'Search for:', 'ks-portfolio' ); ?>
|
||
```
|
||
(And the same for any other `'label'` text-domain references in `404.php`.)
|
||
|
||
- [ ] **Step 4: Dark surface for page-hero partial**
|
||
|
||
In `views/partials/page-hero.php`:
|
||
1. Namespace swap.
|
||
2. Replace the existing `bg-cover bg-no-repeat mb-12 py-12 lg:py-16 bg-dark text-light overflow-hidden` with:
|
||
```php
|
||
<section class="page-hero py-12 lg:py-16">
|
||
```
|
||
3. Replace the `<h1>` color classes `text-light` with `page-hero__title` (let CSS handle it).
|
||
|
||
- [ ] **Step 5: Add page-hero CSS**
|
||
|
||
Append to `styles/components/page-hero.css` (create new file and import from `styles/theme.css`):
|
||
```css
|
||
.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); }
|
||
```
|
||
|
||
- [ ] **Step 6: Build + commit**
|
||
|
||
Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build`
|
||
|
||
```bash
|
||
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
|
||
git add index.php single.php page.php search.php 404.php views/partials/page-hero.php styles/ static/dist/theme.css
|
||
git commit -m "feat(templates): namespace swap + dark surface + remove picsum fallbacks"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 17: Button styles for dark surface
|
||
|
||
**Files:**
|
||
- Modify: `styles/blocks/buttons.css`
|
||
|
||
**Goal:** Verify button classes render with WCAG-AA contrast against `#100e0b`. Add a `button--outline` variant if missing (used by the home + archive + single-project templates).
|
||
|
||
- [ ] **Step 1: Inspect existing button styles**
|
||
|
||
Read `styles/blocks/buttons.css`. Confirm it defines (or can be extended to define) the classes: `button`, `button--primary`, `button--outline`, `button--secondary`, `button--white`, `button--black`. Note what's present.
|
||
|
||
- [ ] **Step 2: Append dark-surface button styles**
|
||
|
||
Append to `styles/blocks/buttons.css`:
|
||
```css
|
||
.button {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-weight: 600;
|
||
font-size: 1rem;
|
||
line-height: 1.1;
|
||
padding: 0.5rem 2rem;
|
||
border-radius: 0.5rem;
|
||
border: 3px solid transparent;
|
||
text-decoration: none;
|
||
transition: color 200ms ease, background-color 200ms ease, border-color 200ms ease;
|
||
}
|
||
|
||
.button:active { transform: scale(0.99); }
|
||
@media (prefers-reduced-motion: reduce) { .button:active { transform: none; } }
|
||
|
||
.button:focus-visible { outline: 2px solid var(--color-primary, #b6c4ff); outline-offset: 3px; }
|
||
|
||
.button--primary {
|
||
background: var(--color-primary, #b6c4ff);
|
||
color: var(--color-on-primary, #05297a);
|
||
border-color: var(--color-primary, #b6c4ff);
|
||
}
|
||
.button--primary:hover { background: #dce1ff; border-color: #dce1ff; }
|
||
|
||
.button--outline {
|
||
background: transparent;
|
||
color: var(--color-on-surface, #e3e1e9);
|
||
border-color: var(--color-outline, #8f909d);
|
||
}
|
||
.button--outline:hover { border-color: var(--color-primary, #b6c4ff); color: var(--color-primary, #b6c4ff); }
|
||
|
||
.button--secondary {
|
||
background: var(--color-surface-container-high, #292a2f);
|
||
color: var(--color-on-surface, #e3e1e9);
|
||
border-color: var(--color-outline-variant, #444651);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: Build + commit**
|
||
|
||
Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npm run build`
|
||
|
||
```bash
|
||
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
|
||
git add styles/blocks/buttons.css static/dist/theme.css
|
||
git commit -m "feat(buttons): dark-surface button variants with WCAG-AA contrast"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 18: Playwright + axe specs
|
||
|
||
**Files:**
|
||
- Create: `tests/home.spec.js`
|
||
- Create: `tests/archive.spec.js`
|
||
- Create: `tests/single.spec.js`
|
||
|
||
**Goal:** Browser + accessibility tests for the new Carbon Blue surfaces.
|
||
|
||
- [ ] **Step 1: Write tests/home.spec.js**
|
||
|
||
Create `tests/home.spec.js`:
|
||
```js
|
||
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('.home-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([]);
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **Step 2: Write tests/archive.spec.js**
|
||
|
||
Create `tests/archive.spec.js`:
|
||
```js
|
||
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([]);
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **Step 3: Write tests/single.spec.js**
|
||
|
||
Create `tests/single.spec.js`:
|
||
```js
|
||
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) => {
|
||
// Pick the first project; skip the test gracefully if none exist.
|
||
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();
|
||
if (await firstLink.count() === 0) {
|
||
test.skip(true, 'No projects to test detail page');
|
||
return;
|
||
}
|
||
|
||
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([]);
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **Step 4: Run all Playwright tests**
|
||
|
||
Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && npx playwright test --reporter=list`
|
||
Expected: all four specs (`site-a11y`, `home`, `archive`, `single`) pass. If any fail, debug the specific violation, fix in the template/CSS, re-run.
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
|
||
git add tests/home.spec.js tests/archive.spec.js tests/single.spec.js
|
||
git commit -m "test: add Playwright + axe specs for home, archive, single"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 19: PHPCS clean run
|
||
|
||
**Files:**
|
||
- All PHP files in theme
|
||
|
||
**Goal:** `composer run lint` exits 0.
|
||
|
||
- [ ] **Step 1: Run PHPCS**
|
||
|
||
Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && composer run lint 2>&1 | tail -80`
|
||
|
||
- [ ] **Step 2: Fix violations inline**
|
||
|
||
For each reported violation, fix the file. Common ones expected: missing `wp_kses_post` on `the_content()` calls already correctly used; trailing whitespace; array alignment; `WordPress.Security.EscapeOutput` — fix by adding the right `esc_*` function.
|
||
|
||
- [ ] **Step 3: Re-run until clean**
|
||
|
||
Repeat step 1 until the command exits with no violations (or only the documented exclusions in `.phpcs.xml`).
|
||
|
||
- [ ] **Step 4: Commit any fixes**
|
||
|
||
```bash
|
||
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
|
||
git add -A
|
||
git commit -m "style: PHPCS clean across theme PHP" --allow-empty
|
||
```
|
||
|
||
---
|
||
|
||
## Task 20: Static security audit
|
||
|
||
**Files:**
|
||
- Audit only (no file changes unless finding requires them)
|
||
|
||
**Goal:** Identify any Critical/High security issues. Produce a written report that becomes part of the final testing report.
|
||
|
||
- [ ] **Step 1: Run PHPCS WordPress.Security ruleset**
|
||
|
||
Run: `cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio" && ./vendor/bin/phpcs --standard=.phpcs.xml --sniffs=WordPress.Security.EscapeOutput,WordPress.Security.NonceVerification,WordPress.Security.ValidatedSanitizedInput . 2>&1 | tail -80`
|
||
Expected: zero findings (since we already passed PHPCS in Task 19). If there are findings, fix inline.
|
||
|
||
- [ ] **Step 2: Grep for unsanitized superglobal access**
|
||
|
||
Run (PowerShell):
|
||
```powershell
|
||
Select-String -Path "C:\Users\ksolo\Herd\portfolio\wp-content\themes\ks-portfolio" -Include "*.php" -Recurse -Pattern '\$_(GET|POST|REQUEST)\[' | ForEach-Object { $_.Path + ":" + $_.LineNumber + " " + $_.Line }
|
||
```
|
||
Expected: empty result (theme has no direct superglobal access). Any finding is a security issue — fix by switching to `filter_input` + `sanitize_*` or to WordPress API equivalents.
|
||
|
||
- [ ] **Step 3: Audit new templates manually**
|
||
|
||
For each new template (`front-page.php`, `archive-projects.php`, `single-projects.php`, `taxonomy-project-type.php`) and each new partial (`views/partials/aura-bg.php`, `views/components/project-card.php`, `views/components/project-stat-card.php`, `views/components/project-meta-row.php`), check:
|
||
- Every dynamic output uses the right `esc_*` for context.
|
||
- Every state-changing form has `wp_nonce_field` (none in this theme — none expected).
|
||
- External URLs use `esc_url` and add `rel="noopener noreferrer"` when `target="_blank"`.
|
||
- `set_query_var` / `get_template_part` data is escaped in the consumer partials.
|
||
|
||
- [ ] **Step 4: Write the security section of the final report**
|
||
|
||
Append to `docs/superpowers/specs/2026-08-16-portfolio-2026-testing-report.md` (we'll create the file in Task 22):
|
||
```
|
||
## Security audit
|
||
|
||
- PHPCS WordPress.Security: <PASS / count of findings>
|
||
- Direct superglobal access (grep): <PASS / count of findings>
|
||
- Manual template audit: <PASS / list of findings with severity>
|
||
|
||
Severity summary: Critical: 0, High: 0, Medium: 0, Low: 0, Info: 0.
|
||
```
|
||
|
||
- [ ] **Step 5: Commit audit notes (if any fixes were made)**
|
||
|
||
```bash
|
||
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
|
||
git add -A
|
||
git commit -m "security: remediate audit findings" --allow-empty
|
||
```
|
||
|
||
---
|
||
|
||
## Task 21: Replace `screenshot.png` with Carbon Blue capture
|
||
|
||
**Files:**
|
||
- Modify: `screenshot.png`
|
||
|
||
**Goal:** Drop the Basic WP screenshot and replace it with a Carbon Blue capture.
|
||
|
||
- [ ] **Step 1: Capture the home page screenshot**
|
||
|
||
Run the existing Playwright test (it already saves `test-results/home.png`) or run explicitly:
|
||
```bash
|
||
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
|
||
npx playwright test tests/home.spec.js --reporter=list
|
||
```
|
||
|
||
- [ ] **Step 2: Copy the capture to theme root**
|
||
|
||
Run (PowerShell):
|
||
```powershell
|
||
Copy-Item "C:\Users\ksolo\Herd\portfolio\wp-content\themes\ks-portfolio\test-results\home.png" "C:\Users\ksolo\Herd\portfolio\wp-content\themes\ks-portfolio\screenshot.png" -Force
|
||
```
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
|
||
git rm screenshot.png
|
||
git add screenshot.png
|
||
git commit -m "chore: replace Basic WP screenshot with Carbon Blue home capture"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 22: Final testing report
|
||
|
||
**Files:**
|
||
- Create: `docs/superpowers/specs/2026-08-16-portfolio-2026-testing-report.md`
|
||
|
||
**Goal:** Ship a complete report covering all four test categories (browser, functional, unit, pen-test), issues encountered, and recommendations.
|
||
|
||
- [ ] **Step 1: Write the report**
|
||
|
||
Create `docs/superpowers/specs/2026-08-16-portfolio-2026-testing-report.md` with the following sections (replace `<…>` with actual numbers):
|
||
|
||
```markdown
|
||
# Portfolio 2026 — Testing Report
|
||
|
||
**Date:** 2026-08-16
|
||
**Theme:** ks-portfolio (Portfolio 2026)
|
||
**Local base URL:** http://portfolio.test
|
||
|
||
## 1. Scope
|
||
|
||
Browser (Chromium + axe-core), functional (Playwright), unit (PHPCS + WPCS), static security audit. Firefox/WebKit/mobile and OWASP ZAP were out of scope per user choice.
|
||
|
||
## 2. Browser + functional (Playwright)
|
||
|
||
Specs run:
|
||
- `tests/site-a11y.spec.js` — homepage, blog index, 404
|
||
- `tests/home.spec.js` — Carbon Blue hero + recent projects
|
||
- `tests/archive.spec.js` — project archive grid or empty state
|
||
- `tests/single.spec.js` — project detail hero + sidebar
|
||
|
||
Result: <PASS / FAIL — count of passing specs>
|
||
Screenshots: `test-results/home.png`, `test-results/archive.png`, `test-results/single.png`.
|
||
|
||
## 3. Unit / lint (PHPCS)
|
||
|
||
Command: `composer run lint`
|
||
Result: <PASS / FAIL — count of remaining violations>
|
||
|
||
## 4. Static security audit
|
||
|
||
<see "Security audit" block from Task 20>
|
||
|
||
## 5. Issues encountered
|
||
|
||
<list — severity-ranked>
|
||
|
||
## 6. Recommendations for future improvement
|
||
|
||
<list — non-blocking>
|
||
```
|
||
|
||
- [ ] **Step 2: Commit**
|
||
|
||
```bash
|
||
cd "C:/Users/ksolo/Herd/portfolio/wp-content/themes/ks-portfolio"
|
||
git add docs/superpowers/specs/2026-08-16-portfolio-2026-testing-report.md
|
||
git commit -m "docs: add Portfolio 2026 testing report"
|
||
```
|
||
|
||
---
|
||
|
||
## Self-Review
|
||
|
||
**Spec coverage:**
|
||
- §3 Identity/naming → Tasks 5, 21, 22. ✓
|
||
- §4.1 Body surface → Task 2. ✓
|
||
- §4.2 Carbon Blue partial → Task 4. ✓
|
||
- §4.3 Tokens → Task 3. ✓
|
||
- §5.1 front-page → Task 13. ✓
|
||
- §5.2 archive-projects → Task 10. ✓
|
||
- §5.3 single-projects → Task 12. ✓
|
||
- §5.4 taxonomy-project-type → Task 11. ✓
|
||
- §5.5 Other templates → Task 16. ✓
|
||
- §6.1 Buttons → Task 17. ✓
|
||
- §6.2 Project card → Task 7. ✓
|
||
- §6.3 Stat card → Task 8. ✓
|
||
- §6.4 Navigation → Task 14. ✓
|
||
- §6.5 Header layout → Task 14. ✓
|
||
- §7 Accessibility → Tasks 17, 18. ✓
|
||
- §8 Security → Tasks 19, 20. ✓
|
||
- §9 Testing → Tasks 1, 18, 19, 20, 22. ✓
|
||
- §10 File-level → all tasks together. ✓
|
||
- §12 Definition of done → Task 22. ✓
|
||
|
||
**Placeholder scan:** no TBDs / "TODO" / "implement later".
|
||
|
||
**Type consistency:**
|
||
- `Projects::get_*` signatures used in Tasks 7, 8, 9, 10, 11, 12, 13 match the definitions in Task 6.
|
||
- `Projects::format_stat` returns `'—'` when empty — used by stat-card in Task 12. ✓
|
||
- Card partial names (`project-card`, `project-stat-card`, `project-meta-row`) consistent across Tasks 7–12. ✓
|
||
|
||
Plan complete and saved to `docs/superpowers/plans/2026-08-16-portfolio-2026-implementation.md`. Two execution options:
|
||
|
||
**1. Subagent-Driven (recommended)** - I dispatch a fresh subagent per task, review between tasks, fast iteration.
|
||
|
||
**2. Inline Execution** - Execute tasks in this session using executing-plans, batch execution with checkpoints.
|
||
|
||
Which approach?
|