Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Added 4 Work archive component patterns (LS-1616), each an individually insertable pattern built entirely from existing adaptive semantic tokens — no new `theme.json`/`dark.json` tokens were needed:
- `patterns/cards/work-project-card.php` — a case-study card bound to real Portfolio post data via WordPress block bindings (LS-1617): `core/post-title`, `core/post-excerpt`, `core/post-terms` (badge bound to `project-group`, tag pills bound to `project-tag`), and `core/read-more` for the permalink. Intended as the Post Template content inside a Query Loop scoped to the `project` post type.
- `patterns/cards/work-discuss-project-list.php` — a static checklist card reusing the existing `tick-accent` list style.
- `patterns/cards/work-engagement-stat.php` — a stat segment with a split-colour big number (accent-coloured suffix) and monospace uppercase labels.
- `patterns/cards/work-next-steps-card.php` — a compact related-route button.
- Matching section/block styles: `styles/sections/cards/card-case-study.json`, `card-banner-tint.json`, `card-chip.json`, `card-divider-top.json`, `card-checklist.json`, `stat-segment.json`, `card-link-row.json`, and two new `core/post-terms` block styles (`styles/blocks/post-terms/badge-brand.json`, `tag-pills.json`) for rendering dynamically-bound taxonomy terms as a badge bar and individual tag pills respectively.
- Colour, computed values (background tints, borders) are kept out of pattern-level block attributes and defined only in these external style files — WordPress's block validator can't reliably round-trip a raw `color-mix()` value placed directly in a block's own `style` attribute.
- Added hover-lift behaviour for the two interactive cards (`is-style-card-case-study`, `is-style-card-link-row`) to `assets/css/animations.css`, reusing the existing `card.hover` shadow contract, and extended the `is-style-link-arrow-accent` style/CSS to also support `core/read-more` (previously `core/paragraph` only).
- Added a `Menu Item Card` pattern (`patterns/menu/menu-item-card.php`) as the single reusable source of the mega-menu list item structure (icon well, title/description, hover-reveal trailing arrow), used across the Work, Solutions, Pricing, Insights, and About mega menus — Services is excluded, it uses its own per-phase item style. Deliberately a plain registered pattern, not a Synced Pattern, so each insertion stays independently editable per menu item (LS-1618).
- Added 6 real mega menu template parts for Ollie Menu Designer's Dropdown Menu block, each with final content, no shared placeholder scaffold: `parts/work-mega-menu.html`, `parts/solutions-mega-menu.html`, `parts/pricing-mega-menu.html`, `parts/insights-mega-menu.html`, `parts/about-mega-menu.html` (Default item styling), and `parts/services-mega-menu.html` (Service item styling, 6 lifecycle-phase columns). All registered in `theme.json` `templateParts` (LS-1618).
- Added `Mega Menu Item - Default` and `Mega Menu Item - Service` block styles (`styles/blocks/groups/mega-menu-item-default.json`, `mega-menu-item-service.json`) as the single reusable, registered source of a row's structural styling (padding, radius) — the icon well itself carries no separate style, just plain inline block attributes, so it has no independent interactive behaviour of its own.
Expand Down
206 changes: 206 additions & 0 deletions assets/css/animations.css
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,35 @@
transform: rotate(-5deg) scale(0.98);
}

/* Card - Work Category */
.is-style-card-category {
--ls-card-category-border-rest: var(--wp--custom--color--border--card);
--ls-card-category-border-active: var(--wp--custom--color--link--accent);
--ls-card-category-shadow-rest: var(--wp--custom--shadow--elevation--100);
--ls-card-category-shadow-active: var(--wp--custom--shadow--card--hover);
--ls-card-category-lift: -4px;
position: relative;
overflow: hidden;
border: 1px solid var(--ls-card-category-border-rest);
border-radius: var(--wp--preset--border-radius--300);
background: var(--wp--custom--color--surface--card);
box-shadow: var(--ls-card-category-shadow-rest);
gap: var(--wp--preset--spacing--20);
padding: var(--wp--preset--spacing--20);
transition: border-color var(--wp--custom--animation--duration--base) var(--wp--custom--animation--easing--standard), box-shadow var(--wp--custom--animation--duration--medium) var(--wp--custom--animation--easing--standard), transform var(--wp--custom--animation--duration--medium) var(--wp--custom--animation--easing--emphasised);
}
.is-style-card-category:hover, .is-style-card-category:focus-within {
border-color: var(--ls-card-category-border-active);
box-shadow: var(--ls-card-category-shadow-active);
transform: translateY(var(--ls-card-category-lift));
}
.is-style-card-category .ls-card-category__content {
flex: 1 1 auto;
display: flex;
flex-direction: column;
gap: var(--wp--preset--spacing--10);
}

/* Card - Services */
.is-style-card-services {
--ls-card-services-border-rest: var(--wp--custom--color--border--card);
Expand Down Expand Up @@ -600,12 +629,50 @@
margin: 0;
}

/* Card - Link Row */
.is-style-card-link-row {
--ls-card-link-row-border-rest: var(--wp--custom--color--border--card);
--ls-card-link-row-border-active: var(--wp--custom--color--link--accent);
--ls-card-link-row-background-active: color-mix(in srgb, var(--wp--custom--color--link--accent) 10%, transparent);
--ls-card-link-row-icon-active: var(--wp--custom--color--text--brand);
position: relative;
border-color: var(--ls-card-link-row-border-rest);
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);
}
.is-style-card-link-row:hover, .is-style-card-link-row:focus-within {
background-color: var(--ls-card-link-row-background-active);
border-color: var(--ls-card-link-row-border-active);
}
.is-style-card-link-row:hover .wp-block-outermost-icon-block .icon-container, .is-style-card-link-row:focus-within .wp-block-outermost-icon-block .icon-container {
color: var(--ls-card-link-row-icon-active) !important;
}
.is-style-card-link-row {
/* Stretched-link pattern: the visible <a> only wraps the title text, but its
* ::before covers the whole card so the entire row is clickable — while
* keeping a single real, keyboard-focusable link (not a fake click handler). */
}
.is-style-card-link-row .ls-card-link-row__link {
color: inherit;
text-decoration: none;
}
.is-style-card-link-row .ls-card-link-row__link::before {
content: "";
position: absolute;
inset: 0;
}
.is-style-card-link-row .ls-card-link-row__link:focus-visible {
outline: 2px solid var(--wp--custom--color--focus--ring);
outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
.wp-block-paragraph.is-style-link-arrow-accent a,
.wp-block-paragraph.is-style-link-arrow-accent a::after,
.is-style-glass-card,
.is-style-card-feature,
.is-style-card-feature :is(.ls-card__icon-shell, .ls-card-feature__icon-shell),
.is-style-card-category,
.is-style-card-link-row,
.is-style-card-services,
.is-style-card-services::before,
.is-style-card-services :is(.ls-card__icon-shell, .ls-card-services__icon-shell),
Expand All @@ -618,6 +685,8 @@
.is-style-glass-card:focus-within,
.is-style-card-feature:hover,
.is-style-card-feature:focus-within,
.is-style-card-category:hover,
.is-style-card-category:focus-within,
.is-style-card-services:hover,
.is-style-card-services:focus-within {
transform: none;
Expand All @@ -643,6 +712,73 @@
transform: none;
}
}
/*
* Stat Segment: its border-inline-end divider (styles/sections/cards/stat-segment.json) reads as
* a vertical rule between adjacent columns in a `wp:columns` row. Below core/columns' own native
* stacking breakpoint (WP core hardcodes this at `max-width: 781px` in
* wp-includes/blocks/columns/style.css — mirrored here as the `columns-stack` entry in
* theme.json's settings.custom.layout.break-points), each column becomes full-width, so that same
* border would just show as a stray line down the right edge of every stacked segment. Drop it at
* that same breakpoint instead.
*/
@media (max-width: 781.98px) {
.is-style-stat-segment {
border-inline-end: none;
}
}
/*
* The last column in a stat row (e.g. "Tokens") reuses is-style-stat-segment for its shared
* spacing/colour contract like every sibling, but shouldn't carry a trailing divider — there's no
* column after it to divide from. Suppress it by position instead of duplicating the shared
* style's properties on a class-less wrapper.
*/
.wp-block-columns > .wp-block-column:last-child > .is-style-stat-segment {
border-inline-end: none;
}

/* Card - Work Project (WooCommerce platform colour) */
.is-style-card-banner-tint-woocommerce {
padding: var(--wp--preset--spacing--40) var(--wp--preset--spacing--20);
background-color: color-mix(in srgb, var(--wp--custom--color--card--platform--woocommerce) 7%, transparent);
}

.is-style-badge-woocommerce {
width: 100%;
max-width: 100%;
box-sizing: border-box;
color: var(--wp--custom--color--card--platform--woocommerce);
font-family: var(--wp--preset--font-family--monospace);
font-size: var(--wp--preset--font-size--100);
font-weight: var(--wp--custom--typography--font-weight--bold);
text-transform: uppercase;
background-color: color-mix(in srgb, var(--wp--custom--color--card--platform--woocommerce) 8%, transparent);
border-radius: var(--wp--preset--border-radius--200);
padding: var(--wp--preset--spacing--5) var(--wp--preset--spacing--10);
letter-spacing: 1px;
}

/* Badge - multi-term wrapping (WordPress and WooCommerce both bind to
project-group, which can carry more than one term per post; core/post-terms
forces its links to display:block by default, which stacks each term
onto its own line and leaves the ", " separator orphaned on its own row) */
:is(.is-style-badge-brand, .is-style-badge-woocommerce) {
display: flex;
flex-wrap: wrap;
align-items: baseline;
}
:is(.is-style-badge-brand, .is-style-badge-woocommerce) a {
display: inline;
min-width: 0;
max-width: 100%;
white-space: normal;
overflow-wrap: anywhere;
color: inherit;
text-decoration: none;
}
:is(.is-style-badge-brand, .is-style-badge-woocommerce) .wp-block-post-terms__separator {
margin-inline-end: var(--wp--preset--spacing--5);
}

/********** Button Motion **********/
.wp-block-button:not([class*=is-style-]),
.wp-block-button.is-style-fill,
Expand Down Expand Up @@ -809,6 +945,24 @@
inset-inline-end: var(--ls-button-outline-icon-inline-start);
}

/* Button - Secondary */
.wp-block-button.is-style-button-secondary .wp-block-button__link:hover,
.wp-block-button.is-style-button-secondary .wp-block-button__link:focus-visible {
background-color: var(--wp--custom--color--button--fill--background-hover);
border-color: var(--wp--custom--color--button--fill--border-hover);
color: var(--wp--custom--color--button--fill--text-hover);
}

.wp-block-button.is-style-button-secondary-outline .wp-block-button__link:hover,
.wp-block-button.is-style-button-secondary-outline .wp-block-button__link:focus-visible {
/* button.outline has no dedicated border-hover token — reuse background-hover (brand-500) so
* the border matches the solid fill it's transitioning to, same as the filled variant's border
* matching its own background-hover above. */
background-color: var(--wp--custom--color--button--outline--background-hover);
border-color: var(--wp--custom--color--button--outline--background-hover);
color: var(--wp--custom--color--button--outline--text-hover);
}

/* Glass Button */
.wp-block-button.is-style-glass-button {
max-inline-size: 100%;
Expand Down Expand Up @@ -1478,6 +1632,21 @@
}
}
/********** Site Header **********/
/*
* Below core/navigation's own native overlay-menu breakpoint (core WP hardcodes this at
* `min-width: 600px` in wp-includes/blocks/navigation/style.css — mirrored here as the
* `nav-toggle` entry in theme.json's settings.custom.layout.break-points, synced via
* `npm run sync:breakpoints`), the nav collapses to its own hamburger toggle automatically.
* Hide only the "Start a project" CTA button at that same breakpoint — not search, which stays
* the site's only search entry point on mobile (the nav overlay drawer doesn't include it).
* With `justifyContent:"space-between"` already set on the header row, hiding just the buttons
* still leaves enough room for the collapsed nav's hamburger to sit at the far right.
*/
@media (max-width: 599.98px) {
.site-header__actions .wp-block-buttons {
display: none;
}
}
/*
* Header icon buttons (search, light/dark toggle) and the gradient CTA. The base circular-button
* shell (border/background/sizing) matches styles/blocks/groups/header-icon-button.json — kept
Expand Down Expand Up @@ -1560,3 +1729,40 @@
transition: none;
}
}
/********** Taxonomy Filter (ls-plugin/taxonomy-filter) **********/
.wp-block-ls-plugin-taxonomy-filter.taxonomy-filter--buttons {
display: flex;
flex-wrap: wrap;
gap: var(--wp--preset--spacing--10);
}
.wp-block-ls-plugin-taxonomy-filter.taxonomy-filter--buttons .wp-element-button {
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid var(--wp--preset--color--neutral-200);
border-radius: var(--wp--preset--border-radius--500);
padding: var(--wp--preset--spacing--10) var(--wp--preset--spacing--20);
background: transparent;
color: var(--wp--custom--color--text--muted);
font-family: var(--wp--preset--font-family--body);
font-weight: var(--wp--custom--typography--font-weight--semibold);
font-size: var(--wp--preset--font-size--100);
text-decoration: none;
white-space: nowrap;
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), color var(--wp--custom--animation--duration--base) var(--wp--custom--animation--easing--standard);
}
.wp-block-ls-plugin-taxonomy-filter.taxonomy-filter--buttons .wp-element-button:hover, .wp-block-ls-plugin-taxonomy-filter.taxonomy-filter--buttons .wp-element-button:focus-visible {
border-color: var(--wp--custom--color--link--accent);
background-color: transparent;
color: var(--wp--custom--color--link--accent);
}
.wp-block-ls-plugin-taxonomy-filter.taxonomy-filter--buttons .wp-element-button.taxonomy-filter-current {
border-color: var(--wp--custom--color--card--platform--wordpress);
background-color: var(--wp--custom--color--card--platform--wordpress);
color: var(--wp--custom--color--text--on-dark);
}
.wp-block-ls-plugin-taxonomy-filter.taxonomy-filter--buttons .wp-element-button.taxonomy-filter-current:hover, .wp-block-ls-plugin-taxonomy-filter.taxonomy-filter--buttons .wp-element-button.taxonomy-filter-current:focus-visible {
border-color: var(--wp--custom--color--card--platform--wordpress);
background-color: var(--wp--custom--color--card--platform--wordpress);
color: var(--wp--custom--color--text--on-dark);
}
3 changes: 3 additions & 0 deletions functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
/** Loads site header assets */
require get_template_directory() . '/inc/header.php';

/** Loads Portfolio card platform-colour swapping */
require get_template_directory() . '/inc/portfolio-card-colors.php';

/**
* Sets up theme supports.
*/
Expand Down
5 changes: 5 additions & 0 deletions inc/animations.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,11 @@ function ls_theme_register_effect_block_styles() {
'name' => 'card-feature',
'label' => __( 'Card - Feature', 'ls-theme' ),
),
array(
'block_name' => 'core/group',
'name' => 'card-category',
'label' => __( 'Card - Category', 'ls-theme' ),
),
array(
'block_name' => 'core/group',
'name' => 'card-solutions',
Expand Down
59 changes: 59 additions & 0 deletions inc/portfolio-card-colors.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php
/**
* Portfolio Card Platform Colours.
*
* The Work Project Card colours its banner and badge by the post's
* project-group term (WordPress/WooCommerce/etc). Since a Query Loop
* renders identical block markup for every post, the WooCommerce variant
* class has to be swapped in per-post at render time — it can't be set
* statically in the pattern.
*
* @package ls-theme
*/

if ( ! defined( 'ABSPATH' ) ) {
exit;
}

/**
* Swap the Work Project Card's banner/badge class to the WooCommerce
* variant when the current post's project-group term is WooCommerce.
* Every other term falls back to the default (WordPress-tinted) variant.
*
* @param string $block_content Rendered block HTML.
* @param array $block Parsed block data.
* @return string
*/
function ls_theme_portfolio_card_platform_class( $block_content, $block ) {
if ( 'core/group' !== $block['blockName'] && 'core/post-terms' !== $block['blockName'] ) {
return $block_content;
}

$class_name = $block['attrs']['className'] ?? '';

$is_banner = false !== strpos( $class_name, 'is-style-card-banner-tint' );
$is_badge = false !== strpos( $class_name, 'is-style-badge-brand' );

if ( ! $is_banner && ! $is_badge ) {
return $block_content;
}

$post_id = get_the_ID();

if ( ! $post_id ) {
return $block_content;
}

$terms = wp_get_post_terms( $post_id, 'project-group', array( 'fields' => 'slugs' ) );

if ( is_wp_error( $terms ) || empty( $terms ) || ! in_array( 'woocommerce', $terms, true ) ) {
return $block_content;
}

if ( $is_banner ) {
return str_replace( 'is-style-card-banner-tint', 'is-style-card-banner-tint-woocommerce', $block_content );
}

return str_replace( 'is-style-badge-brand', 'is-style-badge-woocommerce', $block_content );
}
add_filter( 'render_block', 'ls_theme_portfolio_card_platform_class', 10, 2 );
Loading
Loading