From af8e13d3a85a325a9d9dcdbe9259b7ae06370545 Mon Sep 17 00:00:00 2001 From: Keith Solomon Date: Sun, 16 Aug 2026 12:04:21 -0500 Subject: [PATCH] Add Portfolio 2026 implementation plan --- ...026-08-16-portfolio-2026-implementation.md | 2372 +++++++++++++++++ 1 file changed, 2372 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-16-portfolio-2026-implementation.md diff --git a/docs/superpowers/plans/2026-08-16-portfolio-2026-implementation.md b/docs/superpowers/plans/2026-08-16-portfolio-2026-implementation.md new file mode 100644 index 0000000..092db9f --- /dev/null +++ b/docs/superpowers/plans/2026-08-16-portfolio-2026-implementation.md @@ -0,0 +1,2372 @@ +# 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 `

`. 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 + + +``` + +- [ ] **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 +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|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|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 +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'; +?> + +
+
+ + + + +
+ +

+ + + +

+ + +

+ + +
+ 0 ) : ?> +
+ + + + 0 ) : ?> + + +
+ + +
+ + + + + + + + + + +
+
+
+``` + +- [ ] **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 + +
+ + + + + +
+``` + +- [ ] **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 + +
+ + + + + + + +
+``` + +- [ ] **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 + +
+
+
+
+

+

+
+ + +
+ +
+ +
+ 2, + 'prev_text' => '« ' . esc_html__( 'Previous', 'ks-portfolio' ), + 'next_text' => esc_html__( 'Next', 'ks-portfolio' ) . ' »', + ) + ); + ?> +
+ +
+

+

+
+ +
+
+
+ + '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 +name : __( 'Projects', 'ks-portfolio' ); +?> +
+
+
+
+

+

+ + + +

+
+ + +
+ +
+ +
+ 2, + 'prev_text' => '« ' . esc_html__( 'Previous', 'ks-portfolio' ), + 'next_text' => esc_html__( 'Next', 'ks-portfolio' ) . ' »', + ) + ); + ?> +
+ +
+

+

+
+ +
+
+
+ + '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 +name ) : ''; + $excerpt = has_excerpt( $post_id ) ? get_the_excerpt( $post_id ) : ''; + ?> +
+
+
+ +
+

+ + + + +

+

+ + + v + +

+ +

+ + +
+ + + + + v + + + + + + + + +
+
+ +
+
+ + + + +
+

+ +
+
+ + +
+
+
+
+ '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 + +
+
+
+
+ +

+ + +

+ + +
+ + > + + + + + > + + + +
+
+
+
+
+ +
+
+

+ + → + +
+
+ + 'projects', + 'posts_per_page' => 3, + 'no_found_rows' => true, + ) + ); + + if ( $recent->have_posts() ) : ?> +
+ have_posts() ) : + $recent->the_post(); + get_template_part( 'views/components/project-card' ); + endwhile; + wp_reset_postdata(); + ?> +
+ +
+

+

+
+ +
+ + '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 +
+ + + + +
+ + + +
+
+``` + +- [ ] **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 `