diff --git a/CHANGELOG.md b/CHANGELOG.md index 90b3461..eb25727 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] — Add flatten-proof phase colour and a phase-services dynamic block (LS-4179) + +### Fixed + +- Phase page colour (hero buttons, breadcrumb "current page" text) no longer goes black/white when a phase section's pattern is flattened by the block editor, and no longer stays hardcoded to Discover's green on the other five phase pages. Added `--ls-phase-accent`/`--ls-phase-accent-on-dark` custom properties (`src/scss/structural/phase-journey-nav.scss`), set per phase via the existing `page-slug-{phase}` body class, so colour is resolved by CSS on every request instead of being computed in PHP or baked into a pattern's stored content. `styles/blocks/buttons/button-phase-primary.json`, `button-phase-outline.json`, `src/scss/structural/button-phase.scss`, and `src/scss/structural/phase-hero.scss` were hardwired directly to Discover's token — swapped all four to the new shared property. +- `patterns/sections/phase-services-in-phase.php` selected its "Services in this phase" cards via `get_queried_object()` inside the pattern's own PHP, which only re-evaluates while the pattern stays a live `wp:pattern` reference, falling back to Discover's services once flattened. Moved that selection into a new `ls-theme/phase-services` dynamic block (`blocks/phase-services/`), which resolves the current phase from block context at real render time and isn't affected by pattern flattening. + +([#72](https://github.com/lightspeedwp/ls-theme/pull/72)) + +--- + ## [Unreleased] — Refine Phase Where To Go Next section to match prototype (LS-4179) ### Changed diff --git a/assets/css/button-phase.css b/assets/css/button-phase.css index 0ee6cf4..6225899 100644 --- a/assets/css/button-phase.css +++ b/assets/css/button-phase.css @@ -1 +1 @@ -.wp-block-button.is-style-button-phase-primary,.wp-block-button.is-style-button-phase-outline{border-radius:var(--wp--preset--border-radius--500)}.wp-block-button.is-style-button-phase-primary:hover,.wp-block-button.is-style-button-phase-outline:hover{border-radius:var(--wp--preset--border-radius--300)}.wp-block-button.is-style-button-phase-primary .wp-block-button__link,.wp-block-button.is-style-button-phase-outline .wp-block-button__link{border-radius:inherit}.wp-block-button.is-style-button-phase-primary:hover .wp-block-button__link,.wp-block-button.is-style-button-phase-outline:hover .wp-block-button__link{border-radius:var(--wp--preset--border-radius--300)}.wp-block-button.is-style-button-phase-outline:hover .wp-block-button__link{color:var(--wp--custom--color--text--on-light);border-color:rgba(0,0,0,0)}.wp-block-button.is-style-button-phase-outline .wp-block-button__link::before{content:"";position:absolute;top:50%;left:50%;min-width:100%;min-height:100%;aspect-ratio:1;transform:translate(-50%, -50%) scale(0);border-radius:50%;background-color:var(--wp--custom--color--phase--discover-on-dark);opacity:0;z-index:-1}.wp-block-button.is-style-button-phase-outline:hover .wp-block-button__link::before{transform:translate(-50%, -50%) scale(1.42);opacity:1} +.wp-block-button.is-style-button-phase-primary,.wp-block-button.is-style-button-phase-outline{border-radius:var(--wp--preset--border-radius--500)}.wp-block-button.is-style-button-phase-primary:hover,.wp-block-button.is-style-button-phase-outline:hover{border-radius:var(--wp--preset--border-radius--300)}.wp-block-button.is-style-button-phase-primary .wp-block-button__link,.wp-block-button.is-style-button-phase-outline .wp-block-button__link{border-radius:inherit}.wp-block-button.is-style-button-phase-primary:hover .wp-block-button__link,.wp-block-button.is-style-button-phase-outline:hover .wp-block-button__link{border-radius:var(--wp--preset--border-radius--300)}.wp-block-button.is-style-button-phase-outline:hover .wp-block-button__link{color:var(--wp--custom--color--text--on-light);border-color:rgba(0,0,0,0)}.wp-block-button.is-style-button-phase-outline .wp-block-button__link::before{content:"";position:absolute;top:50%;left:50%;min-width:100%;min-height:100%;aspect-ratio:1;transform:translate(-50%, -50%) scale(0);border-radius:50%;background-color:var(--ls-phase-accent-on-dark);opacity:0;z-index:-1}.wp-block-button.is-style-button-phase-outline:hover .wp-block-button__link::before{transform:translate(-50%, -50%) scale(1.42);opacity:1} diff --git a/assets/css/phase-hero.css b/assets/css/phase-hero.css index efd1230..1d38367 100644 --- a/assets/css/phase-hero.css +++ b/assets/css/phase-hero.css @@ -1 +1 @@ -.ls-phase-hero{position:relative;overflow:hidden;background-image:linear-gradient(color-mix(in srgb, var(--wp--custom--color--text--on-dark) 2%, transparent) 1px, transparent 1px),linear-gradient(90deg, color-mix(in srgb, var(--wp--custom--color--text--on-dark) 2%, transparent) 1px, transparent 1px),radial-gradient(60% 50% at 50% 22%, color-mix(in srgb, var(--wp--custom--color--phase--discover-on-dark) 14%, transparent) 0%, transparent 70%),linear-gradient(158deg, var(--wp--custom--color--surface--band-start) 0%, var(--wp--custom--color--surface--band-end) 100%);background-position:0 0,0 0,0 0,0 0;background-repeat:repeat,repeat,no-repeat,no-repeat;background-size:32px 32px,32px 32px,auto,auto}.ls-phase-hero .ls-breadcrumbs-on-dark a{color:var(--wp--custom--color--text--on-dark-muted)}.ls-phase-hero .breadcrumb_last{color:var(--wp--custom--color--phase--discover-on-dark)}.ls-phase-hero__share{background-color:color-mix(in srgb, var(--wp--custom--color--text--on-dark) 5%, transparent);border-color:color-mix(in srgb, var(--wp--custom--color--text--on-dark) 8%, transparent);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-phase-hero__share:hover,.ls-phase-hero__share:focus-within{background-color:color-mix(in srgb, var(--wp--custom--color--text--on-dark) 9%, transparent);border-color:color-mix(in srgb, var(--wp--custom--color--text--on-dark) 16%, transparent)}@media(prefers-reduced-motion: reduce){.ls-phase-hero__share{transition:none}} +.ls-phase-hero{position:relative;overflow:hidden;background-image:linear-gradient(color-mix(in srgb, var(--wp--custom--color--text--on-dark) 2%, transparent) 1px, transparent 1px),linear-gradient(90deg, color-mix(in srgb, var(--wp--custom--color--text--on-dark) 2%, transparent) 1px, transparent 1px),radial-gradient(60% 50% at 50% 22%, color-mix(in srgb, var(--ls-phase-accent-on-dark) 14%, transparent) 0%, transparent 70%),linear-gradient(158deg, var(--wp--custom--color--surface--band-start) 0%, var(--wp--custom--color--surface--band-end) 100%);background-position:0 0,0 0,0 0,0 0;background-repeat:repeat,repeat,no-repeat,no-repeat;background-size:32px 32px,32px 32px,auto,auto}.ls-phase-hero .ls-breadcrumbs-on-dark a{color:var(--wp--custom--color--text--on-dark-muted)}.ls-phase-hero .breadcrumb_last{color:var(--ls-phase-accent-on-dark)}.ls-phase-hero__share{background-color:color-mix(in srgb, var(--wp--custom--color--text--on-dark) 5%, transparent);border-color:color-mix(in srgb, var(--wp--custom--color--text--on-dark) 8%, transparent);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-phase-hero__share:hover,.ls-phase-hero__share:focus-within{background-color:color-mix(in srgb, var(--wp--custom--color--text--on-dark) 9%, transparent);border-color:color-mix(in srgb, var(--wp--custom--color--text--on-dark) 16%, transparent)}@media(prefers-reduced-motion: reduce){.ls-phase-hero__share{transition:none}} diff --git a/assets/css/phase-journey-nav.css b/assets/css/phase-journey-nav.css index b9db564..5da286c 100644 --- a/assets/css/phase-journey-nav.css +++ b/assets/css/phase-journey-nav.css @@ -1 +1 @@ -.ls-phase-journey-nav__step--discover{--ls-phase-item-accent: var(--wp--custom--color--phase--discover-on-dark, var(--wp--custom--color--phase--discover))}.ls-phase-journey-nav__step--create{--ls-phase-item-accent: var(--wp--custom--color--phase--create-on-dark, var(--wp--custom--color--phase--create))}.ls-phase-journey-nav__step--build{--ls-phase-item-accent: var(--wp--custom--color--phase--build-on-dark, var(--wp--custom--color--phase--build))}.ls-phase-journey-nav__step--launch{--ls-phase-item-accent: var(--wp--custom--color--phase--launch-on-dark, var(--wp--custom--color--phase--launch))}.ls-phase-journey-nav__step--grow{--ls-phase-item-accent: var(--wp--custom--color--phase--grow-on-dark, var(--wp--custom--color--phase--grow))}.ls-phase-journey-nav__step--evolve{--ls-phase-item-accent: var(--wp--custom--color--phase--evolve-on-dark, var(--wp--custom--color--phase--evolve))}.ls-phase-journey-nav__link a{transition:color var(--wp--custom--animation--duration--base) var(--wp--custom--animation--easing--standard)}.ls-phase-journey-nav__link a{color:color-mix(in srgb, var(--wp--custom--color--text--on-dark-muted) 70%, transparent)}.ls-phase-journey-nav__link a:hover,.ls-phase-journey-nav__link a:focus-visible{color:var(--wp--custom--color--text--on-dark)}.ls-phase-journey-nav__dot{display:none}body.page-slug-discover .ls-phase-journey-nav__step--discover .ls-phase-journey-nav__link a{color:var(--ls-phase-item-accent)}body.page-slug-discover .ls-phase-journey-nav__step--discover .ls-phase-journey-nav__link a:hover,body.page-slug-discover .ls-phase-journey-nav__step--discover .ls-phase-journey-nav__link a:focus-visible{color:var(--wp--custom--color--text--on-dark)}body.page-slug-discover .ls-phase-journey-nav__step--discover .ls-phase-journey-nav__dot{display:inline-flex}body.page-slug-create .ls-phase-journey-nav__step--create .ls-phase-journey-nav__link a{color:var(--ls-phase-item-accent)}body.page-slug-create .ls-phase-journey-nav__step--create .ls-phase-journey-nav__link a:hover,body.page-slug-create .ls-phase-journey-nav__step--create .ls-phase-journey-nav__link a:focus-visible{color:var(--wp--custom--color--text--on-dark)}body.page-slug-create .ls-phase-journey-nav__step--create .ls-phase-journey-nav__dot{display:inline-flex}body.page-slug-build .ls-phase-journey-nav__step--build .ls-phase-journey-nav__link a{color:var(--ls-phase-item-accent)}body.page-slug-build .ls-phase-journey-nav__step--build .ls-phase-journey-nav__link a:hover,body.page-slug-build .ls-phase-journey-nav__step--build .ls-phase-journey-nav__link a:focus-visible{color:var(--wp--custom--color--text--on-dark)}body.page-slug-build .ls-phase-journey-nav__step--build .ls-phase-journey-nav__dot{display:inline-flex}body.page-slug-launch .ls-phase-journey-nav__step--launch .ls-phase-journey-nav__link a{color:var(--ls-phase-item-accent)}body.page-slug-launch .ls-phase-journey-nav__step--launch .ls-phase-journey-nav__link a:hover,body.page-slug-launch .ls-phase-journey-nav__step--launch .ls-phase-journey-nav__link a:focus-visible{color:var(--wp--custom--color--text--on-dark)}body.page-slug-launch .ls-phase-journey-nav__step--launch .ls-phase-journey-nav__dot{display:inline-flex}body.page-slug-grow .ls-phase-journey-nav__step--grow .ls-phase-journey-nav__link a{color:var(--ls-phase-item-accent)}body.page-slug-grow .ls-phase-journey-nav__step--grow .ls-phase-journey-nav__link a:hover,body.page-slug-grow .ls-phase-journey-nav__step--grow .ls-phase-journey-nav__link a:focus-visible{color:var(--wp--custom--color--text--on-dark)}body.page-slug-grow .ls-phase-journey-nav__step--grow .ls-phase-journey-nav__dot{display:inline-flex}body.page-slug-evolve .ls-phase-journey-nav__step--evolve .ls-phase-journey-nav__link a{color:var(--ls-phase-item-accent)}body.page-slug-evolve .ls-phase-journey-nav__step--evolve .ls-phase-journey-nav__link a:hover,body.page-slug-evolve .ls-phase-journey-nav__step--evolve .ls-phase-journey-nav__link a:focus-visible{color:var(--wp--custom--color--text--on-dark)}body.page-slug-evolve .ls-phase-journey-nav__step--evolve .ls-phase-journey-nav__dot{display:inline-flex}.ls-phase-journey-nav__link a:focus-visible{outline:2px solid var(--wp--custom--color--text--on-dark);outline-offset:3px;border-radius:var(--wp--preset--border-radius--100)}@media(prefers-reduced-motion: reduce){.ls-phase-journey-nav__link a{transition:none}}@media(max-width: 781px){.ls-phase-journey-nav__items{overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none}.ls-phase-journey-nav__items::-webkit-scrollbar{display:none}.ls-phase-journey-nav__step{flex-shrink:0}} +.ls-phase-journey-nav__step--discover{--ls-phase-item-accent: var(--wp--custom--color--phase--discover-on-dark, var(--wp--custom--color--phase--discover))}.ls-phase-journey-nav__step--create{--ls-phase-item-accent: var(--wp--custom--color--phase--create-on-dark, var(--wp--custom--color--phase--create))}.ls-phase-journey-nav__step--build{--ls-phase-item-accent: var(--wp--custom--color--phase--build-on-dark, var(--wp--custom--color--phase--build))}.ls-phase-journey-nav__step--launch{--ls-phase-item-accent: var(--wp--custom--color--phase--launch-on-dark, var(--wp--custom--color--phase--launch))}.ls-phase-journey-nav__step--grow{--ls-phase-item-accent: var(--wp--custom--color--phase--grow-on-dark, var(--wp--custom--color--phase--grow))}.ls-phase-journey-nav__step--evolve{--ls-phase-item-accent: var(--wp--custom--color--phase--evolve-on-dark, var(--wp--custom--color--phase--evolve))}.ls-phase-journey-nav__link a{transition:color var(--wp--custom--animation--duration--base) var(--wp--custom--animation--easing--standard)}.ls-phase-journey-nav__link a{color:color-mix(in srgb, var(--wp--custom--color--text--on-dark-muted) 70%, transparent)}.ls-phase-journey-nav__link a:hover,.ls-phase-journey-nav__link a:focus-visible{color:var(--wp--custom--color--text--on-dark)}.ls-phase-journey-nav__dot{display:none}body.page-slug-discover{--ls-phase-accent-on-dark: var(--wp--custom--color--phase--discover-on-dark, var(--wp--custom--color--phase--discover))}body.page-slug-create{--ls-phase-accent-on-dark: var(--wp--custom--color--phase--create-on-dark, var(--wp--custom--color--phase--create))}body.page-slug-build{--ls-phase-accent-on-dark: var(--wp--custom--color--phase--build-on-dark, var(--wp--custom--color--phase--build))}body.page-slug-launch{--ls-phase-accent-on-dark: var(--wp--custom--color--phase--launch-on-dark, var(--wp--custom--color--phase--launch))}body.page-slug-grow{--ls-phase-accent-on-dark: var(--wp--custom--color--phase--grow-on-dark, var(--wp--custom--color--phase--grow))}body.page-slug-evolve{--ls-phase-accent-on-dark: var(--wp--custom--color--phase--evolve-on-dark, var(--wp--custom--color--phase--evolve))}body.page-slug-discover .ls-phase-journey-nav__step--discover .ls-phase-journey-nav__link a{color:var(--ls-phase-item-accent)}body.page-slug-discover .ls-phase-journey-nav__step--discover .ls-phase-journey-nav__link a:hover,body.page-slug-discover .ls-phase-journey-nav__step--discover .ls-phase-journey-nav__link a:focus-visible{color:var(--wp--custom--color--text--on-dark)}body.page-slug-discover .ls-phase-journey-nav__step--discover .ls-phase-journey-nav__dot{display:inline-flex}body.page-slug-create .ls-phase-journey-nav__step--create .ls-phase-journey-nav__link a{color:var(--ls-phase-item-accent)}body.page-slug-create .ls-phase-journey-nav__step--create .ls-phase-journey-nav__link a:hover,body.page-slug-create .ls-phase-journey-nav__step--create .ls-phase-journey-nav__link a:focus-visible{color:var(--wp--custom--color--text--on-dark)}body.page-slug-create .ls-phase-journey-nav__step--create .ls-phase-journey-nav__dot{display:inline-flex}body.page-slug-build .ls-phase-journey-nav__step--build .ls-phase-journey-nav__link a{color:var(--ls-phase-item-accent)}body.page-slug-build .ls-phase-journey-nav__step--build .ls-phase-journey-nav__link a:hover,body.page-slug-build .ls-phase-journey-nav__step--build .ls-phase-journey-nav__link a:focus-visible{color:var(--wp--custom--color--text--on-dark)}body.page-slug-build .ls-phase-journey-nav__step--build .ls-phase-journey-nav__dot{display:inline-flex}body.page-slug-launch .ls-phase-journey-nav__step--launch .ls-phase-journey-nav__link a{color:var(--ls-phase-item-accent)}body.page-slug-launch .ls-phase-journey-nav__step--launch .ls-phase-journey-nav__link a:hover,body.page-slug-launch .ls-phase-journey-nav__step--launch .ls-phase-journey-nav__link a:focus-visible{color:var(--wp--custom--color--text--on-dark)}body.page-slug-launch .ls-phase-journey-nav__step--launch .ls-phase-journey-nav__dot{display:inline-flex}body.page-slug-grow .ls-phase-journey-nav__step--grow .ls-phase-journey-nav__link a{color:var(--ls-phase-item-accent)}body.page-slug-grow .ls-phase-journey-nav__step--grow .ls-phase-journey-nav__link a:hover,body.page-slug-grow .ls-phase-journey-nav__step--grow .ls-phase-journey-nav__link a:focus-visible{color:var(--wp--custom--color--text--on-dark)}body.page-slug-grow .ls-phase-journey-nav__step--grow .ls-phase-journey-nav__dot{display:inline-flex}body.page-slug-evolve .ls-phase-journey-nav__step--evolve .ls-phase-journey-nav__link a{color:var(--ls-phase-item-accent)}body.page-slug-evolve .ls-phase-journey-nav__step--evolve .ls-phase-journey-nav__link a:hover,body.page-slug-evolve .ls-phase-journey-nav__step--evolve .ls-phase-journey-nav__link a:focus-visible{color:var(--wp--custom--color--text--on-dark)}body.page-slug-evolve .ls-phase-journey-nav__step--evolve .ls-phase-journey-nav__dot{display:inline-flex}.ls-phase-journey-nav__link a:focus-visible{outline:2px solid var(--wp--custom--color--text--on-dark);outline-offset:3px;border-radius:var(--wp--preset--border-radius--100)}@media(prefers-reduced-motion: reduce){.ls-phase-journey-nav__link a{transition:none}}@media(max-width: 781px){.ls-phase-journey-nav__items{overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none}.ls-phase-journey-nav__items::-webkit-scrollbar{display:none}.ls-phase-journey-nav__step{flex-shrink:0}} diff --git a/blocks/phase-services/block.json b/blocks/phase-services/block.json new file mode 100644 index 0000000..e64ee3e --- /dev/null +++ b/blocks/phase-services/block.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "ls-theme/phase-services", + "title": "Phase Services", + "category": "text", + "description": "Renders the current lifecycle phase page's \"Services in this phase\" heading and service cards. Implements CodeRabbit's request-time rendering fix for patterns/sections/phase-services-in-phase.php: PHP phase-detection baked into a pattern's own content only re-evaluates while the pattern stays a live wp:pattern reference, and is lost if WordPress flattens it, so the phase-dependent selection and markup now live in this dynamic block's render.php instead, which runs at real request time regardless of how the surrounding pattern is stored.", + "textdomain": "ls-theme", + "usesContext": [ "postId" ], + "render": "file:./render.php" +} diff --git a/blocks/phase-services/index.js b/blocks/phase-services/index.js new file mode 100644 index 0000000..065d90f --- /dev/null +++ b/blocks/phase-services/index.js @@ -0,0 +1,34 @@ +/** + * Editor-side registration for ls-theme/phase-services. + * + * The PHP registration in inc/blocks.php (register_block_type against this folder's block.json) + * only tells WordPress how to render this block on the front end — the block editor's own JS + * registry is entirely separate, and needs its own wp.blocks.registerBlockType() call by the same + * name or it shows "Your site doesn't include support for this block" and renders nothing in the + * canvas. ServerSideRender is used for `edit` (rather than reimplementing the markup in JS) so the + * editor preview always matches render.php's actual output, including the current phase's real + * content and colour — no separate editor-only representation to keep in sync. + * + * Plain global-based script (no build step), per AGENTS.md "Avoid inventing a build pipeline". + * + * @package ls-theme + */ +( function ( blocks, element, serverSideRender ) { + var el = element.createElement; + + blocks.registerBlockType( 'ls-theme/phase-services', { + title: 'Phase Services', + category: 'text', + usesContext: [ 'postId' ], + description: 'Renders the current phase page\'s "Services in this phase" heading and service cards.', + edit: function ( props ) { + return el( serverSideRender, { + block: 'ls-theme/phase-services', + urlQueryArgs: { post_id: props.context ? props.context.postId : undefined }, + } ); + }, + save: function () { + return null; + }, + } ); +} )( window.wp.blocks, window.wp.element, window.wp.serverSideRender ); diff --git a/blocks/phase-services/render.php b/blocks/phase-services/render.php new file mode 100644 index 0000000..28bf9bc --- /dev/null +++ b/blocks/phase-services/render.php @@ -0,0 +1,213 @@ + array( + array( + 'label' => __( 'Discovery', 'ls-theme' ), + 'description' => __( 'Stakeholder workshops, audits, scoping and feasibility — the evidence layer that comes before design or build.', 'ls-theme' ), + 'url' => '/services/discovery/', + 'icon' => 'search', + ), + ), + 'create' => array( + array( + 'label' => __( 'Content', 'ls-theme' ), + 'description' => __( 'Content modelling, editorial workflow and governance that holds up at scale.', 'ls-theme' ), + 'url' => '/services/content/', + 'icon' => 'file-text', + ), + array( + 'label' => __( 'Design', 'ls-theme' ), + 'description' => __( 'Design systems, accessible patterns and Figma→WordPress parity. Already a deeper page.', 'ls-theme' ), + 'url' => '/services/design/', + 'icon' => 'paint-brush', + ), + ), + 'build' => array( + array( + 'label' => __( 'Development', 'ls-theme' ), + 'description' => __( 'Block themes, WooCommerce, integrations and platform refactors built for long-term health.', 'ls-theme' ), + 'url' => '/services/development/', + 'icon' => 'code', + ), + array( + 'label' => __( 'Migrations', 'ls-theme' ), + 'description' => __( 'Audits, mapping and redirects that take legacy platforms apart without losing traction.', 'ls-theme' ), + 'url' => '/services/migrations/', + 'icon' => 'arrows-left-right', + ), + ), + 'launch' => array( + array( + 'label' => __( 'Hosting', 'ls-theme' ), + 'description' => __( 'Managed environments that match the platform, the workflows and the support model around it.', 'ls-theme' ), + 'url' => '/services/hosting/', + 'icon' => 'cloud', + ), + array( + 'label' => __( 'Performance', 'ls-theme' ), + 'description' => __( 'Core Web Vitals, caching, query review and template optimisation against real production data.', 'ls-theme' ), + 'url' => '/services/performance/', + 'icon' => 'gauge', + ), + array( + 'label' => __( 'Security', 'ls-theme' ), + 'description' => __( 'Audits, hardening guidance, monitoring and recovery planning that reduces risk before incidents.', 'ls-theme' ), + 'url' => '/services/security/', + 'icon' => 'shield', + ), + array( + 'label' => __( 'Training', 'ls-theme' ), + 'description' => __( 'Role-specific training and reference materials that move teams from dependency to confidence.', 'ls-theme' ), + 'url' => '/services/training/', + 'icon' => 'graduation-cap', + ), + ), + 'grow' => array( + array( + 'label' => __( 'Support', 'ls-theme' ), + 'description' => __( 'Maintenance, incident response and quiet improvement so platforms keep getting easier to run.', 'ls-theme' ), + 'url' => '/services/support/', + 'icon' => 'lifebuoy', + ), + array( + 'label' => __( 'SEO', 'ls-theme' ), + 'description' => __( 'Technical SEO, internal linking, schema and the publishing discipline that supports visibility.', 'ls-theme' ), + 'url' => '/services/seo/', + 'icon' => 'chart-line-up', + ), + array( + 'label' => __( 'Accessibility', 'ls-theme' ), + 'description' => __( 'Audits, remediation and the semantic structure that helps WCAG 2.2 AA stick over time.', 'ls-theme' ), + 'url' => '/services/accessibility/', + 'icon' => 'wheelchair', + ), + array( + 'label' => __( 'Email marketing', 'ls-theme' ), + 'description' => __( 'Journey planning, consent flows and the connection between email and the WordPress platform behind it.', 'ls-theme' ), + 'url' => '/services/email-marketing/', + 'icon' => 'envelope', + ), + ), + 'evolve' => array( + array( + 'label' => __( 'AI', 'ls-theme' ), + 'description' => __( 'AI-readiness reviews, governance and workflow planning — practical use without messy adoption.', 'ls-theme' ), + 'url' => '/services/ai/', + 'icon' => 'special-interests', + ), + ), +); + +$ls_phase_labels = array( + 'discover' => __( 'Discover', 'ls-theme' ), + 'create' => __( 'Create', 'ls-theme' ), + 'build' => __( 'Build', 'ls-theme' ), + 'launch' => __( 'Launch', 'ls-theme' ), + 'grow' => __( 'Grow', 'ls-theme' ), + 'evolve' => __( 'Evolve', 'ls-theme' ), +); + +$ls_post_id = isset( $block->context['postId'] ) ? (int) $block->context['postId'] : (int) get_the_ID(); +if ( ! $ls_post_id ) { + $ls_post_id = (int) get_queried_object_id(); +} + +$ls_current_phase_slug = ''; +$ls_queried_post = get_post( $ls_post_id ); +if ( $ls_queried_post instanceof WP_Post ) { + $ls_current_phase_slug = $ls_queried_post->post_name; +} + +$ls_active_phase_slug = isset( $ls_phase_services_by_phase[ $ls_current_phase_slug ] ) ? $ls_current_phase_slug : 'discover'; +$ls_active_phase_label = $ls_phase_labels[ $ls_active_phase_slug ]; +$ls_active_services = $ls_phase_services_by_phase[ $ls_active_phase_slug ]; +$ls_service_rows = array_chunk( $ls_active_services, 4 ); +$ls_phase_accent = 'var(--wp--custom--color--phase--' . $ls_active_phase_slug . ')'; + +ob_start(); +?> + +
+ +

+ + + +

+ + + +

+ +
+ + + $ls_row ) : ?> + +
+ + +
+ +
+ +
+ + + +
+ +

+ + + +
+ +

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

+ +

@@ -52,7 +52,7 @@
-

+

diff --git a/patterns/sections/phase-services-in-phase.php b/patterns/sections/phase-services-in-phase.php index 9d6ff5f..248ba09 100644 --- a/patterns/sections/phase-services-in-phase.php +++ b/patterns/sections/phase-services-in-phase.php @@ -5,30 +5,15 @@ * Categories: featured * Block Types: core/pattern * Description: Shared "Services in the [Phase] phase" section for all six lifecycle phase pages. - * Unlike the other phase sections, this one needs no per-page copy edits at all: it detects the - * current page by slug (same technique as phase-journey-nav.php) and renders only that phase's - * service cards from the single $ls_phase_services_by_phase map below, whose labels/descriptions/ - * URLs/icons are kept in sync with the master list in services-service-tiles.php. Each card reuses - * the shared is-style-card-service-tile shell (background/border/radius/padding — not overridden - * here, since that style is also used by ~16 other patterns sitewide) and the shared - * is-style-link-arrow-accent CTA treatment, but does NOT reuse the shared ls-icon-well-brand icon - * well: that class is a fixed, non-phase colour used elsewhere, so this pattern renders its own - * smaller, phase-accented circular icon well locally instead (same border/radius/colour-mix - * technique already used by phase-hero.php's own badge). Cards render in a left-aligned flex row - * (layout.selfStretch:"fixed" per card, the native WP mechanism also used by - * phase-journey-nav.php's label/items split) rather than core/columns, since a single service - * (e.g. Discover, which has exactly one) would otherwise stretch a lone core/column to the row's - * full width. Main heading uses H3 (not H2) to close a pre-existing H2→H4 heading-level skip on - * this section (H2 eyebrow-adjacent heading straight to H4 card titles) rather than only to - * shrink it visually — this section's own explicit fontSize/level attributes are local to this - * file only; no shared heading style exists sitewide to avoid touching. The CTA link's phase - * accent is set via src/scss/structural/phase-services-in-phase.scss, scoped by the - * page-slug-{phase} body class (see inc/phase-page-body-class.php) rather than an inline custom - * property, for the same block-validation reason documented in phase-journey-nav.scss. - * - * IMPORTANT — insert this as a live reference, not a flattened copy: like phase-journey-nav.php, - * the phase-detection below only re-evaluates per request if this stays a live - * `` reference block. + * The section shell below is static and phase-agnostic; the phase-dependent heading and service + * cards are rendered by the ls-theme/phase-services dynamic block (see + * blocks/phase-services/render.php) — CodeRabbit's requested fix (PR #64): that block's render.php + * holds the phase → services map (labels/descriptions/URLs/icons, kept in sync with the master + * list in services-service-tiles.php), reads the current page via block context rather than + * get_queried_object(), and re-runs on every real request regardless of whether this surrounding + * pattern stays a live `wp:pattern` reference or gets flattened by the editor — a dynamic block's + * render callback isn't affected by pattern flattening the way PHP baked directly into a pattern's + * own stored content is. * Keywords: phase, discover, create, build, launch, grow, evolve, services, cards, section * Viewport Width: 1280 * Inserter: true @@ -36,181 +21,9 @@ * @package ls-theme */ -$ls_phase_services_by_phase = array( - 'discover' => array( - array( - 'label' => __( 'Discovery', 'ls-theme' ), - 'description' => __( 'Stakeholder workshops, audits, scoping and feasibility — the evidence layer that comes before design or build.', 'ls-theme' ), - 'url' => '/services/discovery/', - 'icon' => 'search', - ), - ), - 'create' => array( - array( - 'label' => __( 'Content', 'ls-theme' ), - 'description' => __( 'Content modelling, editorial workflow and governance that holds up at scale.', 'ls-theme' ), - 'url' => '/services/content/', - 'icon' => 'file-text', - ), - array( - 'label' => __( 'Design', 'ls-theme' ), - 'description' => __( 'Design systems, accessible patterns and Figma→WordPress parity. Already a deeper page.', 'ls-theme' ), - 'url' => '/services/design/', - 'icon' => 'paint-brush', - ), - ), - 'build' => array( - array( - 'label' => __( 'Development', 'ls-theme' ), - 'description' => __( 'Block themes, WooCommerce, integrations and platform refactors built for long-term health.', 'ls-theme' ), - 'url' => '/services/development/', - 'icon' => 'code', - ), - array( - 'label' => __( 'Migrations', 'ls-theme' ), - 'description' => __( 'Audits, mapping and redirects that take legacy platforms apart without losing traction.', 'ls-theme' ), - 'url' => '/services/migrations/', - 'icon' => 'arrows-left-right', - ), - ), - 'launch' => array( - array( - 'label' => __( 'Hosting', 'ls-theme' ), - 'description' => __( 'Managed environments that match the platform, the workflows and the support model around it.', 'ls-theme' ), - 'url' => '/services/hosting/', - 'icon' => 'cloud', - ), - array( - 'label' => __( 'Performance', 'ls-theme' ), - 'description' => __( 'Core Web Vitals, caching, query review and template optimisation against real production data.', 'ls-theme' ), - 'url' => '/services/performance/', - 'icon' => 'gauge', - ), - array( - 'label' => __( 'Security', 'ls-theme' ), - 'description' => __( 'Audits, hardening guidance, monitoring and recovery planning that reduces risk before incidents.', 'ls-theme' ), - 'url' => '/services/security/', - 'icon' => 'shield', - ), - array( - 'label' => __( 'Training', 'ls-theme' ), - 'description' => __( 'Role-specific training and reference materials that move teams from dependency to confidence.', 'ls-theme' ), - 'url' => '/services/training/', - 'icon' => 'graduation-cap', - ), - ), - 'grow' => array( - array( - 'label' => __( 'Support', 'ls-theme' ), - 'description' => __( 'Maintenance, incident response and quiet improvement so platforms keep getting easier to run.', 'ls-theme' ), - 'url' => '/services/support/', - 'icon' => 'lifebuoy', - ), - array( - 'label' => __( 'SEO', 'ls-theme' ), - 'description' => __( 'Technical SEO, internal linking, schema and the publishing discipline that supports visibility.', 'ls-theme' ), - 'url' => '/services/seo/', - 'icon' => 'chart-line-up', - ), - array( - 'label' => __( 'Accessibility', 'ls-theme' ), - 'description' => __( 'Audits, remediation and the semantic structure that helps WCAG 2.2 AA stick over time.', 'ls-theme' ), - 'url' => '/services/accessibility/', - 'icon' => 'wheelchair', - ), - array( - 'label' => __( 'Email marketing', 'ls-theme' ), - 'description' => __( 'Journey planning, consent flows and the connection between email and the WordPress platform behind it.', 'ls-theme' ), - 'url' => '/services/email-marketing/', - 'icon' => 'envelope', - ), - ), - 'evolve' => array( - array( - 'label' => __( 'AI', 'ls-theme' ), - 'description' => __( 'AI-readiness reviews, governance and workflow planning — practical use without messy adoption.', 'ls-theme' ), - 'url' => '/services/ai/', - 'icon' => 'special-interests', - ), - ), -); - -$ls_current_phase_slug = ''; -$ls_services_queried_obj = get_queried_object(); -if ( $ls_services_queried_obj instanceof WP_Post ) { - $ls_current_phase_slug = $ls_services_queried_obj->post_name; -} - -$ls_phase_labels = array( - 'discover' => __( 'Discover', 'ls-theme' ), - 'create' => __( 'Create', 'ls-theme' ), - 'build' => __( 'Build', 'ls-theme' ), - 'launch' => __( 'Launch', 'ls-theme' ), - 'grow' => __( 'Grow', 'ls-theme' ), - 'evolve' => __( 'Evolve', 'ls-theme' ), -); - -$ls_active_phase_label = isset( $ls_phase_labels[ $ls_current_phase_slug ] ) ? $ls_phase_labels[ $ls_current_phase_slug ] : __( 'Discover', 'ls-theme' ); -$ls_active_services = isset( $ls_phase_services_by_phase[ $ls_current_phase_slug ] ) ? $ls_phase_services_by_phase[ $ls_current_phase_slug ] : $ls_phase_services_by_phase['discover']; -$ls_service_rows = array_chunk( $ls_active_services, 4 ); -$ls_active_phase_slug = isset( $ls_phase_services_by_phase[ $ls_current_phase_slug ] ) ? $ls_current_phase_slug : 'discover'; -$ls_phase_accent = 'var(--wp--custom--color--phase--' . $ls_active_phase_slug . ')'; ?>
- - -
- -

- - - -

- - - -

- -
- - - $ls_row ) : ?> - -
- - -
- -
- -
- - - -
- -

- - - -
- -

- -
- -
- - - - - -
- - -
- - +
diff --git a/src/scss/structural/button-phase.scss b/src/scss/structural/button-phase.scss index a6f52f0..7d3319e 100644 --- a/src/scss/structural/button-phase.scss +++ b/src/scss/structural/button-phase.scss @@ -52,7 +52,7 @@ aspect-ratio: 1; transform: translate(-50%, -50%) scale(0); border-radius: 50%; - background-color: var(--wp--custom--color--phase--discover-on-dark); + background-color: var(--ls-phase-accent-on-dark); opacity: 0; z-index: -1; } diff --git a/src/scss/structural/phase-hero.scss b/src/scss/structural/phase-hero.scss index 6b6b43f..dc87cf5 100644 --- a/src/scss/structural/phase-hero.scss +++ b/src/scss/structural/phase-hero.scss @@ -10,10 +10,8 @@ * blog-hero.scss: an inline `style="background:..."` always wins the cascade over any external * stylesheet's background-image, so the two can't coexist on this block. * - * Hardwired to phase.discover-on-dark rather than a CSS custom property — this file only needs to - * serve the Discover hero today. When Create/Build/Launch/Grow/Evolve heroes are built, either - * duplicate this file per phase (fast, proven-safe) or generalise it to a shared --ls-phase-accent - * custom property at that point, once it's clear which is actually needed — not guessed now. + * The radial glow reads var(--ls-phase-accent-on-dark), set per phase by the page-slug-{phase} body + * class in phase-journey-nav.scss, so it follows whichever phase page the hero is on. */ .ls-phase-hero { position: relative; @@ -21,7 +19,7 @@ background-image: linear-gradient(color-mix(in srgb, var(--wp--custom--color--text--on-dark) 2%, transparent) 1px, transparent 1px), linear-gradient(90deg, color-mix(in srgb, var(--wp--custom--color--text--on-dark) 2%, transparent) 1px, transparent 1px), - radial-gradient(60% 50% at 50% 22%, color-mix(in srgb, var(--wp--custom--color--phase--discover-on-dark) 14%, transparent) 0%, transparent 70%), + radial-gradient(60% 50% at 50% 22%, color-mix(in srgb, var(--ls-phase-accent-on-dark) 14%, transparent) 0%, transparent 70%), linear-gradient(158deg, var(--wp--custom--color--surface--band-start) 0%, var(--wp--custom--color--surface--band-end) 100%); background-position: 0 0, 0 0, 0 0, 0 0; background-repeat: repeat, repeat, no-repeat, no-repeat; @@ -29,21 +27,24 @@ } /* - * Breadcrumb (Discover Hero): reuses the existing .ls-breadcrumbs-on-dark class name from + * Breadcrumb (phase hero): reuses the existing .ls-breadcrumbs-on-dark class name from * blog-hero.php, but NOT its `a` colour fix — that rule lives in blog-hero.scss/blog-hero.css, * which is only enqueued on the Blog archive template (see the is_page_template() condition in * inc/animations.php's ls_theme_get_effect_styles()), so it never loads on this page at all. Left * unstyled, "Home" fell back to the sitewide global link colour (bright cyan in the active dark * style variation) instead of a muted grey. The fix below is scoped to `.ls-phase-hero` — this * file's own already-page-scoped selector — so it can never affect the Blog archive's identical - * class name or any other page. + * class name or any other page. The current-page crumb reads var(--ls-phase-accent-on-dark) + * (src/scss/structural/phase-journey-nav.scss), set per phase by the page-slug-{phase} body class, + * rather than the single phase.discover-on-dark token this was originally hardwired to when only + * Discover existed. */ .ls-phase-hero .ls-breadcrumbs-on-dark a { color: var(--wp--custom--color--text--on-dark-muted); } .ls-phase-hero .breadcrumb_last { - color: var(--wp--custom--color--phase--discover-on-dark); + color: var(--ls-phase-accent-on-dark); } /* diff --git a/src/scss/structural/phase-journey-nav.scss b/src/scss/structural/phase-journey-nav.scss index a8535bc..7160c90 100644 --- a/src/scss/structural/phase-journey-nav.scss +++ b/src/scss/structural/phase-journey-nav.scss @@ -83,6 +83,19 @@ * `.is-active`-style rule) because the selector needs both the current page's body class AND the * matching step's own modifier class to agree on which one item lights up. */ +// JSON limitation: a page-level custom property keyed off a conditional body class has no +// theme.json equivalent — see AGENTS.md Theme-First Approach. `--ls-phase-accent-on-dark` is the +// shared source of truth for every permanently-dark, phase-coloured element sitewide (phase-hero's +// pill/breadcrumb, button-phase-primary/outline) so none of them need to hardcode a single phase's +// colour the way they did before only Discover existed. Same fallback as this file's own +// --ls-phase-item-accent above: falls back from a phase's "-on-dark" token (only +// phase.discover-on-dark exists yet) to the phase's normal token. +@each $ls-phase in discover, create, build, launch, grow, evolve { + body.page-slug-#{$ls-phase} { + --ls-phase-accent-on-dark: var(--wp--custom--color--phase--#{$ls-phase}-on-dark, var(--wp--custom--color--phase--#{$ls-phase})); + } +} + @each $ls-phase in discover, create, build, launch, grow, evolve { body.page-slug-#{$ls-phase} .ls-phase-journey-nav__step--#{$ls-phase} { .ls-phase-journey-nav__link a { diff --git a/styles/blocks/buttons/button-phase-outline.json b/styles/blocks/buttons/button-phase-outline.json index a6076e5..a34f119 100644 --- a/styles/blocks/buttons/button-phase-outline.json +++ b/styles/blocks/buttons/button-phase-outline.json @@ -6,14 +6,14 @@ "blockTypes": [ "core/button" ], - "description": "Outline pill CTA pairing Button - Phase Primary on the phase-page heroes and CTAs. Uses the exact same horizontal padding token (spacing|40) as Button - Phase Primary so both buttons share one spacing system — this one simply reads wider because its label is longer, not because of a mismatched padding scale. Rest state: transparent background, thin phase-colour border, phase-colour text, no arrow glyph — deliberately arrow-free (this is the secondary/lower-emphasis action; only Primary carries an arrow). On hover: a centre-out circle reveal (adapted from the 'Flow Button' component, https://21st.dev/@xubohuah/components/flow-button) fills the button, the border fades to transparent, text flips to a contrasting colour, and the corners tighten slightly — none of that depends on an arrow, so it works cleanly without one. All hover-dependent CSS lives in src/scss/animations/_button-motion.scss, since a bare :hover in a style JSON's css field is silently stripped by WordPress. Same on-dark-constant reasoning and 'hardwired to Discover for now' note as button-phase-primary.json.", + "description": "Outline pill CTA pairing Button - Phase Primary on the phase-page heroes and CTAs. Uses the exact same horizontal padding token (spacing|40) as Button - Phase Primary so both buttons share one spacing system — this one simply reads wider because its label is longer, not because of a mismatched padding scale. Rest state: transparent background, thin phase-colour border, phase-colour text, no arrow glyph — deliberately arrow-free (this is the secondary/lower-emphasis action; only Primary carries an arrow). On hover: a centre-out circle reveal (adapted from the 'Flow Button' component, https://21st.dev/@xubohuah/components/flow-button) fills the button, the border fades to transparent, text flips to a contrasting colour, and the corners tighten slightly — none of that depends on an arrow, so it works cleanly without one. All hover-dependent CSS lives in src/scss/animations/_button-motion.scss, since a bare :hover in a style JSON's css field is silently stripped by WordPress. Same on-dark-constant reasoning and var(--ls-phase-accent-on-dark) source as button-phase-primary.json.", "styles": { "color": { "background": "transparent", - "text": "var:custom|color|phase|discover-on-dark" + "text": "var(--ls-phase-accent-on-dark)" }, "border": { - "color": "var:custom|color|phase|discover-on-dark", + "color": "var(--ls-phase-accent-on-dark)", "radius": "var:preset|border-radius|500", "style": "solid", "width": "1px" diff --git a/styles/blocks/buttons/button-phase-primary.json b/styles/blocks/buttons/button-phase-primary.json index a7ec53a..e2dd3e3 100644 --- a/styles/blocks/buttons/button-phase-primary.json +++ b/styles/blocks/buttons/button-phase-primary.json @@ -6,10 +6,10 @@ "blockTypes": [ "core/button" ], - "description": "Pill CTA for the phase-page heroes and CTAs (Discover, and eventually Create/Build/Launch/Grow/Evolve). Background/text stay constant across the light/dark style variation toggle, matching the on-dark token family — the hero itself is permanently dark, so this can't use the sitewide button.fill tokens, which flip colour with the variation. Stays filled at all times (approved design decision). Uses the exact same horizontal padding token (spacing|40) as Button - Phase Outline so both buttons share one spacing system and only their content — not an artificial equal-width rule — determines each one's width. The arrow is a normal inline flex item after the label (gap-based spacing, not an absolutely-positioned overlay) with a small hover nudge; earlier attempts at an absolutely-positioned enter/exit arrow swap needed ever-larger padding just to keep the arrow clear of the text and were reverted for exactly that reason — do not reintroduce that approach. Border-radius tightens slightly on hover, matching Button - Phase Outline's own hover treatment. Rest state (colour, shape, arrow glyph) lives here; the hover states live in src/scss/animations/_button-motion.scss, since a bare :hover in a style JSON's css field is silently stripped by WordPress. Currently hardwired to phase.discover-on-dark rather than a CSS custom property — narrowed to what Discover actually needs now; generalise to a shared --ls-button-phase-accent custom property (set by whichever hero uses it) once a second phase page needs this same style, rather than guessing at that shape today.", + "description": "Pill CTA for the phase-page heroes and CTAs (Discover, Create, Build, Launch, Grow, Evolve). Background/text stay constant across the light/dark style variation toggle, matching the on-dark token family — the hero itself is permanently dark, so this can't use the sitewide button.fill tokens, which flip colour with the variation. Stays filled at all times (approved design decision). Uses the exact same horizontal padding token (spacing|40) as Button - Phase Outline so both buttons share one spacing system and only their content — not an artificial equal-width rule — determines each one's width. The arrow is a normal inline flex item after the label (gap-based spacing, not an absolutely-positioned overlay) with a small hover nudge; earlier attempts at an absolutely-positioned enter/exit arrow swap needed ever-larger padding just to keep the arrow clear of the text and were reverted for exactly that reason — do not reintroduce that approach. Border-radius tightens slightly on hover, matching Button - Phase Outline's own hover treatment. Rest state (colour, shape, arrow glyph) lives here; the hover states live in src/scss/animations/_button-motion.scss, since a bare :hover in a style JSON's css field is silently stripped by WordPress. Background reads var(--ls-phase-accent-on-dark) (src/scss/structural/phase-journey-nav.scss), set per phase by the page-slug-{phase} body class, rather than the single phase.discover-on-dark token this was originally hardwired to when only Discover existed.", "styles": { "color": { - "background": "var:custom|color|phase|discover-on-dark", + "background": "var(--ls-phase-accent-on-dark)", "text": "var:custom|color|text|on-light" }, "border": {