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
3 changes: 3 additions & 0 deletions functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
/** Loads GSAP integration file */
require get_template_directory() . '/inc/gsap.php';

/** Loads the presets integration files */
require get_template_directory() . '/inc/presets.php';

/**
* Sets up theme supports.
*/
Expand Down
96 changes: 96 additions & 0 deletions inc/presets.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php
namespace ls_theme\includes;

/**
* Register Modular Theme.json Presets.
*
* Loads separate JSON preset files from /styles/presets/ and merges them
* into theme.json via the wp_theme_json_data_theme filter. This allows
* design tokens (colours, typography, spacing, shadows, radii, borders,
* layout) and CSS utility classes (aspect ratios, flex/grid helpers,
* spacing utilities, text truncation) to be maintained in individual
* files for better organisation and reusability across projects.
*
* @package ls_theme\includes
* @since 1.0.0
*/

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

/**
* Merges modular preset JSON files into the theme's theme.json data.
*
* Automatically discovers and loads all .json files from the /styles/presets/
* directory and its subdirectories (e.g., /styles/presets/blocks/). Each preset
* file contains a slice of `settings` or `styles` that would otherwise live
* directly in theme.json. Files are loaded alphabetically, and WordPress's
* native WP_Theme_JSON::merge() handles the merging logic.
*
* @since ls_theme 1.0
*
* @param WP_Theme_JSON_Data $theme_json The theme JSON data object.
* @return WP_Theme_JSON_Data The modified theme JSON data object.
*/
function merge_preset_files( $theme_json ) {
$preset_dir = get_template_directory() . '/styles/presets/';

// Automatically discover all JSON files in the presets directory.
if ( ! is_dir( $preset_dir ) ) {
return $theme_json;
}

// Recursively find all JSON files in presets directory and subdirectories.
$preset_files = get_preset_files_recursive( $preset_dir );

if ( empty( $preset_files ) ) {
return $theme_json;
}

// Sort alphabetically for predictable loading order.
sort( $preset_files );

foreach ( $preset_files as $file ) {
$preset_data = json_decode( file_get_contents( $file ), true ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents

if ( ! is_array( $preset_data ) ) {
continue;
}

// Use WordPress's native merge method via update_with().
$theme_json->update_with( $preset_data );
}
Comment on lines +37 to +63
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation performs recursive directory scanning and JSON decoding on every execution of the wp_theme_json_data_theme filter. This can lead to unnecessary disk I/O and CPU overhead, especially on sites with many preset files or without persistent object caching.

Additionally, using wp_json_file_decode() is more idiomatic in WordPress and provides better handling for JSON files compared to file_get_contents() and json_decode().

Consider caching the decoded preset data in a static variable to improve performance.

	static $cached_presets = null;

	if ( null === $cached_presets ) {
		$cached_presets = array();
		$preset_dir     = get_template_directory() . '/styles/presets/';

		if ( is_dir( $preset_dir ) ) {
			$preset_files = get_preset_files_recursive( $preset_dir );
			if ( ! empty( $preset_files ) ) {
				sort( $preset_files );
				foreach ( $preset_files as $file ) {
					$data = wp_json_file_decode( $file, array( 'associative' => true ) );
					if ( is_array( $data ) ) {
						$cached_presets[] = $data;
					}
				}
			}
		}
	}

	foreach ( $cached_presets as $preset_data ) {
		$theme_json->update_with( $preset_data );
	}


return $theme_json;
}

/**
* Recursively retrieves all JSON files from a directory and its subdirectories.
*
* @since ls_theme 1.1
*
* @param string $directory The directory path to scan for JSON files.
* @return array Array of file paths to JSON files.
*/
function get_preset_files_recursive( $directory ) {
$files = array();

// Get JSON files in the current directory.
$json_files = glob( $directory . '*.json' );
if ( ! empty( $json_files ) ) {
$files = array_merge( $files, $json_files );
}

// Get subdirectories and recursively scan them.
$subdirs = glob( $directory . '*', GLOB_ONLYDIR );
if ( ! empty( $subdirs ) ) {
foreach ( $subdirs as $subdir ) {
$files = array_merge( $files, get_preset_files_recursive( trailingslashit( $subdir ) ) );
}
}

return $files;
}

add_filter( 'wp_theme_json_data_theme', __NAMESPACE__ . '\merge_preset_files' );
38 changes: 38 additions & 0 deletions styles/presets/blocks/core-button.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"styles": {
"blocks": {
"core/button": {
"variations": {
"outline": {
"border": {
"color": "var:preset|color|brand-500",
"style": "solid",
"width": "2px",
"radius": "var:preset|border-radius|200"
},
"color": {
"background": "var:preset|color|base",
"text": "var:preset|color|contrast"
},
"spacing": {
"padding": {
"top": "1rem",
"right": "4rem",
"bottom": "1rem",
"left": "1.5rem"
}
},
"typography": {
"fontSize": "var:preset|font-size|200",
"fontWeight": "700",
"letterSpacing": "0.08em"
},
"css": "&{--ls-button-outline-accent-background:var(--wp--preset--color--brand-500);--ls-button-outline-accent-colour:var(--wp--preset--color--base);--ls-button-outline-icon-radius:var(--wp--preset--border-radius--100);}"
}
}
}
}
}
}
40 changes: 40 additions & 0 deletions styles/presets/buttons.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"settings": {
"custom": {
"button-padding": {
"top": "1rem",
"right": "4rem",
"bottom": "1rem",
"left": "1.5rem"
}
}
},
"styles": {
"elements": {
"button": {
"color": {
"text": "var:preset|color|base"
},
"border": {
"radius": "var:preset|border-radius|200"
},
"spacing": {
"padding": {
"top": "1rem",
"right": "4rem",
"bottom": "1rem",
"left": "1.5rem"
}
},
"typography": {
"fontSize": "var:preset|font-size|200",
"fontWeight": "700",
"letterSpacing": "0.08em"
},
"css": "&{--ls-button-fill-background:var(--wp--preset--color--brand-500);--ls-button-fill-icon-colour:var(--wp--preset--color--base);--ls-button-fill-hover-text:var(--wp--preset--color--contrast);--ls-button-fill-duration:500ms;--ls-button-fill-enter-delay:80ms;--ls-button-active-scale:0.98;}"
}
}
}
}
12 changes: 12 additions & 0 deletions styles/presets/layout.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"settings": {
"appearanceTools": true,
"layout": {
"contentSize": "800px",
"wideSize": "1360px"
},
"useRootPaddingAwareAlignments": true
}
}
18 changes: 18 additions & 0 deletions styles/presets/links.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"styles": {
"elements": {
"link": {
"color": {
"text": "var(--wp--preset--color--accent-500)"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For better consistency with other preset files (like buttons.json) and to leverage WordPress's internal preset resolution, use the var:preset|color|accent-500 syntax instead of the full CSS variable string.

Suggested change
"text": "var(--wp--preset--color--accent-500)"
"text": "var:preset|color|accent-500"

},
":hover": {
"color": {
"text": "var(--wp--preset--color--accent-two-500)"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Use the var:preset syntax for consistency.

Suggested change
"text": "var(--wp--preset--color--accent-two-500)"
"text": "var:preset|color|accent-two-500"

}
}
}
}
}
}
41 changes: 41 additions & 0 deletions styles/presets/radii.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"settings": {
"border": {
"radius": true,
"radiusSizes": [
{
"name": "none",
"slug": "0",
"size": "0"
},
{
"name": "small",
"slug": "100",
"size": "4px"
},
{
"name": "medium",
"slug": "200",
"size": "8px"
},
{
"name": "large",
"slug": "300",
"size": "16px"
},
{
"name": "x-large",
"slug": "400",
"size": "24px"
},
{
"name": "round",
"slug": "500",
"size": "9999px"
}
]
}
}
}
41 changes: 41 additions & 0 deletions styles/presets/shadows.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"settings": {
"shadow": {
"defaultPresets": false,
"presets": [
{
"name": "Tiny",
"slug": "100",
"shadow": "0.5px 2px 3px 0.5px rgba(17, 17, 17, 0.2)"
},
{
"name": "Base",
"slug": "200",
"shadow": "0.5px 2px 6px 1px rgba(17, 17, 17, 0.2)"
},
{
"name": "Small",
"slug": "300",
"shadow": "1px 4px 12px 4px rgba(17, 17, 17, 0.2)"
},
{
"name": "Medium",
"slug": "400",
"shadow": "1px 4px 12px 4px rgba(17, 17, 17, 0.3)"
},
{
"name": "Large",
"slug": "500",
"shadow": "1px 4px 12px 4px rgba(17, 17, 17, 0.3)"
},
{
"name": "X-Large",
"slug": "600",
"shadow": "2px 6px 12px 6px rgba(17, 17, 17, 0.3)"
}
]
}
}
}
85 changes: 85 additions & 0 deletions styles/presets/spacing.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"settings": {
"spacing": {
"defaultSpacingSizes": false,
"spacingSizes": [
{
"name": "XXS",
"slug": "5",
"size": "clamp(0.250rem, calc(0.227rem + 0.006vw), 0.312rem)"
},
{
"name": "XS",
"slug": "10",
"size": "clamp(0.500rem, calc(0.454rem + 0.012vw), 0.625rem)"
},
{
"name": "S",
"slug": "20",
"size": "clamp(0.875rem, calc(0.736rem + 0.036vw), 1.250rem)"
},
{
"name": "M",
"slug": "30",
"size": "clamp(1.250rem, calc(1.018rem + 0.060vw), 1.875rem)"
},
{
"name": "L",
"slug": "40",
"size": "clamp(1.625rem, calc(1.300rem + 0.083vw), 2.500rem)"
},
{
"name": "XL",
"slug": "50",
"size": "clamp(2.062rem, calc(1.668rem + 0.101vw), 3.125rem)"
},
{
"name": "XXL",
"slug": "60",
"size": "clamp(2.312rem, calc(1.779rem + 0.137vw), 3.750rem)"
},
{
"name": "XXXL",
"slug": "70",
"size": "clamp(2.625rem, calc(1.975rem + 0.167vw), 4.375rem)"
},
{
"name": "XXXXL",
"slug": "80",
"size": "clamp(3.000rem, calc(2.257rem + 0.190vw), 5.000rem)"
},
{
"name": "Gigantic",
"slug": "90",
"size": "clamp(3.500rem, calc(2.711rem + 0.202vw), 5.625rem)"
},
{
"name": "Colossal",
"slug": "100",
"size": "clamp(4.000rem, calc(3.164rem + 0.214vw), 6.250rem)"
}
],
"units": [
"%",
"em",
"px",
"rem",
"vh",
"vw"
]
}
},
"styles": {
"spacing": {
"blockGap": "var(--wp--preset--spacing--30)",
"padding": {
"bottom": "0",
"left": "var(--wp--preset--spacing--20)",
"right": "var(--wp--preset--spacing--20)",
"top": "0"
}
}
}
}
Loading
Loading