From ddf4fa763fa939e9cd33f0f99ea88ecbd0b4f940 Mon Sep 17 00:00:00 2001 From: Brandonma21 Date: Mon, 7 Sep 2026 12:03:51 +0200 Subject: [PATCH 1/7] Build Services page hero pattern (LS-1598) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hero - Add patterns/hero/services-hero.php: breadcrumb, eyebrow, heading, description, and primary/secondary CTAs - Add wrapped row of all 14 service tags, colour-coded by lifecycle Phase (Discover/Create/Build/Launch/Grow/Evolve) to match the mega menu convention, each linking to its real /services/ page - Add decorative "Services / Lifecycle" preview card reusing the existing is-style-glass-card shell Tokens & assets - No new colour tokens, font sizes, or global styles required — reuses existing semantic tokens and presets throughout - Source fresh Phosphor "regular" icons, embedded as real SVG markup Validation - php -l, patterns:escape, security:scan, and phpcs --standard=WordPress all pass clean on the new pattern --- patterns/hero/services-hero.php | 226 ++++++++++++++++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 patterns/hero/services-hero.php diff --git a/patterns/hero/services-hero.php b/patterns/hero/services-hero.php new file mode 100644 index 0000000..1efb8fb --- /dev/null +++ b/patterns/hero/services-hero.php @@ -0,0 +1,226 @@ + __( 'Discovery', 'ls-theme' ), + 'url' => '/services/discovery/', + 'phase' => 'discover', + 'icon' => '', + ), + array( + 'label' => __( 'Content', 'ls-theme' ), + 'url' => '/services/content/', + 'phase' => 'create', + 'icon' => '', + ), + array( + 'label' => __( 'Design', 'ls-theme' ), + 'url' => '/services/design/', + 'phase' => 'create', + 'icon' => '', + ), + array( + 'label' => __( 'Development', 'ls-theme' ), + 'url' => '/services/development/', + 'phase' => 'build', + 'icon' => '', + ), + array( + 'label' => __( 'Migrations', 'ls-theme' ), + 'url' => '/services/migrations/', + 'phase' => 'build', + 'icon' => '', + ), + array( + 'label' => __( 'Hosting', 'ls-theme' ), + 'url' => '/services/hosting/', + 'phase' => 'launch', + 'icon' => '', + ), + array( + 'label' => __( 'Performance', 'ls-theme' ), + 'url' => '/services/performance/', + 'phase' => 'launch', + 'icon' => '', + ), + array( + 'label' => __( 'Security', 'ls-theme' ), + 'url' => '/services/security/', + 'phase' => 'launch', + 'icon' => '', + ), + array( + 'label' => __( 'Training', 'ls-theme' ), + 'url' => '/services/training/', + 'phase' => 'launch', + 'icon' => '', + ), + array( + 'label' => __( 'Support', 'ls-theme' ), + 'url' => '/services/support/', + 'phase' => 'grow', + 'icon' => '', + ), + array( + 'label' => __( 'SEO', 'ls-theme' ), + 'url' => '/services/seo/', + 'phase' => 'grow', + 'icon' => '', + ), + array( + 'label' => __( 'Accessibility', 'ls-theme' ), + 'url' => '/services/accessibility/', + 'phase' => 'grow', + 'icon' => '', + ), + array( + 'label' => __( 'Email marketing', 'ls-theme' ), + 'url' => '/services/email-marketing/', + 'phase' => 'grow', + 'icon' => '', + ), + array( + 'label' => __( 'AI', 'ls-theme' ), + 'url' => '/services/ai/', + 'phase' => 'evolve', + 'icon' => '', + ), +); +?> + +
+ + +
+ + + + + + +
+ +
+ + + +

+ +
+ + + +
+ + +
+ + +

+ + + +
+ +

+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ + +
+ +
+ + + +

+ +
+ + +
+ +
+ + + +
+ +
+ + +

+ + + +
+ +
+ + + +
+ + + +
+ + + +
+ +
+ + + +
+ +

+ + + +

+ +
+ +
+ +
+ +
+ +
+ +
+ From 26c955ce8202ab3a6a47c0d4bc5086755d343f31 Mon Sep 17 00:00:00 2001 From: Brandonma21 Date: Mon, 7 Sep 2026 14:27:58 +0200 Subject: [PATCH 2/7] Refine Services hero pill styling and layout spacing (LS-1598) Layout fix - Fix breadcrumb-to-badge gap reading too tight by bumping the eyebrow badge's top margin from spacing|20 to spacing|30 Service pill styling - Switch pill label text to neutral text.default, keep icons phase-coloured for lifecycle-phase association - Add a subtle phase-tinted background and phase-tinted border to each pill (colour-mix over existing phase tokens) - Increase pill padding for a larger, more substantial pill - Add hover/focus-within state: stronger background/border tint, translateY(-1px), subtle shadow - Add a :focus-visible outline ring for keyboard accessibility New structural stylesheet - Add src/scss/structural/services-hero.scss for the pill interactive states (no block-supports equivalent exists for hover/focus-visible), wired into build scripts and the bundle-detection/enqueue system the same way as other hero bundles (inc/animations.php, functions.php editor styles) Context - Rest-state pill colours are set inline per instance (each pill mixes its own phase token); hover/focus overrides need !important to beat that inline specificity, documented in the stylesheet --- assets/css/services-hero.css | 1 + functions.php | 1 + inc/animations.php | 8 ++++ package.json | 6 +-- patterns/hero/services-hero.php | 49 ++++++++++++----------- src/scss/structural/services-hero.scss | 55 ++++++++++++++++++++++++++ 6 files changed, 94 insertions(+), 26 deletions(-) create mode 100644 assets/css/services-hero.css create mode 100644 src/scss/structural/services-hero.scss diff --git a/assets/css/services-hero.css b/assets/css/services-hero.css new file mode 100644 index 0000000..fb1ee29 --- /dev/null +++ b/assets/css/services-hero.css @@ -0,0 +1 @@ +.ls-service-pill{transition:background-color var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard),border-color var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard),transform var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard),box-shadow var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard)}.ls-service-pill:hover,.ls-service-pill:focus-within{transform:translateY(-1px);box-shadow:var(--wp--custom--shadow--elevation--100)}.ls-service-pill a:focus-visible{outline:2px solid var(--wp--custom--color--focus--ring);outline-offset:2px;border-radius:var(--wp--preset--border-radius--100)}.ls-service-pill--discover:hover,.ls-service-pill--discover:focus-within{background-color:color-mix(in srgb, var(--wp--custom--color--phase--discover) 16%, transparent) !important;border-color:color-mix(in srgb, var(--wp--custom--color--phase--discover) 55%, var(--wp--custom--color--border--card)) !important}.ls-service-pill--create:hover,.ls-service-pill--create:focus-within{background-color:color-mix(in srgb, var(--wp--custom--color--phase--create) 16%, transparent) !important;border-color:color-mix(in srgb, var(--wp--custom--color--phase--create) 55%, var(--wp--custom--color--border--card)) !important}.ls-service-pill--build:hover,.ls-service-pill--build:focus-within{background-color:color-mix(in srgb, var(--wp--custom--color--phase--build) 16%, transparent) !important;border-color:color-mix(in srgb, var(--wp--custom--color--phase--build) 55%, var(--wp--custom--color--border--card)) !important}.ls-service-pill--launch:hover,.ls-service-pill--launch:focus-within{background-color:color-mix(in srgb, var(--wp--custom--color--phase--launch) 16%, transparent) !important;border-color:color-mix(in srgb, var(--wp--custom--color--phase--launch) 55%, var(--wp--custom--color--border--card)) !important}.ls-service-pill--grow:hover,.ls-service-pill--grow:focus-within{background-color:color-mix(in srgb, var(--wp--custom--color--phase--grow) 16%, transparent) !important;border-color:color-mix(in srgb, var(--wp--custom--color--phase--grow) 55%, var(--wp--custom--color--border--card)) !important}.ls-service-pill--evolve:hover,.ls-service-pill--evolve:focus-within{background-color:color-mix(in srgb, var(--wp--custom--color--phase--evolve) 16%, transparent) !important;border-color:color-mix(in srgb, var(--wp--custom--color--phase--evolve) 55%, var(--wp--custom--color--border--card)) !important}@media(prefers-reduced-motion: reduce){.ls-service-pill{transition:none}.ls-service-pill:hover,.ls-service-pill:focus-within{transform:none}} diff --git a/functions.php b/functions.php index 0280953..e81eafd 100644 --- a/functions.php +++ b/functions.php @@ -70,6 +70,7 @@ function ls_theme_setup() { add_editor_style( 'assets/css/card-shells.css' ); add_editor_style( 'assets/css/cta-buttons.css' ); add_editor_style( 'assets/css/home-hero.css' ); + add_editor_style( 'assets/css/services-hero.css' ); add_editor_style( 'assets/css/work-hero.css' ); add_editor_style( 'assets/css/work-single-hero.css' ); add_editor_style( 'assets/css/blog-hero.css' ); diff --git a/inc/animations.php b/inc/animations.php index 4f4f755..d47d781 100644 --- a/inc/animations.php +++ b/inc/animations.php @@ -71,6 +71,7 @@ function ls_theme_get_bundle_render_markers() { ), ), 'home-hero' => array( 'classes' => array( 'ls-home-hero-section' ) ), + 'services-hero' => array( 'classes' => array( 'ls-service-pill' ) ), 'work-hero' => array( 'classes' => array( 'ls-work-hero' ) ), 'work-single-hero' => array( 'classes' => array( 'ls-work-single-meta' ) ), 'blog-hero' => array( 'classes' => array( 'ls-blog-hero' ) ), @@ -261,6 +262,13 @@ function ls_theme_get_effect_styles( $context = 'front' ) { 'contexts' => array( 'front', 'editor' ), 'condition' => 'is_front_page', ), + 'services-hero' => array( + 'handle' => 'ls-theme-services-hero', + 'path' => 'assets/css/services-hero.css', + 'contexts' => array( 'front', 'editor' ), + // No page template yet (LS-1598 in progress) — same reasoning as card-shells/ + // cta-buttons above, relies on render_block marker detection instead of a condition. + ), 'work-hero' => array( 'handle' => 'ls-theme-work-hero', 'path' => 'assets/css/work-hero.css', diff --git a/package.json b/package.json index 3b761ea..df901cf 100644 --- a/package.json +++ b/package.json @@ -4,15 +4,15 @@ "description": "LightSpeed Theme is a custom WordPress block theme built by LightSpeed for fast, accessible, maintainable websites using the WordPress Site Editor and block editor.", "type": "module", "scripts": { - "build:css": "sass --no-source-map --no-charset --style=compressed src/scss/animations.scss:assets/css/animations.css src/scss/gsap-animations.scss:assets/css/gsap-animations.css src/scss/structural/taxonomy-filter.scss:assets/css/taxonomy-filter.css src/scss/structural/work-project-card.scss:assets/css/work-project-card.css src/scss/structural/work-archive-sections.scss:assets/css/work-archive-sections.css src/scss/structural/card-shells.scss:assets/css/card-shells.css src/scss/structural/cta-buttons.scss:assets/css/cta-buttons.css src/scss/structural/home-hero.scss:assets/css/home-hero.css src/scss/structural/work-hero.scss:assets/css/work-hero.css src/scss/structural/work-single-hero.scss:assets/css/work-single-hero.css src/scss/structural/blog-hero.scss:assets/css/blog-hero.css src/scss/structural/blog-all-articles.scss:assets/css/blog-all-articles.css src/scss/structural/blog-writing-cta.scss:assets/css/blog-writing-cta.css src/scss/structural/faq.scss:assets/css/faq.css src/scss/structural/links.scss:assets/css/links.css src/scss/structural/button-secondary.scss:assets/css/button-secondary.css src/scss/structural/featured-work.scss:assets/css/featured-work.css src/scss/structural/where-to-fit.scss:assets/css/where-to-fit.css src/scss/structural/homepage-cta.scss:assets/css/homepage-cta.css src/scss/structural/stats-bar.scss:assets/css/stats-bar.css src/scss/structural/homepage-card-rows.scss:assets/css/homepage-card-rows.css src/scss/structural/homepage-why-lightspeed.scss:assets/css/homepage-why-lightspeed.css src/scss/structural/search-results.scss:assets/css/search-results.css", - "build:css:dev": "sass --no-source-map --no-charset --style=expanded src/scss/animations.scss:assets/css/animations.css src/scss/gsap-animations.scss:assets/css/gsap-animations.css src/scss/structural/taxonomy-filter.scss:assets/css/taxonomy-filter.css src/scss/structural/work-project-card.scss:assets/css/work-project-card.css src/scss/structural/work-archive-sections.scss:assets/css/work-archive-sections.css src/scss/structural/card-shells.scss:assets/css/card-shells.css src/scss/structural/cta-buttons.scss:assets/css/cta-buttons.css src/scss/structural/home-hero.scss:assets/css/home-hero.css src/scss/structural/work-hero.scss:assets/css/work-hero.css src/scss/structural/work-single-hero.scss:assets/css/work-single-hero.css src/scss/structural/blog-hero.scss:assets/css/blog-hero.css src/scss/structural/blog-all-articles.scss:assets/css/blog-all-articles.css src/scss/structural/blog-writing-cta.scss:assets/css/blog-writing-cta.css src/scss/structural/faq.scss:assets/css/faq.css src/scss/structural/links.scss:assets/css/links.css src/scss/structural/button-secondary.scss:assets/css/button-secondary.css src/scss/structural/featured-work.scss:assets/css/featured-work.css src/scss/structural/where-to-fit.scss:assets/css/where-to-fit.css src/scss/structural/homepage-cta.scss:assets/css/homepage-cta.css src/scss/structural/stats-bar.scss:assets/css/stats-bar.css src/scss/structural/homepage-card-rows.scss:assets/css/homepage-card-rows.css src/scss/structural/homepage-why-lightspeed.scss:assets/css/homepage-why-lightspeed.css src/scss/structural/search-results.scss:assets/css/search-results.css", + "build:css": "sass --no-source-map --no-charset --style=compressed src/scss/animations.scss:assets/css/animations.css src/scss/gsap-animations.scss:assets/css/gsap-animations.css src/scss/structural/taxonomy-filter.scss:assets/css/taxonomy-filter.css src/scss/structural/work-project-card.scss:assets/css/work-project-card.css src/scss/structural/work-archive-sections.scss:assets/css/work-archive-sections.css src/scss/structural/card-shells.scss:assets/css/card-shells.css src/scss/structural/cta-buttons.scss:assets/css/cta-buttons.css src/scss/structural/home-hero.scss:assets/css/home-hero.css src/scss/structural/work-hero.scss:assets/css/work-hero.css src/scss/structural/work-single-hero.scss:assets/css/work-single-hero.css src/scss/structural/blog-hero.scss:assets/css/blog-hero.css src/scss/structural/blog-all-articles.scss:assets/css/blog-all-articles.css src/scss/structural/blog-writing-cta.scss:assets/css/blog-writing-cta.css src/scss/structural/faq.scss:assets/css/faq.css src/scss/structural/links.scss:assets/css/links.css src/scss/structural/button-secondary.scss:assets/css/button-secondary.css src/scss/structural/featured-work.scss:assets/css/featured-work.css src/scss/structural/where-to-fit.scss:assets/css/where-to-fit.css src/scss/structural/homepage-cta.scss:assets/css/homepage-cta.css src/scss/structural/stats-bar.scss:assets/css/stats-bar.css src/scss/structural/homepage-card-rows.scss:assets/css/homepage-card-rows.css src/scss/structural/homepage-why-lightspeed.scss:assets/css/homepage-why-lightspeed.css src/scss/structural/search-results.scss:assets/css/search-results.css src/scss/structural/services-hero.scss:assets/css/services-hero.css", + "build:css:dev": "sass --no-source-map --no-charset --style=expanded src/scss/animations.scss:assets/css/animations.css src/scss/gsap-animations.scss:assets/css/gsap-animations.css src/scss/structural/taxonomy-filter.scss:assets/css/taxonomy-filter.css src/scss/structural/work-project-card.scss:assets/css/work-project-card.css src/scss/structural/work-archive-sections.scss:assets/css/work-archive-sections.css src/scss/structural/card-shells.scss:assets/css/card-shells.css src/scss/structural/cta-buttons.scss:assets/css/cta-buttons.css src/scss/structural/home-hero.scss:assets/css/home-hero.css src/scss/structural/work-hero.scss:assets/css/work-hero.css src/scss/structural/work-single-hero.scss:assets/css/work-single-hero.css src/scss/structural/blog-hero.scss:assets/css/blog-hero.css src/scss/structural/blog-all-articles.scss:assets/css/blog-all-articles.css src/scss/structural/blog-writing-cta.scss:assets/css/blog-writing-cta.css src/scss/structural/faq.scss:assets/css/faq.css src/scss/structural/links.scss:assets/css/links.css src/scss/structural/button-secondary.scss:assets/css/button-secondary.css src/scss/structural/featured-work.scss:assets/css/featured-work.css src/scss/structural/where-to-fit.scss:assets/css/where-to-fit.css src/scss/structural/homepage-cta.scss:assets/css/homepage-cta.css src/scss/structural/stats-bar.scss:assets/css/stats-bar.css src/scss/structural/homepage-card-rows.scss:assets/css/homepage-card-rows.css src/scss/structural/homepage-why-lightspeed.scss:assets/css/homepage-why-lightspeed.css src/scss/structural/search-results.scss:assets/css/search-results.css src/scss/structural/services-hero.scss:assets/css/services-hero.css", "schema:validate": "node theme-utils.mjs validate-schema", "theme:validate": "node theme-utils.mjs validate-theme", "patterns:escape": "node theme-utils.mjs escape-patterns", "security:scan": "node theme-utils.mjs security-scan", "lint": "npm run lint:json", "lint:json": "node --input-type=module --eval \"import { readFileSync } from 'fs'; import { glob } from 'glob'; const files = await glob(['theme.json', 'styles/**/*.json']); let ok = true; for (const f of files) { try { JSON.parse(readFileSync(f, 'utf8')); } catch (e) { console.error('Invalid JSON:', f, e.message); ok = false; } } if (ok) console.log('All JSON files are valid.'); else process.exit(1);\"", - "watch:css": "sass --watch --no-source-map --no-charset --style=expanded src/scss/animations.scss:assets/css/animations.css src/scss/gsap-animations.scss:assets/css/gsap-animations.css src/scss/structural/taxonomy-filter.scss:assets/css/taxonomy-filter.css src/scss/structural/work-project-card.scss:assets/css/work-project-card.css src/scss/structural/work-archive-sections.scss:assets/css/work-archive-sections.css src/scss/structural/card-shells.scss:assets/css/card-shells.css src/scss/structural/cta-buttons.scss:assets/css/cta-buttons.css src/scss/structural/home-hero.scss:assets/css/home-hero.css src/scss/structural/work-hero.scss:assets/css/work-hero.css src/scss/structural/work-single-hero.scss:assets/css/work-single-hero.css src/scss/structural/blog-hero.scss:assets/css/blog-hero.css src/scss/structural/blog-all-articles.scss:assets/css/blog-all-articles.css src/scss/structural/blog-writing-cta.scss:assets/css/blog-writing-cta.css src/scss/structural/faq.scss:assets/css/faq.css src/scss/structural/links.scss:assets/css/links.css src/scss/structural/button-secondary.scss:assets/css/button-secondary.css src/scss/structural/featured-work.scss:assets/css/featured-work.css src/scss/structural/where-to-fit.scss:assets/css/where-to-fit.css src/scss/structural/homepage-cta.scss:assets/css/homepage-cta.css src/scss/structural/stats-bar.scss:assets/css/stats-bar.css src/scss/structural/homepage-card-rows.scss:assets/css/homepage-card-rows.css src/scss/structural/homepage-why-lightspeed.scss:assets/css/homepage-why-lightspeed.css src/scss/structural/search-results.scss:assets/css/search-results.css" + "watch:css": "sass --watch --no-source-map --no-charset --style=expanded src/scss/animations.scss:assets/css/animations.css src/scss/gsap-animations.scss:assets/css/gsap-animations.css src/scss/structural/taxonomy-filter.scss:assets/css/taxonomy-filter.css src/scss/structural/work-project-card.scss:assets/css/work-project-card.css src/scss/structural/work-archive-sections.scss:assets/css/work-archive-sections.css src/scss/structural/card-shells.scss:assets/css/card-shells.css src/scss/structural/cta-buttons.scss:assets/css/cta-buttons.css src/scss/structural/home-hero.scss:assets/css/home-hero.css src/scss/structural/work-hero.scss:assets/css/work-hero.css src/scss/structural/work-single-hero.scss:assets/css/work-single-hero.css src/scss/structural/blog-hero.scss:assets/css/blog-hero.css src/scss/structural/blog-all-articles.scss:assets/css/blog-all-articles.css src/scss/structural/blog-writing-cta.scss:assets/css/blog-writing-cta.css src/scss/structural/faq.scss:assets/css/faq.css src/scss/structural/links.scss:assets/css/links.css src/scss/structural/button-secondary.scss:assets/css/button-secondary.css src/scss/structural/featured-work.scss:assets/css/featured-work.css src/scss/structural/where-to-fit.scss:assets/css/where-to-fit.css src/scss/structural/homepage-cta.scss:assets/css/homepage-cta.css src/scss/structural/stats-bar.scss:assets/css/stats-bar.css src/scss/structural/homepage-card-rows.scss:assets/css/homepage-card-rows.css src/scss/structural/homepage-why-lightspeed.scss:assets/css/homepage-why-lightspeed.css src/scss/structural/search-results.scss:assets/css/search-results.css src/scss/structural/services-hero.scss:assets/css/services-hero.css" }, "devDependencies": { "@axe-core/playwright": "^4.13.0", diff --git a/patterns/hero/services-hero.php b/patterns/hero/services-hero.php index 1efb8fb..9c4fe2a 100644 --- a/patterns/hero/services-hero.php +++ b/patterns/hero/services-hero.php @@ -102,15 +102,15 @@
- +
- -
+ +
@@ -131,7 +131,7 @@

- +

@@ -156,15 +156,18 @@ - -
+ +
- -

+ +

@@ -179,38 +182,38 @@
- -

+ +

- +
- -
+ +
- -
+ +
- -
+ +
- -
+ +
- -

+ +

- -

+ +

diff --git a/src/scss/structural/services-hero.scss b/src/scss/structural/services-hero.scss new file mode 100644 index 0000000..c35a373 --- /dev/null +++ b/src/scss/structural/services-hero.scss @@ -0,0 +1,55 @@ +/********** Services Hero (Services page only) **********/ + +/* + * Services Hero (LS-1598, single consumer patterns/hero/services-hero.php). The service-tag + * pills' rest-state background/border tints are set inline per instance (each pill mixes its + * own phase token), because block-supports has no way to express six distinct phase-derived + * colours. Interactive states (:hover, :focus-within, :focus-visible) have no block-supports + * equivalent at all, so they live here. + * + * The `!important` on the hover/focus-within background-color and border-color declarations is + * required, not a shortcut: an inline `style` attribute always outranks an external stylesheet + * rule regardless of pseudo-class or selector specificity, so without it these two properties + * could never change on hover. transform/box-shadow aren't set inline at rest, so they don't + * need it. + */ +.ls-service-pill { + transition: + background-color var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard), + border-color var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard), + transform var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard), + box-shadow var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard); + + &:hover, + &:focus-within { + transform: translateY(-1px); + box-shadow: var(--wp--custom--shadow--elevation--100); + } + + a:focus-visible { + outline: 2px solid var(--wp--custom--color--focus--ring); + outline-offset: 2px; + border-radius: var(--wp--preset--border-radius--100); + } +} + +@each $phase in discover, create, build, launch, grow, evolve { + .ls-service-pill--#{$phase} { + &:hover, + &:focus-within { + background-color: color-mix(in srgb, var(--wp--custom--color--phase--#{$phase}) 16%, transparent) !important; + border-color: color-mix(in srgb, var(--wp--custom--color--phase--#{$phase}) 55%, var(--wp--custom--color--border--card)) !important; + } + } +} + +@media (prefers-reduced-motion: reduce) { + .ls-service-pill { + transition: none; + + &:hover, + &:focus-within { + transform: none; + } + } +} From 065e1290d68847c5442d6eea9c2c438525bb34cc Mon Sep 17 00:00:00 2001 From: Brandonma21 Date: Mon, 7 Sep 2026 15:37:00 +0200 Subject: [PATCH 3/7] Add Services "Linked decisions" section, fix pill click targets (LS-1598) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add patterns/sections/services-linked-decisions.php: six-step process pill row (Discover/Create/Build/Launch/Grow/Evolve) matching the site's lifecycle-phase colours, linking to new /services/ phase pages - Neutral pills with phase-only badge, hover/focus-within states, :focus-visible outline - Fix whole-pill click target on both hero and Section 1 pills — only the text was clickable before; anchor now stays position:static so its ::before overlay sizes against the full pill, not just the text - Add src/scss/structural/services-linked-decisions.scss for the states/pseudo-elements JSON can't express, wired into build/enqueue - Correct services-hero.scss's CSS-limitation comments to AGENTS.md's exact format --- assets/css/services-hero.css | 2 +- assets/css/services-linked-decisions.css | 1 + functions.php | 1 + inc/animations.php | 85 ++++++----- package.json | 6 +- .../sections/services-linked-decisions.php | 135 ++++++++++++++++++ src/scss/structural/services-hero.scss | 43 ++++-- .../structural/services-linked-decisions.scss | 91 ++++++++++++ 8 files changed, 312 insertions(+), 52 deletions(-) create mode 100644 assets/css/services-linked-decisions.css create mode 100644 patterns/sections/services-linked-decisions.php create mode 100644 src/scss/structural/services-linked-decisions.scss diff --git a/assets/css/services-hero.css b/assets/css/services-hero.css index fb1ee29..3c509ec 100644 --- a/assets/css/services-hero.css +++ b/assets/css/services-hero.css @@ -1 +1 @@ -.ls-service-pill{transition:background-color var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard),border-color var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard),transform var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard),box-shadow var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard)}.ls-service-pill:hover,.ls-service-pill:focus-within{transform:translateY(-1px);box-shadow:var(--wp--custom--shadow--elevation--100)}.ls-service-pill a:focus-visible{outline:2px solid var(--wp--custom--color--focus--ring);outline-offset:2px;border-radius:var(--wp--preset--border-radius--100)}.ls-service-pill--discover:hover,.ls-service-pill--discover:focus-within{background-color:color-mix(in srgb, var(--wp--custom--color--phase--discover) 16%, transparent) !important;border-color:color-mix(in srgb, var(--wp--custom--color--phase--discover) 55%, var(--wp--custom--color--border--card)) !important}.ls-service-pill--create:hover,.ls-service-pill--create:focus-within{background-color:color-mix(in srgb, var(--wp--custom--color--phase--create) 16%, transparent) !important;border-color:color-mix(in srgb, var(--wp--custom--color--phase--create) 55%, var(--wp--custom--color--border--card)) !important}.ls-service-pill--build:hover,.ls-service-pill--build:focus-within{background-color:color-mix(in srgb, var(--wp--custom--color--phase--build) 16%, transparent) !important;border-color:color-mix(in srgb, var(--wp--custom--color--phase--build) 55%, var(--wp--custom--color--border--card)) !important}.ls-service-pill--launch:hover,.ls-service-pill--launch:focus-within{background-color:color-mix(in srgb, var(--wp--custom--color--phase--launch) 16%, transparent) !important;border-color:color-mix(in srgb, var(--wp--custom--color--phase--launch) 55%, var(--wp--custom--color--border--card)) !important}.ls-service-pill--grow:hover,.ls-service-pill--grow:focus-within{background-color:color-mix(in srgb, var(--wp--custom--color--phase--grow) 16%, transparent) !important;border-color:color-mix(in srgb, var(--wp--custom--color--phase--grow) 55%, var(--wp--custom--color--border--card)) !important}.ls-service-pill--evolve:hover,.ls-service-pill--evolve:focus-within{background-color:color-mix(in srgb, var(--wp--custom--color--phase--evolve) 16%, transparent) !important;border-color:color-mix(in srgb, var(--wp--custom--color--phase--evolve) 55%, var(--wp--custom--color--border--card)) !important}@media(prefers-reduced-motion: reduce){.ls-service-pill{transition:none}.ls-service-pill:hover,.ls-service-pill:focus-within{transform:none}} +.ls-service-pill{position:relative;transition:background-color var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard),border-color var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard),transform var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard),box-shadow var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard)}.ls-service-pill:hover,.ls-service-pill:focus-within{transform:translateY(-1px);box-shadow:var(--wp--custom--shadow--elevation--100)}.ls-service-pill a::before{content:"";position:absolute;inset:0;z-index:var(--wp--custom--z-index--base)}.ls-service-pill a:focus-visible{outline:2px solid var(--wp--custom--color--focus--ring);outline-offset:2px;border-radius:var(--wp--preset--border-radius--100)}.ls-service-pill--discover:hover,.ls-service-pill--discover:focus-within{background-color:color-mix(in srgb, var(--wp--custom--color--phase--discover) 16%, transparent) !important;border-color:color-mix(in srgb, var(--wp--custom--color--phase--discover) 55%, var(--wp--custom--color--border--card)) !important}.ls-service-pill--create:hover,.ls-service-pill--create:focus-within{background-color:color-mix(in srgb, var(--wp--custom--color--phase--create) 16%, transparent) !important;border-color:color-mix(in srgb, var(--wp--custom--color--phase--create) 55%, var(--wp--custom--color--border--card)) !important}.ls-service-pill--build:hover,.ls-service-pill--build:focus-within{background-color:color-mix(in srgb, var(--wp--custom--color--phase--build) 16%, transparent) !important;border-color:color-mix(in srgb, var(--wp--custom--color--phase--build) 55%, var(--wp--custom--color--border--card)) !important}.ls-service-pill--launch:hover,.ls-service-pill--launch:focus-within{background-color:color-mix(in srgb, var(--wp--custom--color--phase--launch) 16%, transparent) !important;border-color:color-mix(in srgb, var(--wp--custom--color--phase--launch) 55%, var(--wp--custom--color--border--card)) !important}.ls-service-pill--grow:hover,.ls-service-pill--grow:focus-within{background-color:color-mix(in srgb, var(--wp--custom--color--phase--grow) 16%, transparent) !important;border-color:color-mix(in srgb, var(--wp--custom--color--phase--grow) 55%, var(--wp--custom--color--border--card)) !important}.ls-service-pill--evolve:hover,.ls-service-pill--evolve:focus-within{background-color:color-mix(in srgb, var(--wp--custom--color--phase--evolve) 16%, transparent) !important;border-color:color-mix(in srgb, var(--wp--custom--color--phase--evolve) 55%, var(--wp--custom--color--border--card)) !important}@media(prefers-reduced-motion: reduce){.ls-service-pill{transition:none}.ls-service-pill:hover,.ls-service-pill:focus-within{transform:none}} diff --git a/assets/css/services-linked-decisions.css b/assets/css/services-linked-decisions.css new file mode 100644 index 0000000..6700041 --- /dev/null +++ b/assets/css/services-linked-decisions.css @@ -0,0 +1 @@ +.ls-process-pill{position:relative;transition:border-color var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard),transform var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard),box-shadow var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard)}.ls-process-pill:hover,.ls-process-pill:focus-within{border-color:var(--wp--custom--color--border--field);transform:translateY(-1px);box-shadow:var(--wp--custom--shadow--elevation--100)}.ls-process-pill .ls-process-pill__label a::before{content:"";position:absolute;inset:0;z-index:var(--wp--custom--z-index--base)}.ls-process-pill .ls-process-pill__label a:focus-visible{outline:2px solid var(--wp--custom--color--focus--ring);outline-offset:2px;border-radius:var(--wp--preset--border-radius--100)}.ls-process-pill .ls-process-pill__badge{transition:background-color var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard),border-color var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard)}.ls-process-pill--discover:hover .ls-process-pill__badge,.ls-process-pill--discover:focus-within .ls-process-pill__badge{background-color:color-mix(in srgb, var(--wp--custom--color--phase--discover) 20%, transparent) !important;border-color:color-mix(in srgb, var(--wp--custom--color--phase--discover) 50%, transparent) !important}.ls-process-pill--create:hover .ls-process-pill__badge,.ls-process-pill--create:focus-within .ls-process-pill__badge{background-color:color-mix(in srgb, var(--wp--custom--color--phase--create) 20%, transparent) !important;border-color:color-mix(in srgb, var(--wp--custom--color--phase--create) 50%, transparent) !important}.ls-process-pill--build:hover .ls-process-pill__badge,.ls-process-pill--build:focus-within .ls-process-pill__badge{background-color:color-mix(in srgb, var(--wp--custom--color--phase--build) 20%, transparent) !important;border-color:color-mix(in srgb, var(--wp--custom--color--phase--build) 50%, transparent) !important}.ls-process-pill--launch:hover .ls-process-pill__badge,.ls-process-pill--launch:focus-within .ls-process-pill__badge{background-color:color-mix(in srgb, var(--wp--custom--color--phase--launch) 20%, transparent) !important;border-color:color-mix(in srgb, var(--wp--custom--color--phase--launch) 50%, transparent) !important}.ls-process-pill--grow:hover .ls-process-pill__badge,.ls-process-pill--grow:focus-within .ls-process-pill__badge{background-color:color-mix(in srgb, var(--wp--custom--color--phase--grow) 20%, transparent) !important;border-color:color-mix(in srgb, var(--wp--custom--color--phase--grow) 50%, transparent) !important}.ls-process-pill--evolve:hover .ls-process-pill__badge,.ls-process-pill--evolve:focus-within .ls-process-pill__badge{background-color:color-mix(in srgb, var(--wp--custom--color--phase--evolve) 20%, transparent) !important;border-color:color-mix(in srgb, var(--wp--custom--color--phase--evolve) 50%, transparent) !important}@media(prefers-reduced-motion: reduce){.ls-process-pill{transition:none}.ls-process-pill:hover,.ls-process-pill:focus-within{transform:none}.ls-process-pill__badge{transition:none}} diff --git a/functions.php b/functions.php index e81eafd..e7fde85 100644 --- a/functions.php +++ b/functions.php @@ -71,6 +71,7 @@ function ls_theme_setup() { add_editor_style( 'assets/css/cta-buttons.css' ); add_editor_style( 'assets/css/home-hero.css' ); add_editor_style( 'assets/css/services-hero.css' ); + add_editor_style( 'assets/css/services-linked-decisions.css' ); add_editor_style( 'assets/css/work-hero.css' ); add_editor_style( 'assets/css/work-single-hero.css' ); add_editor_style( 'assets/css/blog-hero.css' ); diff --git a/inc/animations.php b/inc/animations.php index d47d781..5b705c1 100644 --- a/inc/animations.php +++ b/inc/animations.php @@ -55,11 +55,11 @@ function ls_theme_get_local_asset_version( $path ) { */ function ls_theme_get_bundle_render_markers() { return array( - 'taxonomy-filter' => array( 'blocks' => array( 'ls-plugin/taxonomy-filter' ) ), - 'work-project-card' => array( + 'taxonomy-filter' => array( 'blocks' => array( 'ls-plugin/taxonomy-filter' ) ), + 'work-project-card' => array( 'classes' => array( 'is-style-card-case-study', 'ls-tag-pills', 'ls-card-banner-tint', 'ls-platform-tag-brand', 'ls-platform-tag-woocommerce' ), ), - 'work-archive-sections' => array( + 'work-archive-sections' => array( 'classes' => array( 'ls-icon-well-accent', 'ls-icon-well-brand', @@ -70,19 +70,20 @@ function ls_theme_get_bundle_render_markers() { 'ls-card-divider-both', ), ), - 'home-hero' => array( 'classes' => array( 'ls-home-hero-section' ) ), - 'services-hero' => array( 'classes' => array( 'ls-service-pill' ) ), - 'work-hero' => array( 'classes' => array( 'ls-work-hero' ) ), - 'work-single-hero' => array( 'classes' => array( 'ls-work-single-meta' ) ), - 'blog-hero' => array( 'classes' => array( 'ls-blog-hero' ) ), - 'blog-all-articles' => array( 'classes' => array( 'is-style-card-post', 'ls-post-card-cta' ) ), - 'blog-writing-cta' => array( 'classes' => array( 'ls-writing-cta', 'ls-code-panel' ) ), - 'button-secondary' => array( 'classes' => array( 'is-style-button-secondary' ) ), - 'featured-work' => array( 'classes' => array( 'ls-featured-work-grid', 'ls-featured-work-card__divider' ) ), - 'where-to-fit' => array( 'classes' => array( 'ls-package-card' ) ), - 'homepage-cta' => array( 'classes' => array( 'ls-homepage-cta' ) ), - 'stats-bar' => array( 'classes' => array( 'ls-stats-row', 'ls-stat-item' ) ), - 'homepage-card-rows' => array( 'classes' => array( 'ls-homepage-card-row', 'ls-what-we-build-row' ) ), + 'home-hero' => array( 'classes' => array( 'ls-home-hero-section' ) ), + 'services-hero' => array( 'classes' => array( 'ls-service-pill' ) ), + 'services-linked-decisions' => array( 'classes' => array( 'ls-process-pill' ) ), + 'work-hero' => array( 'classes' => array( 'ls-work-hero' ) ), + 'work-single-hero' => array( 'classes' => array( 'ls-work-single-meta' ) ), + 'blog-hero' => array( 'classes' => array( 'ls-blog-hero' ) ), + 'blog-all-articles' => array( 'classes' => array( 'is-style-card-post', 'ls-post-card-cta' ) ), + 'blog-writing-cta' => array( 'classes' => array( 'ls-writing-cta', 'ls-code-panel' ) ), + 'button-secondary' => array( 'classes' => array( 'is-style-button-secondary' ) ), + 'featured-work' => array( 'classes' => array( 'ls-featured-work-grid', 'ls-featured-work-card__divider' ) ), + 'where-to-fit' => array( 'classes' => array( 'ls-package-card' ) ), + 'homepage-cta' => array( 'classes' => array( 'ls-homepage-cta' ) ), + 'stats-bar' => array( 'classes' => array( 'ls-stats-row', 'ls-stat-item' ) ), + 'homepage-card-rows' => array( 'classes' => array( 'ls-homepage-card-row', 'ls-what-we-build-row' ) ), ); } @@ -200,7 +201,7 @@ function ls_theme_print_late_effect_styles() { function ls_theme_get_effect_styles( $context = 'front' ) { $effects = array( // Genuinely global, sitewide styling — always loads, no condition. - 'effects' => array( + 'effects' => array( 'handle' => 'ls-theme-effects', 'path' => 'assets/css/animations.css', 'contexts' => array( 'front', 'editor' ), @@ -211,7 +212,7 @@ function ls_theme_get_effect_styles( $context = 'front' ) { // work-archive-sections' icon-well classes are also used by three homepage sections). // Conditions only apply in the 'front' context; the editor keeps loading every bundle // unconditionally so any pattern still looks correct when edited in isolation. - 'taxonomy-filter' => array( + 'taxonomy-filter' => array( 'handle' => 'ls-theme-taxonomy-filter', 'path' => 'assets/css/taxonomy-filter.css', 'contexts' => array( 'front', 'editor' ), @@ -220,7 +221,7 @@ function ls_theme_get_effect_styles( $context = 'front' ) { return is_post_type_archive( 'project' ) || is_page_template( 'page-blog-archive' ); }, ), - 'work-project-card' => array( + 'work-project-card' => array( 'handle' => 'ls-theme-work-project-card', 'path' => 'assets/css/work-project-card.css', 'contexts' => array( 'front', 'editor' ), @@ -230,7 +231,7 @@ function ls_theme_get_effect_styles( $context = 'front' ) { return is_front_page() || is_post_type_archive( 'project' ); }, ), - 'work-archive-sections' => array( + 'work-archive-sections' => array( 'handle' => 'ls-theme-work-archive-sections', 'path' => 'assets/css/work-archive-sections.css', 'contexts' => array( 'front', 'editor' ), @@ -240,7 +241,7 @@ function ls_theme_get_effect_styles( $context = 'front' ) { return is_front_page() || is_post_type_archive( 'project' ); }, ), - 'card-shells' => array( + 'card-shells' => array( 'handle' => 'ls-theme-card-shells', 'path' => 'assets/css/card-shells.css', 'contexts' => array( 'front', 'editor' ), @@ -250,26 +251,32 @@ function ls_theme_get_effect_styles( $context = 'front' ) { // unstyled on every single use, not just a rare off-template case — so at ~10 KB // compressed it's cheaper to load unconditionally than to risk that. ), - 'cta-buttons' => array( + 'cta-buttons' => array( 'handle' => 'ls-theme-cta-buttons', 'path' => 'assets/css/cta-buttons.css', 'contexts' => array( 'front', 'editor' ), // Same reasoning as card-shells above (~5 KB compressed). ), - 'home-hero' => array( + 'home-hero' => array( 'handle' => 'ls-theme-home-hero', 'path' => 'assets/css/home-hero.css', 'contexts' => array( 'front', 'editor' ), 'condition' => 'is_front_page', ), - 'services-hero' => array( + 'services-hero' => array( 'handle' => 'ls-theme-services-hero', 'path' => 'assets/css/services-hero.css', 'contexts' => array( 'front', 'editor' ), // No page template yet (LS-1598 in progress) — same reasoning as card-shells/ // cta-buttons above, relies on render_block marker detection instead of a condition. ), - 'work-hero' => array( + 'services-linked-decisions' => array( + 'handle' => 'ls-theme-services-linked-decisions', + 'path' => 'assets/css/services-linked-decisions.css', + 'contexts' => array( 'front', 'editor' ), + // Same reasoning as services-hero above. + ), + 'work-hero' => array( 'handle' => 'ls-theme-work-hero', 'path' => 'assets/css/work-hero.css', 'contexts' => array( 'front', 'editor' ), @@ -277,7 +284,7 @@ function ls_theme_get_effect_styles( $context = 'front' ) { return is_post_type_archive( 'project' ); }, ), - 'work-single-hero' => array( + 'work-single-hero' => array( 'handle' => 'ls-theme-work-single-hero', 'path' => 'assets/css/work-single-hero.css', 'contexts' => array( 'front', 'editor' ), @@ -285,7 +292,7 @@ function ls_theme_get_effect_styles( $context = 'front' ) { return is_singular( 'project' ); }, ), - 'blog-hero' => array( + 'blog-hero' => array( 'handle' => 'ls-theme-blog-hero', 'path' => 'assets/css/blog-hero.css', 'contexts' => array( 'front', 'editor' ), @@ -293,7 +300,7 @@ function ls_theme_get_effect_styles( $context = 'front' ) { return is_page_template( 'page-blog-archive' ); }, ), - 'blog-all-articles' => array( + 'blog-all-articles' => array( 'handle' => 'ls-theme-blog-all-articles', 'path' => 'assets/css/blog-all-articles.css', 'contexts' => array( 'front', 'editor' ), @@ -301,7 +308,7 @@ function ls_theme_get_effect_styles( $context = 'front' ) { return is_page_template( 'page-blog-archive' ); }, ), - 'blog-writing-cta' => array( + 'blog-writing-cta' => array( 'handle' => 'ls-theme-blog-writing-cta', 'path' => 'assets/css/blog-writing-cta.css', 'contexts' => array( 'front', 'editor' ), @@ -309,7 +316,7 @@ function ls_theme_get_effect_styles( $context = 'front' ) { return is_page_template( 'page-blog-archive' ); }, ), - 'faq' => array( + 'faq' => array( 'handle' => 'ls-theme-faq', 'path' => 'assets/css/faq.css', 'contexts' => array( 'front', 'editor' ), @@ -324,12 +331,12 @@ function ls_theme_get_effect_styles( $context = 'front' ) { // of animations.css rather than kept there under the header/footer exception. The header // and footer template parts render on every page anyway, so a presence condition would // never actually skip loading this bundle — left unconditional. - 'links' => array( + 'links' => array( 'handle' => 'ls-theme-links', 'path' => 'assets/css/links.css', 'contexts' => array( 'front', 'editor' ), ), - 'button-secondary' => array( + 'button-secondary' => array( 'handle' => 'ls-theme-button-secondary', 'path' => 'assets/css/button-secondary.css', 'contexts' => array( 'front', 'editor' ), @@ -343,43 +350,43 @@ function ls_theme_get_effect_styles( $context = 'front' ) { return is_front_page() || is_post_type_archive( 'project' ) || is_404(); }, ), - 'featured-work' => array( + 'featured-work' => array( 'handle' => 'ls-theme-featured-work', 'path' => 'assets/css/featured-work.css', 'contexts' => array( 'front', 'editor' ), 'condition' => 'is_front_page', ), - 'where-to-fit' => array( + 'where-to-fit' => array( 'handle' => 'ls-theme-where-to-fit', 'path' => 'assets/css/where-to-fit.css', 'contexts' => array( 'front', 'editor' ), 'condition' => 'is_front_page', ), - 'homepage-cta' => array( + 'homepage-cta' => array( 'handle' => 'ls-theme-homepage-cta', 'path' => 'assets/css/homepage-cta.css', 'contexts' => array( 'front', 'editor' ), 'condition' => 'is_front_page', ), - 'stats-bar' => array( + 'stats-bar' => array( 'handle' => 'ls-theme-stats-bar', 'path' => 'assets/css/stats-bar.css', 'contexts' => array( 'front', 'editor' ), 'condition' => 'is_front_page', ), - 'homepage-card-rows' => array( + 'homepage-card-rows' => array( 'handle' => 'ls-theme-homepage-card-rows', 'path' => 'assets/css/homepage-card-rows.css', 'contexts' => array( 'front', 'editor' ), 'condition' => 'is_front_page', ), - 'homepage-why-lightspeed' => array( + 'homepage-why-lightspeed' => array( 'handle' => 'ls-theme-homepage-why-lightspeed', 'path' => 'assets/css/homepage-why-lightspeed.css', 'contexts' => array( 'front', 'editor' ), 'condition' => 'is_front_page', ), - 'search-results' => array( + 'search-results' => array( 'handle' => 'ls-theme-search-results', 'path' => 'assets/css/search-results.css', 'contexts' => array( 'front', 'editor' ), diff --git a/package.json b/package.json index df901cf..0b3480f 100644 --- a/package.json +++ b/package.json @@ -4,15 +4,15 @@ "description": "LightSpeed Theme is a custom WordPress block theme built by LightSpeed for fast, accessible, maintainable websites using the WordPress Site Editor and block editor.", "type": "module", "scripts": { - "build:css": "sass --no-source-map --no-charset --style=compressed src/scss/animations.scss:assets/css/animations.css src/scss/gsap-animations.scss:assets/css/gsap-animations.css src/scss/structural/taxonomy-filter.scss:assets/css/taxonomy-filter.css src/scss/structural/work-project-card.scss:assets/css/work-project-card.css src/scss/structural/work-archive-sections.scss:assets/css/work-archive-sections.css src/scss/structural/card-shells.scss:assets/css/card-shells.css src/scss/structural/cta-buttons.scss:assets/css/cta-buttons.css src/scss/structural/home-hero.scss:assets/css/home-hero.css src/scss/structural/work-hero.scss:assets/css/work-hero.css src/scss/structural/work-single-hero.scss:assets/css/work-single-hero.css src/scss/structural/blog-hero.scss:assets/css/blog-hero.css src/scss/structural/blog-all-articles.scss:assets/css/blog-all-articles.css src/scss/structural/blog-writing-cta.scss:assets/css/blog-writing-cta.css src/scss/structural/faq.scss:assets/css/faq.css src/scss/structural/links.scss:assets/css/links.css src/scss/structural/button-secondary.scss:assets/css/button-secondary.css src/scss/structural/featured-work.scss:assets/css/featured-work.css src/scss/structural/where-to-fit.scss:assets/css/where-to-fit.css src/scss/structural/homepage-cta.scss:assets/css/homepage-cta.css src/scss/structural/stats-bar.scss:assets/css/stats-bar.css src/scss/structural/homepage-card-rows.scss:assets/css/homepage-card-rows.css src/scss/structural/homepage-why-lightspeed.scss:assets/css/homepage-why-lightspeed.css src/scss/structural/search-results.scss:assets/css/search-results.css src/scss/structural/services-hero.scss:assets/css/services-hero.css", - "build:css:dev": "sass --no-source-map --no-charset --style=expanded src/scss/animations.scss:assets/css/animations.css src/scss/gsap-animations.scss:assets/css/gsap-animations.css src/scss/structural/taxonomy-filter.scss:assets/css/taxonomy-filter.css src/scss/structural/work-project-card.scss:assets/css/work-project-card.css src/scss/structural/work-archive-sections.scss:assets/css/work-archive-sections.css src/scss/structural/card-shells.scss:assets/css/card-shells.css src/scss/structural/cta-buttons.scss:assets/css/cta-buttons.css src/scss/structural/home-hero.scss:assets/css/home-hero.css src/scss/structural/work-hero.scss:assets/css/work-hero.css src/scss/structural/work-single-hero.scss:assets/css/work-single-hero.css src/scss/structural/blog-hero.scss:assets/css/blog-hero.css src/scss/structural/blog-all-articles.scss:assets/css/blog-all-articles.css src/scss/structural/blog-writing-cta.scss:assets/css/blog-writing-cta.css src/scss/structural/faq.scss:assets/css/faq.css src/scss/structural/links.scss:assets/css/links.css src/scss/structural/button-secondary.scss:assets/css/button-secondary.css src/scss/structural/featured-work.scss:assets/css/featured-work.css src/scss/structural/where-to-fit.scss:assets/css/where-to-fit.css src/scss/structural/homepage-cta.scss:assets/css/homepage-cta.css src/scss/structural/stats-bar.scss:assets/css/stats-bar.css src/scss/structural/homepage-card-rows.scss:assets/css/homepage-card-rows.css src/scss/structural/homepage-why-lightspeed.scss:assets/css/homepage-why-lightspeed.css src/scss/structural/search-results.scss:assets/css/search-results.css src/scss/structural/services-hero.scss:assets/css/services-hero.css", + "build:css": "sass --no-source-map --no-charset --style=compressed src/scss/animations.scss:assets/css/animations.css src/scss/gsap-animations.scss:assets/css/gsap-animations.css src/scss/structural/taxonomy-filter.scss:assets/css/taxonomy-filter.css src/scss/structural/work-project-card.scss:assets/css/work-project-card.css src/scss/structural/work-archive-sections.scss:assets/css/work-archive-sections.css src/scss/structural/card-shells.scss:assets/css/card-shells.css src/scss/structural/cta-buttons.scss:assets/css/cta-buttons.css src/scss/structural/home-hero.scss:assets/css/home-hero.css src/scss/structural/work-hero.scss:assets/css/work-hero.css src/scss/structural/work-single-hero.scss:assets/css/work-single-hero.css src/scss/structural/blog-hero.scss:assets/css/blog-hero.css src/scss/structural/blog-all-articles.scss:assets/css/blog-all-articles.css src/scss/structural/blog-writing-cta.scss:assets/css/blog-writing-cta.css src/scss/structural/faq.scss:assets/css/faq.css src/scss/structural/links.scss:assets/css/links.css src/scss/structural/button-secondary.scss:assets/css/button-secondary.css src/scss/structural/featured-work.scss:assets/css/featured-work.css src/scss/structural/where-to-fit.scss:assets/css/where-to-fit.css src/scss/structural/homepage-cta.scss:assets/css/homepage-cta.css src/scss/structural/stats-bar.scss:assets/css/stats-bar.css src/scss/structural/homepage-card-rows.scss:assets/css/homepage-card-rows.css src/scss/structural/homepage-why-lightspeed.scss:assets/css/homepage-why-lightspeed.css src/scss/structural/search-results.scss:assets/css/search-results.css src/scss/structural/services-hero.scss:assets/css/services-hero.css src/scss/structural/services-linked-decisions.scss:assets/css/services-linked-decisions.css", + "build:css:dev": "sass --no-source-map --no-charset --style=expanded src/scss/animations.scss:assets/css/animations.css src/scss/gsap-animations.scss:assets/css/gsap-animations.css src/scss/structural/taxonomy-filter.scss:assets/css/taxonomy-filter.css src/scss/structural/work-project-card.scss:assets/css/work-project-card.css src/scss/structural/work-archive-sections.scss:assets/css/work-archive-sections.css src/scss/structural/card-shells.scss:assets/css/card-shells.css src/scss/structural/cta-buttons.scss:assets/css/cta-buttons.css src/scss/structural/home-hero.scss:assets/css/home-hero.css src/scss/structural/work-hero.scss:assets/css/work-hero.css src/scss/structural/work-single-hero.scss:assets/css/work-single-hero.css src/scss/structural/blog-hero.scss:assets/css/blog-hero.css src/scss/structural/blog-all-articles.scss:assets/css/blog-all-articles.css src/scss/structural/blog-writing-cta.scss:assets/css/blog-writing-cta.css src/scss/structural/faq.scss:assets/css/faq.css src/scss/structural/links.scss:assets/css/links.css src/scss/structural/button-secondary.scss:assets/css/button-secondary.css src/scss/structural/featured-work.scss:assets/css/featured-work.css src/scss/structural/where-to-fit.scss:assets/css/where-to-fit.css src/scss/structural/homepage-cta.scss:assets/css/homepage-cta.css src/scss/structural/stats-bar.scss:assets/css/stats-bar.css src/scss/structural/homepage-card-rows.scss:assets/css/homepage-card-rows.css src/scss/structural/homepage-why-lightspeed.scss:assets/css/homepage-why-lightspeed.css src/scss/structural/search-results.scss:assets/css/search-results.css src/scss/structural/services-hero.scss:assets/css/services-hero.css src/scss/structural/services-linked-decisions.scss:assets/css/services-linked-decisions.css", "schema:validate": "node theme-utils.mjs validate-schema", "theme:validate": "node theme-utils.mjs validate-theme", "patterns:escape": "node theme-utils.mjs escape-patterns", "security:scan": "node theme-utils.mjs security-scan", "lint": "npm run lint:json", "lint:json": "node --input-type=module --eval \"import { readFileSync } from 'fs'; import { glob } from 'glob'; const files = await glob(['theme.json', 'styles/**/*.json']); let ok = true; for (const f of files) { try { JSON.parse(readFileSync(f, 'utf8')); } catch (e) { console.error('Invalid JSON:', f, e.message); ok = false; } } if (ok) console.log('All JSON files are valid.'); else process.exit(1);\"", - "watch:css": "sass --watch --no-source-map --no-charset --style=expanded src/scss/animations.scss:assets/css/animations.css src/scss/gsap-animations.scss:assets/css/gsap-animations.css src/scss/structural/taxonomy-filter.scss:assets/css/taxonomy-filter.css src/scss/structural/work-project-card.scss:assets/css/work-project-card.css src/scss/structural/work-archive-sections.scss:assets/css/work-archive-sections.css src/scss/structural/card-shells.scss:assets/css/card-shells.css src/scss/structural/cta-buttons.scss:assets/css/cta-buttons.css src/scss/structural/home-hero.scss:assets/css/home-hero.css src/scss/structural/work-hero.scss:assets/css/work-hero.css src/scss/structural/work-single-hero.scss:assets/css/work-single-hero.css src/scss/structural/blog-hero.scss:assets/css/blog-hero.css src/scss/structural/blog-all-articles.scss:assets/css/blog-all-articles.css src/scss/structural/blog-writing-cta.scss:assets/css/blog-writing-cta.css src/scss/structural/faq.scss:assets/css/faq.css src/scss/structural/links.scss:assets/css/links.css src/scss/structural/button-secondary.scss:assets/css/button-secondary.css src/scss/structural/featured-work.scss:assets/css/featured-work.css src/scss/structural/where-to-fit.scss:assets/css/where-to-fit.css src/scss/structural/homepage-cta.scss:assets/css/homepage-cta.css src/scss/structural/stats-bar.scss:assets/css/stats-bar.css src/scss/structural/homepage-card-rows.scss:assets/css/homepage-card-rows.css src/scss/structural/homepage-why-lightspeed.scss:assets/css/homepage-why-lightspeed.css src/scss/structural/search-results.scss:assets/css/search-results.css src/scss/structural/services-hero.scss:assets/css/services-hero.css" + "watch:css": "sass --watch --no-source-map --no-charset --style=expanded src/scss/animations.scss:assets/css/animations.css src/scss/gsap-animations.scss:assets/css/gsap-animations.css src/scss/structural/taxonomy-filter.scss:assets/css/taxonomy-filter.css src/scss/structural/work-project-card.scss:assets/css/work-project-card.css src/scss/structural/work-archive-sections.scss:assets/css/work-archive-sections.css src/scss/structural/card-shells.scss:assets/css/card-shells.css src/scss/structural/cta-buttons.scss:assets/css/cta-buttons.css src/scss/structural/home-hero.scss:assets/css/home-hero.css src/scss/structural/work-hero.scss:assets/css/work-hero.css src/scss/structural/work-single-hero.scss:assets/css/work-single-hero.css src/scss/structural/blog-hero.scss:assets/css/blog-hero.css src/scss/structural/blog-all-articles.scss:assets/css/blog-all-articles.css src/scss/structural/blog-writing-cta.scss:assets/css/blog-writing-cta.css src/scss/structural/faq.scss:assets/css/faq.css src/scss/structural/links.scss:assets/css/links.css src/scss/structural/button-secondary.scss:assets/css/button-secondary.css src/scss/structural/featured-work.scss:assets/css/featured-work.css src/scss/structural/where-to-fit.scss:assets/css/where-to-fit.css src/scss/structural/homepage-cta.scss:assets/css/homepage-cta.css src/scss/structural/stats-bar.scss:assets/css/stats-bar.css src/scss/structural/homepage-card-rows.scss:assets/css/homepage-card-rows.css src/scss/structural/homepage-why-lightspeed.scss:assets/css/homepage-why-lightspeed.css src/scss/structural/search-results.scss:assets/css/search-results.css src/scss/structural/services-hero.scss:assets/css/services-hero.css src/scss/structural/services-linked-decisions.scss:assets/css/services-linked-decisions.css" }, "devDependencies": { "@axe-core/playwright": "^4.13.0", diff --git a/patterns/sections/services-linked-decisions.php b/patterns/sections/services-linked-decisions.php new file mode 100644 index 0000000..da96f42 --- /dev/null +++ b/patterns/sections/services-linked-decisions.php @@ -0,0 +1,135 @@ + '01', + 'label' => __( 'Discover', 'ls-theme' ), + 'phase' => 'discover', + 'url' => '/services/discover/', + ), + array( + 'index' => '02', + 'label' => __( 'Create', 'ls-theme' ), + 'phase' => 'create', + 'url' => '/services/create/', + ), + array( + 'index' => '03', + 'label' => __( 'Build', 'ls-theme' ), + 'phase' => 'build', + 'url' => '/services/build/', + ), + array( + 'index' => '04', + 'label' => __( 'Launch', 'ls-theme' ), + 'phase' => 'launch', + 'url' => '/services/launch/', + ), + array( + 'index' => '05', + 'label' => __( 'Grow', 'ls-theme' ), + 'phase' => 'grow', + 'url' => '/services/grow/', + ), + array( + 'index' => '06', + 'label' => __( 'Evolve', 'ls-theme' ), + 'phase' => 'evolve', + 'url' => '/services/evolve/', + ), +); + +$ls_step_arrow_icon = ''; +?> + +
+ + +
+ + +
+ + +
+ + +
+ +
+ + + +

+ +
+ + + +

+ +
+ + + +
+ + +
+ +

+ +
+ + + +
+ $ls_step ) : + $ls_step_phase_color = 'var(--wp--custom--color--phase--' . $ls_step['phase'] . ')'; + $ls_badge_bg = 'color-mix(in srgb, ' . $ls_step_phase_color . ' 10%, transparent)'; + $ls_badge_border = 'color-mix(in srgb, ' . $ls_step_phase_color . ' 30%, transparent)'; + $ls_pill_class = 'ls-process-pill ls-process-pill--' . $ls_step['phase']; + ?> + + +
+ +

+ + + +

+ +
+ + + + +
+ + + + +
+ +
+ +
+ +
+ +
+ diff --git a/src/scss/structural/services-hero.scss b/src/scss/structural/services-hero.scss index c35a373..24647ca 100644 --- a/src/scss/structural/services-hero.scss +++ b/src/scss/structural/services-hero.scss @@ -3,17 +3,19 @@ /* * Services Hero (LS-1598, single consumer patterns/hero/services-hero.php). The service-tag * pills' rest-state background/border tints are set inline per instance (each pill mixes its - * own phase token), because block-supports has no way to express six distinct phase-derived - * colours. Interactive states (:hover, :focus-within, :focus-visible) have no block-supports - * equivalent at all, so they live here. - * - * The `!important` on the hover/focus-within background-color and border-color declarations is - * required, not a shortcut: an inline `style` attribute always outranks an external stylesheet - * rule regardless of pseudo-class or selector specificity, so without it these two properties - * could never change on hover. transform/box-shadow aren't set inline at rest, so they don't - * need it. + * own phase token via colour-mix()) because theme.json has no key for six distinct + * phase-derived tints — that part is not this file's concern. What lives here is only the + * states theme.json genuinely cannot express: see the JSON limitation comments below. */ .ls-service-pill { + // JSON limitation: position:relative here so the whole-pill ::before overlay below has a + // containing block to size against — see AGENTS.md Theme-First Approach ("width", "max-width" + // and similar structural properties JSON has no key for). + position: relative; + + // JSON limitation: block-level :hover/:focus-within has no theme.json pseudo-state key for + // an arbitrary custom class (theme.json's elements.*.:hover only covers link/button/heading/ + // caption/cite sitewide, not a scoped custom component) — see AGENTS.md Theme-First Approach. transition: background-color var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard), border-color var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard), @@ -26,6 +28,22 @@ box-shadow: var(--wp--custom--shadow--elevation--100); } + // JSON limitation: content:"" pseudo-elements have no theme.json equivalent. Only the label + // text is a real , but the whole pill should be clickable, so this ::before is stretched + // to the pill's full box (same technique already used by "Card - Services", see + // styles/sections/cards/card-services.json's .ls-card-services__cta ::before). The anchor + // itself must stay position:static (no position/z-index of its own) so this ::before's + // containing block resolves up to .ls-service-pill instead of shrinking to the anchor's own + // small text box. + a::before { + content: ""; + position: absolute; + inset: 0; + z-index: var(--wp--custom--z-index--base); + } + + // JSON limitation: :focus-visible has no theme.json pseudo-state key — see AGENTS.md + // Theme-First Approach. a:focus-visible { outline: 2px solid var(--wp--custom--color--focus--ring); outline-offset: 2px; @@ -35,6 +53,13 @@ @each $phase in discover, create, build, launch, grow, evolve { .ls-service-pill--#{$phase} { + // JSON limitation: same :hover/:focus-within gap as above. The `!important` here is also + // required, separately: background-color/border-color are set inline per instance at rest + // (colour-mix over each pill's own phase token, itself a JSON limitation — six distinct + // phase-derived tints have no theme.json key), and an inline style attribute always + // outranks an external stylesheet rule regardless of pseudo-class or specificity, so + // without !important these two properties could never change on hover. transform/ + // box-shadow aren't set inline at rest, so they don't need it. &:hover, &:focus-within { background-color: color-mix(in srgb, var(--wp--custom--color--phase--#{$phase}) 16%, transparent) !important; diff --git a/src/scss/structural/services-linked-decisions.scss b/src/scss/structural/services-linked-decisions.scss new file mode 100644 index 0000000..5ae2bb4 --- /dev/null +++ b/src/scss/structural/services-linked-decisions.scss @@ -0,0 +1,91 @@ +/********** Services - Linked Decisions (Services page only) **********/ + +/* + * Services Linked Decisions (LS-1598, single consumer + * patterns/sections/services-linked-decisions.php). The process pills stay neutral overall + * (white/transparent background, dark label) with only the number badge carrying the phase + * colour, so the row doesn't compete with the hero's own phase-tinted tag row above it. See + * the JSON limitation comments below for what specifically requires CSS here. + */ +.ls-process-pill { + position: relative; + + // JSON limitation: block-level :hover/:focus-within has no theme.json pseudo-state key for + // an arbitrary custom class (theme.json's elements.*.:hover only covers link/button/heading/ + // caption/cite sitewide, not a scoped custom component) — see AGENTS.md Theme-First Approach. + transition: + border-color var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard), + transform var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard), + box-shadow var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard); + + &:hover, + &:focus-within { + border-color: var(--wp--custom--color--border--field); + transform: translateY(-1px); + box-shadow: var(--wp--custom--shadow--elevation--100); + } + + .ls-process-pill__label a { + // JSON limitation: content:"" pseudo-elements have no theme.json equivalent — see AGENTS.md + // Theme-First Approach. Only the label text is a real , but the whole pill should be + // clickable, so this ::before is stretched to the pill's full box (same technique already + // used by "Card - Services", see styles/sections/cards/card-services.json's + // .ls-card-services__cta ::before). The anchor itself must stay position:static (no + // position/z-index of its own) so this ::before's containing block resolves up to + // .ls-process-pill (which has position:relative) instead of shrinking to the anchor's own + // small text box — that was the original bug here. + &::before { + content: ""; + position: absolute; + inset: 0; + z-index: var(--wp--custom--z-index--base); + } + + // JSON limitation: :focus-visible has no theme.json pseudo-state key — see AGENTS.md + // Theme-First Approach. + &:focus-visible { + outline: 2px solid var(--wp--custom--color--focus--ring); + outline-offset: 2px; + border-radius: var(--wp--preset--border-radius--100); + } + } + + .ls-process-pill__badge { + transition: + background-color var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard), + border-color var(--wp--custom--animation--duration--responsive) var(--wp--custom--animation--easing--standard); + } +} + +@each $phase in discover, create, build, launch, grow, evolve { + .ls-process-pill--#{$phase} { + // JSON limitation: same :hover/:focus-within gap as above. The `!important` here is also + // required, separately: the badge's background-color/border-color are set inline per + // instance at rest (colour-mix over each pill's own phase token, itself a JSON + // limitation — six distinct phase-derived tints have no theme.json key), and an inline + // style attribute always outranks an external stylesheet rule regardless of pseudo-class, + // so without !important these two properties could never change on hover. + &:hover, + &:focus-within { + .ls-process-pill__badge { + background-color: color-mix(in srgb, var(--wp--custom--color--phase--#{$phase}) 20%, transparent) !important; + border-color: color-mix(in srgb, var(--wp--custom--color--phase--#{$phase}) 50%, transparent) !important; + } + } + } +} + +@media (prefers-reduced-motion: reduce) { + .ls-process-pill { + transition: none; + + &:hover, + &:focus-within { + transform: none; + } + } + + .ls-process-pill__badge { + transition: none; + } +} From 8d12829d524392034ddb403a077d51a8e6ef6bd2 Mon Sep 17 00:00:00 2001 From: Brandonma21 Date: Mon, 7 Sep 2026 16:09:00 +0200 Subject: [PATCH 4/7] Add Services "Service clusters" section (LS-1598) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add patterns/sections/services-service-clusters.php: "Five ways the work groups together" — 5 cluster cards in an asymmetric 3+2 layout, each with an icon well, index number, description, and a footer row of clickable tag links to the existing individual service pages - Add styles/sections/cards/card-cluster.json: new shared card shell (no existing one fit — background and single-link behaviour differ from Card - Category) - Add src/scss/structural/services-service-clusters.scss for the tag hover states, wired into build/enqueue like the other sections Fixes - Equal card height via dimensions.minHeight:100%, footer tags pinned to the bottom via margin-top:auto - Fix a real bug: second card row's blockGap was nested outside spacing instead of inside it, silently using WordPress's default gap instead of matching row 1 - Distinguish the section's background from Section 1's using the existing surface.card token (matches homepage-what-we-build's established alternating-section convention) --- assets/css/services-service-clusters.css | 1 + functions.php | 1 + inc/animations.php | 7 + package.json | 6 +- .../sections/services-service-clusters.php | 248 ++++++++++++++++++ .../structural/services-service-clusters.scss | 36 +++ styles/sections/cards/card-cluster.json | 32 +++ 7 files changed, 328 insertions(+), 3 deletions(-) create mode 100644 assets/css/services-service-clusters.css create mode 100644 patterns/sections/services-service-clusters.php create mode 100644 src/scss/structural/services-service-clusters.scss create mode 100644 styles/sections/cards/card-cluster.json diff --git a/assets/css/services-service-clusters.css b/assets/css/services-service-clusters.css new file mode 100644 index 0000000..671f69f --- /dev/null +++ b/assets/css/services-service-clusters.css @@ -0,0 +1 @@ +.ls-cluster-tag{transition:background-color var(--wp--custom--animation--duration--base) var(--wp--custom--animation--easing--standard),border-color var(--wp--custom--animation--duration--base) var(--wp--custom--animation--easing--standard)}.ls-cluster-tag:hover,.ls-cluster-tag:focus-within{background-color:color-mix(in srgb, var(--wp--custom--color--link--accent) 10%, transparent);border-color:var(--wp--custom--color--link--accent)}.ls-cluster-tag:hover .wp-block-outermost-icon-block .icon-container,.ls-cluster-tag:focus-within .wp-block-outermost-icon-block .icon-container{color:var(--wp--custom--color--text--brand) !important}@media(prefers-reduced-motion: reduce){.ls-cluster-tag{transition:none}} diff --git a/functions.php b/functions.php index e7fde85..bcfd72a 100644 --- a/functions.php +++ b/functions.php @@ -72,6 +72,7 @@ function ls_theme_setup() { add_editor_style( 'assets/css/home-hero.css' ); add_editor_style( 'assets/css/services-hero.css' ); add_editor_style( 'assets/css/services-linked-decisions.css' ); + add_editor_style( 'assets/css/services-service-clusters.css' ); add_editor_style( 'assets/css/work-hero.css' ); add_editor_style( 'assets/css/work-single-hero.css' ); add_editor_style( 'assets/css/blog-hero.css' ); diff --git a/inc/animations.php b/inc/animations.php index 5b705c1..a32a014 100644 --- a/inc/animations.php +++ b/inc/animations.php @@ -73,6 +73,7 @@ function ls_theme_get_bundle_render_markers() { 'home-hero' => array( 'classes' => array( 'ls-home-hero-section' ) ), 'services-hero' => array( 'classes' => array( 'ls-service-pill' ) ), 'services-linked-decisions' => array( 'classes' => array( 'ls-process-pill' ) ), + 'services-service-clusters' => array( 'classes' => array( 'ls-cluster-tag' ) ), 'work-hero' => array( 'classes' => array( 'ls-work-hero' ) ), 'work-single-hero' => array( 'classes' => array( 'ls-work-single-meta' ) ), 'blog-hero' => array( 'classes' => array( 'ls-blog-hero' ) ), @@ -276,6 +277,12 @@ function ls_theme_get_effect_styles( $context = 'front' ) { 'contexts' => array( 'front', 'editor' ), // Same reasoning as services-hero above. ), + 'services-service-clusters' => array( + 'handle' => 'ls-theme-services-service-clusters', + 'path' => 'assets/css/services-service-clusters.css', + 'contexts' => array( 'front', 'editor' ), + // Same reasoning as services-hero above. + ), 'work-hero' => array( 'handle' => 'ls-theme-work-hero', 'path' => 'assets/css/work-hero.css', diff --git a/package.json b/package.json index 0b3480f..723faed 100644 --- a/package.json +++ b/package.json @@ -4,15 +4,15 @@ "description": "LightSpeed Theme is a custom WordPress block theme built by LightSpeed for fast, accessible, maintainable websites using the WordPress Site Editor and block editor.", "type": "module", "scripts": { - "build:css": "sass --no-source-map --no-charset --style=compressed src/scss/animations.scss:assets/css/animations.css src/scss/gsap-animations.scss:assets/css/gsap-animations.css src/scss/structural/taxonomy-filter.scss:assets/css/taxonomy-filter.css src/scss/structural/work-project-card.scss:assets/css/work-project-card.css src/scss/structural/work-archive-sections.scss:assets/css/work-archive-sections.css src/scss/structural/card-shells.scss:assets/css/card-shells.css src/scss/structural/cta-buttons.scss:assets/css/cta-buttons.css src/scss/structural/home-hero.scss:assets/css/home-hero.css src/scss/structural/work-hero.scss:assets/css/work-hero.css src/scss/structural/work-single-hero.scss:assets/css/work-single-hero.css src/scss/structural/blog-hero.scss:assets/css/blog-hero.css src/scss/structural/blog-all-articles.scss:assets/css/blog-all-articles.css src/scss/structural/blog-writing-cta.scss:assets/css/blog-writing-cta.css src/scss/structural/faq.scss:assets/css/faq.css src/scss/structural/links.scss:assets/css/links.css src/scss/structural/button-secondary.scss:assets/css/button-secondary.css src/scss/structural/featured-work.scss:assets/css/featured-work.css src/scss/structural/where-to-fit.scss:assets/css/where-to-fit.css src/scss/structural/homepage-cta.scss:assets/css/homepage-cta.css src/scss/structural/stats-bar.scss:assets/css/stats-bar.css src/scss/structural/homepage-card-rows.scss:assets/css/homepage-card-rows.css src/scss/structural/homepage-why-lightspeed.scss:assets/css/homepage-why-lightspeed.css src/scss/structural/search-results.scss:assets/css/search-results.css src/scss/structural/services-hero.scss:assets/css/services-hero.css src/scss/structural/services-linked-decisions.scss:assets/css/services-linked-decisions.css", - "build:css:dev": "sass --no-source-map --no-charset --style=expanded src/scss/animations.scss:assets/css/animations.css src/scss/gsap-animations.scss:assets/css/gsap-animations.css src/scss/structural/taxonomy-filter.scss:assets/css/taxonomy-filter.css src/scss/structural/work-project-card.scss:assets/css/work-project-card.css src/scss/structural/work-archive-sections.scss:assets/css/work-archive-sections.css src/scss/structural/card-shells.scss:assets/css/card-shells.css src/scss/structural/cta-buttons.scss:assets/css/cta-buttons.css src/scss/structural/home-hero.scss:assets/css/home-hero.css src/scss/structural/work-hero.scss:assets/css/work-hero.css src/scss/structural/work-single-hero.scss:assets/css/work-single-hero.css src/scss/structural/blog-hero.scss:assets/css/blog-hero.css src/scss/structural/blog-all-articles.scss:assets/css/blog-all-articles.css src/scss/structural/blog-writing-cta.scss:assets/css/blog-writing-cta.css src/scss/structural/faq.scss:assets/css/faq.css src/scss/structural/links.scss:assets/css/links.css src/scss/structural/button-secondary.scss:assets/css/button-secondary.css src/scss/structural/featured-work.scss:assets/css/featured-work.css src/scss/structural/where-to-fit.scss:assets/css/where-to-fit.css src/scss/structural/homepage-cta.scss:assets/css/homepage-cta.css src/scss/structural/stats-bar.scss:assets/css/stats-bar.css src/scss/structural/homepage-card-rows.scss:assets/css/homepage-card-rows.css src/scss/structural/homepage-why-lightspeed.scss:assets/css/homepage-why-lightspeed.css src/scss/structural/search-results.scss:assets/css/search-results.css src/scss/structural/services-hero.scss:assets/css/services-hero.css src/scss/structural/services-linked-decisions.scss:assets/css/services-linked-decisions.css", + "build:css": "sass --no-source-map --no-charset --style=compressed src/scss/animations.scss:assets/css/animations.css src/scss/gsap-animations.scss:assets/css/gsap-animations.css src/scss/structural/taxonomy-filter.scss:assets/css/taxonomy-filter.css src/scss/structural/work-project-card.scss:assets/css/work-project-card.css src/scss/structural/work-archive-sections.scss:assets/css/work-archive-sections.css src/scss/structural/card-shells.scss:assets/css/card-shells.css src/scss/structural/cta-buttons.scss:assets/css/cta-buttons.css src/scss/structural/home-hero.scss:assets/css/home-hero.css src/scss/structural/work-hero.scss:assets/css/work-hero.css src/scss/structural/work-single-hero.scss:assets/css/work-single-hero.css src/scss/structural/blog-hero.scss:assets/css/blog-hero.css src/scss/structural/blog-all-articles.scss:assets/css/blog-all-articles.css src/scss/structural/blog-writing-cta.scss:assets/css/blog-writing-cta.css src/scss/structural/faq.scss:assets/css/faq.css src/scss/structural/links.scss:assets/css/links.css src/scss/structural/button-secondary.scss:assets/css/button-secondary.css src/scss/structural/featured-work.scss:assets/css/featured-work.css src/scss/structural/where-to-fit.scss:assets/css/where-to-fit.css src/scss/structural/homepage-cta.scss:assets/css/homepage-cta.css src/scss/structural/stats-bar.scss:assets/css/stats-bar.css src/scss/structural/homepage-card-rows.scss:assets/css/homepage-card-rows.css src/scss/structural/homepage-why-lightspeed.scss:assets/css/homepage-why-lightspeed.css src/scss/structural/search-results.scss:assets/css/search-results.css src/scss/structural/services-hero.scss:assets/css/services-hero.css src/scss/structural/services-linked-decisions.scss:assets/css/services-linked-decisions.css src/scss/structural/services-service-clusters.scss:assets/css/services-service-clusters.css", + "build:css:dev": "sass --no-source-map --no-charset --style=expanded src/scss/animations.scss:assets/css/animations.css src/scss/gsap-animations.scss:assets/css/gsap-animations.css src/scss/structural/taxonomy-filter.scss:assets/css/taxonomy-filter.css src/scss/structural/work-project-card.scss:assets/css/work-project-card.css src/scss/structural/work-archive-sections.scss:assets/css/work-archive-sections.css src/scss/structural/card-shells.scss:assets/css/card-shells.css src/scss/structural/cta-buttons.scss:assets/css/cta-buttons.css src/scss/structural/home-hero.scss:assets/css/home-hero.css src/scss/structural/work-hero.scss:assets/css/work-hero.css src/scss/structural/work-single-hero.scss:assets/css/work-single-hero.css src/scss/structural/blog-hero.scss:assets/css/blog-hero.css src/scss/structural/blog-all-articles.scss:assets/css/blog-all-articles.css src/scss/structural/blog-writing-cta.scss:assets/css/blog-writing-cta.css src/scss/structural/faq.scss:assets/css/faq.css src/scss/structural/links.scss:assets/css/links.css src/scss/structural/button-secondary.scss:assets/css/button-secondary.css src/scss/structural/featured-work.scss:assets/css/featured-work.css src/scss/structural/where-to-fit.scss:assets/css/where-to-fit.css src/scss/structural/homepage-cta.scss:assets/css/homepage-cta.css src/scss/structural/stats-bar.scss:assets/css/stats-bar.css src/scss/structural/homepage-card-rows.scss:assets/css/homepage-card-rows.css src/scss/structural/homepage-why-lightspeed.scss:assets/css/homepage-why-lightspeed.css src/scss/structural/search-results.scss:assets/css/search-results.css src/scss/structural/services-hero.scss:assets/css/services-hero.css src/scss/structural/services-linked-decisions.scss:assets/css/services-linked-decisions.css src/scss/structural/services-service-clusters.scss:assets/css/services-service-clusters.css", "schema:validate": "node theme-utils.mjs validate-schema", "theme:validate": "node theme-utils.mjs validate-theme", "patterns:escape": "node theme-utils.mjs escape-patterns", "security:scan": "node theme-utils.mjs security-scan", "lint": "npm run lint:json", "lint:json": "node --input-type=module --eval \"import { readFileSync } from 'fs'; import { glob } from 'glob'; const files = await glob(['theme.json', 'styles/**/*.json']); let ok = true; for (const f of files) { try { JSON.parse(readFileSync(f, 'utf8')); } catch (e) { console.error('Invalid JSON:', f, e.message); ok = false; } } if (ok) console.log('All JSON files are valid.'); else process.exit(1);\"", - "watch:css": "sass --watch --no-source-map --no-charset --style=expanded src/scss/animations.scss:assets/css/animations.css src/scss/gsap-animations.scss:assets/css/gsap-animations.css src/scss/structural/taxonomy-filter.scss:assets/css/taxonomy-filter.css src/scss/structural/work-project-card.scss:assets/css/work-project-card.css src/scss/structural/work-archive-sections.scss:assets/css/work-archive-sections.css src/scss/structural/card-shells.scss:assets/css/card-shells.css src/scss/structural/cta-buttons.scss:assets/css/cta-buttons.css src/scss/structural/home-hero.scss:assets/css/home-hero.css src/scss/structural/work-hero.scss:assets/css/work-hero.css src/scss/structural/work-single-hero.scss:assets/css/work-single-hero.css src/scss/structural/blog-hero.scss:assets/css/blog-hero.css src/scss/structural/blog-all-articles.scss:assets/css/blog-all-articles.css src/scss/structural/blog-writing-cta.scss:assets/css/blog-writing-cta.css src/scss/structural/faq.scss:assets/css/faq.css src/scss/structural/links.scss:assets/css/links.css src/scss/structural/button-secondary.scss:assets/css/button-secondary.css src/scss/structural/featured-work.scss:assets/css/featured-work.css src/scss/structural/where-to-fit.scss:assets/css/where-to-fit.css src/scss/structural/homepage-cta.scss:assets/css/homepage-cta.css src/scss/structural/stats-bar.scss:assets/css/stats-bar.css src/scss/structural/homepage-card-rows.scss:assets/css/homepage-card-rows.css src/scss/structural/homepage-why-lightspeed.scss:assets/css/homepage-why-lightspeed.css src/scss/structural/search-results.scss:assets/css/search-results.css src/scss/structural/services-hero.scss:assets/css/services-hero.css src/scss/structural/services-linked-decisions.scss:assets/css/services-linked-decisions.css" + "watch:css": "sass --watch --no-source-map --no-charset --style=expanded src/scss/animations.scss:assets/css/animations.css src/scss/gsap-animations.scss:assets/css/gsap-animations.css src/scss/structural/taxonomy-filter.scss:assets/css/taxonomy-filter.css src/scss/structural/work-project-card.scss:assets/css/work-project-card.css src/scss/structural/work-archive-sections.scss:assets/css/work-archive-sections.css src/scss/structural/card-shells.scss:assets/css/card-shells.css src/scss/structural/cta-buttons.scss:assets/css/cta-buttons.css src/scss/structural/home-hero.scss:assets/css/home-hero.css src/scss/structural/work-hero.scss:assets/css/work-hero.css src/scss/structural/work-single-hero.scss:assets/css/work-single-hero.css src/scss/structural/blog-hero.scss:assets/css/blog-hero.css src/scss/structural/blog-all-articles.scss:assets/css/blog-all-articles.css src/scss/structural/blog-writing-cta.scss:assets/css/blog-writing-cta.css src/scss/structural/faq.scss:assets/css/faq.css src/scss/structural/links.scss:assets/css/links.css src/scss/structural/button-secondary.scss:assets/css/button-secondary.css src/scss/structural/featured-work.scss:assets/css/featured-work.css src/scss/structural/where-to-fit.scss:assets/css/where-to-fit.css src/scss/structural/homepage-cta.scss:assets/css/homepage-cta.css src/scss/structural/stats-bar.scss:assets/css/stats-bar.css src/scss/structural/homepage-card-rows.scss:assets/css/homepage-card-rows.css src/scss/structural/homepage-why-lightspeed.scss:assets/css/homepage-why-lightspeed.css src/scss/structural/search-results.scss:assets/css/search-results.css src/scss/structural/services-hero.scss:assets/css/services-hero.css src/scss/structural/services-linked-decisions.scss:assets/css/services-linked-decisions.css src/scss/structural/services-service-clusters.scss:assets/css/services-service-clusters.css" }, "devDependencies": { "@axe-core/playwright": "^4.13.0", diff --git a/patterns/sections/services-service-clusters.php b/patterns/sections/services-service-clusters.php new file mode 100644 index 0000000..c3640ab --- /dev/null +++ b/patterns/sections/services-service-clusters.php @@ -0,0 +1,248 @@ +'; + +$ls_service_icons = array( + 'discovery' => '', + 'content' => '', + 'design' => '', + 'development' => '', + 'migrations' => '', + 'hosting' => '', + 'training' => '', + 'support' => '', + 'ai' => '', + 'seo' => '', +); + +$ls_service_urls = array( + 'discovery' => '/services/discovery/', + 'content' => '/services/content/', + 'design' => '/services/design/', + 'development' => '/services/development/', + 'migrations' => '/services/migrations/', + 'hosting' => '/services/hosting/', + 'training' => '/services/training/', + 'support' => '/services/support/', + 'ai' => '/services/ai/', + 'seo' => '/services/seo/', +); + +$ls_service_labels = array( + 'discovery' => __( 'Discovery', 'ls-theme' ), + 'content' => __( 'Content', 'ls-theme' ), + 'design' => __( 'Design', 'ls-theme' ), + 'development' => __( 'Development', 'ls-theme' ), + 'migrations' => __( 'Migrations', 'ls-theme' ), + 'hosting' => __( 'Hosting', 'ls-theme' ), + 'training' => __( 'Training', 'ls-theme' ), + 'support' => __( 'Support', 'ls-theme' ), + 'ai' => __( 'AI', 'ls-theme' ), + 'seo' => __( 'SEO', 'ls-theme' ), +); + +$ls_clusters = array( + array( + 'index' => '01', + 'icon' => '', + 'title' => __( 'Discovery and content', 'ls-theme' ), + 'description' => __( 'For scope clarity, content structure, risk reduction and better planning before design or build decisions harden.', 'ls-theme' ), + 'tags' => array( 'discovery', 'content' ), + ), + array( + 'index' => '02', + 'icon' => '', + 'title' => __( 'Design systems and accessibility', 'ls-theme' ), + 'description' => __( 'For clearer interfaces, stronger consistency and more maintainable design-to-development translation.', 'ls-theme' ), + 'tags' => array( 'design' ), + ), + array( + 'index' => '03', + 'icon' => '', + 'title' => __( 'Engineering and migrations', 'ls-theme' ), + 'description' => __( 'For custom WordPress work, integrations, migrations and cleaner long-term architecture.', 'ls-theme' ), + 'tags' => array( 'development', 'migrations' ), + ), + array( + 'index' => '04', + 'icon' => '', + 'title' => __( 'Launch and infrastructure', 'ls-theme' ), + 'description' => __( 'For hosting, readiness, security-minded thinking and performance-aware go-live planning.', 'ls-theme' ), + 'tags' => array( 'hosting', 'training' ), + ), + array( + 'index' => '05', + 'icon' => '', + 'title' => __( 'Support and optimisation', 'ls-theme' ), + 'description' => __( 'For maintenance, issue resolution, performance review, training and long-term platform continuity.', 'ls-theme' ), + 'tags' => array( 'support', 'ai', 'seo' ), + ), +); + +/** + * Renders one service-cluster card. A local closure, not a named function — this file can be + * included more than once per request (pattern registration + a live "wp:pattern" reference), + * and a top-level `function` declaration here would fatal with "cannot redeclare" on the second + * include. + * + * @param array $ls_cluster Cluster data (index, icon, title, description, tags). + */ +$ls_render_cluster = function ( $ls_cluster ) use ( $ls_service_icons, $ls_service_urls, $ls_service_labels, $ls_arrow_icon ) { + ?> + + + + + +
+ + +
+ + +
+ + +
+ +
+ +
+ + + +

+ +
+ + + +

+ +
+ + + +
+ +
+ +

+ +
+ +
+ +
+ + + +
+ + +
+ +
+ + + +
+ +
+ + + +
+ +
+ +
+ + + +
+ + +
+ +
+ + + +
+ +
+ +
+ +
+ +
+ diff --git a/src/scss/structural/services-service-clusters.scss b/src/scss/structural/services-service-clusters.scss new file mode 100644 index 0000000..ddc8260 --- /dev/null +++ b/src/scss/structural/services-service-clusters.scss @@ -0,0 +1,36 @@ +/********** Services - Service Clusters (Services page only) **********/ + +/* + * Services Service Clusters (LS-1598, single consumer + * patterns/sections/services-service-clusters.php). Rest-state look, the stretched-link + * ::before, and :focus-visible all live in styles/sections/cards/card-cluster.json — this file + * only carries the hover-state motion, matching the existing "Card - Link Row" convention (see + * .is-style-card-link-row in src/scss/structural/work-archive-sections.scss): a background/ + * border-color tint swap on hover, nothing heavier. The card itself has no hover-lift — unlike + * Card - Category, it isn't a single link, so it shouldn't look interactive as a whole. + */ +.ls-cluster-tag { + // JSON limitation: block-level :hover/:focus-within has no theme.json pseudo-state key for + // an arbitrary custom class (theme.json's elements.*.:hover only covers link/button/heading/ + // caption/cite sitewide, not a scoped custom component) — see AGENTS.md Theme-First Approach. + transition: + background-color var(--wp--custom--animation--duration--base) var(--wp--custom--animation--easing--standard), + border-color var(--wp--custom--animation--duration--base) var(--wp--custom--animation--easing--standard); + + &:hover, + &:focus-within { + background-color: color-mix(in srgb, var(--wp--custom--color--link--accent) 10%, transparent); + border-color: var(--wp--custom--color--link--accent); + } + + &:hover .wp-block-outermost-icon-block .icon-container, + &:focus-within .wp-block-outermost-icon-block .icon-container { + color: var(--wp--custom--color--text--brand) !important; + } +} + +@media (prefers-reduced-motion: reduce) { + .ls-cluster-tag { + transition: none; + } +} diff --git a/styles/sections/cards/card-cluster.json b/styles/sections/cards/card-cluster.json new file mode 100644 index 0000000..1e260b7 --- /dev/null +++ b/styles/sections/cards/card-cluster.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://schemas.wp.org/wp/6.9/theme.json", + "version": 3, + "title": "Card - Cluster", + "slug": "card-cluster", + "blockTypes": [ + "core/group" + ], + "description": "Service-cluster card shell: icon well + index number header, H3 heading, supporting copy, and a footer row of small clickable tag links (each its own stretched-link pill), separated by a top divider. Unlike Card - Category, the card itself is not a single link — only the footer tags are. Non-expressible structural properties (position, overflow, the nested content flex rule, each tag's own stretched-link containing block) stay in this css field; the tag hover-tint transition stays in src/scss/structural/services-service-clusters.scss (a bare &:hover in this css field is silently dropped, same class of bug as @media/@supports stripping).", + "styles": { + "color": { + "background": "var:custom|color|surface|canvas", + "text": "var:custom|color|text|default" + }, + "border": { + "color": "var:custom|color|border|card", + "radius": "var:preset|border-radius|300", + "style": "solid", + "width": "1px" + }, + "spacing": { + "blockGap": "var:preset|spacing|20", + "padding": { + "top": "var:preset|spacing|30", + "right": "var:preset|spacing|30", + "bottom": "var:preset|spacing|30", + "left": "var:preset|spacing|30" + } + }, + "css": "position: relative;\noverflow: hidden;\n\n& .ls-card-cluster__content {\n\tflex: 1 1 auto;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: var(--wp--preset--spacing--10);\n}\n\n& .ls-card-cluster__tags {\n\tborder-top: 1px solid var(--wp--custom--color--border--card);\n}\n\n& .ls-cluster-tag {\n\tposition: relative;\n}\n\n& .ls-cluster-tag__link::before {\n\tcontent: \"\";\n\tposition: absolute;\n\tinset: 0;\n}\n\n& .ls-cluster-tag__link:focus-visible {\n\toutline: 2px solid var(--wp--custom--color--focus--ring);\n\toutline-offset: 2px;\n}" + } +} From 9f75e738381ef6e1dbbaa8eb00ae314f3d292b3e Mon Sep 17 00:00:00 2001 From: Brandonma21 Date: Wed, 9 Sep 2026 14:12:12 +0200 Subject: [PATCH 5/7] Migrate Services patterns to the Core Icon block (LS-3229) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Icon block conversion - Replace outermost/icon-block with core/icon across 3 Services files - Reference lightspeed/{name} icons instead of inline SVG - Cover all 47 instances (15 + 6 + 26), zero remaining outermost/icon-block Icons used - services-hero: dot, search, file-text, paint-brush, code, arrows-left-right, cloud, gauge, shield, graduation-cap, lifebuoy, chart-line-up, wheelchair, envelope, special-interests - services-linked-decisions: dot, arrow-right - services-service-clusters: dot, search, paint-brush, code, rocket, question, arrows-left-right, cloud, graduation-cap, lifebuoy, sparkle, chart-line-up, arrow-right, file-text Structural change - These 3 files use PHP arrays/loops to render icons dynamically (per-service, per-cluster, per-step), unlike the static per-instance blocks in Batches 1-4 - Converted PHP icon arrays to store lightspeed/{name} slugs instead of raw SVG, and rewrote loop templates to emit wp:icon dynamically - Removed now-unused $ls_step_arrow_icon / $ls_arrow_icon SVG variables - sparkle/question used here vs special-interests/help elsewhere for the same shapes — file-specific disambiguation, consistent with earlier batches Verification - Zero remaining outermost/icon-block references across all 3 files - PHP block-rendering test (including loop execution) confirms correct SVG output and slug resolution, no errors --- patterns/hero/services-hero.php | 36 ++++++------- .../sections/services-linked-decisions.php | 10 +--- .../sections/services-service-clusters.php | 50 ++++++++----------- 3 files changed, 38 insertions(+), 58 deletions(-) diff --git a/patterns/hero/services-hero.php b/patterns/hero/services-hero.php index 9c4fe2a..84771a1 100644 --- a/patterns/hero/services-hero.php +++ b/patterns/hero/services-hero.php @@ -17,85 +17,85 @@ 'label' => __( 'Discovery', 'ls-theme' ), 'url' => '/services/discovery/', 'phase' => 'discover', - 'icon' => '', + 'icon' => 'search', ), array( 'label' => __( 'Content', 'ls-theme' ), 'url' => '/services/content/', 'phase' => 'create', - 'icon' => '', + 'icon' => 'file-text', ), array( 'label' => __( 'Design', 'ls-theme' ), 'url' => '/services/design/', 'phase' => 'create', - 'icon' => '', + 'icon' => 'paint-brush', ), array( 'label' => __( 'Development', 'ls-theme' ), 'url' => '/services/development/', 'phase' => 'build', - 'icon' => '', + 'icon' => 'code', ), array( 'label' => __( 'Migrations', 'ls-theme' ), 'url' => '/services/migrations/', 'phase' => 'build', - 'icon' => '', + 'icon' => 'arrows-left-right', ), array( 'label' => __( 'Hosting', 'ls-theme' ), 'url' => '/services/hosting/', 'phase' => 'launch', - 'icon' => '', + 'icon' => 'cloud', ), array( 'label' => __( 'Performance', 'ls-theme' ), 'url' => '/services/performance/', 'phase' => 'launch', - 'icon' => '', + 'icon' => 'gauge', ), array( 'label' => __( 'Security', 'ls-theme' ), 'url' => '/services/security/', 'phase' => 'launch', - 'icon' => '', + 'icon' => 'shield', ), array( 'label' => __( 'Training', 'ls-theme' ), 'url' => '/services/training/', 'phase' => 'launch', - 'icon' => '', + 'icon' => 'graduation-cap', ), array( 'label' => __( 'Support', 'ls-theme' ), 'url' => '/services/support/', 'phase' => 'grow', - 'icon' => '', + 'icon' => 'lifebuoy', ), array( 'label' => __( 'SEO', 'ls-theme' ), 'url' => '/services/seo/', 'phase' => 'grow', - 'icon' => '', + 'icon' => 'chart-line-up', ), array( 'label' => __( 'Accessibility', 'ls-theme' ), 'url' => '/services/accessibility/', 'phase' => 'grow', - 'icon' => '', + 'icon' => 'wheelchair', ), array( 'label' => __( 'Email marketing', 'ls-theme' ), 'url' => '/services/email-marketing/', 'phase' => 'grow', - 'icon' => '', + 'icon' => 'envelope', ), array( 'label' => __( 'AI', 'ls-theme' ), 'url' => '/services/ai/', 'phase' => 'evolve', - 'icon' => '', + 'icon' => 'special-interests', ), ); ?> @@ -111,9 +111,7 @@
- -
- +

@@ -162,9 +160,7 @@ ?>
- -
- +

diff --git a/patterns/sections/services-linked-decisions.php b/patterns/sections/services-linked-decisions.php index da96f42..97833a0 100644 --- a/patterns/sections/services-linked-decisions.php +++ b/patterns/sections/services-linked-decisions.php @@ -50,8 +50,6 @@ 'url' => '/services/evolve/', ), ); - -$ls_step_arrow_icon = ''; ?>
@@ -67,9 +65,7 @@
- -
- +

@@ -117,9 +113,7 @@ - -
- + diff --git a/patterns/sections/services-service-clusters.php b/patterns/sections/services-service-clusters.php index c3640ab..2f1257d 100644 --- a/patterns/sections/services-service-clusters.php +++ b/patterns/sections/services-service-clusters.php @@ -12,19 +12,17 @@ * @package ls-theme */ -$ls_arrow_icon = ''; - $ls_service_icons = array( - 'discovery' => '', - 'content' => '', - 'design' => '', - 'development' => '', - 'migrations' => '', - 'hosting' => '', - 'training' => '', - 'support' => '', - 'ai' => '', - 'seo' => '', + 'discovery' => 'search', + 'content' => 'file-text', + 'design' => 'paint-brush', + 'development' => 'code', + 'migrations' => 'arrows-left-right', + 'hosting' => 'cloud', + 'training' => 'graduation-cap', + 'support' => 'lifebuoy', + 'ai' => 'sparkle', + 'seo' => 'chart-line-up', ); $ls_service_urls = array( @@ -56,35 +54,35 @@ $ls_clusters = array( array( 'index' => '01', - 'icon' => '', + 'icon' => 'search', 'title' => __( 'Discovery and content', 'ls-theme' ), 'description' => __( 'For scope clarity, content structure, risk reduction and better planning before design or build decisions harden.', 'ls-theme' ), 'tags' => array( 'discovery', 'content' ), ), array( 'index' => '02', - 'icon' => '', + 'icon' => 'paint-brush', 'title' => __( 'Design systems and accessibility', 'ls-theme' ), 'description' => __( 'For clearer interfaces, stronger consistency and more maintainable design-to-development translation.', 'ls-theme' ), 'tags' => array( 'design' ), ), array( 'index' => '03', - 'icon' => '', + 'icon' => 'code', 'title' => __( 'Engineering and migrations', 'ls-theme' ), 'description' => __( 'For custom WordPress work, integrations, migrations and cleaner long-term architecture.', 'ls-theme' ), 'tags' => array( 'development', 'migrations' ), ), array( 'index' => '04', - 'icon' => '', + 'icon' => 'rocket', 'title' => __( 'Launch and infrastructure', 'ls-theme' ), 'description' => __( 'For hosting, readiness, security-minded thinking and performance-aware go-live planning.', 'ls-theme' ), 'tags' => array( 'hosting', 'training' ), ), array( 'index' => '05', - 'icon' => '', + 'icon' => 'question', 'title' => __( 'Support and optimisation', 'ls-theme' ), 'description' => __( 'For maintenance, issue resolution, performance review, training and long-term platform continuity.', 'ls-theme' ), 'tags' => array( 'support', 'ai', 'seo' ), @@ -99,7 +97,7 @@ * * @param array $ls_cluster Cluster data (index, icon, title, description, tags). */ -$ls_render_cluster = function ( $ls_cluster ) use ( $ls_service_icons, $ls_service_urls, $ls_service_labels, $ls_arrow_icon ) { +$ls_render_cluster = function ( $ls_cluster ) use ( $ls_service_icons, $ls_service_urls, $ls_service_labels ) { ?>
@@ -108,9 +106,7 @@
- -
- +
@@ -138,17 +134,13 @@
- -
- +

- -
- +
@@ -173,9 +165,7 @@
- -
- +

From ec2086ab8278c2855f507fcb1cbf8d0929c2e18c Mon Sep 17 00:00:00 2001 From: Brandonma21 Date: Wed, 9 Sep 2026 14:17:06 +0200 Subject: [PATCH 6/7] Add CHANGELOG entry for Services icon-block migration (LS-3229) Documents PR #50 per this repo's changelog convention. --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d09cdaf..df0c10f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- +## [Unreleased] — Migrate Services patterns to the Core Icon block (LS-3229) + +### Changed + +- Replaced `outermost/icon-block` (third-party "Icon Block" plugin) with WordPress 7.1's native Core Icon block (`core/icon`) across the 3 Services page patterns — `patterns/hero/services-hero.php`, `patterns/sections/services-linked-decisions.php`, `patterns/sections/services-service-clusters.php` — referencing icons from the `lightspeed` collection registered in `ls-plugin`. Final batch of the Core Icon block migration (LS-3229). +- Converted the PHP icon arrays in these files (per-service, per-cluster, per-step) from raw inline SVG to bare `lightspeed/{name}` slugs, and rewrote their loop templates to emit `core/icon` block markup dynamically per iteration, removing the now-unused raw-SVG helper variables. + +See [PR #50](https://github.com/lightspeedwp/ls-theme/pull/50). + +--- + ## [Unreleased] — Fix legacy outermost/icon-block CSS selectors broken by Core Icon migration (LS-3720) ### Fixed From feb44df4fbd2875e6726093e9bea42b19d6aefe7 Mon Sep 17 00:00:00 2001 From: Brandonma21 Date: Thu, 10 Sep 2026 07:46:24 +0200 Subject: [PATCH 7/7] Update CHANGELOG.md Context - Documents the Services page hero, linked-decisions, and service-clusters patterns added in PR #51 --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 052ce67..cfe5dd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- +## [Unreleased] — Build Services page: hero, linked decisions, and service clusters (LS-1598) + +### Added + +- Added `patterns/hero/services-hero.php`: breadcrumb, eyebrow, heading, description, CTAs, and a lifecycle-phase-coloured row of all 14 service tags linking to their `/services/` pages. +- Added `patterns/sections/services-linked-decisions.php`: a six-step process pill row (Discover/Create/Build/Launch/Grow/Evolve) linking to new `/services/` phase pages, with a fixed whole-pill click target. +- Added `patterns/sections/services-service-clusters.php`: five cluster cards in an asymmetric 3+2 layout grouping all 14 services, each linking out to its individual service page. +- Added `styles/sections/cards/card-cluster.json`, a new shared card shell for the service-clusters section. + +### Fixed + +- Fixed the second service-clusters card row's `blockGap` being nested outside `spacing` instead of inside it, which silently fell back to WordPress's default gap instead of matching row 1. + ## [Unreleased] — Fix button.fill color contrast (LS-2937) ### Fixed