This commit is contained in:
Matt Batchelder
2026-02-20 21:28:00 -05:00
commit 19ee98c68d
37 changed files with 9405 additions and 0 deletions

64
theme/inc/ajax.php Normal file
View File

@@ -0,0 +1,64 @@
<?php
/**
* AJAX Contact Form Handler
*
* Receives submissions from the oribi/contact-section block form,
* validates input, and sends an email via wp_mail().
*
* @package OTS_Theme
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
add_action( 'wp_ajax_oribi_contact', 'oribi_handle_contact' );
add_action( 'wp_ajax_nopriv_oribi_contact', 'oribi_handle_contact' );
/**
* Process the contact form AJAX request.
*/
function oribi_handle_contact() {
// Verify nonce
if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'oribi_contact_nonce' ) ) {
wp_send_json_error( 'Security check failed. Please refresh the page and try again.' );
}
$name = isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : '';
$email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';
$interest = isset( $_POST['interest'] ) ? sanitize_text_field( wp_unslash( $_POST['interest'] ) ) : '';
$message = isset( $_POST['message'] ) ? sanitize_textarea_field( wp_unslash( $_POST['message'] ) ) : '';
// Validate required fields
if ( empty( $name ) || empty( $email ) || empty( $message ) ) {
wp_send_json_error( 'Please fill in all required fields.' );
}
if ( ! is_email( $email ) ) {
wp_send_json_error( 'Please enter a valid email address.' );
}
// Build the email
$to = get_option( 'admin_email' );
$subject = sprintf( '[OTS Theme] New inquiry from %s', $name );
$body = sprintf(
"Name: %s\nEmail: %s\nInterested In: %s\n\nMessage:\n%s",
$name,
$email,
$interest ? $interest : 'Not specified',
$message
);
$headers = [
'Content-Type: text/plain; charset=UTF-8',
sprintf( 'Reply-To: %s <%s>', $name, $email ),
];
$sent = wp_mail( $to, $subject, $body, $headers );
if ( $sent ) {
wp_send_json_success( "Thanks! We'll get back to you shortly." );
} else {
wp_send_json_error( 'Something went wrong. Please try again or email us directly.' );
}
}

110
theme/inc/enqueue.php Normal file
View File

@@ -0,0 +1,110 @@
<?php
/**
* Asset Enqueuing — frontend styles, scripts, and editor additions.
*
* @package OTS_Theme
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/* ── Frontend assets ───────────────────────────────────────── */
add_action( 'wp_enqueue_scripts', function () {
// Main stylesheet (supplements theme.json generated styles)
wp_enqueue_style(
'oribi-main',
ORIBI_URI . '/assets/css/main.css',
[],
ORIBI_VERSION
);
// Main JS — dark mode, sticky header, mobile nav, scroll animations
wp_enqueue_script(
'oribi-main',
ORIBI_URI . '/assets/js/main.js',
[],
ORIBI_VERSION,
true
);
// Localize AJAX endpoint for the contact form
wp_localize_script( 'oribi-main', 'oribiAjax', [
'url' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'oribi_contact_nonce' ),
] );
// Threaded comments (if ever enabled)
if ( is_singular() && comments_open() && get_option( 'thread_comments' ) ) {
wp_enqueue_script( 'comment-reply' );
}
} );
/* ── Font Awesome 6 Free (CDN) ─────────────────────────────── */
add_action( 'wp_enqueue_scripts', function () {
wp_enqueue_style(
'font-awesome',
'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css',
[],
null
);
} );
add_action( 'enqueue_block_editor_assets', function () {
wp_enqueue_style(
'font-awesome',
'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css',
[],
null
);
} );
/* ── Google Fonts — dynamic based on theme settings ────────── */
add_action( 'wp_enqueue_scripts', 'oribi_enqueue_selected_fonts' );
add_action( 'enqueue_block_editor_assets', 'oribi_enqueue_selected_fonts' );
/**
* Enqueue Google Fonts stylesheets for the selected body & heading fonts.
*/
function oribi_enqueue_selected_fonts() {
if ( ! function_exists( 'oribi_get_setting' ) ) {
return;
}
$slugs = array_unique( array_filter( [
oribi_get_setting( 'font_family' ),
oribi_get_setting( 'font_heading' ),
] ) );
foreach ( $slugs as $slug ) {
$url = oribi_get_google_font_url( $slug );
if ( $url ) {
wp_enqueue_style(
'oribi-gf-' . $slug,
$url,
[],
null
);
}
}
}
/* ── Generated theme CSS (colour / font / spacing overrides) ─ */
add_action( 'wp_enqueue_scripts', 'oribi_enqueue_generated_css', 20 );
add_action( 'enqueue_block_editor_assets', 'oribi_enqueue_generated_css', 20 );
/**
* Enqueue the dynamically generated CSS file that applies design-token
* overrides from the admin settings page.
*/
function oribi_enqueue_generated_css() {
if ( ! function_exists( 'oribi_generated_css_url' ) ) {
return;
}
$url = oribi_generated_css_url();
$ver = get_theme_mod( 'oribi_css_version', '1' );
wp_enqueue_style( 'oribi-custom-tokens', $url, [ 'oribi-main' ], $ver );
}

260
theme/inc/font-manager.php Normal file
View File

@@ -0,0 +1,260 @@
<?php
/**
* Font Manager — Bridges the WordPress Font Library with theme settings.
*
* Uses the built-in Font Library (WP 6.5+) for registration, discovery,
* and @font-face generation. The admin settings page uses the helpers
* in this file to populate font-family dropdowns.
*
* @package OTS_Theme
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Register a curated set of Google Fonts with WordPress's Font Library
* so they appear immediately in the admin settings page dropdown.
*
* Admins can add more fonts via Appearance → Editor → Fonts at any time.
*/
add_action( 'after_setup_theme', function () {
/*
* WordPress 6.5+ ships wp_register_font_family() / wp_register_font_face()
* via the Fonts API (WP_Fonts or WP_Webfonts depending on merge iteration).
* We wrap the calls so the theme degrades gracefully on older installs.
*/
if ( ! function_exists( 'wp_register_webfont_provider' ) && ! class_exists( 'WP_Fonts' ) ) {
// Font Library is available in WP 6.5+; on older versions the
// Google Fonts CDN link in enqueue.php serves as the fallback.
return;
}
} );
/**
* Return every font family available to this WordPress installation.
*
* Sources (merged, de-duped by slug):
* 1. theme.json → settings.typography.fontFamilies
* 2. Font Library → any fonts the user installed via Site Editor
* 3. Bundled list → a small curated set of popular Google Fonts
*
* @return array<int,array{slug:string,name:string,fontFamily:string}>
*/
function oribi_get_available_fonts() {
$fonts = [];
/* ── 1. theme.json registered families ─────────────────── */
$theme_json = WP_Theme_JSON_Resolver::get_merged_data()->get_data();
if ( ! empty( $theme_json['settings']['typography']['fontFamilies']['theme'] ) ) {
foreach ( $theme_json['settings']['typography']['fontFamilies']['theme'] as $f ) {
$fonts[ $f['slug'] ] = [
'slug' => $f['slug'],
'name' => $f['name'] ?? $f['slug'],
'fontFamily' => $f['fontFamily'],
];
}
}
/* ── 2. Font Library (user-installed via Site Editor) ───── */
$font_families = oribi_query_font_library();
foreach ( $font_families as $f ) {
$slug = sanitize_title( $f['slug'] ?? $f['name'] );
if ( ! isset( $fonts[ $slug ] ) ) {
$fonts[ $slug ] = [
'slug' => $slug,
'name' => $f['name'],
'fontFamily' => $f['fontFamily'] ?? "'{$f['name']}', sans-serif",
];
}
}
/* ── 3. Bundled Google Fonts (always shown as available) ── */
$bundled = oribi_get_bundled_google_fonts();
foreach ( $bundled as $slug => $data ) {
if ( ! isset( $fonts[ $slug ] ) ) {
$fonts[ $slug ] = $data;
}
}
// Sort alphabetically by display name.
uasort( $fonts, function ( $a, $b ) {
return strcasecmp( $a['name'], $b['name'] );
} );
return array_values( $fonts );
}
/**
* Query the WP Font Library (wp_font_family post type) for installed fonts.
*
* @return array<int,array{slug:string,name:string,fontFamily:string}>
*/
function oribi_query_font_library() {
$results = [];
// Font Library stores each font family as a wp_font_family post (WP 6.5+).
$query = new WP_Query( [
'post_type' => 'wp_font_family',
'posts_per_page' => 100,
'post_status' => 'publish',
'no_found_rows' => true,
] );
if ( $query->have_posts() ) {
foreach ( $query->posts as $post ) {
$content = json_decode( $post->post_content, true );
if ( $content ) {
$results[] = [
'slug' => $content['slug'] ?? sanitize_title( $post->post_title ),
'name' => $post->post_title,
'fontFamily' => $content['fontFamily'] ?? "'{$post->post_title}', sans-serif",
];
}
}
}
wp_reset_postdata();
return $results;
}
/**
* Return a curated set of popular Google Fonts bundled with the theme.
*
* These are always shown in the font selector even if the user hasn't
* explicitly installed them via the Font Library. WordPress will load
* them from Google Fonts CDN when selected.
*
* @return array<string,array{slug:string,name:string,fontFamily:string,googleUrl:string}>
*/
function oribi_get_bundled_google_fonts() {
return [
'inter' => [
'slug' => 'inter',
'name' => 'Inter',
'fontFamily' => "'Inter', system-ui, -apple-system, sans-serif",
'googleUrl' => 'https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap',
],
'roboto' => [
'slug' => 'roboto',
'name' => 'Roboto',
'fontFamily' => "'Roboto', system-ui, sans-serif",
'googleUrl' => 'https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700;900&display=swap',
],
'open-sans' => [
'slug' => 'open-sans',
'name' => 'Open Sans',
'fontFamily' => "'Open Sans', system-ui, sans-serif",
'googleUrl' => 'https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600;700;800&display=swap',
],
'poppins' => [
'slug' => 'poppins',
'name' => 'Poppins',
'fontFamily' => "'Poppins', system-ui, sans-serif",
'googleUrl' => 'https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700;800;900&display=swap',
],
'lato' => [
'slug' => 'lato',
'name' => 'Lato',
'fontFamily' => "'Lato', system-ui, sans-serif",
'googleUrl' => 'https://fonts.googleapis.com/css2?family=Lato:wght@300;400;700;900&display=swap',
],
'montserrat' => [
'slug' => 'montserrat',
'name' => 'Montserrat',
'fontFamily' => "'Montserrat', system-ui, sans-serif",
'googleUrl' => 'https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700;800;900&display=swap',
],
'source-sans-3' => [
'slug' => 'source-sans-3',
'name' => 'Source Sans 3',
'fontFamily' => "'Source Sans 3', system-ui, sans-serif",
'googleUrl' => 'https://fonts.googleapis.com/css2?family=Source+Sans+3:wght@300;400;500;600;700;800;900&display=swap',
],
'nunito' => [
'slug' => 'nunito',
'name' => 'Nunito',
'fontFamily' => "'Nunito', system-ui, sans-serif",
'googleUrl' => 'https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;500;600;700;800;900&display=swap',
],
'raleway' => [
'slug' => 'raleway',
'name' => 'Raleway',
'fontFamily' => "'Raleway', system-ui, sans-serif",
'googleUrl' => 'https://fonts.googleapis.com/css2?family=Raleway:wght@300;400;500;600;700;800;900&display=swap',
],
'dm-sans' => [
'slug' => 'dm-sans',
'name' => 'DM Sans',
'fontFamily' => "'DM Sans', system-ui, sans-serif",
'googleUrl' => 'https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&display=swap',
],
'work-sans' => [
'slug' => 'work-sans',
'name' => 'Work Sans',
'fontFamily' => "'Work Sans', system-ui, sans-serif",
'googleUrl' => 'https://fonts.googleapis.com/css2?family=Work+Sans:wght@300;400;500;600;700;800;900&display=swap',
],
'plus-jakarta-sans' => [
'slug' => 'plus-jakarta-sans',
'name' => 'Plus Jakarta Sans',
'fontFamily' => "'Plus Jakarta Sans', system-ui, sans-serif",
'googleUrl' => 'https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap',
],
'system-ui' => [
'slug' => 'system-ui',
'name' => 'System UI (no download)',
'fontFamily' => "system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif",
'googleUrl' => '',
],
];
}
/**
* Resolve a font slug to its CSS font-family value.
*
* Checks (in order): bundled list → Font Library → theme.json.
*
* @param string $slug Font slug (e.g. 'inter', 'roboto').
* @return string CSS font-family value.
*/
function oribi_get_font_family_css( $slug ) {
if ( empty( $slug ) ) {
$slug = 'inter';
}
// Bundled fonts (includes Google Fonts URL for enqueue).
$bundled = oribi_get_bundled_google_fonts();
if ( isset( $bundled[ $slug ] ) ) {
return $bundled[ $slug ]['fontFamily'];
}
// Search all available fonts.
$fonts = oribi_get_available_fonts();
foreach ( $fonts as $f ) {
if ( $f['slug'] === $slug ) {
return $f['fontFamily'];
}
}
// Ultimate fallback.
return "'Inter', system-ui, -apple-system, sans-serif";
}
/**
* Return the Google Fonts stylesheet URL for a given font slug,
* or empty string if not a bundled Google Font.
*
* @param string $slug Font slug.
* @return string URL or ''.
*/
function oribi_get_google_font_url( $slug ) {
$bundled = oribi_get_bundled_google_fonts();
return isset( $bundled[ $slug ] ) ? $bundled[ $slug ]['googleUrl'] : '';
}

63
theme/inc/setup.php Normal file
View File

@@ -0,0 +1,63 @@
<?php
/**
* Theme Setup — registers supports, menus, patterns, and editor styles.
*
* @package OTS_Theme
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/* ── Theme supports ────────────────────────────────────────── */
add_action( 'after_setup_theme', function () {
// FSE / block essentials
add_theme_support( 'wp-block-styles' );
add_theme_support( 'responsive-embeds' );
add_theme_support( 'editor-styles' );
// Classic fallback supports (still respected by block themes)
add_theme_support( 'automatic-feed-links' );
add_theme_support( 'title-tag' );
add_theme_support( 'post-thumbnails' );
add_theme_support( 'custom-logo', [
'height' => 40,
'width' => 180,
'flex-height' => true,
'flex-width' => true,
] );
add_theme_support( 'html5', [
'search-form',
'comment-form',
'comment-list',
'gallery',
'caption',
'style',
'script',
] );
// Load main.css inside the block editor so previews match the frontend
add_editor_style( 'assets/css/main.css' );
// Navigation menus (used by oribi/site-header block)
register_nav_menus( [
'primary' => __( 'Primary Menu', 'ots-theme' ),
'footer' => __( 'Footer Menu', 'ots-theme' ),
] );
} );
/* ── Block pattern categories ──────────────────────────────── */
add_action( 'init', function () {
register_block_pattern_category( 'oribi-pages', [
'label' => __( 'Oribi Tech — Pages', 'ots-theme' ),
] );
register_block_pattern_category( 'oribi-sections', [
'label' => __( 'Oribi Tech — Sections', 'ots-theme' ),
] );
} );
/* ── Remove core block patterns if desired ─────────────────── */
add_action( 'after_setup_theme', function () {
remove_theme_support( 'core-block-patterns' );
} );

View File

@@ -0,0 +1,111 @@
<?php
/**
* Theme Defaults — Provides default values for all customizable design tokens.
*
* These defaults match the original hardcoded values so existing sites
* see no visual change when upgrading.
*
* @package OTS_Theme
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Return the full array of default design-token values.
*
* Keys mirror the theme-mod names used throughout the customisation system.
*
* @return array<string,string>
*/
function oribi_get_theme_defaults() {
return [
/* ── Light-mode colour palette ──────────────────────── */
'color_primary' => '#D83302',
'color_primary_dk' => '#B52B02',
'color_primary_lt' => '#FEF0EB',
'color_accent' => '#00757c',
'color_accent_dk' => '#005a60',
'color_accent_lt' => '#E6F4F5',
'color_dark' => '#0D1321',
'color_dark_2' => '#1A2236',
'color_text' => '#2D3748',
'color_text_muted' => '#718096',
'color_border' => '#E2E8F0',
'color_bg' => '#FFFFFF',
'color_bg_alt' => '#FFF8F5',
/* ── Dark-mode colour palette ───────────────────────── */
'dark_primary' => '#FF6B3D',
'dark_primary_dk' => '#D83302',
'dark_primary_lt' => 'rgba(216,51,2,0.15)',
'dark_accent' => '#00757c',
'dark_accent_dk' => '#005a60',
'dark_accent_lt' => 'rgba(0,117,124,0.15)',
'dark_dark' => '#E2E8F0',
'dark_dark_2' => '#CBD5E0',
'dark_text' => '#CBD5E0',
'dark_text_muted' => '#A0AEC0',
'dark_border' => '#2D3748',
'dark_bg' => '#0F1724',
'dark_bg_alt' => '#151F30',
'dark_bg_dark' => '#0A0F1A',
'dark_heading' => '#F7FAFC',
'dark_card_bg' => '#151F30',
/* ── Typography ─────────────────────────────────────── */
'font_family' => 'inter', // slug from WP Font Library
'font_heading' => '', // empty = same as body
/* ── Border radii ───────────────────────────────────── */
'radius_sm' => '6',
'radius_md' => '12',
'radius_lg' => '20',
'radius_xl' => '32',
/* ── Spacing / layout ───────────────────────────────── */
'container_max' => '1200',
'container_pad_min' => '1',
'container_pad_max' => '2',
'wide_size' => '1536',
];
}
/**
* Retrieve a single design-token value, respecting saved theme-mod overrides.
*
* @param string $key Token name (e.g. 'color_primary').
* @return string
*/
function oribi_get_setting( $key ) {
$defaults = oribi_get_theme_defaults();
$default = isset( $defaults[ $key ] ) ? $defaults[ $key ] : '';
return get_theme_mod( 'oribi_' . $key, $default );
}
/**
* Persist the current set of defaults into theme-mods (one-time migration).
*
* Called the first time the settings page is loaded, so that existing sites
* start with their current visual identity already stored.
*/
function oribi_maybe_seed_defaults() {
if ( get_theme_mod( 'oribi_defaults_seeded' ) ) {
return;
}
$defaults = oribi_get_theme_defaults();
foreach ( $defaults as $key => $value ) {
// Only set if the user hasn't already saved a value.
if ( false === get_theme_mod( 'oribi_' . $key, false ) ) {
set_theme_mod( 'oribi_' . $key, $value );
}
}
set_theme_mod( 'oribi_defaults_seeded', true );
}

View File

@@ -0,0 +1,306 @@
<?php
/**
* Theme Generator — Builds and caches a CSS file from saved design tokens.
*
* Reads theme-mods written by the admin settings page and produces a
* static CSS file in the uploads directory. The file is enqueued after
* main.css so that custom values override defaults.
*
* @package OTS_Theme
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Return the absolute filesystem path of the generated CSS file.
*
* Multi-site aware — each site gets its own file.
*
* @return string
*/
function oribi_generated_css_path() {
$upload_dir = wp_upload_dir();
$blog_id = get_current_blog_id();
return trailingslashit( $upload_dir['basedir'] ) . "oribi-theme-{$blog_id}-custom.css";
}
/**
* Return the public URL of the generated CSS file.
*
* @return string
*/
function oribi_generated_css_url() {
$upload_dir = wp_upload_dir();
$blog_id = get_current_blog_id();
return trailingslashit( $upload_dir['baseurl'] ) . "oribi-theme-{$blog_id}-custom.css";
}
/**
* Helper: convert a hex colour like #D83302 to its "r,g,b" string.
*
* @param string $hex Hex colour with or without leading #.
* @return string e.g. "216,51,2"
*/
function oribi_hex_to_rgb( $hex ) {
$hex = ltrim( $hex, '#' );
if ( strlen( $hex ) === 3 ) {
$hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
}
if ( strlen( $hex ) !== 6 ) {
return '0,0,0';
}
$r = hexdec( substr( $hex, 0, 2 ) );
$g = hexdec( substr( $hex, 2, 2 ) );
$b = hexdec( substr( $hex, 4, 2 ) );
return "{$r},{$g},{$b}";
}
/**
* Build the CSS string from current theme-mod values.
*
* @return string Complete CSS content.
*/
function oribi_build_css() {
$s = 'oribi_get_setting'; // shorthand
// Gather values.
$primary = $s( 'color_primary' );
$primary_dk = $s( 'color_primary_dk' );
$primary_lt = $s( 'color_primary_lt' );
$accent = $s( 'color_accent' );
$accent_dk = $s( 'color_accent_dk' );
$accent_lt = $s( 'color_accent_lt' );
$dark = $s( 'color_dark' );
$dark_2 = $s( 'color_dark_2' );
$text = $s( 'color_text' );
$text_muted = $s( 'color_text_muted' );
$border = $s( 'color_border' );
$bg = $s( 'color_bg' );
$bg_alt = $s( 'color_bg_alt' );
// Dark mode.
$dk_primary = $s( 'dark_primary' );
$dk_primary_dk = $s( 'dark_primary_dk' );
$dk_primary_lt = $s( 'dark_primary_lt' );
$dk_accent = $s( 'dark_accent' );
$dk_accent_dk = $s( 'dark_accent_dk' );
$dk_accent_lt = $s( 'dark_accent_lt' );
$dk_dark = $s( 'dark_dark' );
$dk_dark_2 = $s( 'dark_dark_2' );
$dk_text = $s( 'dark_text' );
$dk_text_muted = $s( 'dark_text_muted' );
$dk_border = $s( 'dark_border' );
$dk_bg = $s( 'dark_bg' );
$dk_bg_alt = $s( 'dark_bg_alt' );
$dk_bg_dark = $s( 'dark_bg_dark' );
$dk_heading = $s( 'dark_heading' );
$dk_card_bg = $s( 'dark_card_bg' );
// Typography.
$body_font_slug = $s( 'font_family' );
$heading_font_slug = $s( 'font_heading' );
$body_font_css = oribi_get_font_family_css( $body_font_slug );
$heading_font_css = $heading_font_slug ? oribi_get_font_family_css( $heading_font_slug ) : $body_font_css;
// Border radii.
$r_sm = intval( $s( 'radius_sm' ) );
$r_md = intval( $s( 'radius_md' ) );
$r_lg = intval( $s( 'radius_lg' ) );
$r_xl = intval( $s( 'radius_xl' ) );
// Layout.
$c_max = intval( $s( 'container_max' ) );
$c_pad_min = floatval( $s( 'container_pad_min' ) );
$c_pad_max = floatval( $s( 'container_pad_max' ) );
$wide = intval( $s( 'wide_size' ) );
$primary_rgb = oribi_hex_to_rgb( $primary );
$accent_rgb = oribi_hex_to_rgb( $accent );
$dk_primary_rgb = oribi_hex_to_rgb( $dk_primary );
// Build CSS.
$css = <<<CSS
/* ================================================================
OTS Theme — Generated Theme Overrides
Generated: %s
================================================================ */
/* ── WordPress preset colour overrides ─────────────────────────── */
:root {
--wp--preset--color--primary: {$primary};
--wp--preset--color--primary-dk: {$primary_dk};
--wp--preset--color--primary-lt: {$primary_lt};
--wp--preset--color--accent: {$accent};
--wp--preset--color--accent-dk: {$accent_dk};
--wp--preset--color--accent-lt: {$accent_lt};
--wp--preset--color--dark: {$dark};
--wp--preset--color--dark-2: {$dark_2};
--wp--preset--color--text: {$text};
--wp--preset--color--text-muted: {$text_muted};
--wp--preset--color--border: {$border};
--wp--preset--color--bg: {$bg};
--wp--preset--color--bg-alt: {$bg_alt};
}
/* ── Light-mode aliases (consumed by main.css) ─────────────────── */
:root,
[data-theme="light"] {
--color-primary: {$primary};
--color-primary-dk: {$primary_dk};
--color-primary-lt: {$primary_lt};
--color-primary-rgb: {$primary_rgb};
--color-accent: {$accent};
--color-accent-dk: {$accent_dk};
--color-accent-lt: {$accent_lt};
--color-accent-rgb: {$accent_rgb};
--color-dark: {$dark};
--color-dark-2: {$dark_2};
--color-text: {$text};
--color-text-muted: {$text_muted};
--color-border: {$border};
--color-bg: {$bg};
--color-bg-alt: {$bg_alt};
--color-bg-dark: {$dark};
--color-heading: {$dark};
--header-scrolled-bg: rgba(255,255,255,.97);
--header-scrolled-text: {$text};
--card-bg: {$bg};
--form-bg: {$bg_alt};
--form-bg-focus: {$bg};
/* ── Typography ── */
--font-sans: {$body_font_css};
--font-heading: {$heading_font_css};
/* ── Border radii ── */
--radius-sm: {$r_sm}px;
--radius-md: {$r_md}px;
--radius-lg: {$r_lg}px;
--radius-xl: {$r_xl}px;
--wp--custom--radius--sm: {$r_sm}px;
--wp--custom--radius--md: {$r_md}px;
--wp--custom--radius--lg: {$r_lg}px;
--wp--custom--radius--xl: {$r_xl}px;
/* ── Layout ── */
--container-max: {$c_max}px;
--container-pad: clamp({$c_pad_min}rem, 5vw, {$c_pad_max}rem);
--wp--custom--container--max: {$c_max}px;
--wp--custom--container--pad: clamp({$c_pad_min}rem, 5vw, {$c_pad_max}rem);
}
/* ── Dark mode overrides ───────────────────────────────────────── */
[data-theme="dark"] {
--wp--custom--dark--primary: {$dk_primary};
--wp--custom--dark--primary-dk: {$dk_primary_dk};
--wp--custom--dark--primary-lt: {$dk_primary_lt};
--wp--custom--dark--accent: {$dk_accent};
--wp--custom--dark--accent-dk: {$dk_accent_dk};
--wp--custom--dark--accent-lt: {$dk_accent_lt};
--wp--custom--dark--dark: {$dk_dark};
--wp--custom--dark--dark-2: {$dk_dark_2};
--wp--custom--dark--text: {$dk_text};
--wp--custom--dark--text-muted: {$dk_text_muted};
--wp--custom--dark--border: {$dk_border};
--wp--custom--dark--bg: {$dk_bg};
--wp--custom--dark--bg-alt: {$dk_bg_alt};
--wp--custom--dark--bg-dark: {$dk_bg_dark};
--wp--custom--dark--heading: {$dk_heading};
--wp--custom--dark--card-bg: {$dk_card_bg};
--color-primary: {$dk_primary};
--color-primary-dk: {$dk_primary_dk};
--color-primary-lt: {$dk_primary_lt};
--color-primary-rgb: {$dk_primary_rgb};
--color-accent: {$dk_accent};
--color-accent-dk: {$dk_accent_dk};
--color-accent-lt: {$dk_accent_lt};
--color-dark: {$dk_dark};
--color-dark-2: {$dk_dark_2};
--color-text: {$dk_text};
--color-text-muted: {$dk_text_muted};
--color-border: {$dk_border};
--color-bg: {$dk_bg};
--color-bg-alt: {$dk_bg_alt};
--color-bg-dark: {$dk_bg_dark};
--color-heading: {$dk_heading};
--header-scrolled-bg: rgba(15,23,36,.97);
--header-scrolled-text: {$dk_text};
--card-bg: {$dk_card_bg};
--form-bg: {$dk_card_bg};
--form-bg-focus: #1A2538;
}
/* ── Typography application ────────────────────────────────────── */
body {
font-family: var(--font-sans);
}
h1, h2, h3, h4, h5, h6,
.wp-block-heading {
font-family: var(--font-heading, var(--font-sans));
}
/* ── WordPress layout overrides ────────────────────────────────── */
.wp-site-blocks > .wp-block-group,
.wp-site-blocks > .alignfull {
max-width: none;
}
body > .is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)) {
max-width: {$c_max}px;
}
CSS;
return sprintf( $css, gmdate( 'Y-m-d H:i:s' ) );
}
/**
* Write the generated CSS file to disk.
*
* @return bool|WP_Error True on success, WP_Error on failure.
*/
function oribi_write_generated_css() {
$css = oribi_build_css();
$path = oribi_generated_css_path();
// Ensure directory exists.
$dir = dirname( $path );
if ( ! file_exists( $dir ) ) {
wp_mkdir_p( $dir );
}
// Use WP_Filesystem for safe file writing.
require_once ABSPATH . 'wp-admin/includes/file.php';
global $wp_filesystem;
if ( ! WP_Filesystem() ) {
return new WP_Error( 'fs', __( 'Could not initialise filesystem.', 'ots-theme' ) );
}
$result = $wp_filesystem->put_contents( $path, $css, FS_CHMOD_FILE );
if ( ! $result ) {
return new WP_Error( 'write', __( 'Could not write generated CSS file.', 'ots-theme' ) );
}
// Bump version number so browsers cache-bust.
$version = intval( get_theme_mod( 'oribi_css_version', 0 ) ) + 1;
set_theme_mod( 'oribi_css_version', $version );
return true;
}
/**
* Regenerate the CSS file if it doesn't exist yet (e.g. first page load).
*
* Hooked early so the file is ready before wp_enqueue_scripts fires.
*/
add_action( 'init', function () {
if ( ! file_exists( oribi_generated_css_path() ) ) {
oribi_write_generated_css();
}
} );

View File

@@ -0,0 +1,732 @@
<?php
/**
* Theme Settings — Custom admin page for configuring colours, fonts,
* spacing, and border-radius design tokens.
*
* Appearance → Theme Design Settings
*
* @package OTS_Theme
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/* ── Register the admin page ───────────────────────────────────── */
add_action( 'admin_menu', function () {
add_theme_page(
__( 'Theme Design Settings', 'ots-theme' ),
__( 'Theme Design', 'ots-theme' ),
'edit_theme_options',
'oribi-theme-settings',
'oribi_render_settings_page'
);
} );
/* ── Enqueue admin-only assets ─────────────────────────────────── */
add_action( 'admin_enqueue_scripts', function ( $hook ) {
if ( 'appearance_page_oribi-theme-settings' !== $hook ) {
return;
}
// WordPress colour picker.
wp_enqueue_style( 'wp-color-picker' );
wp_enqueue_script( 'wp-color-picker' );
// Google Fonts for preview.
wp_enqueue_style(
'oribi-admin-google-fonts',
'https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Roboto:wght@400;700&family=Open+Sans:wght@400;700&family=Poppins:wght@400;600;700&family=Lato:wght@400;700&family=Montserrat:wght@400;600;700&family=Source+Sans+3:wght@400;600;700&family=Nunito:wght@400;600;700&family=Raleway:wght@400;600;700&family=DM+Sans:wght@400;500;700&family=Work+Sans:wght@400;500;600;700&family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap',
[],
null
);
// Inline CSS + JS for the settings page.
wp_add_inline_style( 'wp-color-picker', oribi_admin_inline_css() );
wp_add_inline_script( 'wp-color-picker', oribi_admin_inline_js(), 'after' );
} );
/* ── Handle form submission ────────────────────────────────────── */
add_action( 'admin_init', function () {
if (
! isset( $_POST['oribi_settings_nonce'] ) ||
! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['oribi_settings_nonce'] ) ), 'oribi_save_settings' )
) {
return;
}
if ( ! current_user_can( 'edit_theme_options' ) ) {
return;
}
$defaults = oribi_get_theme_defaults();
// Determine action — save or reset.
$action = isset( $_POST['oribi_action'] ) ? sanitize_text_field( wp_unslash( $_POST['oribi_action'] ) ) : 'save';
if ( 'reset' === $action ) {
foreach ( $defaults as $key => $value ) {
set_theme_mod( 'oribi_' . $key, $value );
}
} else {
// Sanitize & save each setting.
foreach ( $defaults as $key => $default ) {
$posted = isset( $_POST[ 'oribi_' . $key ] )
? wp_unslash( $_POST[ 'oribi_' . $key ] ) // phpcs:ignore
: $default;
// Determine sanitisation method by key prefix/type.
if ( strpos( $key, 'color_' ) === 0 || strpos( $key, 'dark_' ) === 0 ) {
// Colour values (hex or rgba).
$posted = oribi_sanitize_color( $posted );
} elseif ( strpos( $key, 'font_' ) === 0 ) {
$posted = sanitize_text_field( $posted );
} elseif ( strpos( $key, 'radius_' ) === 0 || strpos( $key, 'container_' ) === 0 || $key === 'wide_size' ) {
$posted = sanitize_text_field( $posted );
} else {
$posted = sanitize_text_field( $posted );
}
set_theme_mod( 'oribi_' . $key, $posted );
}
}
// Regenerate CSS.
$result = oribi_write_generated_css();
if ( is_wp_error( $result ) ) {
add_settings_error( 'oribi_settings', 'css_error', $result->get_error_message(), 'error' );
} else {
$msg = 'reset' === $action
? __( 'Settings reset to defaults. CSS regenerated.', 'ots-theme' )
: __( 'Settings saved. CSS regenerated.', 'ots-theme' );
add_settings_error( 'oribi_settings', 'saved', $msg, 'success' );
}
// Store errors/notices in transient so they survive the redirect.
set_transient( 'oribi_settings_notices', get_settings_errors( 'oribi_settings' ), 30 );
// PRG redirect.
wp_safe_redirect( admin_url( 'themes.php?page=oribi-theme-settings' ) );
exit;
} );
/**
* Sanitize a colour value (hex or rgba).
*
* @param string $value Raw colour value.
* @return string Sanitized colour.
*/
function oribi_sanitize_color( $value ) {
$value = trim( $value );
// Allow rgba(...) values (used for dark mode light tints).
if ( preg_match( '/^rgba?\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*(,\s*[\d.]+\s*)?\)$/', $value ) ) {
return $value;
}
// Standard hex.
return sanitize_hex_color( $value ) ?? '#000000';
}
/* ── Render the settings page ──────────────────────────────────── */
function oribi_render_settings_page() {
// Seed defaults on first visit.
oribi_maybe_seed_defaults();
// Show any saved notices.
$notices = get_transient( 'oribi_settings_notices' );
if ( $notices ) {
delete_transient( 'oribi_settings_notices' );
foreach ( $notices as $notice ) {
printf(
'<div class="notice notice-%s is-dismissible"><p>%s</p></div>',
esc_attr( $notice['type'] ),
esc_html( $notice['message'] )
);
}
}
$s = 'oribi_get_setting';
$fonts = oribi_get_available_fonts();
?>
<div class="wrap oribi-settings-wrap">
<h1><?php esc_html_e( 'Theme Design Settings', 'ots-theme' ); ?></h1>
<p class="description" style="margin-bottom:24px;">
<?php esc_html_e( 'Customise colours, typography, spacing, and border radii. Changes are applied site-wide via a generated CSS file.', 'ots-theme' ); ?>
</p>
<form method="post" id="oribi-settings-form">
<?php wp_nonce_field( 'oribi_save_settings', 'oribi_settings_nonce' ); ?>
<input type="hidden" name="oribi_action" id="oribi-action-field" value="save" />
<!-- Tabs -->
<nav class="oribi-tabs" role="tablist">
<button type="button" class="oribi-tab active" data-tab="colors" role="tab"><?php esc_html_e( 'Colours', 'ots-theme' ); ?></button>
<button type="button" class="oribi-tab" data-tab="dark" role="tab"><?php esc_html_e( 'Dark Mode', 'ots-theme' ); ?></button>
<button type="button" class="oribi-tab" data-tab="typography" role="tab"><?php esc_html_e( 'Typography', 'ots-theme' ); ?></button>
<button type="button" class="oribi-tab" data-tab="spacing" role="tab"><?php esc_html_e( 'Spacing & Layout', 'ots-theme' ); ?></button>
<button type="button" class="oribi-tab" data-tab="radius" role="tab"><?php esc_html_e( 'Border Radius', 'ots-theme' ); ?></button>
</nav>
<div class="oribi-panels-wrap">
<div class="oribi-panels">
<!-- ═══ COLOURS ══════════════════════════════════════ -->
<div class="oribi-panel active" data-panel="colors">
<h2><?php esc_html_e( 'Light Mode Colour Palette', 'ots-theme' ); ?></h2>
<p class="description"><?php esc_html_e( 'These colours apply when dark mode is off.', 'ots-theme' ); ?></p>
<div class="oribi-color-grid">
<?php
$light_colors = [
'color_primary' => __( 'Primary', 'ots-theme' ),
'color_primary_dk' => __( 'Primary Dark', 'ots-theme' ),
'color_primary_lt' => __( 'Primary Light', 'ots-theme' ),
'color_accent' => __( 'Accent', 'ots-theme' ),
'color_accent_dk' => __( 'Accent Dark', 'ots-theme' ),
'color_accent_lt' => __( 'Accent Light', 'ots-theme' ),
'color_dark' => __( 'Dark', 'ots-theme' ),
'color_dark_2' => __( 'Dark 2', 'ots-theme' ),
'color_text' => __( 'Text', 'ots-theme' ),
'color_text_muted' => __( 'Text Muted', 'ots-theme' ),
'color_border' => __( 'Border', 'ots-theme' ),
'color_bg' => __( 'Background', 'ots-theme' ),
'color_bg_alt' => __( 'Background Alt', 'ots-theme' ),
];
foreach ( $light_colors as $key => $label ) :
$val = $s( $key );
?>
<div class="oribi-color-field">
<label for="oribi_<?php echo esc_attr( $key ); ?>">
<?php echo esc_html( $label ); ?>
</label>
<input
type="text"
id="oribi_<?php echo esc_attr( $key ); ?>"
name="oribi_<?php echo esc_attr( $key ); ?>"
value="<?php echo esc_attr( $val ); ?>"
class="oribi-color-picker"
data-default-color="<?php echo esc_attr( oribi_get_theme_defaults()[ $key ] ); ?>"
/>
</div>
<?php endforeach; ?>
</div>
</div>
<!-- ═══ DARK MODE ════════════════════════════════════ -->
<div class="oribi-panel" data-panel="dark">
<h2><?php esc_html_e( 'Dark Mode Colour Palette', 'ots-theme' ); ?></h2>
<p class="description"><?php esc_html_e( 'These colours apply when dark mode is active. Some values support rgba() notation.', 'ots-theme' ); ?></p>
<div class="oribi-color-grid">
<?php
$dark_colors = [
'dark_primary' => __( 'Primary', 'ots-theme' ),
'dark_primary_dk' => __( 'Primary Dark', 'ots-theme' ),
'dark_primary_lt' => __( 'Primary Light', 'ots-theme' ),
'dark_accent' => __( 'Accent', 'ots-theme' ),
'dark_accent_dk' => __( 'Accent Dark', 'ots-theme' ),
'dark_accent_lt' => __( 'Accent Light', 'ots-theme' ),
'dark_dark' => __( 'Dark (Text)', 'ots-theme' ),
'dark_dark_2' => __( 'Dark 2 (Text)', 'ots-theme' ),
'dark_text' => __( 'Body Text', 'ots-theme' ),
'dark_text_muted' => __( 'Text Muted', 'ots-theme' ),
'dark_border' => __( 'Border', 'ots-theme' ),
'dark_bg' => __( 'Background', 'ots-theme' ),
'dark_bg_alt' => __( 'Background Alt', 'ots-theme' ),
'dark_bg_dark' => __( 'Background Darker', 'ots-theme' ),
'dark_heading' => __( 'Heading', 'ots-theme' ),
'dark_card_bg' => __( 'Card Background', 'ots-theme' ),
];
foreach ( $dark_colors as $key => $label ) :
$val = $s( $key );
$is_rgba = ( strpos( $val, 'rgba' ) !== false );
?>
<div class="oribi-color-field">
<label for="oribi_<?php echo esc_attr( $key ); ?>">
<?php echo esc_html( $label ); ?>
</label>
<?php if ( $is_rgba ) : ?>
<input
type="text"
id="oribi_<?php echo esc_attr( $key ); ?>"
name="oribi_<?php echo esc_attr( $key ); ?>"
value="<?php echo esc_attr( $val ); ?>"
class="regular-text oribi-rgba-input"
placeholder="rgba(r,g,b,a)"
/>
<span class="oribi-color-swatch" style="background:<?php echo esc_attr( $val ); ?>;"></span>
<?php else : ?>
<input
type="text"
id="oribi_<?php echo esc_attr( $key ); ?>"
name="oribi_<?php echo esc_attr( $key ); ?>"
value="<?php echo esc_attr( $val ); ?>"
class="oribi-color-picker"
data-default-color="<?php echo esc_attr( oribi_get_theme_defaults()[ $key ] ); ?>"
/>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
</div>
<!-- ═══ TYPOGRAPHY ═══════════════════════════════════ -->
<div class="oribi-panel" data-panel="typography">
<h2><?php esc_html_e( 'Typography', 'ots-theme' ); ?></h2>
<p class="description">
<?php
printf(
/* translators: %s = link to Font Library */
esc_html__( 'Select from available fonts below. To add more fonts, use the %s in the Site Editor.', 'ots-theme' ),
'<a href="' . esc_url( admin_url( 'site-editor.php' ) ) . '">' . esc_html__( 'Font Library', 'ots-theme' ) . '</a>'
);
?>
</p>
<table class="form-table" role="presentation">
<tr>
<th scope="row">
<label for="oribi_font_family"><?php esc_html_e( 'Body Font', 'ots-theme' ); ?></label>
</th>
<td>
<select id="oribi_font_family" name="oribi_font_family" class="oribi-font-select">
<?php foreach ( $fonts as $f ) : ?>
<option
value="<?php echo esc_attr( $f['slug'] ); ?>"
data-font-family="<?php echo esc_attr( $f['fontFamily'] ); ?>"
<?php selected( $s( 'font_family' ), $f['slug'] ); ?>
>
<?php echo esc_html( $f['name'] ); ?>
</option>
<?php endforeach; ?>
</select>
<p class="description"><?php esc_html_e( 'Applied to body text, paragraphs, and UI elements.', 'ots-theme' ); ?></p>
</td>
</tr>
<tr>
<th scope="row">
<label for="oribi_font_heading"><?php esc_html_e( 'Heading Font', 'ots-theme' ); ?></label>
</th>
<td>
<select id="oribi_font_heading" name="oribi_font_heading" class="oribi-font-select">
<option value=""><?php esc_html_e( '— Same as body font —', 'ots-theme' ); ?></option>
<?php foreach ( $fonts as $f ) : ?>
<option
value="<?php echo esc_attr( $f['slug'] ); ?>"
data-font-family="<?php echo esc_attr( $f['fontFamily'] ); ?>"
<?php selected( $s( 'font_heading' ), $f['slug'] ); ?>
>
<?php echo esc_html( $f['name'] ); ?>
</option>
<?php endforeach; ?>
</select>
<p class="description"><?php esc_html_e( 'Applied to h1h6 headings. Leave blank to use the body font.', 'ots-theme' ); ?></p>
</td>
</tr>
</table>
<!-- Font preview -->
<div class="oribi-font-preview" id="oribi-font-preview">
<h3 style="margin:0 0 8px;"><?php esc_html_e( 'Font Preview', 'ots-theme' ); ?></h3>
<div class="oribi-font-preview-heading" id="oribi-preview-heading">
The quick brown fox jumps over the lazy dog
</div>
<div class="oribi-font-preview-body" id="oribi-preview-body">
The quick brown fox jumps over the lazy dog. 0123456789
</div>
</div>
</div>
<!-- ═══ SPACING & LAYOUT ═════════════════════════════ -->
<div class="oribi-panel" data-panel="spacing">
<h2><?php esc_html_e( 'Spacing & Layout', 'ots-theme' ); ?></h2>
<table class="form-table" role="presentation">
<tr>
<th scope="row">
<label for="oribi_container_max"><?php esc_html_e( 'Container Max Width (px)', 'ots-theme' ); ?></label>
</th>
<td>
<input type="number" id="oribi_container_max" name="oribi_container_max"
value="<?php echo esc_attr( $s( 'container_max' ) ); ?>"
min="600" max="2400" step="10" class="small-text" />
<p class="description"><?php esc_html_e( 'Maximum width of the main content area (default: 1200).', 'ots-theme' ); ?></p>
</td>
</tr>
<tr>
<th scope="row">
<label for="oribi_wide_size"><?php esc_html_e( 'Wide Block Width (px)', 'ots-theme' ); ?></label>
</th>
<td>
<input type="number" id="oribi_wide_size" name="oribi_wide_size"
value="<?php echo esc_attr( $s( 'wide_size' ) ); ?>"
min="800" max="3000" step="10" class="small-text" />
<p class="description"><?php esc_html_e( 'Width of "wide" aligned blocks (default: 1536).', 'ots-theme' ); ?></p>
</td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'Horizontal Padding', 'ots-theme' ); ?></th>
<td>
<label>
<?php esc_html_e( 'Min:', 'ots-theme' ); ?>
<input type="number" name="oribi_container_pad_min"
value="<?php echo esc_attr( $s( 'container_pad_min' ) ); ?>"
min="0" max="10" step="0.25" class="small-text" /> rem
</label>
&nbsp;&nbsp;
<label>
<?php esc_html_e( 'Max:', 'ots-theme' ); ?>
<input type="number" name="oribi_container_pad_max"
value="<?php echo esc_attr( $s( 'container_pad_max' ) ); ?>"
min="0" max="10" step="0.25" class="small-text" /> rem
</label>
<p class="description"><?php esc_html_e( 'Responsive horizontal padding using clamp(min, 5vw, max). Default: 1rem 2rem.', 'ots-theme' ); ?></p>
</td>
</tr>
</table>
</div>
<!-- ═══ BORDER RADIUS ════════════════════════════════ -->
<div class="oribi-panel" data-panel="radius">
<h2><?php esc_html_e( 'Border Radius', 'ots-theme' ); ?></h2>
<p class="description"><?php esc_html_e( 'Four preset sizes used across all blocks. Values in pixels.', 'ots-theme' ); ?></p>
<table class="form-table" role="presentation">
<?php
$radii = [
'radius_sm' => [ 'SM', __( 'Buttons, badges, small elements', 'ots-theme' ) ],
'radius_md' => [ 'MD', __( 'Cards, inputs, mid-size components', 'ots-theme' ) ],
'radius_lg' => [ 'LG', __( 'Sections, larger surfaces', 'ots-theme' ) ],
'radius_xl' => [ 'XL', __( 'Pills, fully rounded elements', 'ots-theme' ) ],
];
foreach ( $radii as $key => $info ) :
?>
<tr>
<th scope="row">
<label for="oribi_<?php echo esc_attr( $key ); ?>">
<?php echo esc_html( $info[0] ); ?>
</label>
</th>
<td>
<input type="number" id="oribi_<?php echo esc_attr( $key ); ?>"
name="oribi_<?php echo esc_attr( $key ); ?>"
value="<?php echo esc_attr( $s( $key ) ); ?>"
min="0" max="100" step="1" class="small-text" /> px
<span class="oribi-radius-preview" style="border-radius:<?php echo esc_attr( $s( $key ) ); ?>px;"></span>
<p class="description"><?php echo esc_html( $info[1] ); ?></p>
</td>
</tr>
<?php endforeach; ?>
</table>
</div>
</div><!-- .oribi-panels -->
<!-- ═══ LIVE PREVIEW SIDEBAR ═════════════════════════ -->
<aside class="oribi-preview-sidebar" id="oribi-preview-sidebar">
<h3><?php esc_html_e( 'Preview', 'ots-theme' ); ?></h3>
<div class="oribi-preview-card" id="oribi-preview-card">
<div class="oribi-preview-hero" id="preview-hero">
<h2 id="preview-hero-title"><?php esc_html_e( 'Hero Heading', 'ots-theme' ); ?></h2>
<p id="preview-hero-text"><?php esc_html_e( 'Body text preview. The quick brown fox jumps over the lazy dog.', 'ots-theme' ); ?></p>
<button id="preview-btn-primary"><?php esc_html_e( 'Primary Button', 'ots-theme' ); ?></button>
<button id="preview-btn-accent"><?php esc_html_e( 'Accent Button', 'ots-theme' ); ?></button>
</div>
<div class="oribi-preview-section" id="preview-card-section">
<div class="oribi-prev-card" id="preview-card-1">
<h4><?php esc_html_e( 'Card Title', 'ots-theme' ); ?></h4>
<p><?php esc_html_e( 'Card body text with muted small text below.', 'ots-theme' ); ?></p>
<small><?php esc_html_e( 'Muted text', 'ots-theme' ); ?></small>
</div>
</div>
</div>
</aside>
</div><!-- .oribi-panels-wrap -->
<!-- Actions -->
<div class="oribi-actions">
<?php submit_button( __( 'Save Changes', 'ots-theme' ), 'primary', 'submit', false ); ?>
<button type="button" id="oribi-reset-btn" class="button button-secondary">
<?php esc_html_e( 'Reset to Defaults', 'ots-theme' ); ?>
</button>
</div>
</form>
</div>
<?php
}
/* ── Inline CSS for the admin page ─────────────────────────────── */
function oribi_admin_inline_css() {
return <<<'CSS'
/* ── Settings page layout ── */
.oribi-settings-wrap { max-width: 1400px; }
/* ── Tabs ── */
.oribi-tabs {
display: flex; gap: 0; border-bottom: 2px solid #c3c4c7;
margin-bottom: 0; background: #f0f0f1;
}
.oribi-tab {
padding: 12px 20px; border: none; background: transparent;
cursor: pointer; font-size: 14px; font-weight: 500;
border-bottom: 2px solid transparent; margin-bottom: -2px;
color: #50575e; transition: all .15s ease;
}
.oribi-tab:hover { color: #1d2327; background: #fff; }
.oribi-tab.active {
color: #1d2327; background: #fff;
border-bottom-color: #2271b1; font-weight: 600;
}
/* ── Panels + Preview sidebar ── */
.oribi-panels-wrap {
display: grid;
grid-template-columns: 1fr 320px;
gap: 24px;
margin-top: 0;
}
@media (max-width: 1100px) {
.oribi-panels-wrap { grid-template-columns: 1fr; }
.oribi-preview-sidebar { order: -1; }
}
.oribi-panel { display: none; padding: 24px; background: #fff; border: 1px solid #c3c4c7; border-top: none; }
.oribi-panel.active { display: block; }
.oribi-panel h2 { margin-top: 0; }
/* ── Colour grid ── */
.oribi-color-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 16px; margin-top: 16px;
}
.oribi-color-field { display: flex; flex-direction: column; gap: 4px; }
.oribi-color-field label { font-weight: 500; font-size: 13px; }
.oribi-rgba-input { font-family: monospace; font-size: 13px; }
.oribi-color-swatch {
display: inline-block; width: 28px; height: 28px;
border: 1px solid #ddd; border-radius: 4px; margin-top: 4px;
vertical-align: middle;
}
/* ── Font preview ── */
.oribi-font-preview {
margin-top: 24px; padding: 20px;
background: #f9f9f9; border: 1px solid #ddd; border-radius: 8px;
}
.oribi-font-preview-heading {
font-size: 28px; font-weight: 700; line-height: 1.3; margin-bottom: 8px;
}
.oribi-font-preview-body {
font-size: 16px; line-height: 1.65; color: #555;
}
/* ── Radius preview ── */
.oribi-radius-preview {
display: inline-block; width: 40px; height: 40px;
background: #2271b1; vertical-align: middle; margin-left: 12px;
}
/* ── Preview sidebar ── */
.oribi-preview-sidebar {
position: sticky; top: 32px; align-self: start;
padding: 20px; background: #fff; border: 1px solid #c3c4c7;
border-radius: 8px;
}
.oribi-preview-sidebar h3 { margin-top: 0; }
.oribi-preview-card { border-radius: 8px; overflow: hidden; }
.oribi-preview-hero {
padding: 24px 16px; text-align: center;
transition: all .2s ease;
}
.oribi-preview-hero h2 { margin: 0 0 8px; font-size: 22px; }
.oribi-preview-hero p { font-size: 14px; margin: 0 0 16px; }
.oribi-preview-hero button {
padding: 8px 16px; border: none; color: #fff;
border-radius: 6px; cursor: default; margin: 4px;
font-size: 13px; font-weight: 600;
}
.oribi-preview-section {
padding: 16px; transition: all .2s ease;
}
.oribi-prev-card {
padding: 16px; border: 1px solid #e2e8f0;
transition: all .2s ease;
}
.oribi-prev-card h4 { margin: 0 0 6px; font-size: 15px; }
.oribi-prev-card p { margin: 0 0 4px; font-size: 13px; }
.oribi-prev-card small { font-size: 12px; }
/* ── Actions bar ── */
.oribi-actions {
display: flex; align-items: center; gap: 12px;
margin-top: 24px; padding-top: 16px;
border-top: 1px solid #c3c4c7;
}
.oribi-actions .button-primary { margin: 0 !important; }
CSS;
}
/* ── Inline JS for the admin page ──────────────────────────────── */
function oribi_admin_inline_js() {
return <<<'JS'
document.addEventListener('DOMContentLoaded', function() {
/* ── Tabs ── */
document.querySelectorAll('.oribi-tab').forEach(function(tab) {
tab.addEventListener('click', function() {
document.querySelectorAll('.oribi-tab').forEach(function(t) { t.classList.remove('active'); });
document.querySelectorAll('.oribi-panel').forEach(function(p) { p.classList.remove('active'); });
tab.classList.add('active');
var panel = document.querySelector('[data-panel="' + tab.dataset.tab + '"]');
if (panel) panel.classList.add('active');
});
});
/* ── Colour pickers ── */
if (typeof jQuery !== 'undefined' && jQuery.fn.wpColorPicker) {
jQuery('.oribi-color-picker').wpColorPicker({
change: debounce(updatePreview, 150),
clear: debounce(updatePreview, 150)
});
}
/* ── Font preview ── */
document.querySelectorAll('.oribi-font-select').forEach(function(sel) {
sel.addEventListener('change', updateFontPreview);
});
updateFontPreview();
/* ── Reset button ── */
document.getElementById('oribi-reset-btn').addEventListener('click', function() {
if (confirm('Reset all settings to factory defaults? This cannot be undone.')) {
document.getElementById('oribi-action-field').value = 'reset';
document.getElementById('oribi-settings-form').submit();
}
});
/* ── Live preview updater ── */
function updatePreview() {
var get = function(id) {
var el = document.getElementById(id);
if (!el) return '';
// wpColorPicker stores hex in the text input
return el.value || '';
};
var hero = document.getElementById('preview-hero');
var card = document.getElementById('preview-card-1');
var section = document.getElementById('preview-card-section');
var btnP = document.getElementById('preview-btn-primary');
var btnA = document.getElementById('preview-btn-accent');
var heroTitle = document.getElementById('preview-hero-title');
var heroText = document.getElementById('preview-hero-text');
if (hero) {
hero.style.backgroundColor = get('oribi_color_dark') || '#0D1321';
if (heroTitle) heroTitle.style.color = '#fff';
if (heroText) heroText.style.color = 'rgba(255,255,255,.8)';
}
if (btnP) {
btnP.style.backgroundColor = get('oribi_color_primary') || '#D83302';
btnP.style.borderRadius = (get('oribi_radius_sm') || '6') + 'px';
}
if (btnA) {
btnA.style.backgroundColor = get('oribi_color_accent') || '#00757c';
btnA.style.borderRadius = (get('oribi_radius_sm') || '6') + 'px';
}
if (section) {
section.style.backgroundColor = get('oribi_color_bg_alt') || '#FFF8F5';
}
if (card) {
card.style.backgroundColor = get('oribi_color_bg') || '#fff';
card.style.borderColor = get('oribi_color_border') || '#E2E8F0';
card.style.borderRadius = (get('oribi_radius_md') || '12') + 'px';
var h4 = card.querySelector('h4');
if (h4) h4.style.color = get('oribi_color_dark') || '#0D1321';
var p = card.querySelector('p');
if (p) p.style.color = get('oribi_color_text') || '#2D3748';
var sm = card.querySelector('small');
if (sm) sm.style.color = get('oribi_color_text_muted') || '#718096';
}
// Radius previews.
document.querySelectorAll('.oribi-radius-preview').forEach(function(el) {
var inp = el.parentElement.querySelector('input[type="number"]');
if (inp) el.style.borderRadius = inp.value + 'px';
});
}
function updateFontPreview() {
var bodySelect = document.getElementById('oribi_font_family');
var headSelect = document.getElementById('oribi_font_heading');
var previewHead = document.getElementById('oribi-preview-heading');
var previewBody = document.getElementById('oribi-preview-body');
if (bodySelect && previewBody) {
var opt = bodySelect.options[bodySelect.selectedIndex];
previewBody.style.fontFamily = opt.dataset.fontFamily || 'system-ui';
}
if (headSelect && previewHead) {
var hOpt = headSelect.options[headSelect.selectedIndex];
if (hOpt.value) {
previewHead.style.fontFamily = hOpt.dataset.fontFamily || 'system-ui';
} else if (bodySelect) {
var bOpt = bodySelect.options[bodySelect.selectedIndex];
previewHead.style.fontFamily = bOpt.dataset.fontFamily || 'system-ui';
}
}
// Also update preview sidebar heading/body fonts.
var heroTitle = document.getElementById('preview-hero-title');
var heroText = document.getElementById('preview-hero-text');
var cardH4 = document.querySelector('.oribi-prev-card h4');
var cardP = document.querySelector('.oribi-prev-card p');
if (headSelect) {
var hf = headSelect.options[headSelect.selectedIndex];
var headingFont = hf.value
? (hf.dataset.fontFamily || 'system-ui')
: (bodySelect ? bodySelect.options[bodySelect.selectedIndex].dataset.fontFamily : 'system-ui');
if (heroTitle) heroTitle.style.fontFamily = headingFont;
if (cardH4) cardH4.style.fontFamily = headingFont;
}
if (bodySelect) {
var bf = bodySelect.options[bodySelect.selectedIndex];
var bodyFont = bf.dataset.fontFamily || 'system-ui';
if (heroText) heroText.style.fontFamily = bodyFont;
if (cardP) cardP.style.fontFamily = bodyFont;
}
}
function debounce(fn, ms) {
var timer;
return function() {
clearTimeout(timer);
timer = setTimeout(fn, ms);
};
}
// Initial preview render.
updatePreview();
// Attach change listeners to all inputs for live preview.
document.querySelectorAll('input[type="number"]').forEach(function(inp) {
inp.addEventListener('input', debounce(updatePreview, 100));
});
document.querySelectorAll('.oribi-rgba-input').forEach(function(inp) {
inp.addEventListener('input', debounce(updatePreview, 200));
});
});
JS;
}