Skip to content
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@
"minimum-stability": "dev",
"prefer-stable": true,
"config": {
"platform": {
"php": "8.2"
},
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true,
"phpstan/extension-installer": true
Expand Down
23 changes: 13 additions & 10 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 33 additions & 0 deletions inc/Abstracts/AbstractThemeFeature.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php
/**
* Theme feature base class.
*
* @package rtCamp\Theme\Elementary\Abstracts
*/

declare( strict_types = 1 );

namespace rtCamp\Theme\Elementary\Abstracts;

use rtCamp\WPFramework\Contracts\Abstracts\AbstractFeature;
use rtCamp\WPFramework\Utils\FeatureSelector;
use rtCamp\Theme\Elementary\Main;
use rtCamp\Theme\Elementary\Core\FeatureRegistry;

/**
* Class AbstractThemeFeature
*
* Abstract base for all theme features. Wires AbstractFeature to the theme's
* shared FeatureRegistry via Main::get_shared().
*
* @since 1.0.0
*/
abstract class AbstractThemeFeature extends AbstractFeature {

/**
* {@inheritDoc}
*/
protected function get_feature_registry(): FeatureSelector {
return Main::get_instance()->get_shared( FeatureRegistry::class );
}
}
35 changes: 35 additions & 0 deletions inc/Core/FeatureRegistry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php
/**
* Theme feature registry.
*
* @package rtCamp\Theme\Elementary\Core
*/

declare( strict_types = 1 );

namespace rtCamp\Theme\Elementary\Core;

use rtCamp\WPFramework\Contracts\Interfaces\Shareable;
use rtCamp\WPFramework\Utils\FeatureSelector;

/**
* Class FeatureRegistry
*
* The theme's shared feature-flag registry. Flags are registered into it by
* each ThemeFeature subclass on construction, so the admin settings page
* discovers them automatically without a central list.
*
* Shared option: elementary_features (one row; value is {flag_key => bool})
* Override constants: ELEMENTARY_FEATURE_{FLAG}
*
* @since 1.0.0
*/
final class FeatureRegistry extends FeatureSelector implements Shareable {

/**
* Constructor.
*/
public function __construct() {
parent::__construct( 'elementary' );
}
}
23 changes: 23 additions & 0 deletions inc/Helpers/Util.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

use rtCamp\Theme\Elementary\Core\Components;
use rtCamp\Theme\Elementary\Core\Encryption;
use rtCamp\Theme\Elementary\Core\FeatureRegistry;
use rtCamp\Theme\Elementary\Core\Templates;
use rtCamp\Theme\Elementary\Main;

Expand Down Expand Up @@ -158,4 +159,26 @@ private static function encryptor(): Encryption {

return $encryptor;
}

/**
* Whether a theme feature flag is enabled.
*
* Delegates to the shared FeatureRegistry. Safe to call at hook time;
* do not call from constructors during Main::load() since that runs before
* hook registration — use can_register() via AbstractFeature instead.
*
* @param string $flag Feature-flag slug, e.g. 'author-bio'.
*
* @return bool True if enabled, false otherwise.
*/
public static function is_feature_enabled( string $flag ): bool {
/**
* Shared feature registry.
*
* @var FeatureRegistry $features
*/
$features = Main::get_instance()->get_shared( FeatureRegistry::class );

return $features->is_enabled( $flag );
}
}
7 changes: 5 additions & 2 deletions inc/Main.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
namespace rtCamp\Theme\Elementary;

use rtCamp\WPFramework\Contracts\Traits\{Singleton, Loader};
use rtCamp\Theme\Elementary\Core\{Assets, Components, Encryption, Menu, Templates, ThemeSetup};
use rtCamp\Theme\Elementary\Modules\{BlockExtensions\MediaTextInteractive, Settings\ThemeOptions, Shortcodes\AuthorBio};
use rtCamp\Theme\Elementary\Core\{Assets, Components, Encryption, FeatureRegistry, Menu, Templates, ThemeSetup};
use rtCamp\Theme\Elementary\Modules\{BlockExtensions\MediaTextInteractive, Settings\FeaturesSettingsPage, Settings\ThemeOptions, Shortcodes\AuthorBio};

/**
* Class Main
Expand All @@ -33,15 +33,18 @@ class Main {
Components::class,
Templates::class,
Encryption::class,
FeatureRegistry::class,
MediaTextInteractive::class,
ThemeOptions::class,
FeaturesSettingsPage::class,
AuthorBio::class,
];

/**
* Constructor.
*/
protected function __construct() {
static::$instance = $this;
$this->load( self::CLASSES );
}
}
29 changes: 22 additions & 7 deletions inc/Modules/BlockExtensions/MediaTextInteractive.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,33 @@
namespace rtCamp\Theme\Elementary\Modules\BlockExtensions;

use WP_HTML_Tag_Processor;
use rtCamp\WPFramework\Contracts\Interfaces\Registrable;
use rtCamp\Theme\Elementary\Abstracts\AbstractThemeFeature;

/**
* Class MediaTextInteractive
*
* Gated behind the `media-text-interactive` feature flag (Settings →
* Features), enabled by default; toggling the flag takes effect on the next
* request, since registration is decided once at load.
*/
class MediaTextInteractive implements Registrable {
class MediaTextInteractive extends AbstractThemeFeature {

/**
* {@inheritDoc}
*/
protected function get_slug(): string {
return 'media-text-interactive';
}

/**
* Register hooks.
* {@inheritDoc}
*/
protected function get_description(): string {
return __( 'Adds interactive behaviour to the core Media & Text block — play/pause controls, scroll-triggered video, and column layout enhancements.', 'elementary-theme' );
}

/**
* {@inheritDoc}
*/
public function register_hooks(): void {
add_filter( 'render_block_core/button', [ $this, 'render_block_core_button' ], 10, 2 );
Expand Down Expand Up @@ -60,10 +78,6 @@ public function render_block_core_columns( string $block_content, array $block )
return $block_content;
}

/**
* Enqueue the module script, The prefix `@` is used to indicate that the script is a module.
* This handle with the prefix `@` will be used in other scripts to import this module.
*/
wp_enqueue_script_module(
'@elementary/media-text',
sprintf( '%s/js/modules/media-text.js', ELEMENTARY_THEME_BUILD_URI ),
Expand Down Expand Up @@ -93,6 +107,7 @@ public function render_block_core_video( string $block_content, array $block ):
if ( ! isset( $block['attrs']['className'] ) || ! str_contains( $block['attrs']['className'], 'elementary-media-text-interactive' ) ) {
return $block_content;
}

$p = new WP_HTML_Tag_Processor( $block_content );

$p->next_tag();
Expand Down
48 changes: 48 additions & 0 deletions inc/Modules/Settings/FeaturesSettingsPage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php
/**
* Features settings page.
*
* @package rtCamp\Theme\Elementary\Modules\Settings
*/

declare( strict_types = 1 );

namespace rtCamp\Theme\Elementary\Modules\Settings;

use rtCamp\Theme\Elementary\Core\FeatureRegistry;
use rtCamp\Theme\Elementary\Main;
use rtCamp\WPFramework\Utils\FeatureSelectorSettingsPage;

/**
* Class FeaturesSettingsPage
*
* Admin UI for the theme's feature flags, at Settings → Features (slug
* `elementary-features`). The framework page renders one checkbox per flag
* registered on the injected selector and shows constant-overridden flags as
* locked — only the titles are overridden here, for the theme text domain.
*
* @since 1.0.0
*/
final class FeaturesSettingsPage extends FeatureSelectorSettingsPage {

/**
* {@inheritDoc}
*/
protected function get_selector(): FeatureRegistry {
return Main::get_instance()->get_shared( FeatureRegistry::class );
}

/**
* {@inheritDoc}
*/
protected function get_page_title(): string {
return __( 'Elementary Features', 'elementary-theme' );
}

/**
* {@inheritDoc}
*/
protected function get_menu_title(): string {
return __( 'Features', 'elementary-theme' );
}
}
30 changes: 23 additions & 7 deletions inc/Modules/Shortcodes/AuthorBio.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,39 @@

namespace rtCamp\Theme\Elementary\Modules\Shortcodes;

use rtCamp\Theme\Elementary\Abstracts\AbstractThemeFeature;
use rtCamp\Theme\Elementary\Helpers\Util;
use rtCamp\WPFramework\Contracts\Interfaces\Registrable;

/**
* Class AuthorBio
*
* Example consumer of the theme's TemplateLoader. Registers the
* `[elementary_author_bio]` shortcode, which renders the `author-bio`
* template part through Util::get_template() — a child theme can override the
* markup by shipping its own template-parts/author-bio.php.
* Registers the [elementary_author_bio] shortcode, which renders the
* `author-bio` template part through Util::get_template().
*
* Gated behind the `author-bio` feature flag (Settings → Features), enabled
* by default; toggling the flag takes effect on the next request, since
* registration is decided once at load.
*
* @since 1.0.0
*/
final class AuthorBio implements Registrable {
final class AuthorBio extends AbstractThemeFeature {

/**
* {@inheritDoc}
*/
protected function get_slug(): string {
return 'author-bio';
}

/**
* {@inheritDoc}
*/
protected function get_description(): string {
return __( 'Enables the [elementary_author_bio] shortcode, which renders an author biography block on posts and pages.', 'elementary-theme' );
}

/**
* Register hooks.
* {@inheritDoc}
*
* @since 1.0.0
*/
Expand Down
Loading