diff --git a/.stylelintrc.json b/.stylelintrc.json
index c94dbbd..17af94e 100644
--- a/.stylelintrc.json
+++ b/.stylelintrc.json
@@ -9,7 +9,6 @@
"assets/css/fontawesome6/**",
"assets/css/flexslider.css",
"assets/js/**",
- "inc/libraries/**",
"layouts/**"
],
"rules": {
diff --git a/CLAUDE.md b/CLAUDE.md
index 838b9a8..0c0e2a0 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -56,12 +56,12 @@ The version lives in **four** places, all currently in sync at 1.2.20: `style.cs
Nearly all theme behavior is driven by `get_theme_mod()` reads scattered across templates. The chain:
1. `inc/customizer.php` (~1500 lines) registers the `shapely_main_options` panel and its sections/controls.
-2. `inc/libraries/epsilon-framework/` — a **vendored** third-party customizer framework (Macho Themes) supplying `Epsilon_Control_Toggle`, `Epsilon_Control_Slider`, `Epsilon_Section_Pro`, `Epsilon_Section_Recommended_Actions`. `Shapely::init_epsilon()` passes a whitelist of controls/sections to load. Classes resolve through `class-epsilon-autoloader.php`, which maps `Foo_Bar` → `class-foo-bar.php` across a fixed directory list.
+2. `inc/custom-controls/` — the theme's own customizer controls and sections: `Shapely_Custom_Label`, `Shapely_Logo_Dimensions`, `Shapely_Control_Range` (a range input with a value readout), and `Shapely_Section_Link` (a section rendered as a single outbound button). Everything else uses core control types. The vendored Epsilon framework that used to supply these was removed in 1.3.0.
3. `shapely_get_theme_options()` in `inc/extras.php` builds a CSS string from those theme mods. `shapely_enqueue_theme_options_css()` buffers it and hands it to `wp_add_inline_style( 'shapely-style', … )`, so it rides along with the main stylesheet rather than being echoed straight into `wp_head`.
So **adding a color/typography option means two edits**: a control in `inc/customizer.php` *and* a selector block in `shapely_get_theme_options()`. Sanitizers (`shapely_sanitize_checkbox`, `shapely_sanitize_layout`, …) live at the bottom of `customizer.php`.
-The Epsilon JS bundles (`assets/js/epsilon-framework-*.js`) are webpack output from TypeScript/Vue sources in `inc/libraries/epsilon-framework/assets/vendors/` — do not hand-edit the bundles; rebuild with `npm run build` **inside `inc/libraries/epsilon-framework/`** (its own webpack setup, unrelated to the theme's `npm run build`).
+`theme.json` (v2, matching the WordPress 6.4 floor) is the source of truth for the palette, type scale, heading sizes and layout widths — it is what the block editor reads. The customizer is still where a site owner changes colours: `shapely_enqueue_theme_options_css()` emits each colour theme mod as an override of the matching `--wp--preset--color--*` property, so blocks and the classic front end resolve to one value rather than two palettes disagreeing. **Adding a colour option now means three edits**: a control in `inc/customizer.php`, a selector block in `shapely_get_theme_options()`, and a preset in `theme.json` plus its entry in the `$presets` map.
### Layout resolution
@@ -80,7 +80,7 @@ Blog listings (`index.php`, `archive.php`) dispatch on the `blog_layout_view` th
- **WooCommerce**: theme support in `functions.php`, `woocommerce.php` as the wrapper, `woocommerce/product-searchform.php` as the only template override. The `shop-sidebar` widget area registers only when `shapely_is_woocommerce_activated()`. `woocommerce/` is excluded from phpcs.
- **Jetpack**: `inc/jetpack.php` plus `archive-jetpack-portfolio.php` / `single-jetpack-portfolio.php` for the portfolio CPT (masonry is enqueued only when that CPT exists).
-- **Welcome screen / recommended plugins**: `inc/libraries/welcome-screen/` + `inc/class-shapely-notify-system.php` drive the "Import Demo Content" flow and the recommended-plugins list defined in `Shapely::$recommended_plugins`.
+- **Welcome screen / recommended plugins**: `inc/admin/class-shapely-welcome.php` + `inc/class-shapely-notify-system.php` drive the "Import Demo Content" flow and the recommended-plugins list defined in `Shapely::$recommended_plugins`. The import itself is **not** in the theme: the button posts `shapely_companion_import_content` with a `welcome_nonce` to a handler in the shapely-companion plugin, so that action name, its `import` values and the nonce name are a fixed contract — changing any of them breaks the import against every released version of the plugin. Plugin installation is handed to core's `wp.updates`.
### Frontend JS
@@ -96,14 +96,14 @@ Every optional third-party plugin (FlexSlider, OwlCarousel, imagesLoaded, YTPlay
- Font Awesome 6 splits families: brand glyphs (`fa-github`, `fa-x-twitter`, …) need `fa-brands`, everything else uses `fa`/`fa-solid`. A brand icon rendered with plain `fa` shows a blank box. FA4 `-o` outline suffixes no longer exist. The bundled build is 6.4.2, so icons added later (e.g. `fa-bluesky`, 6.6) are unavailable.
- PHP follows WordPress-Core via `phpcs.ruleset.xml` (plus `PHPCompatibility`; `node_modules/` and `woocommerce/` excluded). Newer files open with an `ABSPATH` guard.
- JS follows `.jshintrc`: `es3`, single quotes, mandatory curly braces, `eqeqeq`. New localized objects must be added to its `globals` whitelist or jshint fails.
-- Text domain is `shapely` (`epsilon-framework` is also allowed by checktextdomain). Translations live in `languages/`.
+- Text domain is `shapely`, and it is the only one allowed by `npm run i18n:check`. Translations live in `languages/`.
- Supported floor: WordPress 6.4 / PHP 7.4, tested to WordPress 6.8 / PHP 8.4 (stated in `style.css`, `readme.txt`, and `functions.php`).
## Repo quirks worth knowing
-- `README.md` documents a git-submodule workflow and `setup.sh` / `setup.bat` — **none of that exists**: `.gitmodules` is empty, no submodules are registered, and the Epsilon framework is vendored directly in `inc/libraries/`.
+- `README.md` documents a git-submodule workflow and `setup.sh` / `setup.bat` — **none of that exists**: `.gitmodules` is empty and no submodules are registered.
- `.gitignore` used to list `Gruntfile.js`, `.jshintrc`, `.travis.yml`, `phpcs.ruleset.xml` and `package-lock.json`. The first three are gone; the last two are now deliberately tracked (`npm ci` needs the lockfile, and the phpcs ruleset is real config).
- The zip builder in `tools/build-zip.mjs` is deny-by-default: a file ships unless `EXCLUDE` matches it. The old Grunt copy task was allow-everything-then-subtract, which is how `CLAUDE.md` and a nested `package.json` ended up inside released zips.
-- The vendored `inc/libraries/` code is third-party but **not** a submodule, so it is edited in place when it blocks a PHP/WordPress upgrade. Two such patches exist: optional-before-required parameters in `class-epsilon-control-section-repeater.php`, and `get_page_by_title()` (deprecated in WP 6.2) in `class-epsilon-welcome-screen.php`.
+- `inc/class-shapely-migrations.php` runs one-time upgrade steps, keyed on the `shapely_migrated_version` option. No theme mod has ever been renamed, so it does not map settings — it carries the state the retired Epsilon framework owned (`shapely_actions_left` → `shapely_dismissed_actions`) and suppresses the onboarding notice on sites that were already established.
- `Shapely_Notify_System` overrides the vendored `check_plugin_is_installed()`/`check_plugin_is_active()` because the parent hardcodes `ABSPATH . 'wp-content/plugins/'` and breaks on relocated content directories.
- `layouts/content-sidebar.css` and `layouts/sidebar-content.css` are leftovers from Underscores and are not enqueued anywhere.
diff --git a/assets/css/editor-style.css b/assets/css/editor-style.css
index 4c6ad50..e75cc02 100644
--- a/assets/css/editor-style.css
+++ b/assets/css/editor-style.css
@@ -1,76 +1,39 @@
/*
* Block editor styles.
*
- * Mirrors the front-end typography from style.css so that what is written in
- * the editor resembles what the visitor sees. Values are copied from the
- * corresponding front-end rules rather than reinvented -- when style.css
- * changes, this file needs the same change.
+ * Deliberately short. theme.json now carries the palette, the type scale, the
+ * heading sizes and the link and button treatments, and WordPress applies those
+ * to the editor canvas on its own -- so anything expressible there has been
+ * removed from here rather than maintained in two places that can drift.
*
- * Scoped by the editor itself: WordPress rewrites these selectors to apply
- * inside the editor canvas only, so plain element selectors are correct here
- * and must not be prefixed by hand.
+ * What is left is the handful of front-end rules from style.css that theme.json
+ * has no vocabulary for.
*/
-.editor-styles-wrapper {
- font-family: Raleway, 'Helvetica Neue', Helvetica, Arial, sans-serif;
- font-size: 14px;
- font-weight: 400;
- line-height: 24px;
- color: #8c979e;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
+/*
+ * theme.json sets the body font size, but not the reading rhythm the front end
+ * uses: style.css gives every block-level element a flat 24px bottom margin
+ * rather than the editor's default spacing.
+ */
.editor-styles-wrapper p,
.editor-styles-wrapper ul,
.editor-styles-wrapper ol,
.editor-styles-wrapper table,
-.editor-styles-wrapper blockquote {
- margin-top: 0;
- margin-bottom: 24px;
-}
-
+.editor-styles-wrapper blockquote,
.editor-styles-wrapper h1,
.editor-styles-wrapper h2,
.editor-styles-wrapper h3,
.editor-styles-wrapper h4,
.editor-styles-wrapper h5,
.editor-styles-wrapper h6 {
- font-family: inherit;
- font-weight: 400;
- color: #0e1015;
margin-top: 0;
margin-bottom: 24px;
}
-.editor-styles-wrapper h1 {
- font-size: 60px;
-}
-
-.editor-styles-wrapper h2 {
- font-size: 45px;
-}
-
-.editor-styles-wrapper h3 {
- font-size: 30px;
-}
-
-.editor-styles-wrapper h4 {
- font-size: 25px;
-}
-
-.editor-styles-wrapper h5 {
- font-size: 22px;
- line-height: 28px;
-}
-
-.editor-styles-wrapper h6 {
- font-size: 18px;
- line-height: 25px;
-}
-
-/* The front end drops heading sizes sharply on small screens; match it so the
- editor's mobile preview is not wildly larger than the real thing. */
+/*
+ * Headings drop sharply on narrow screens on the front end. Without this the
+ * editor's mobile preview shows a 60px h1 where the visitor sees 32px.
+ */
@media all and (max-width: 767px) {
.editor-styles-wrapper h1 {
@@ -78,11 +41,7 @@
}
}
-.editor-styles-wrapper a {
- color: #745cf9;
- font-weight: 600;
-}
-
+/* style.css blockquote: no rule, larger type, its own colour. */
.editor-styles-wrapper blockquote {
overflow: hidden;
padding: 32px 0;
diff --git a/assets/css/welcome.css b/assets/css/welcome.css
new file mode 100644
index 0000000..a8f5ea6
--- /dev/null
+++ b/assets/css/welcome.css
@@ -0,0 +1,139 @@
+/*
+ * Shapely welcome screen.
+ *
+ * Deliberately plain: this is a WordPress admin page, so it borrows core's
+ * type and colour rather than introducing a second visual language inside
+ * wp-admin. Replaces welcome-screen/css/welcome.css from the Epsilon bundle.
+ */
+
+.shapely-welcome__title {
+ margin-bottom: 0.2em;
+}
+
+.shapely-welcome__intro {
+ max-width: 60em;
+ margin-top: 0;
+ color: #50575e;
+ font-size: 14px;
+}
+
+.shapely-welcome__tabs {
+ margin-bottom: 1.5em;
+}
+
+.shapely-welcome__count {
+ display: inline-block;
+ min-width: 1.6em;
+ padding: 0 0.4em;
+ border-radius: 9px;
+ background: #d63638;
+ color: #fff;
+ font-size: 11px;
+ line-height: 1.6;
+ text-align: center;
+}
+
+/* ---- card grid ---- */
+
+.shapely-cards {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
+ gap: 20px;
+ max-width: 1100px;
+}
+
+.shapely-card {
+ padding: 20px;
+ border: 1px solid #dcdcde;
+ background: #fff;
+ border-radius: 4px;
+}
+
+.shapely-card h3 {
+ margin-top: 0;
+}
+
+.shapely-card p:last-child {
+ margin-bottom: 0;
+}
+
+/* ---- recommended actions ---- */
+
+.shapely-actions {
+ max-width: 800px;
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.shapely-action {
+ position: relative;
+ margin: 0 0 16px;
+ padding: 20px 48px 20px 20px;
+ border: 1px solid #dcdcde;
+ background: #fff;
+ border-radius: 4px;
+}
+
+.shapely-action h3 {
+ margin-top: 0;
+}
+
+.shapely-action__dismiss {
+ position: absolute;
+ top: 12px;
+ right: 12px;
+ padding: 0;
+ border: 0;
+ background: none;
+ color: #a7aaad;
+ cursor: pointer;
+ line-height: 1;
+}
+
+.shapely-action__dismiss:hover,
+.shapely-action__dismiss:focus {
+ color: #d63638;
+}
+
+.shapely-action__done {
+ color: #008a20;
+ font-weight: 600;
+}
+
+.shapely-action__error {
+ color: #d63638;
+}
+
+.shapely-empty {
+ max-width: 800px;
+ padding: 20px;
+ border: 1px solid #dcdcde;
+ background: #fff;
+ border-radius: 4px;
+}
+
+/* ---- import options ---- */
+
+.import-content-container {
+ display: none;
+ margin-top: 12px;
+ padding-top: 12px;
+ border-top: 1px solid #f0f0f1;
+}
+
+.import-content-container h4 {
+ margin: 12px 0 6px;
+}
+
+.checkbox-group {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px 20px;
+}
+
+.checkbox-group label {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+}
diff --git a/assets/js/welcome.js b/assets/js/welcome.js
new file mode 100644
index 0000000..ac9225c
--- /dev/null
+++ b/assets/js/welcome.js
@@ -0,0 +1,132 @@
+/**
+ * Shapely welcome screen.
+ *
+ * Replaces the Epsilon welcome screen's JS. Two behaviours matter:
+ *
+ * - The demo import posts `shapely_companion_import_content` with a
+ * `welcome_nonce`. That endpoint lives in the Shapely Companion plugin, so
+ * the action name, the `import` value and the nonce field are a fixed
+ * contract and must not be renamed here.
+ * - Plugin installation is left to core's wp.updates rather than reimplemented.
+ */
+( function ( $ ) {
+ 'use strict';
+
+ if ( typeof window.shapelyWelcome === 'undefined' ) {
+ return;
+ }
+
+ var cfg = window.shapelyWelcome;
+
+ /**
+ * Collect the checked import options inside a container.
+ *
+ * The old screen sent 'import-all' when more than one option was ticked,
+ * and the single option's own value when exactly one was. The companion
+ * plugin still branches on those values, so keep the same mapping.
+ */
+ function importSelection( $container ) {
+ var options = [];
+
+ $container.find( 'input[type="checkbox"][name="options"]:checked' ).each( function () {
+ options.push( $( this ).val() );
+ } );
+
+ if ( ! options.length ) {
+ return null;
+ }
+
+ return options.length === 1 ? options[ 0 ] : 'import-all';
+ }
+
+ function runImport( $button ) {
+ var $container = $button.closest( '.shapely-action' );
+ var selection = importSelection( $container );
+
+ if ( null === selection ) {
+ // Nothing ticked: there is nothing to import.
+ return;
+ }
+
+ $button.addClass( 'updating-message' ).prop( 'disabled', true ).text( cfg.strings.importing );
+
+ $.ajax( {
+ url: cfg.ajaxurl,
+ type: 'POST',
+ dataType: 'json',
+ data: {
+ action: 'shapely_companion_import_content',
+ import: selection,
+ nonce: cfg.importNonce
+ }
+ } )
+ .done( function ( response ) {
+ var ok = response && ( response.success === true || ( response.data && response.data.status === true ) );
+
+ if ( ok ) {
+ $container.html( '
' + cfg.strings.imported + ' ' );
+ window.setTimeout( function () {
+ window.location.reload();
+ }, 1500 );
+ return;
+ }
+
+ $container.append( '' + cfg.strings.failed + '
' );
+ $button.removeClass( 'updating-message' ).prop( 'disabled', false );
+ } )
+ .fail( function () {
+ $container.append( '' + cfg.strings.failed + '
' );
+ $button.removeClass( 'updating-message' ).prop( 'disabled', false );
+ } );
+ }
+
+ function dismissAction( $button ) {
+ var $item = $button.closest( '.shapely-action' );
+ var id = $item.data( 'action-id' );
+
+ if ( ! id ) {
+ return;
+ }
+
+ $.ajax( {
+ url: cfg.ajaxurl,
+ type: 'POST',
+ dataType: 'json',
+ data: {
+ action: 'shapely_dismiss_action',
+ id: id,
+ nonce: cfg.dismissNonce
+ }
+ } ).done( function () {
+ $item.slideUp( 200, function () {
+ $item.remove();
+ } );
+ } );
+ }
+
+ $( function () {
+ $( document ).on( 'click', '[data-action="import_demo"]', function ( e ) {
+ e.preventDefault();
+ runImport( $( this ) );
+ } );
+
+ $( document ).on( 'click', '.shapely-action__dismiss', function ( e ) {
+ e.preventDefault();
+ dismissAction( $( this ) );
+ } );
+
+ // Toggle the advanced import options.
+ $( document ).on( 'click', '.epsilon-hidden-content-toggler', function ( e ) {
+ e.preventDefault();
+ $( $( this ).attr( 'href' ) ).slideToggle( 150 );
+ } );
+
+ // The core notice dismiss button is generated by wp.a11y, so hook its click.
+ $( document ).on( 'click', '.shapely-welcome-notice .notice-dismiss', function () {
+ $.post( cfg.ajaxurl, {
+ action: 'shapely_dismiss_notice',
+ nonce: cfg.dismissNonce
+ } );
+ } );
+ } );
+} )( jQuery );
diff --git a/changelog.txt b/changelog.txt
index d3ff5b3..a081488 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -1,3 +1,13 @@
+= 1.3.0 =
+* Removed the vendored Epsilon customizer framework. It was 144 files and 1.3 MB -- 52% of the theme -- and provided 31 toggle controls, one slider, two customizer sections and the welcome screen. All of those are now built on core WordPress APIs. The download is roughly a third smaller and the theme ships 51 PHP files instead of 88
+* Added theme.json, so the palette, type scale and layout widths have a single source of truth that the block editor reads. Colour options in the customizer still work exactly as before -- each one now overrides the matching theme.json value rather than maintaining a second palette
+* Fixed "Wide width" and "Full width" doing nothing on blocks. The theme has declared support for them since 2018 without ever shipping the CSS they need
+* Fixed images in post content overflowing their column. Bootstrap 3 only makes images responsive via a class that block markup never carries
+* Rebuilt the welcome screen and the demo importer on core APIs. The old AJAX endpoint accepted a class name and a method name from the request and called them; dismissals now run through a single handler with a nonce and a capability check
+* Fixed 11 customizer controls that would have disappeared entirely if the framework had ever failed to load, because their fallback registrations were incomplete
+* Renamed the bootstrap, flexslider, owl.carousel and owl.carousel.theme asset handles to be theme-specific. Generic handles can be claimed by a plugin first, which silently suppresses the theme's own file -- the same fault that blanked every icon on sites running Elementor in 1.2.20
+* The block editor now offers the theme's palette, and the editor canvas matches the front end
+
= 1.2.21 =
* Fixed the back-to-top arrow sitting off-centre in its button. The 36px button's content box was 22px tall and 10px wide after border and padding, so a 18px glyph with a 27px line box could not centre on either axis; measured 2.8px right and 1.9px high before, sub-pixel after
* Fixed one-page section links in the menu, and the page-builder customizer, using site_url() where home_url() was meant. On installs with WordPress in its own subdirectory those pointed at /wp/ instead of the site address
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 090afaf..5a46732 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -19,7 +19,6 @@ export default [
'assets/js/**/*.min.js',
'assets/js/owl-carousel/**',
'assets/js/dev/**',
- 'inc/libraries/**',
],
},
js.configs.recommended,
diff --git a/functions.php b/functions.php
index 3d3788f..ff08b48 100644
--- a/functions.php
+++ b/functions.php
@@ -52,6 +52,7 @@
require_once get_template_directory() . '/inc/class-shapely.php';
require_once get_template_directory() . '/inc/class-shapely-builder.php';
require_once get_template_directory() . '/inc/block-editor.php';
+require_once get_template_directory() . '/inc/class-shapely-migrations.php';
if ( ! defined( 'SHAPELY_SETUP_LOADED' ) ) {
define( 'SHAPELY_SETUP_LOADED', true );
@@ -300,7 +301,7 @@ function shapely_scripts() {
$uri = get_template_directory_uri();
// Add Bootstrap default CSS
- wp_enqueue_style( 'bootstrap', $uri . '/assets/css/bootstrap.min.css', array(), '3.3.7' );
+ wp_enqueue_style( 'shapely-bootstrap', $uri . '/assets/css/bootstrap.min.css', array(), '3.3.7' );
/*
* Registered under a theme-specific handle rather than the generic
@@ -322,7 +323,7 @@ function shapely_scripts() {
wp_enqueue_style( 'shapely-fonts', 'https://fonts.googleapis.com/css?family=Raleway:100,300,400,500,600,700&display=swap', array(), null );
// Add slider CSS
- wp_enqueue_style( 'flexslider', $uri . '/assets/css/flexslider.css', array(), SHAPELY_VERSION );
+ wp_enqueue_style( 'shapely-flexslider', $uri . '/assets/css/flexslider.css', array(), SHAPELY_VERSION );
//Add custom theme css
wp_enqueue_style( 'shapely-style', get_stylesheet_uri(), array(), SHAPELY_VERSION );
@@ -349,7 +350,7 @@ function shapely_scripts() {
wp_enqueue_script( 'shapely-jquery-compat', $uri . '/assets/js/jquery-compat.js', array( 'jquery' ), SHAPELY_VERSION, true );
// Add slider JS
- wp_enqueue_script( 'flexslider', $uri . '/assets/js/flexslider.min.js', array( 'jquery', 'shapely-jquery-compat' ), '2.7.2', true );
+ wp_enqueue_script( 'shapely-flexslider', $uri . '/assets/js/flexslider.min.js', array( 'jquery', 'shapely-jquery-compat' ), '2.7.2', true );
if ( is_page_template( 'page-templates/template-home.php' ) || is_page_template( 'page-templates/template-widget.php' ) ) {
wp_enqueue_script( 'shapely-parallax', $uri . '/assets/js/parallax.min.js', array( 'jquery' ), SHAPELY_VERSION, true );
@@ -357,9 +358,9 @@ function shapely_scripts() {
/**
* OwlCarousel Library
*/
- wp_enqueue_script( 'owl.carousel', $uri . '/assets/js/owl-carousel/owl.carousel.min.js', array( 'jquery' ), '2.3.4', true );
- wp_enqueue_style( 'owl.carousel', $uri . '/assets/js/owl-carousel/owl.carousel.min.css', array(), '2.3.4' );
- wp_enqueue_style( 'owl.carousel.theme', $uri . '/assets/js/owl-carousel/owl.theme.default.css', array(), '2.3.4' );
+ wp_enqueue_script( 'shapely-owl-carousel', $uri . '/assets/js/owl-carousel/owl.carousel.min.js', array( 'jquery' ), '2.3.4', true );
+ wp_enqueue_style( 'shapely-owl-carousel', $uri . '/assets/js/owl-carousel/owl.carousel.min.css', array(), '2.3.4' );
+ wp_enqueue_style( 'shapely-owl-carousel-theme', $uri . '/assets/js/owl-carousel/owl.theme.default.css', array(), '2.3.4' );
wp_enqueue_script(
'shapely-scripts', $uri . '/assets/js/shapely-scripts.js', array(
diff --git a/inc/admin/class-shapely-welcome.php b/inc/admin/class-shapely-welcome.php
new file mode 100644
index 0000000..85fe776
--- /dev/null
+++ b/inc/admin/class-shapely-welcome.php
@@ -0,0 +1,597 @@
+actions = isset( $config['actions'] ) ? (array) $config['actions'] : array();
+ $this->plugins = isset( $config['plugins'] ) ? (array) $config['plugins'] : array();
+
+ add_action( 'admin_menu', array( $this, 'register_page' ) );
+ add_action( 'admin_enqueue_scripts', array( $this, 'enqueue' ) );
+ add_action( 'admin_notices', array( $this, 'admin_notice' ) );
+ add_action( 'wp_ajax_shapely_dismiss_action', array( $this, 'ajax_dismiss_action' ) );
+ add_action( 'wp_ajax_shapely_dismiss_notice', array( $this, 'ajax_dismiss_notice' ) );
+ }
+
+ /**
+ * Tabs on the welcome screen.
+ *
+ * @return array
+ */
+ private function tabs() {
+ return array(
+ 'getting-started' => esc_html__( 'Getting Started', 'shapely' ),
+ 'recommended-actions' => esc_html__( 'Recommended Actions', 'shapely' ),
+ 'recommended-plugins' => esc_html__( 'Recommended Plugins', 'shapely' ),
+ 'support' => esc_html__( 'Support', 'shapely' ),
+ );
+ }
+
+ /**
+ * Register the page under Appearance.
+ */
+ public function register_page() {
+ add_theme_page(
+ esc_html__( 'Shapely', 'shapely' ),
+ esc_html__( 'About Shapely', 'shapely' ),
+ 'edit_theme_options',
+ self::PAGE,
+ array( $this, 'render' )
+ );
+ }
+
+ /**
+ * Is the current screen our welcome page?
+ *
+ * @return bool
+ */
+ private function is_welcome_screen() {
+ $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
+
+ return $screen && false !== strpos( (string) $screen->id, self::PAGE );
+ }
+
+ /**
+ * Assets.
+ *
+ * @param string $hook Current admin page.
+ */
+ public function enqueue( $hook ) {
+ $on_screen = $this->is_welcome_screen();
+
+ // The dismissible notice appears on every admin page, so its script
+ // has to load there too -- but nothing else does.
+ if ( ! $on_screen && ! $this->should_show_notice() ) {
+ return;
+ }
+
+ $uri = get_template_directory_uri();
+
+ if ( $on_screen ) {
+ wp_enqueue_style( 'shapely-welcome', $uri . '/assets/css/welcome.css', array(), SHAPELY_VERSION );
+ // Core's plugin installer, for the one-click install buttons.
+ wp_enqueue_script( 'plugin-install' );
+ wp_enqueue_script( 'updates' );
+ add_thickbox();
+ }
+
+ wp_enqueue_script( 'shapely-welcome', $uri . '/assets/js/welcome.js', array( 'jquery' ), SHAPELY_VERSION, true );
+
+ wp_localize_script(
+ 'shapely-welcome',
+ 'shapelyWelcome',
+ array(
+ 'ajaxurl' => admin_url( 'admin-ajax.php' ),
+ /*
+ * welcome_nonce is the nonce the Shapely Companion plugin
+ * verifies for the import. Renaming it would break the
+ * import against every released version of the plugin.
+ */
+ 'importNonce' => wp_create_nonce( 'welcome_nonce' ),
+ 'dismissNonce' => wp_create_nonce( 'shapely_dismiss' ),
+ 'strings' => array(
+ 'imported' => esc_html__( 'Demo content was imported successfully.', 'shapely' ),
+ 'importing' => esc_html__( 'Importing…', 'shapely' ),
+ 'failed' => esc_html__( 'There was an error importing the demo content.', 'shapely' ),
+ ),
+ )
+ );
+ }
+
+ /**
+ * Actions the user has not yet dismissed or completed.
+ *
+ * @return array
+ */
+ private function outstanding_actions() {
+ $dismissed = (array) get_option( self::DISMISSED_OPTION, array() );
+ $out = array();
+
+ foreach ( $this->actions as $action ) {
+ if ( empty( $action['id'] ) ) {
+ continue;
+ }
+ if ( in_array( $action['id'], $dismissed, true ) ) {
+ continue;
+ }
+ if ( ! empty( $action['check'] ) ) {
+ continue;
+ }
+ $out[] = $action;
+ }
+
+ return $out;
+ }
+
+ /**
+ * Should the "get started" notice show?
+ *
+ * @return bool
+ */
+ private function should_show_notice() {
+ if ( ! current_user_can( 'edit_theme_options' ) ) {
+ return false;
+ }
+ if ( get_option( 'shapely_welcome_notice_dismissed' ) ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * One-time admin notice pointing at the welcome screen.
+ */
+ public function admin_notice() {
+ if ( ! $this->should_show_notice() || $this->is_welcome_screen() ) {
+ return;
+ }
+ ?>
+
+
+ ' . esc_html__( 'Open the setup page', 'shapely' ) . ''
+ );
+ ?>
+
+
+ esc_html__( 'Not allowed', 'shapely' ) ), 403 );
+ }
+
+ $id = isset( $_POST['id'] ) ? sanitize_key( wp_unslash( $_POST['id'] ) ) : '';
+
+ if ( '' === $id ) {
+ wp_send_json_error( array( 'message' => esc_html__( 'Missing action id', 'shapely' ) ), 400 );
+ }
+
+ $dismissed = (array) get_option( self::DISMISSED_OPTION, array() );
+
+ if ( ! in_array( $id, $dismissed, true ) ) {
+ $dismissed[] = $id;
+ update_option( self::DISMISSED_OPTION, $dismissed, false );
+ }
+
+ wp_send_json_success( array( 'id' => $id ) );
+ }
+
+ /**
+ * Dismiss the admin notice.
+ */
+ public function ajax_dismiss_notice() {
+ check_ajax_referer( 'shapely_dismiss', 'nonce' );
+
+ if ( ! current_user_can( 'edit_theme_options' ) ) {
+ wp_send_json_error( array( 'message' => esc_html__( 'Not allowed', 'shapely' ) ), 403 );
+ }
+
+ update_option( 'shapely_welcome_notice_dismissed', 1, false );
+
+ wp_send_json_success();
+ }
+
+ /**
+ * URL for a tab.
+ *
+ * @param string $tab Tab slug.
+ * @return string
+ */
+ private function tab_url( $tab ) {
+ return admin_url( 'themes.php?page=' . self::PAGE . '&tab=' . rawurlencode( $tab ) );
+ }
+
+ /**
+ * Render the page.
+ */
+ public function render() {
+ if ( ! current_user_can( 'edit_theme_options' ) ) {
+ wp_die( esc_html__( 'You do not have permission to view this page.', 'shapely' ) );
+ }
+
+ $tabs = $this->tabs();
+ // Read-only tab selection; nonce would be meaningless here.
+ $tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'getting-started'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+
+ if ( ! isset( $tabs[ $tab ] ) ) {
+ $tab = 'getting-started';
+ }
+
+ $theme = wp_get_theme( get_template() );
+ ?>
+
+ outstanding_actions() );
+ ?>
+
+ outstanding_actions();
+
+ if ( empty( $actions ) ) {
+ echo '' . esc_html__( 'No recommended actions left to perform.', 'shapely' ) . '
';
+
+ return;
+ }
+
+ echo '';
+
+ foreach ( $actions as $action ) {
+ $id = isset( $action['id'] ) ? $action['id'] : '';
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+ render_plugin_button( $action['plugin_slug'] );
+ }
+
+ if ( ! empty( $action['help'] ) ) {
+ /*
+ * Supplied by the theme itself (Shapely::generate_action_html),
+ * not by user input, and contains the import controls.
+ */
+ echo wp_kses( $action['help'], $this->allowed_help_html() );
+ }
+ ?>
+
+ ';
+ }
+
+ /**
+ * HTML permitted inside an action's help markup.
+ *
+ * @return array
+ */
+ private function allowed_help_html() {
+ return array(
+ 'p' => array( 'class' => array() ),
+ 'div' => array(
+ 'class' => array(),
+ 'id' => array(),
+ ),
+ 'h4' => array(),
+ 'label' => array(),
+ 'a' => array(
+ 'class' => array(),
+ 'href' => array(),
+ 'id' => array(),
+ 'data-action' => array(),
+ 'target' => array(),
+ 'rel' => array(),
+ ),
+ 'input' => array(
+ 'type' => array(),
+ 'name' => array(),
+ 'class' => array(),
+ 'value' => array(),
+ 'checked' => array(),
+ ),
+ 'span' => array( 'class' => array() ),
+ 'strong' => array(),
+ );
+ }
+
+ /**
+ * Recommended plugins tab.
+ */
+ private function render_plugins() {
+ if ( empty( $this->plugins ) ) {
+ echo '' . esc_html__( 'No recommended plugins.', 'shapely' ) . '
';
+
+ return;
+ }
+
+ echo '';
+
+ foreach ( $this->plugins as $slug => $plugin ) {
+ $slug = is_string( $slug ) ? $slug : ( isset( $plugin['slug'] ) ? $plugin['slug'] : '' );
+ $name = isset( $plugin['name'] ) ? $plugin['name'] : $slug;
+ ?>
+
+
+
+
+
+ render_plugin_button( $slug ); ?>
+
+ ';
+ }
+
+ /**
+ * Install / activate button for a wordpress.org plugin.
+ *
+ * Uses core's own installer markup so wp.updates handles the request,
+ * rather than reimplementing plugin installation.
+ *
+ * @param string $slug Plugin directory slug.
+ */
+ private function render_plugin_button( $slug ) {
+ $slug = sanitize_key( $slug );
+
+ if ( '' === $slug ) {
+ return;
+ }
+
+ if ( ! function_exists( 'get_plugins' ) ) {
+ require_once ABSPATH . 'wp-admin/includes/plugin.php';
+ }
+
+ $installed = false;
+ $file = '';
+
+ foreach ( (array) get_plugins() as $plugin_file => $data ) {
+ if ( 0 === strpos( $plugin_file, $slug . '/' ) ) {
+ $installed = true;
+ $file = $plugin_file;
+ break;
+ }
+ }
+
+ if ( ! $installed ) {
+ printf(
+ '%4$s ',
+ esc_attr( $slug ),
+ esc_url( wp_nonce_url( self_admin_url( 'update.php?action=install-plugin&plugin=' . $slug ), 'install-plugin_' . $slug ) ),
+ esc_attr( sprintf( /* translators: %s: plugin slug. */ __( 'Install %s now', 'shapely' ), $slug ) ),
+ esc_html__( 'Install', 'shapely' )
+ );
+
+ return;
+ }
+
+ if ( is_plugin_active( $file ) ) {
+ printf( ' %s ', esc_html__( 'Active', 'shapely' ) );
+
+ return;
+ }
+
+ printf(
+ '%3$s ',
+ esc_attr( $slug ),
+ esc_url( wp_nonce_url( self_admin_url( 'plugins.php?action=activate&plugin=' . rawurlencode( $file ) ), 'activate-plugin_' . $file ) ),
+ esc_html__( 'Activate', 'shapely' )
+ );
+ }
+
+ /**
+ * Support tab.
+ */
+ private function render_support() {
+ ?>
+
+ false map in
+ * shapely_actions_left; the welcome screen keeps a flat list of
+ * dismissed ids. Without this, every action a site owner had already
+ * dealt with would reappear after the update.
+ *
+ * The legacy options are read but not deleted. They are inert, and
+ * leaving them means a downgrade to 1.2.x still finds its own state.
+ */
+ $legacy = get_option( 'shapely_actions_left', array() );
+
+ if ( is_array( $legacy ) && ! empty( $legacy ) ) {
+ $dismissed = (array) get_option( self::DISMISSED_OPTION, array() );
+
+ foreach ( $legacy as $id => $still_outstanding ) {
+ // false = the user dismissed it.
+ if ( ! $still_outstanding && ! in_array( $id, $dismissed, true ) ) {
+ $dismissed[] = sanitize_key( $id );
+ }
+ }
+
+ update_option( self::DISMISSED_OPTION, array_values( array_unique( $dismissed ) ), false );
+ }
+
+ /*
+ * Only greet genuinely new installs. On an established site the
+ * notice would be the one visible sign of an update that is meant to
+ * change nothing.
+ */
+ if ( self::looks_established() ) {
+ update_option( 'shapely_welcome_notice_dismissed', 1, false );
+ }
+ }
+ }
+endif;
+
+Shapely_Migrations::init();
diff --git a/inc/class-shapely-notify-system.php b/inc/class-shapely-notify-system.php
index d9b9a28..11f8e93 100644
--- a/inc/class-shapely-notify-system.php
+++ b/inc/class-shapely-notify-system.php
@@ -4,7 +4,17 @@
/**
* Class Shapely_Notify_System
*/
- class Shapely_Notify_System extends Epsilon_Notify_System {
+ /**
+ * Plugin / content state checks for the welcome screen.
+ *
+ * Previously extended Epsilon_Notify_System. Nothing was actually inherited:
+ * every self:: call in this class resolves to a method defined here, the two
+ * methods that looked inherited (check_plugin_is_installed, check_plugin_is_active)
+ * were already overridden because the parent hardcoded
+ * ABSPATH . 'wp-content/plugins/' and broke on relocated content directories,
+ * and no parent-only method is referenced anywhere in the theme.
+ */
+ class Shapely_Notify_System {
/**
* @param $ver
*
diff --git a/inc/class-shapely.php b/inc/class-shapely.php
index 060e568..b7194b5 100644
--- a/inc/class-shapely.php
+++ b/inc/class-shapely.php
@@ -73,9 +73,6 @@ function __construct() {
$this->load_class();
- // Initialize epsilon framework first
- $this->init_epsilon();
-
// Set up recommended actions and welcome screen on init
add_action('init', array($this, 'setup_recommended_actions'), 0);
add_action('init', array($this, 'init_welcome_screen'), 1);
@@ -91,21 +88,8 @@ public function load_class() {
return;
}
- require_once get_template_directory() . '/inc/libraries/epsilon-framework/class-epsilon-autoloader.php';
require_once get_template_directory() . '/inc/class-shapely-notify-system.php';
- require_once get_template_directory() . '/inc/libraries/welcome-screen/class-epsilon-welcome-screen.php';
-
- }
-
- public function init_epsilon() {
-
- $args = array(
- 'controls' => array( 'slider', 'toggle' ), // array of controls to load
- 'sections' => array( 'recommended-actions', 'pro' ), // array of sections to load
- 'backup' => false,
- );
-
- new Epsilon_Framework( $args );
+ require_once get_template_directory() . '/inc/admin/class-shapely-welcome.php';
}
@@ -115,40 +99,52 @@ public function init_welcome_screen() {
$this->setup_recommended_actions();
}
- Epsilon_Welcome_Screen::get_instance(
- $config = array(
- 'theme-name' => 'Shapely',
- 'theme-slug' => 'shapely',
- 'actions' => $this->recommended_actions,
- 'plugins' => $this->recommended_plugins,
+ Shapely_Welcome::get_instance(
+ array(
+ 'actions' => $this->recommended_actions,
+ 'plugins' => $this->recommended_plugins,
)
);
}
public function init_customizer( $wp_customize ) {
- $current_theme = wp_get_theme();
+ require_once get_template_directory() . '/inc/custom-controls/class-shapely-section-link.php';
+
+ $wp_customize->register_section_type( 'Shapely_Section_Link' );
+
+ /*
+ * Epsilon_Section_Recommended_Actions rendered the recommended actions,
+ * the recommended plugins and a set of social links inside the
+ * customizer -- a second copy of what the welcome screen already shows.
+ * Rather than maintain two implementations of the same list that can
+ * drift apart, the customizer now links to the one that is complete.
+ *
+ * Section id preserved so any stored customizer state keyed on it, and
+ * the deep link from the old admin notice, keep resolving.
+ */
$wp_customize->add_section(
- new Epsilon_Section_Recommended_Actions(
+ new Shapely_Section_Link(
$wp_customize, 'epsilon_recomended_section', array(
- 'title' => esc_html__( 'Recomended Actions', 'shapely' ),
- 'social_text' => esc_html( $current_theme->get( 'Author' ) ) . esc_html__( ' is social :', 'shapely' ),
- 'plugin_text' => esc_html__( 'Recomended Plugins :', 'shapely' ),
- 'actions' => $this->recommended_actions,
- 'plugins' => $this->recommended_plugins,
- 'theme_specific_option' => $this->theme_slug . '_show_required_actions',
- 'theme_specific_plugin_option' => $this->theme_slug . '_show_required_plugins',
- 'facebook' => 'https://www.facebook.com/colorlib',
- 'twitter' => 'https://twitter.com/colorlib',
- 'wp_review' => true,
- 'priority' => 0,
+ 'title' => esc_html__( 'Recommended Actions', 'shapely' ),
+ 'button_text' => esc_html__( 'View', 'shapely' ),
+ 'button_url' => admin_url( 'themes.php?page=shapely-welcome&tab=recommended-actions' ),
+ 'priority' => 0,
)
)
);
-
}
public function setup_recommended_actions() {
+ /*
+ * load_class() only runs for admin and customizer requests, so anything
+ * calling this directly outside those would hit an undefined class.
+ * Cheap to make that impossible rather than rely on call order.
+ */
+ if ( ! class_exists( 'Shapely_Notify_System' ) ) {
+ require_once get_template_directory() . '/inc/class-shapely-notify-system.php';
+ }
+
$this->recommended_actions = apply_filters(
'shapely_required_actions', array(
array(
diff --git a/inc/custom-controls/class-shapely-control-range.php b/inc/custom-controls/class-shapely-control-range.php
new file mode 100644
index 0000000..a628530
--- /dev/null
+++ b/inc/custom-controls/class-shapely-control-range.php
@@ -0,0 +1,82 @@
+id;
+ $output_id = $input_id . '-value';
+ $attrs = wp_parse_args(
+ $this->input_attrs,
+ array(
+ 'min' => 0,
+ 'max' => 100,
+ 'step' => 1,
+ )
+ );
+ $describedby = '';
+
+ if ( ! empty( $this->description ) ) {
+ $describedby = '_customize-description-' . $this->id;
+ }
+ ?>
+
+ label ) ) : ?>
+ label ); ?>
+
+
+
+ description ) ) : ?>
+ description ); ?>
+
+
+
+ aria-describedby=""
+ min=""
+ max=""
+ step=""
+ value="value() ); ?>"
+ oninput="document.getElementById('').value = this.value"
+ link(); ?>
+ />
+ value() ); ?>
+
+ button_text;
+ $json['button_url'] = $this->button_url;
+
+ return $json;
+ }
+
+ /**
+ * Underscore template for the section.
+ *
+ * Rendered in place of the usual accordion heading, so the whole row is
+ * the link rather than something that expands to reveal controls.
+ */
+ protected function render_template() {
+ ?>
+
+ {{ data.title }}
+ <# if ( data.button_text && data.button_url ) { #>
+
+ {{ data.button_text }}
+
+ <# } #>
+
+ get_setting( 'blogname' )->transport = 'postMessage';
- $wp_customize->get_setting( 'blogdescription' )->transport = 'postMessage';
- $wp_customize->get_setting( 'header_textcolor' )->transport = 'postMessage';
- $wp_customize->get_setting( 'custom_logo' )->transport = 'refresh';
+ /*
+ * These are core settings, but a plugin is free to remove any of them --
+ * and get_setting() then returns null, so assigning ->transport straight
+ * onto the result is a fatal on PHP 8 rather than a notice.
+ */
+ $transports = array(
+ 'blogname' => 'postMessage',
+ 'blogdescription' => 'postMessage',
+ 'header_textcolor' => 'postMessage',
+ 'custom_logo' => 'refresh',
+ );
+
+ foreach ( $transports as $setting_id => $transport ) {
+ $setting = $wp_customize->get_setting( $setting_id );
+
+ if ( $setting instanceof WP_Customize_Setting ) {
+ $setting->transport = $transport;
+ }
+ }
// Abort if selective refresh is not available.
if ( ! isset( $wp_customize->selective_refresh ) ) {
@@ -81,9 +96,18 @@ function shapely_customizer( $wp_customize ) {
// Load custom controls
require_once get_template_directory() . '/inc/custom-controls/class-shapely-logo-dimensions.php';
require_once get_template_directory() . '/inc/custom-controls/class-shapely-custom-label.php';
+ require_once get_template_directory() . '/inc/custom-controls/class-shapely-section-link.php';
+ require_once get_template_directory() . '/inc/custom-controls/class-shapely-control-range.php';
+ $wp_customize->register_section_type( 'Shapely_Section_Link' );
+
+ /*
+ * Section id kept as 'epsilon-section-pro'. It is not a setting, so nothing
+ * is stored against it, but renaming it would drop any customizer state
+ * keyed on the id for no benefit.
+ */
$wp_customize->add_section(
- new Epsilon_Section_Pro(
+ new Shapely_Section_Link(
$wp_customize, 'epsilon-section-pro', array(
'title' => esc_html__( 'Theme documentation', 'shapely' ),
'button_text' => esc_html__( 'Learn more', 'shapely' ),
@@ -187,28 +211,15 @@ function shapely_customizer( $wp_customize ) {
)
);
- if ( class_exists( 'Epsilon_Control_Toggle' ) ) {
- $wp_customize->add_control(
- new Epsilon_Control_Toggle(
- $wp_customize, 'shapely_placeholder_image_enabled', array(
- 'label' => esc_html__( 'Show Placeholder Images', 'shapely' ),
- 'description' => esc_html__( 'Show/hide placeholder images for posts without a featured image', 'shapely' ),
- 'section' => 'shapely_blog_section',
- 'priority' => 25,
- )
- )
- );
- } else {
- $wp_customize->add_control(
- 'shapely_placeholder_image_enabled', array(
+ $wp_customize->add_control(
+ 'shapely_placeholder_image_enabled', array(
+ 'type' => 'checkbox',
'label' => esc_html__( 'Show Placeholder Images', 'shapely' ),
'description' => esc_html__( 'Show/hide placeholder images for posts without a featured image', 'shapely' ),
'section' => 'shapely_blog_section',
'priority' => 25,
- 'type' => 'checkbox',
- )
- );
- }
+ )
+ );
$wp_customize->add_setting(
'shapely_placeholder_image', array(
@@ -316,28 +327,15 @@ function shapely_customizer( $wp_customize ) {
)
);
- if ( class_exists( 'Epsilon_Control_Toggle' ) ) {
- $wp_customize->add_control(
- new Epsilon_Control_Toggle(
- $wp_customize, 'top_callout', array(
- 'label' => esc_html__( 'Show Blog Title', 'shapely' ),
- 'description' => esc_html__( 'Show/hide the title from the Blog Page', 'shapely' ),
- 'section' => 'shapely_blog_section',
- 'priority' => 20,
- )
- )
- );
- } else {
- $wp_customize->add_control(
- 'top_callout', array(
+ $wp_customize->add_control(
+ 'top_callout', array(
+ 'type' => 'checkbox',
'label' => esc_html__( 'Show Blog Title', 'shapely' ),
'description' => esc_html__( 'Show/hide the title from the Blog Page', 'shapely' ),
'section' => 'shapely_blog_section',
'priority' => 20,
- 'type' => 'checkbox',
- )
- );
- }
+ )
+ );
$wp_customize->add_setting(
'hide_post_title', array(
@@ -346,24 +344,13 @@ function shapely_customizer( $wp_customize ) {
)
);
- if ( class_exists( 'Epsilon_Control_Toggle' ) ) {
- $wp_customize->add_control(
- new Epsilon_Control_Toggle(
- $wp_customize, 'hide_post_title', array(
- 'label' => esc_html__( 'Title in Blog Post', 'shapely' ),
- 'section' => 'wpseo_breadcrumbs_customizer_section',
- )
- )
- );
- } else {
- $wp_customize->add_control(
- 'hide_post_title', array(
+ $wp_customize->add_control(
+ 'hide_post_title', array(
+ 'type' => 'checkbox',
'label' => esc_html__( 'Title in Blog Post', 'shapely' ),
'section' => 'wpseo_breadcrumbs_customizer_section',
- 'type' => 'checkbox',
- )
- );
- }
+ )
+ );
$wp_customize->add_setting(
'blog_name', array(
@@ -387,26 +374,14 @@ function shapely_customizer( $wp_customize ) {
)
);
- if ( class_exists( 'Epsilon_Control_Toggle' ) ) {
- $wp_customize->add_control(
- new Epsilon_Control_Toggle(
- $wp_customize, 'mobile_menu_on_desktop', array(
- 'label' => esc_html__( 'Mobile Menu on Desktop', 'shapely' ),
- 'description' => esc_html__( 'Always the menu will be like the mobile menu', 'shapely' ),
- 'section' => 'shapely_main_section',
- )
- )
- );
- } else {
- $wp_customize->add_control(
- 'mobile_menu_on_desktop', array(
+ $wp_customize->add_control(
+ 'mobile_menu_on_desktop', array(
+ 'type' => 'checkbox',
'label' => esc_html__( 'Mobile Menu on Desktop', 'shapely' ),
'description' => esc_html__( 'Always the menu will be like the mobile menu', 'shapely' ),
'section' => 'shapely_main_section',
- 'type' => 'checkbox',
- )
- );
- }
+ )
+ );
$wp_customize->add_setting(
'footer_callout_text', array(
@@ -467,26 +442,14 @@ function shapely_customizer( $wp_customize ) {
)
);
- if ( class_exists( 'Epsilon_Control_Toggle' ) ) {
- $wp_customize->add_control(
- new Epsilon_Control_Toggle(
- $wp_customize, 'shapely_transparent_header', array(
- 'label' => esc_html__( 'Transparent header', 'shapely' ),
- 'description' => esc_html__( 'Toggling this to ON will make the header have a transparent background', 'shapely' ),
- 'section' => 'shapely_main_section',
- )
- )
- );
- } else {
- $wp_customize->add_control(
- 'shapely_transparent_header', array(
+ $wp_customize->add_control(
+ 'shapely_transparent_header', array(
+ 'type' => 'checkbox',
'label' => esc_html__( 'Transparent header', 'shapely' ),
'description' => esc_html__( 'Toggling this to ON will make the header have a transparent background', 'shapely' ),
'section' => 'shapely_main_section',
- 'type' => 'checkbox',
- )
- );
- }
+ )
+ );
// transparent header: opacity range slider
$wp_customize->add_setting(
@@ -496,35 +459,21 @@ function shapely_customizer( $wp_customize ) {
)
);
- if ( class_exists( 'Epsilon_Control_Slider' ) ) {
- $wp_customize->add_control(
- new Epsilon_Control_Slider(
- $wp_customize, 'shapely_sticky_header_transparency', array(
- 'label' => esc_html__( 'Sticky header background opacity', 'shapely' ),
- 'description' => esc_html__( 'Increase the header background opacity', 'shapely' ),
- 'section' => 'shapely_main_section',
- 'type' => 'epsilon-slider',
- 'choices' => array(
- 'min' => 10,
- 'max' => 100,
- 'step' => 5,
- ),
- 'active_callback' => 'active_callback_toggle_choice',
-
- )
- )
- );
- } else {
- $wp_customize->add_control(
- 'shapely_sticky_header_transparency', array(
+ $wp_customize->add_control(
+ new Shapely_Control_Range(
+ $wp_customize, 'shapely_sticky_header_transparency', array(
'label' => esc_html__( 'Sticky header background opacity', 'shapely' ),
'description' => esc_html__( 'Increase the header background opacity', 'shapely' ),
'section' => 'shapely_main_section',
- 'type' => 'number',
+ 'input_attrs' => array(
+ 'min' => 10,
+ 'max' => 100,
+ 'step' => 5,
+ ),
'active_callback' => 'active_callback_toggle_choice',
)
- );
- }
+ )
+ );
// sticky header
$wp_customize->add_setting(
@@ -534,28 +483,14 @@ function shapely_customizer( $wp_customize ) {
)
);
- if ( class_exists( 'Epsilon_Control_Toggle' ) ) {
- $wp_customize->add_control(
- new Epsilon_Control_Toggle(
- $wp_customize, 'shapely_sticky_header', array(
- 'label' => esc_html__( 'Sticky header', 'shapely' ),
- 'description' => esc_html__( 'Toggling this to ON will make your header stick to the top of the browser bar', 'shapely' ),
- 'section' => 'shapely_main_section',
-
- )
- )
- );
- } else {
- $wp_customize->add_control(
- 'shapely_sticky_header', array(
+ $wp_customize->add_control(
+ 'shapely_sticky_header', array(
+ 'type' => 'checkbox',
'label' => esc_html__( 'Sticky header', 'shapely' ),
'description' => esc_html__( 'Toggling this to ON will make your header stick to the top of the browser bar', 'shapely' ),
'section' => 'shapely_main_section',
- 'type' => 'checkbox',
-
- )
- );
- }
+ )
+ );
/**
*
@@ -658,162 +593,99 @@ function shapely_customizer( $wp_customize ) {
);
// Single Post Settings
- if ( class_exists( 'Epsilon_Control_Toggle' ) ) {
-
- $wp_customize->add_control(
- new Epsilon_Control_Toggle(
- $wp_customize, 'title_in_header', array(
- 'label' => esc_html__( 'Show title in header', 'shapely' ),
- 'description' => esc_html__( 'This will show/hide the post title from callout', 'shapely' ),
- 'section' => 'shapely_single_post_section',
- )
- )
- );
+ $wp_customize->add_control(
+ 'title_in_header', array(
+ 'type' => 'checkbox',
+ 'label' => esc_html__( 'Show title in header', 'shapely' ),
+ 'description' => esc_html__( 'This will show/hide the post title from callout', 'shapely' ),
+ 'section' => 'shapely_single_post_section',
+ )
+ );
- $wp_customize->add_control(
- new Epsilon_Control_Toggle(
- $wp_customize, 'title_above_post', array(
- 'label' => esc_html__( 'Show title above post', 'shapely' ),
- 'description' => esc_html__( 'This will show/hide the post title above post content', 'shapely' ),
- 'section' => 'shapely_single_post_section',
- )
- )
- );
+ $wp_customize->add_control(
+ 'title_above_post', array(
+ 'type' => 'checkbox',
+ 'label' => esc_html__( 'Show title above post', 'shapely' ),
+ 'description' => esc_html__( 'This will show/hide the post title above post content', 'shapely' ),
+ 'section' => 'shapely_single_post_section',
+ )
+ );
- $wp_customize->add_control(
- new Epsilon_Control_Toggle(
- $wp_customize, 'post_date', array(
- 'label' => esc_html__( 'Show the date', 'shapely' ),
- 'description' => esc_html__( 'This will show/hide the date when post was published', 'shapely' ),
- 'section' => 'shapely_single_post_section',
- )
- )
- );
+ $wp_customize->add_control(
+ 'post_date', array(
+ 'type' => 'checkbox',
+ 'label' => esc_html__( 'Show the date', 'shapely' ),
+ 'description' => esc_html__( 'This will show/hide the date when post was published', 'shapely' ),
+ 'section' => 'shapely_single_post_section',
+ )
+ );
- $wp_customize->add_control(
- new Epsilon_Control_Toggle(
- $wp_customize, 'post_author', array(
- 'label' => esc_html__( 'Show the author', 'shapely' ),
- 'description' => esc_html__( 'This will show/hide the author who written the post under the post title', 'shapely' ),
- 'section' => 'shapely_single_post_section',
- )
- )
- );
+ $wp_customize->add_control(
+ 'post_author', array(
+ 'type' => 'checkbox',
+ 'label' => esc_html__( 'Show the author', 'shapely' ),
+ 'description' => esc_html__( 'This will show/hide the author who written the post under the post title', 'shapely' ),
+ 'section' => 'shapely_single_post_section',
+ )
+ );
- $wp_customize->add_control(
- new Epsilon_Control_Toggle(
- $wp_customize, 'post_category', array(
- 'label' => esc_html__( 'Show the category', 'shapely' ),
- 'description' => esc_html__( 'This will show/hide the categories of post', 'shapely' ),
- 'section' => 'shapely_single_post_section',
- )
- )
- );
+ $wp_customize->add_control(
+ 'post_category', array(
+ 'type' => 'checkbox',
+ 'label' => esc_html__( 'Show the category', 'shapely' ),
+ 'description' => esc_html__( 'This will show/hide the categories of post', 'shapely' ),
+ 'section' => 'shapely_single_post_section',
+ )
+ );
- $wp_customize->add_control(
- new Epsilon_Control_Toggle(
- $wp_customize, 'first_letter_caps', array(
- 'label' => esc_html__( 'First Letter Caps', 'shapely' ),
- 'description' => esc_html__( 'This will transform your first letter from a post into uppercase', 'shapely' ),
- 'section' => 'shapely_single_post_section',
- )
- )
- );
- $wp_customize->add_control(
- new Epsilon_Control_Toggle(
- $wp_customize, 'tags_post_meta', array(
- 'label' => esc_html__( 'Tags Post Meta', 'shapely' ),
- 'description' => esc_html__( 'This will show/hide tags from the end of post', 'shapely' ),
- 'section' => 'shapely_single_post_section',
- )
- )
- );
- $wp_customize->add_control(
- new Epsilon_Control_Toggle(
- $wp_customize, 'related_posts_area', array(
- 'label' => esc_html__( 'Related Posts Area', 'shapely' ),
- 'description' => esc_html__( 'This will enable/disable the related posts', 'shapely' ),
- 'section' => 'shapely_single_post_section',
- )
- )
- );
- $wp_customize->add_control(
- new Epsilon_Control_Toggle(
- $wp_customize, 'post_author_area', array(
- 'label' => esc_html__( 'Post Author Area', 'shapely' ),
- 'description' => esc_html__( 'This will show/hide the author box', 'shapely' ),
- 'section' => 'shapely_single_post_section',
- )
- )
- );
- $wp_customize->add_control(
- new Epsilon_Control_Toggle(
- $wp_customize, 'post_author_left_side', array(
- 'label' => esc_html__( 'Post Author Left Side', 'shapely' ),
- 'description' => esc_html__( 'This will move the author box from the bottom of the post on top on the left side', 'shapely' ),
- 'section' => 'shapely_single_post_section',
- )
- )
- );
- $wp_customize->add_control(
- new Epsilon_Control_Toggle(
- $wp_customize, 'post_author_email', array(
- 'label' => esc_html__( 'Show Author Email', 'shapely' ),
- 'description' => esc_html__( 'This will show/hide the author\'s email from the author box', 'shapely' ),
- 'section' => 'shapely_single_post_section',
- )
- )
- );
- } else {
- $wp_customize->add_control(
- 'first_letter_caps', array(
+ $wp_customize->add_control(
+ 'first_letter_caps', array(
+ 'type' => 'checkbox',
'label' => esc_html__( 'First Letter Caps', 'shapely' ),
'description' => esc_html__( 'This will transform your first letter from a post into uppercase', 'shapely' ),
'section' => 'shapely_single_post_section',
- 'type' => 'checkbox',
- )
- );
- $wp_customize->add_control(
- 'tags_post_meta', array(
+ )
+ );
+ $wp_customize->add_control(
+ 'tags_post_meta', array(
+ 'type' => 'checkbox',
'label' => esc_html__( 'Tags Post Meta', 'shapely' ),
'description' => esc_html__( 'This will show/hide tags from the end of post', 'shapely' ),
'section' => 'shapely_single_post_section',
- 'type' => 'checkbox',
- )
- );
- $wp_customize->add_control(
- 'related_posts_area', array(
+ )
+ );
+ $wp_customize->add_control(
+ 'related_posts_area', array(
+ 'type' => 'checkbox',
'label' => esc_html__( 'Related Posts Area', 'shapely' ),
'description' => esc_html__( 'This will enable/disable the related posts', 'shapely' ),
'section' => 'shapely_single_post_section',
- 'type' => 'checkbox',
- )
- );
- $wp_customize->add_control(
- 'post_author_area', array(
+ )
+ );
+ $wp_customize->add_control(
+ 'post_author_area', array(
+ 'type' => 'checkbox',
'label' => esc_html__( 'Post Author Area', 'shapely' ),
'description' => esc_html__( 'This will show/hide the author box', 'shapely' ),
'section' => 'shapely_single_post_section',
- 'type' => 'checkbox',
- )
- );
- $wp_customize->add_control(
- 'post_author_left_side', array(
+ )
+ );
+ $wp_customize->add_control(
+ 'post_author_left_side', array(
+ 'type' => 'checkbox',
'label' => esc_html__( 'Post Author Left Side', 'shapely' ),
'description' => esc_html__( 'This will move the author box from the bottom of the post on top on the left side', 'shapely' ),
'section' => 'shapely_single_post_section',
- 'type' => 'checkbox',
- )
- );
- $wp_customize->add_control(
- 'post_author_email', array(
+ )
+ );
+ $wp_customize->add_control(
+ 'post_author_email', array(
+ 'type' => 'checkbox',
'label' => esc_html__( 'Show Author Email', 'shapely' ),
'description' => esc_html__( 'This will show/hide the author\'s email from the author box', 'shapely' ),
'section' => 'shapely_single_post_section',
- 'type' => 'checkbox',
- )
- );
- } // End if().
+ )
+ );
$wp_customize->add_setting(
'single_post_layout_template', array(
'default' => 'sidebar-right',
@@ -886,26 +758,14 @@ function shapely_customizer( $wp_customize ) {
'sanitize_callback' => 'shapely_sanitize_checkbox',
)
);
- if ( class_exists( 'Epsilon_Control_Toggle' ) ) {
- $wp_customize->add_control(
- new Epsilon_Control_Toggle(
- $wp_customize, 'show_category_on_category_page', array(
- 'label' => esc_html__( 'Show Category on Posts', 'shapely' ),
- 'description' => esc_html__( 'Show/hide posts\' categories from the Category Page', 'shapely' ),
- 'section' => 'shapely_blog_section',
- )
- )
- );
- } else {
- $wp_customize->add_control(
- 'show_category_on_category_page', array(
+ $wp_customize->add_control(
+ 'show_category_on_category_page', array(
+ 'type' => 'checkbox',
'label' => esc_html__( 'Show Category on Posts', 'shapely' ),
'description' => esc_html__( 'Show/hide posts\' categories from the Category Page', 'shapely' ),
'section' => 'shapely_blog_section',
- 'type' => 'checkbox',
- )
- );
- }
+ )
+ );
// Global category display setting
$wp_customize->add_setting(
@@ -914,26 +774,14 @@ function shapely_customizer( $wp_customize ) {
'sanitize_callback' => 'shapely_sanitize_checkbox',
)
);
- if ( class_exists( 'Epsilon_Control_Toggle' ) ) {
- $wp_customize->add_control(
- new Epsilon_Control_Toggle(
- $wp_customize, 'show_categories_globally', array(
- 'label' => esc_html__( 'Show Categories Globally', 'shapely' ),
- 'description' => esc_html__( 'Show/hide categories on all blog pages and single posts', 'shapely' ),
- 'section' => 'shapely_blog_section',
- )
- )
- );
- } else {
- $wp_customize->add_control(
- 'show_categories_globally', array(
+ $wp_customize->add_control(
+ 'show_categories_globally', array(
+ 'type' => 'checkbox',
'label' => esc_html__( 'Show Categories Globally', 'shapely' ),
'description' => esc_html__( 'Show/hide categories on all blog pages and single posts', 'shapely' ),
'section' => 'shapely_blog_section',
- 'type' => 'checkbox',
- )
- );
- }
+ )
+ );
if ( post_type_exists( 'jetpack-portfolio' ) ) {
@@ -970,26 +818,14 @@ function shapely_customizer( $wp_customize ) {
'sanitize_callback' => 'shapely_sanitize_checkbox',
)
);
- if ( class_exists( 'Epsilon_Control_Toggle' ) ) {
- $wp_customize->add_control(
- new Epsilon_Control_Toggle(
- $wp_customize, 'portfolio_archive_title', array(
- 'label' => esc_html__( 'Show Portfolio Archive Title', 'shapely' ),
- 'description' => esc_html__( 'Show/hide the title from the Portfolio Archive Page', 'shapely' ),
- 'section' => 'shapely_projects_section',
- )
- )
- );
- } else {
- $wp_customize->add_control(
- 'portfolio_archive_title', array(
+ $wp_customize->add_control(
+ 'portfolio_archive_title', array(
+ 'type' => 'checkbox',
'label' => esc_html__( 'Show Portfolio Archive Title', 'shapely' ),
'description' => esc_html__( 'Show/hide the title from the Portfolio Archive Page', 'shapely' ),
'section' => 'shapely_projects_section',
- 'type' => 'checkbox',
- )
- );
- }
+ )
+ );
$wp_customize->add_setting(
'portfolio_name', array(
'default' => '',
@@ -1132,162 +968,99 @@ function shapely_customizer( $wp_customize ) {
);
// Single Project Settings
- if ( class_exists( 'Epsilon_Control_Toggle' ) ) {
-
- $wp_customize->add_control(
- new Epsilon_Control_Toggle(
- $wp_customize, 'project_title_in_header', array(
- 'label' => esc_html__( 'Show title in header', 'shapely' ),
- 'description' => esc_html__( 'This will show/hide the project title from callout', 'shapely' ),
- 'section' => 'shapely_single_project_section',
- )
- )
- );
+ $wp_customize->add_control(
+ 'project_title_in_header', array(
+ 'type' => 'checkbox',
+ 'label' => esc_html__( 'Show title in header', 'shapely' ),
+ 'description' => esc_html__( 'This will show/hide the project title from callout', 'shapely' ),
+ 'section' => 'shapely_single_project_section',
+ )
+ );
- $wp_customize->add_control(
- new Epsilon_Control_Toggle(
- $wp_customize, 'title_above_project', array(
- 'label' => esc_html__( 'Show title above project', 'shapely' ),
- 'description' => esc_html__( 'This will show/hide the project title above project content', 'shapely' ),
- 'section' => 'shapely_single_project_section',
- )
- )
- );
+ $wp_customize->add_control(
+ 'title_above_project', array(
+ 'type' => 'checkbox',
+ 'label' => esc_html__( 'Show title above project', 'shapely' ),
+ 'description' => esc_html__( 'This will show/hide the project title above project content', 'shapely' ),
+ 'section' => 'shapely_single_project_section',
+ )
+ );
- $wp_customize->add_control(
- new Epsilon_Control_Toggle(
- $wp_customize, 'project_date', array(
- 'label' => esc_html__( 'Show the date', 'shapely' ),
- 'description' => esc_html__( 'This will show/hide the date when project was published', 'shapely' ),
- 'section' => 'shapely_single_project_section',
- )
- )
- );
+ $wp_customize->add_control(
+ 'project_date', array(
+ 'type' => 'checkbox',
+ 'label' => esc_html__( 'Show the date', 'shapely' ),
+ 'description' => esc_html__( 'This will show/hide the date when project was published', 'shapely' ),
+ 'section' => 'shapely_single_project_section',
+ )
+ );
- $wp_customize->add_control(
- new Epsilon_Control_Toggle(
- $wp_customize, 'project_author', array(
- 'label' => esc_html__( 'Show the author', 'shapely' ),
- 'description' => esc_html__( 'This will show/hide the author who written the project under the project title', 'shapely' ),
- 'section' => 'shapely_single_project_section',
- )
- )
- );
+ $wp_customize->add_control(
+ 'project_author', array(
+ 'type' => 'checkbox',
+ 'label' => esc_html__( 'Show the author', 'shapely' ),
+ 'description' => esc_html__( 'This will show/hide the author who written the project under the project title', 'shapely' ),
+ 'section' => 'shapely_single_project_section',
+ )
+ );
- $wp_customize->add_control(
- new Epsilon_Control_Toggle(
- $wp_customize, 'project_category', array(
- 'label' => esc_html__( 'Show the project type', 'shapely' ),
- 'description' => esc_html__( 'This will show/hide the type of project', 'shapely' ),
- 'section' => 'shapely_single_project_section',
- )
- )
- );
+ $wp_customize->add_control(
+ 'project_category', array(
+ 'type' => 'checkbox',
+ 'label' => esc_html__( 'Show the project type', 'shapely' ),
+ 'description' => esc_html__( 'This will show/hide the type of project', 'shapely' ),
+ 'section' => 'shapely_single_project_section',
+ )
+ );
- $wp_customize->add_control(
- new Epsilon_Control_Toggle(
- $wp_customize, 'project_first_letter_caps', array(
- 'label' => esc_html__( 'First Letter Caps', 'shapely' ),
- 'description' => esc_html__( 'This will transform your first letter from a project into uppercase', 'shapely' ),
- 'section' => 'shapely_single_project_section',
- )
- )
- );
- $wp_customize->add_control(
- new Epsilon_Control_Toggle(
- $wp_customize, 'tags_project_meta', array(
- 'label' => esc_html__( 'Tags Project Meta', 'shapely' ),
- 'description' => esc_html__( 'This will show/hide tags from the end of project', 'shapely' ),
- 'section' => 'shapely_single_project_section',
- )
- )
- );
- $wp_customize->add_control(
- new Epsilon_Control_Toggle(
- $wp_customize, 'related_projects_area', array(
- 'label' => esc_html__( 'Related Projects Area', 'shapely' ),
- 'description' => esc_html__( 'This will enable/disable the related projects', 'shapely' ),
- 'section' => 'shapely_single_project_section',
- )
- )
- );
- $wp_customize->add_control(
- new Epsilon_Control_Toggle(
- $wp_customize, 'project_author_area', array(
- 'label' => esc_html__( 'Project Author Area', 'shapely' ),
- 'description' => esc_html__( 'This will show/hide the author box', 'shapely' ),
- 'section' => 'shapely_single_project_section',
- )
- )
- );
- $wp_customize->add_control(
- new Epsilon_Control_Toggle(
- $wp_customize, 'project_author_left_side', array(
- 'label' => esc_html__( 'Project Author Left Side', 'shapely' ),
- 'description' => esc_html__( 'This will move the author box from the bottom of the project on top on the left side', 'shapely' ),
- 'section' => 'shapely_single_project_section',
- )
- )
- );
- $wp_customize->add_control(
- new Epsilon_Control_Toggle(
- $wp_customize, 'project_author_email', array(
- 'label' => esc_html__( 'Show Author Email', 'shapely' ),
- 'description' => esc_html__( 'This will show/hide the author\'s email from the author box', 'shapely' ),
- 'section' => 'shapely_single_project_section',
- )
- )
- );
- } else {
- $wp_customize->add_control(
- 'project_first_letter_caps', array(
+ $wp_customize->add_control(
+ 'project_first_letter_caps', array(
+ 'type' => 'checkbox',
'label' => esc_html__( 'First Letter Caps', 'shapely' ),
'description' => esc_html__( 'This will transform your first letter from a project into uppercase', 'shapely' ),
'section' => 'shapely_single_project_section',
- 'type' => 'checkbox',
- )
- );
- $wp_customize->add_control(
- 'tags_project_meta', array(
+ )
+ );
+ $wp_customize->add_control(
+ 'tags_project_meta', array(
+ 'type' => 'checkbox',
'label' => esc_html__( 'Tags Project Meta', 'shapely' ),
'description' => esc_html__( 'This will show/hide tags from the end of project', 'shapely' ),
'section' => 'shapely_single_project_section',
- 'type' => 'checkbox',
- )
- );
- $wp_customize->add_control(
- 'related_projects_area', array(
+ )
+ );
+ $wp_customize->add_control(
+ 'related_projects_area', array(
+ 'type' => 'checkbox',
'label' => esc_html__( 'Related Projects Area', 'shapely' ),
'description' => esc_html__( 'This will enable/disable the related projects', 'shapely' ),
'section' => 'shapely_single_project_section',
- 'type' => 'checkbox',
- )
- );
- $wp_customize->add_control(
- 'project_author_area', array(
+ )
+ );
+ $wp_customize->add_control(
+ 'project_author_area', array(
+ 'type' => 'checkbox',
'label' => esc_html__( 'Project Author Area', 'shapely' ),
'description' => esc_html__( 'This will show/hide the author box', 'shapely' ),
'section' => 'shapely_single_project_section',
- 'type' => 'checkbox',
- )
- );
- $wp_customize->add_control(
- 'project_author_left_side', array(
+ )
+ );
+ $wp_customize->add_control(
+ 'project_author_left_side', array(
+ 'type' => 'checkbox',
'label' => esc_html__( 'Project Author Left Side', 'shapely' ),
'description' => esc_html__( 'This will move the author box from the bottom of the project on top on the left side', 'shapely' ),
'section' => 'shapely_single_project_section',
- 'type' => 'checkbox',
- )
- );
- $wp_customize->add_control(
- 'project_author_email', array(
+ )
+ );
+ $wp_customize->add_control(
+ 'project_author_email', array(
+ 'type' => 'checkbox',
'label' => esc_html__( 'Show Author Email', 'shapely' ),
'description' => esc_html__( 'This will show/hide the author\'s email from the author box', 'shapely' ),
'section' => 'shapely_single_project_section',
- 'type' => 'checkbox',
- )
- );
- } // End if().
+ )
+ );
$wp_customize->add_setting(
'single_project_layout_template', array(
'default' => 'sidebar-right',
@@ -1453,6 +1226,44 @@ function shapely_customizer_custom_label_css() {
color: #555;
font-style: italic;
}
+
+ /* Shapely_Control_Range: slider with its value alongside. */
+ .shapely-range {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ }
+ .shapely-range input[type="range"] {
+ flex: 1 1 auto;
+ min-width: 0;
+ }
+ .shapely-range__value {
+ flex: 0 0 auto;
+ min-width: 3ch;
+ text-align: right;
+ font-variant-numeric: tabular-nums;
+ color: #50575e;
+ }
+
+ /* Shapely_Section_Link: a section that is just an outbound button. */
+ .shapely-link-section {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ padding: 12px 15px;
+ border-bottom: 1px solid #ddd;
+ background: #fff;
+ }
+ .shapely-link-section__title {
+ margin: 0;
+ font-size: 14px;
+ font-weight: 600;
+ color: #50575e;
+ }
+ .shapely-link-section__button {
+ flex: 0 0 auto;
+ }
'primary',
+ 'link_hover_color' => 'primary-hover',
+ 'button_color' => 'primary',
+ 'button_hover_color' => 'primary-hover',
+ );
+
+ $vars = array();
+ foreach ( $presets as $mod => $slug ) {
+ $value = get_theme_mod( $mod );
+ if ( $value ) {
+ // Later keys win, which is why button_* follows link_*.
+ $vars[ $slug ] = $value;
+ }
+ }
+
+ if ( ! empty( $vars ) ) {
+ $declarations = '';
+ foreach ( $vars as $slug => $value ) {
+ $declarations .= '--wp--preset--color--' . sanitize_key( $slug ) . ':' . esc_attr( $value ) . ';';
+ }
+ $css = ':root{' . $declarations . '}' . $css;
+ }
+
// Drop the wrapping