WP test container created
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
/**
|
||||
* @package TLIG
|
||||
*/
|
||||
?>
|
||||
<?php wp_footer(); ?>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
/**
|
||||
* TLIG theme setup.
|
||||
*
|
||||
* @package TLIG
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
const TLIG_VERSION = '0.1.0';
|
||||
|
||||
require_once __DIR__ . '/inc/nav.php';
|
||||
require_once __DIR__ . '/inc/nav-fields.php';
|
||||
require_once __DIR__ . '/inc/demo-languages.php';
|
||||
|
||||
/**
|
||||
* Theme supports and menu locations.
|
||||
*
|
||||
* One location, not two. The site it replaces maintains a separate desktop tree
|
||||
* (UberMenu) and mobile tree (ShiftNav) per language — 27 menus, which is why
|
||||
* German, Polish and Arabic have no mobile menu at all. Here one tree renders
|
||||
* both; the panel simply stacks below the tablet breakpoint.
|
||||
*/
|
||||
function tlig_setup(): void {
|
||||
add_theme_support( 'title-tag' );
|
||||
add_theme_support( 'post-thumbnails' );
|
||||
add_theme_support( 'html5', [ 'search-form', 'gallery', 'caption', 'style', 'script' ] );
|
||||
|
||||
register_nav_menus( [
|
||||
'primary' => __( 'Primary navigation', 'tlig' ),
|
||||
] );
|
||||
|
||||
load_theme_textdomain( 'tlig', get_template_directory() . '/languages' );
|
||||
}
|
||||
add_action( 'after_setup_theme', 'tlig_setup' );
|
||||
|
||||
/**
|
||||
* The stylesheet is built by Vite into assets/tlig.css.
|
||||
*
|
||||
* Versioned by filemtime so each build busts the browser cache — the same
|
||||
* approach the old child theme uses for tlig_en.css.
|
||||
*/
|
||||
function tlig_enqueue_assets(): void {
|
||||
$rel = '/assets/tlig.css';
|
||||
$path = get_template_directory() . $rel;
|
||||
|
||||
if ( ! file_exists( $path ) ) {
|
||||
return; // Not built yet; say nothing rather than enqueue a 404.
|
||||
}
|
||||
|
||||
wp_enqueue_style( 'tlig', get_template_directory_uri() . $rel, [], (string) filemtime( $path ) );
|
||||
|
||||
$js = get_template_directory() . '/assets/tlig.js';
|
||||
|
||||
if ( file_exists( $js ) ) {
|
||||
wp_enqueue_script(
|
||||
'tlig',
|
||||
get_template_directory_uri() . '/assets/tlig.js',
|
||||
[],
|
||||
(string) filemtime( $js ),
|
||||
[ 'strategy' => 'defer' ]
|
||||
);
|
||||
}
|
||||
}
|
||||
add_action( 'wp_enqueue_scripts', 'tlig_enqueue_assets' );
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
/**
|
||||
* @package TLIG
|
||||
*/
|
||||
?>
|
||||
<!doctype html>
|
||||
<html <?php language_attributes(); ?>>
|
||||
<head>
|
||||
<meta charset="<?php bloginfo( 'charset' ); ?>">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<?php wp_head(); ?>
|
||||
</head>
|
||||
<body <?php body_class(); ?>>
|
||||
<?php wp_body_open(); ?>
|
||||
|
||||
<a class="skip-link visually-hidden" href="#content"><?php esc_html_e( 'Skip to content', 'tlig' ); ?></a>
|
||||
|
||||
<?php tlig_demo_language_bar(); ?>
|
||||
|
||||
<header class="site-header">
|
||||
<div class="site-header__inner">
|
||||
<p class="site-header__title"><a href="<?php echo esc_url( home_url( '/' ) ); ?>"><?php bloginfo( 'name' ); ?></a></p>
|
||||
</div>
|
||||
<?php tlig_nav(); ?>
|
||||
</header>
|
||||
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
/**
|
||||
* Demo scaffolding: a language switcher, standing in for WPML.
|
||||
*
|
||||
* The real site resolves the menu through WPML, which is not installed here and
|
||||
* is not what the demo is arguing about. But the argument *is* about languages
|
||||
* — the claim is that one tree per language renders correctly without anyone
|
||||
* cutting a bitmap — so the demo has to be able to show fifteen of them.
|
||||
*
|
||||
* So the fifteen imported trees are all real menus, and `?lang=xx` picks which
|
||||
* one the `primary` location resolves to. Under a port this file goes away and
|
||||
* WPML's own resolution takes over; nothing else in the theme knows about it.
|
||||
*
|
||||
* @package TLIG
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* The languages the demo has a menu for, in the order they are offered.
|
||||
*
|
||||
* @return array<string,string> Code => endonym.
|
||||
*/
|
||||
function tlig_demo_languages(): array {
|
||||
return [
|
||||
'en' => 'English',
|
||||
'fr' => 'Français',
|
||||
'de' => 'Deutsch',
|
||||
'es' => 'Español',
|
||||
'it' => 'Italiano',
|
||||
'pt' => 'Português',
|
||||
'nl' => 'Nederlands',
|
||||
'da' => 'Dansk',
|
||||
'sv' => 'Svenska',
|
||||
'pl' => 'Polski',
|
||||
'cs' => 'Čeština',
|
||||
'bg' => 'Български',
|
||||
'ru' => 'Русский',
|
||||
'el' => 'Ελληνικά',
|
||||
'ar' => 'العربية',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* The language currently being shown.
|
||||
*/
|
||||
function tlig_demo_lang(): string {
|
||||
$want = isset( $_GET['lang'] ) ? sanitize_key( wp_unslash( $_GET['lang'] ) ) : 'en';
|
||||
|
||||
return isset( tlig_demo_languages()[ $want ] ) ? $want : 'en';
|
||||
}
|
||||
|
||||
/**
|
||||
* Arabic is the reason the whole layout is written in logical properties.
|
||||
*/
|
||||
function tlig_demo_is_rtl(): bool {
|
||||
return 'ar' === tlig_demo_lang();
|
||||
}
|
||||
|
||||
/**
|
||||
* Points the primary location at the menu for the chosen language.
|
||||
*
|
||||
* @param array<string,mixed> $args wp_nav_menu arguments.
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
function tlig_demo_pick_menu( $args ) {
|
||||
if ( ( $args['theme_location'] ?? '' ) !== 'primary' ) {
|
||||
return $args;
|
||||
}
|
||||
|
||||
$menu = wp_get_nav_menu_object( 'TLIG ' . strtoupper( tlig_demo_lang() ) );
|
||||
|
||||
if ( $menu ) {
|
||||
$args['menu'] = $menu;
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
add_filter( 'wp_nav_menu_args', 'tlig_demo_pick_menu' );
|
||||
|
||||
/**
|
||||
* Flips the document for Arabic.
|
||||
*
|
||||
* @param string $output The <html> attributes.
|
||||
* @return string
|
||||
*/
|
||||
function tlig_demo_html_dir( $output ) {
|
||||
if ( ! tlig_demo_is_rtl() ) {
|
||||
return $output;
|
||||
}
|
||||
|
||||
return preg_replace( '/\bdir="[^"]*"/', '', $output ) . ' dir="rtl" lang="ar"';
|
||||
}
|
||||
add_filter( 'language_attributes', 'tlig_demo_html_dir' );
|
||||
|
||||
/**
|
||||
* The switcher itself.
|
||||
*/
|
||||
function tlig_demo_language_bar(): void {
|
||||
$current = tlig_demo_lang();
|
||||
|
||||
echo '<div class="lang-bar"><div class="lang-bar__inner">';
|
||||
echo '<span class="filter-bar__label">' . esc_html__( 'Language', 'tlig' ) . '</span>';
|
||||
echo '<ul class="filter-bar">';
|
||||
|
||||
foreach ( tlig_demo_languages() as $code => $name ) {
|
||||
$is_current = ( $code === $current );
|
||||
|
||||
printf(
|
||||
'<li><a class="%s" href="%s" hreflang="%s"%s>%s</a></li>',
|
||||
esc_attr( $is_current ? 'filter filter--sm is-active' : 'filter filter--sm' ),
|
||||
esc_url( add_query_arg( 'lang', $code, home_url( '/' ) ) ),
|
||||
esc_attr( $code ),
|
||||
$is_current ? ' aria-current="page"' : '',
|
||||
esc_html( $name )
|
||||
);
|
||||
}
|
||||
|
||||
echo '</ul></div></div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Demo scaffolding: force panels open, for screenshots and for the pitch.
|
||||
*
|
||||
* `?open=3` holds the third panel open; `?open=all` drops every panel into the
|
||||
* flow, one under another. The second is the argument in one picture — seven
|
||||
* panels of seven different heights, all drawn from the same 328px parchment
|
||||
* file, where the old site needs a separately cut bitmap for each.
|
||||
*
|
||||
* It is a stylesheet, not a change to the markup, so nothing about the menu
|
||||
* itself depends on it.
|
||||
*/
|
||||
function tlig_demo_open_panels(): void {
|
||||
$open = isset( $_GET['open'] ) ? sanitize_key( wp_unslash( $_GET['open'] ) ) : '';
|
||||
|
||||
if ( '' === $open ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( 'all' === $open ) {
|
||||
$css = '.site-nav__bar{display:block}'
|
||||
. '.site-nav__item{display:block}'
|
||||
. '.site-nav__panel{display:block;position:static;margin-block:0 1.5rem}';
|
||||
} else {
|
||||
$n = max( 1, (int) $open );
|
||||
$css = sprintf(
|
||||
// The bar too: below the tablet breakpoint it is hidden until the
|
||||
// toggle opens it, and a screenshot cannot press the toggle.
|
||||
'.site-nav__bar{display:flex}.site-nav__item:nth-child(%d) > .site-nav__panel{display:block}',
|
||||
$n
|
||||
);
|
||||
}
|
||||
|
||||
printf( "<style id=\"tlig-demo-open\">%s</style>\n", $css );
|
||||
}
|
||||
add_action( 'wp_head', 'tlig_demo_open_panels', 99 );
|
||||
@@ -0,0 +1,234 @@
|
||||
<?php
|
||||
/**
|
||||
* Eight fields on the menu item screen.
|
||||
*
|
||||
* WordPress core has offered `wp_nav_menu_item_custom_fields` since 5.4, so
|
||||
* this needs no plugin and no custom admin page — the fields appear inside the
|
||||
* item WordPress already draws, and save with it.
|
||||
*
|
||||
* Eight against UberMenu's 149. That is a real reduction in control and it is
|
||||
* deliberate: what went away is per-item colour, per-item background image and
|
||||
* per-item height, which is exactly the set that made the old menu a picture
|
||||
* instead of a design. Colour comes from tokens, the ground comes from the
|
||||
* parchment border-image, and height comes from the content.
|
||||
*
|
||||
* @package TLIG
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* The field definitions, in the order they are drawn.
|
||||
*
|
||||
* @return array<string,array<string,mixed>>
|
||||
*/
|
||||
function tlig_nav_fields(): array {
|
||||
return [
|
||||
'panel' => [
|
||||
'type' => 'checkbox',
|
||||
'label' => __( 'Opens a mega panel', 'tlig' ),
|
||||
'help' => __( 'Top-level items only. The panel is as tall as its content.', 'tlig' ),
|
||||
],
|
||||
'heading' => [
|
||||
'type' => 'checkbox',
|
||||
'label' => __( 'Heading, not a link', 'tlig' ),
|
||||
],
|
||||
'span' => [
|
||||
'type' => 'select',
|
||||
'label' => __( 'Column width', 'tlig' ),
|
||||
'options' => [
|
||||
'' => __( 'Automatic', 'tlig' ),
|
||||
'3' => __( 'Quarter', 'tlig' ),
|
||||
'4' => __( 'Third', 'tlig' ),
|
||||
'6' => __( 'Half', 'tlig' ),
|
||||
'8' => __( 'Two thirds', 'tlig' ),
|
||||
'12' => __( 'Full width', 'tlig' ),
|
||||
],
|
||||
],
|
||||
'layout' => [
|
||||
'type' => 'select',
|
||||
'label' => __( 'Image position', 'tlig' ),
|
||||
'options' => [
|
||||
'' => __( 'No image', 'tlig' ),
|
||||
'image_left' => __( 'Beside the label', 'tlig' ),
|
||||
'image_below' => __( 'Above the label', 'tlig' ),
|
||||
],
|
||||
],
|
||||
'image' => [
|
||||
'type' => 'number',
|
||||
'label' => __( 'Image (attachment ID)', 'tlig' ),
|
||||
],
|
||||
'icon' => [
|
||||
'type' => 'select',
|
||||
'label' => __( 'Icon', 'tlig' ),
|
||||
'options' => [
|
||||
'' => __( 'None', 'tlig' ),
|
||||
'search' => __( 'Search', 'tlig' ),
|
||||
'book' => __( 'Book', 'tlig' ),
|
||||
'headphones' => __( 'Headphones', 'tlig' ),
|
||||
],
|
||||
],
|
||||
'feed' => [
|
||||
'type' => 'number',
|
||||
'label' => __( 'List children of page ID', 'tlig' ),
|
||||
'help' => __( 'Generated at render time, so new pages appear by themselves.', 'tlig' ),
|
||||
],
|
||||
'block' => [
|
||||
'type' => 'textarea',
|
||||
'label' => __( 'HTML block', 'tlig' ),
|
||||
'help' => __( 'An escape hatch. Filtered through wp_kses_post, so it cannot carry a stylesheet.', 'tlig' ),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the fields inside the menu item.
|
||||
*
|
||||
* @param int $item_id Menu item post ID.
|
||||
*/
|
||||
function tlig_nav_item_fields( $item_id ): void {
|
||||
$nav = tlig_nav_meta( (int) $item_id );
|
||||
|
||||
wp_nonce_field( 'tlig_nav_' . $item_id, 'tlig_nav_nonce_' . $item_id );
|
||||
|
||||
echo '<fieldset class="field-tlig-nav description-wide" style="margin:10px 0;padding:8px 10px;border:1px solid #dcdcde;">';
|
||||
echo '<legend style="font-weight:600;padding:0 4px;">' . esc_html__( 'TLIG navigation', 'tlig' ) . '</legend>';
|
||||
|
||||
foreach ( tlig_nav_fields() as $key => $field ) {
|
||||
$name = sprintf( 'tlig_nav[%d][%s]', $item_id, $key );
|
||||
$id = sprintf( 'tlig-nav-%d-%s', $item_id, $key );
|
||||
$value = $nav[ $key ] ?? '';
|
||||
|
||||
// `feed` is stored as an array so the walker can grow it later; the
|
||||
// screen only ever asks for the parent, which is the part an editor
|
||||
// would change.
|
||||
if ( 'feed' === $key && is_array( $value ) ) {
|
||||
$value = $value['parent'] ?? '';
|
||||
}
|
||||
|
||||
echo '<p class="description description-wide" style="margin:6px 0;">';
|
||||
|
||||
switch ( $field['type'] ) {
|
||||
case 'checkbox':
|
||||
printf(
|
||||
'<label for="%1$s"><input type="checkbox" id="%1$s" name="%2$s" value="1"%3$s> %4$s</label>',
|
||||
esc_attr( $id ),
|
||||
esc_attr( $name ),
|
||||
checked( ! empty( $value ), true, false ),
|
||||
esc_html( $field['label'] )
|
||||
);
|
||||
break;
|
||||
|
||||
case 'select':
|
||||
printf( '<label for="%s">%s<br>', esc_attr( $id ), esc_html( $field['label'] ) );
|
||||
printf( '<select id="%s" name="%s" class="widefat">', esc_attr( $id ), esc_attr( $name ) );
|
||||
foreach ( $field['options'] as $opt => $text ) {
|
||||
printf(
|
||||
'<option value="%s"%s>%s</option>',
|
||||
esc_attr( $opt ),
|
||||
selected( (string) $value, (string) $opt, false ),
|
||||
esc_html( $text )
|
||||
);
|
||||
}
|
||||
echo '</select></label>';
|
||||
break;
|
||||
|
||||
case 'textarea':
|
||||
printf(
|
||||
'<label for="%1$s">%2$s<br><textarea id="%1$s" name="%3$s" rows="3" class="widefat">%4$s</textarea></label>',
|
||||
esc_attr( $id ),
|
||||
esc_html( $field['label'] ),
|
||||
esc_attr( $name ),
|
||||
esc_textarea( (string) $value )
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
printf(
|
||||
'<label for="%1$s">%2$s<br><input type="number" id="%1$s" name="%3$s" value="%4$s" class="widefat"></label>',
|
||||
esc_attr( $id ),
|
||||
esc_html( $field['label'] ),
|
||||
esc_attr( $name ),
|
||||
esc_attr( (string) $value )
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! empty( $field['help'] ) ) {
|
||||
printf( '<span class="description" style="display:block;color:#646970;">%s</span>', esc_html( $field['help'] ) );
|
||||
}
|
||||
|
||||
echo '</p>';
|
||||
}
|
||||
|
||||
echo '</fieldset>';
|
||||
}
|
||||
add_action( 'wp_nav_menu_item_custom_fields', 'tlig_nav_item_fields', 10, 1 );
|
||||
|
||||
/**
|
||||
* Saves them.
|
||||
*
|
||||
* Stored as one array under `_tlig_nav` rather than eight meta rows: the whole
|
||||
* point is that an item's configuration is small enough to read at a glance.
|
||||
* An empty configuration deletes the row instead of writing an empty array, so
|
||||
* the 214 items that need no settings carry no meta at all.
|
||||
*
|
||||
* @param int $menu_id Menu term ID.
|
||||
* @param int $item_id Menu item post ID.
|
||||
*/
|
||||
function tlig_nav_item_save( $menu_id, $item_id ): void {
|
||||
$nonce = $_POST[ 'tlig_nav_nonce_' . $item_id ] ?? '';
|
||||
|
||||
if ( ! wp_verify_nonce( sanitize_key( $nonce ), 'tlig_nav_' . $item_id ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$raw = $_POST['tlig_nav'][ $item_id ] ?? [];
|
||||
if ( ! is_array( $raw ) ) {
|
||||
$raw = [];
|
||||
}
|
||||
|
||||
$nav = [];
|
||||
|
||||
foreach ( tlig_nav_fields() as $key => $field ) {
|
||||
$value = $raw[ $key ] ?? '';
|
||||
|
||||
switch ( $field['type'] ) {
|
||||
case 'checkbox':
|
||||
if ( ! empty( $value ) ) {
|
||||
$nav[ $key ] = 1;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'select':
|
||||
$value = (string) $value;
|
||||
if ( '' !== $value && isset( $field['options'][ $value ] ) ) {
|
||||
$nav[ $key ] = 'span' === $key ? (int) $value : $value;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'textarea':
|
||||
$value = wp_kses_post( (string) $value );
|
||||
if ( '' !== trim( $value ) ) {
|
||||
$nav[ $key ] = $value;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
$value = (int) $value;
|
||||
if ( $value > 0 ) {
|
||||
$nav[ $key ] = 'feed' === $key
|
||||
? [ 'parent' => $value, 'count' => -1, 'orderby' => 'title' ]
|
||||
: $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( $nav ) {
|
||||
update_post_meta( $item_id, '_tlig_nav', $nav );
|
||||
} else {
|
||||
delete_post_meta( $item_id, '_tlig_nav' );
|
||||
}
|
||||
}
|
||||
add_action( 'wp_update_nav_menu_item', 'tlig_nav_item_save', 10, 2 );
|
||||
@@ -0,0 +1,528 @@
|
||||
<?php
|
||||
/**
|
||||
* The navigation: one tree, rendered from the menu itself.
|
||||
*
|
||||
* What this replaces is worth stating, because it is the whole argument.
|
||||
*
|
||||
* The site it comes from runs UberMenu, whose per-item settings blob carries
|
||||
* 149 keys. Across all 15 desktop menus — 748 items — exactly eight of them are
|
||||
* ever set to something that matters:
|
||||
*
|
||||
* span 167 the column's width, stored as a fraction ("1-4", "2-3")
|
||||
* layout 165 image_left | image_below
|
||||
* image 159 a thumbnail beside the label
|
||||
* panel 96 this top-level item opens a mega panel
|
||||
* heading 94 the item is a heading, not a link
|
||||
* block 56 raw HTML dropped into the panel
|
||||
* icon 52 one of exactly three icons, site-wide
|
||||
* feed 14 list child pages of a given parent
|
||||
*
|
||||
* Everything else in the blob is the plugin's own defaults written back
|
||||
* verbatim — 667 items carry `dt_orderby: name` and `tab_layout: left` without
|
||||
* a tab or a taxonomy anywhere in the menu.
|
||||
*
|
||||
* So the eight live in one `_tlig_nav` array, and the structure comes from the
|
||||
* tree with no configuration at all: depth 0 is the bar, depth 1 a column,
|
||||
* depth 2 a link. No per-item heights, no per-item colours, no background image
|
||||
* per panel — the panel's ground is the parchment border-image, which is one
|
||||
* file at any height.
|
||||
*
|
||||
* @package TLIG
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read an item's TLIG settings.
|
||||
*
|
||||
* @param int $item_id Menu item post ID.
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
function tlig_nav_meta( int $item_id ): array {
|
||||
$meta = get_post_meta( $item_id, '_tlig_nav', true );
|
||||
|
||||
return is_array( $meta ) ? $meta : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* The three icons the entire site uses, inline.
|
||||
*
|
||||
* A whole icon font was loaded for these. Counted across all 15 menus: 24
|
||||
* `fa-search`, 14 `fa-book-open`, 14 `fa-headphones`, and nothing else. Inline
|
||||
* SVG costs no request and inherits `currentColor`, so an icon on the dark
|
||||
* ground needs no second rule.
|
||||
*
|
||||
* @param string $name Icon key.
|
||||
* @return string SVG markup, or '' for an unknown name.
|
||||
*/
|
||||
function tlig_nav_icon( string $name ): string {
|
||||
static $icons = [
|
||||
'search' => '<path d="M11 4a7 7 0 1 0 4.19 12.6l4.1 4.1 1.41-1.42-4.1-4.1A7 7 0 0 0 11 4Zm0 2a5 5 0 1 1 0 10 5 5 0 0 1 0-10Z"/>',
|
||||
'book' => '<path d="M3 5c2.5-1 5-1 8 .5v14c-3-1.5-5.5-1.5-8-.5V5Zm18 0c-2.5-1-5-1-8 .5v14c3-1.5 5.5-1.5 8-.5V5Z"/>',
|
||||
'headphones' => '<path d="M12 3a9 9 0 0 0-9 9v5a3 3 0 0 0 3 3h2v-8H5v0a7 7 0 0 1 14 0v0h-3v8h2a3 3 0 0 0 3-3v-5a9 9 0 0 0-9-9Z"/>',
|
||||
];
|
||||
|
||||
if ( ! isset( $icons[ $name ] ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'<svg class="site-nav__icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false">%s</svg>',
|
||||
$icons[ $name ]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the primary menu.
|
||||
*
|
||||
* @param string $location Registered menu location.
|
||||
*/
|
||||
function tlig_nav( string $location = 'primary' ): void {
|
||||
if ( ! has_nav_menu( $location ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The toggle is part of the menu's own markup rather than the header's, so
|
||||
// `aria-controls` can name the list it opens without the template having to
|
||||
// know the id. Below the tablet breakpoint it is the entire mobile menu.
|
||||
$toggle = sprintf(
|
||||
'<button type="button" class="site-nav__toggle" aria-expanded="false" aria-controls="%s">%s</button>',
|
||||
'tlig-menu',
|
||||
esc_html__( 'Menu', 'tlig' )
|
||||
);
|
||||
|
||||
wp_nav_menu( [
|
||||
'theme_location' => $location,
|
||||
'container' => 'nav',
|
||||
'container_class' => 'site-nav',
|
||||
'container_aria_label' => __( 'Primary navigation', 'tlig' ),
|
||||
'menu_id' => 'tlig-menu',
|
||||
'menu_class' => 'site-nav__bar',
|
||||
'items_wrap' => $toggle . '<ul id="%1$s" class="%2$s">%3$s</ul>',
|
||||
'depth' => 3,
|
||||
'walker' => new TLIG_Nav_Walker(),
|
||||
'fallback_cb' => false,
|
||||
] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns a three-level menu into a bar of mega panels.
|
||||
*
|
||||
* `Walker_Nav_Menu` hands `start_lvl()` a depth but not the item that owns the
|
||||
* level, so the current item is stashed in `start_el()` — the panel wrapper has
|
||||
* to know whose panel it is in order to give it an id for `aria-controls`.
|
||||
*/
|
||||
class TLIG_Nav_Walker extends Walker_Nav_Menu {
|
||||
|
||||
/** @var WP_Post|null The item most recently opened, for start_lvl(). */
|
||||
private $current;
|
||||
|
||||
/** @var array<int,array<string,mixed>> Settings of the open ancestors, by depth. */
|
||||
private $stack = [];
|
||||
|
||||
/** @var array<int,int> How many children each item has, by item ID. */
|
||||
private $siblings = [];
|
||||
|
||||
/**
|
||||
* Counts each item's children before walking.
|
||||
*
|
||||
* A column with no width of its own should share the row with its
|
||||
* neighbours, which means knowing how many there are. UberMenu stored that
|
||||
* as `columns: auto` and worked it out at render time; the walker is handed
|
||||
* the whole tree here, so it can do the same.
|
||||
*
|
||||
* @param array<int,WP_Post> $elements All menu items.
|
||||
* @param int $max_depth Depth limit.
|
||||
* @param mixed ...$args Passed through.
|
||||
* @return string
|
||||
*/
|
||||
public function walk( $elements, $max_depth, ...$args ) {
|
||||
$this->siblings = [];
|
||||
|
||||
foreach ( (array) $elements as $element ) {
|
||||
$parent = (int) ( $element->menu_item_parent ?? 0 );
|
||||
$this->siblings[ $parent ] = ( $this->siblings[ $parent ] ?? 0 ) + 1;
|
||||
}
|
||||
|
||||
return parent::walk( $elements, $max_depth, ...$args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a submenu level.
|
||||
*
|
||||
* Depth 0 → the panel and its grid. Depth 1 → the list of links in a column.
|
||||
*
|
||||
* @param string $output Accumulated markup, by reference.
|
||||
* @param int $depth Depth of the *parent* item.
|
||||
* @param stdClass $args wp_nav_menu arguments.
|
||||
*/
|
||||
public function start_lvl( &$output, $depth = 0, $args = null ) {
|
||||
$indent = str_repeat( "\t", $depth );
|
||||
|
||||
if ( 0 === $depth ) {
|
||||
$id = $this->current ? 'tlig-panel-' . (int) $this->current->ID : '';
|
||||
|
||||
$output .= sprintf(
|
||||
"\n%s<div class=\"site-nav__panel\" id=\"%s\">\n",
|
||||
$indent,
|
||||
esc_attr( $id )
|
||||
);
|
||||
|
||||
// The panel repeats its own item as a link, and the stylesheet shows
|
||||
// it only in the stacked layout. There, a tap on the bar item opens
|
||||
// the panel instead of following the link — which is what a phone
|
||||
// user expects, and what the old site got wrong in the opposite
|
||||
// direction: with hover the only way in, its top-level pages were
|
||||
// unreachable on a phone. This is the way back to that page.
|
||||
if ( $this->current && '' !== trim( (string) $this->current->url ) && '#' !== $this->current->url ) {
|
||||
$output .= sprintf(
|
||||
"%s\t<a class=\"site-nav__self\" href=\"%s\">%s</a>\n",
|
||||
$indent,
|
||||
esc_url( (string) $this->current->url ),
|
||||
esc_html( trim( wp_strip_all_tags( $this->current->title ) ) )
|
||||
);
|
||||
}
|
||||
|
||||
$output .= sprintf( "%s\t<ul class=\"site-nav__grid\">\n", $indent );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$output .= sprintf( "\n%s<ul class=\"site-nav__links\">\n", $indent );
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes a submenu level.
|
||||
*
|
||||
* @param string $output Accumulated markup, by reference.
|
||||
* @param int $depth Depth of the parent item.
|
||||
* @param stdClass $args wp_nav_menu arguments.
|
||||
*/
|
||||
public function end_lvl( &$output, $depth = 0, $args = null ) {
|
||||
$indent = str_repeat( "\t", $depth );
|
||||
|
||||
$output .= 0 === $depth
|
||||
? sprintf( "%s\t</ul>\n%s</div>\n", $indent, $indent )
|
||||
: sprintf( "%s</ul>\n", $indent );
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens an item.
|
||||
*
|
||||
* @param string $output Accumulated markup, by reference.
|
||||
* @param WP_Post $item Menu item.
|
||||
* @param int $depth Depth of the item.
|
||||
* @param stdClass $args wp_nav_menu arguments.
|
||||
* @param int $current_object_id Unused.
|
||||
*/
|
||||
public function start_el( &$output, $item, $depth = 0, $args = null, $current_object_id = 0 ) {
|
||||
$this->current = $item;
|
||||
|
||||
$nav = tlig_nav_meta( (int) $item->ID );
|
||||
$this->stack[ $depth ] = $nav;
|
||||
|
||||
$has_children = in_array( 'menu-item-has-children', (array) $item->classes, true );
|
||||
$indent = str_repeat( "\t", $depth + 1 );
|
||||
|
||||
switch ( $depth ) {
|
||||
case 0:
|
||||
$output .= $indent . $this->bar_item( $item, $nav, $has_children );
|
||||
break;
|
||||
|
||||
case 1:
|
||||
$output .= $indent . $this->column( $item, $nav, $has_children );
|
||||
break;
|
||||
|
||||
default:
|
||||
// A leaf can carry a block or a feed too — the English
|
||||
// TESTIMONIES panel is two of them, and dropping them here is
|
||||
// how they went missing the first time.
|
||||
$output .= $indent . '<li class="site-nav__leaf">'
|
||||
. $this->link( $item, $nav, 'site-nav__link' )
|
||||
. $this->block( $nav )
|
||||
. $this->feed( $nav );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes an item.
|
||||
*
|
||||
* @param string $output Accumulated markup, by reference.
|
||||
* @param WP_Post $item Menu item.
|
||||
* @param int $depth Depth of the item.
|
||||
* @param stdClass $args wp_nav_menu arguments.
|
||||
*/
|
||||
public function end_el( &$output, $item, $depth = 0, $args = null ) {
|
||||
$output .= "</li>\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* A top-level item: the thing you see in the bar.
|
||||
*
|
||||
* @param WP_Post $item Menu item.
|
||||
* @param array<string,mixed> $nav Its TLIG settings.
|
||||
* @param bool $has_children Whether it opens a panel.
|
||||
* @return string
|
||||
*/
|
||||
private function bar_item( WP_Post $item, array $nav, bool $has_children ): string {
|
||||
$panel = $has_children && ! empty( $nav['panel'] );
|
||||
|
||||
$classes = [ 'site-nav__item' ];
|
||||
if ( $panel ) {
|
||||
$classes[] = 'site-nav__item--panel';
|
||||
}
|
||||
if ( in_array( 'current-menu-item', (array) $item->classes, true ) ) {
|
||||
$classes[] = 'is-current';
|
||||
}
|
||||
|
||||
$attrs = '';
|
||||
if ( $panel ) {
|
||||
// The link still goes where it always went; the button beside it is
|
||||
// what opens the panel. A single element cannot both navigate and
|
||||
// disclose, and hover alone leaves touch and keyboard out.
|
||||
$attrs = sprintf(
|
||||
' aria-expanded="false" aria-controls="%s"',
|
||||
esc_attr( 'tlig-panel-' . (int) $item->ID )
|
||||
);
|
||||
}
|
||||
|
||||
$out = sprintf( '<li class="%s">', esc_attr( implode( ' ', $classes ) ) );
|
||||
$out .= $this->link( $item, $nav, 'site-nav__top' );
|
||||
|
||||
if ( $panel ) {
|
||||
$out .= sprintf(
|
||||
'<button type="button" class="site-nav__disclose"%s><span class="visually-hidden">%s</span></button>',
|
||||
$attrs,
|
||||
/* translators: %s: menu item label. */
|
||||
esc_html( sprintf( __( 'Open the %s menu', 'tlig' ), wp_strip_all_tags( $item->title ) ) )
|
||||
);
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* A column inside a panel.
|
||||
*
|
||||
* The width is a grid span, not a pixel height: `--tlig-span` feeds
|
||||
* `grid-column: span var(--tlig-span)`. UberMenu stored it as a fraction of
|
||||
* the row, which is the same thing said less directly.
|
||||
*
|
||||
* @param WP_Post $item Menu item.
|
||||
* @param array<string,mixed> $nav Its TLIG settings.
|
||||
* @param bool $has_children Whether it holds links.
|
||||
* @return string
|
||||
*/
|
||||
private function column( WP_Post $item, array $nav, bool $has_children ): string {
|
||||
$span = isset( $nav['span'] ) ? max( 1, min( 12, (int) $nav['span'] ) ) : 0;
|
||||
|
||||
// No width of its own: divide the row by however many columns this
|
||||
// panel has. Without this every unsized column fell back to a quarter,
|
||||
// which is right for a panel of four and wrong for the 28 panels —
|
||||
// UNITY in DIVERSITY, MISSION, SEARCH, across the languages — that
|
||||
// hold a single column. Those were rendering a 1200px banner into a
|
||||
// quarter of the width.
|
||||
if ( ! $span ) {
|
||||
$count = max( 1, (int) ( $this->siblings[ (int) $item->menu_item_parent ] ?? 1 ) );
|
||||
$span = max( 1, min( 12, (int) round( 12 / $count ) ) );
|
||||
}
|
||||
|
||||
$style = sprintf( ' style="--tlig-span:%d"', $span );
|
||||
|
||||
$out = sprintf( '<li class="site-nav__col"%s>', $style );
|
||||
|
||||
// A heading is an item with its link switched off. It was a link with
|
||||
// `disable_link: on` before; here it is simply not an anchor, which is
|
||||
// also what a screen reader needs to hear.
|
||||
$title = trim( wp_strip_all_tags( $item->title ) );
|
||||
|
||||
$linked = false;
|
||||
|
||||
if ( ! empty( $nav['heading'] ) && '' !== $title ) {
|
||||
$out .= sprintf( '<h3 class="site-nav__heading">%s</h3>', esc_html( $title ) );
|
||||
} elseif ( ! $has_children && '' !== $title ) {
|
||||
$out .= $this->link( $item, $nav, 'site-nav__link' );
|
||||
$linked = true;
|
||||
}
|
||||
|
||||
// `link()` already emits the thumbnail, so only a column that did not
|
||||
// render one needs it here — otherwise a childless column with an image
|
||||
// shows it twice.
|
||||
if ( ! $linked ) {
|
||||
$out .= $this->figure( $nav );
|
||||
}
|
||||
$out .= $this->block( $nav );
|
||||
$out .= $this->feed( $nav );
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* An anchor, with its optional thumbnail and icon.
|
||||
*
|
||||
* @param WP_Post $item Menu item.
|
||||
* @param array<string,mixed> $nav Its TLIG settings.
|
||||
* @param string $class Class for the anchor.
|
||||
* @return string
|
||||
*/
|
||||
private function link( WP_Post $item, array $nav, string $class ): string {
|
||||
$title = trim( wp_strip_all_tags( $item->title ) );
|
||||
$inner = $this->figure( $nav );
|
||||
|
||||
if ( ! empty( $nav['icon'] ) ) {
|
||||
$inner .= tlig_nav_icon( (string) $nav['icon'] );
|
||||
}
|
||||
|
||||
if ( '' !== $title ) {
|
||||
$inner .= sprintf( '<span class="site-nav__label">%s</span>', esc_html( $title ) );
|
||||
}
|
||||
|
||||
if ( '' === $inner ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// An item with its link switched off still has something to show — the
|
||||
// English menu's fourth column is a single picture with `disable_link`
|
||||
// set, and wrapping it in an anchor to nowhere would be a lie to anyone
|
||||
// tabbing through. It renders as the content without the anchor.
|
||||
// No destination either: an item can lose its link because the editor
|
||||
// switched it off, or because it never had one — 137 of the 748 items
|
||||
// point at an `#ubermenu-column` placeholder. Both are content without
|
||||
// a link, and neither should be an anchor to nowhere.
|
||||
if ( ! empty( $nav['heading'] ) || '' === trim( (string) $item->url ) || '#' === $item->url ) {
|
||||
$static = $class . ' is-static';
|
||||
|
||||
if ( '' === $title && ! empty( $nav['image'] ) ) {
|
||||
$static .= ' ' . $class . '--figure';
|
||||
}
|
||||
|
||||
return sprintf( '<span class="%s">%s</span>', esc_attr( $static ), $inner );
|
||||
}
|
||||
|
||||
$classes = [ $class ];
|
||||
if ( ! empty( $nav['layout'] ) ) {
|
||||
$classes[] = $class . '--' . str_replace( '_', '-', (string) $nav['layout'] );
|
||||
}
|
||||
|
||||
// A picture with nothing to label is the column's content, not a
|
||||
// thumbnail beside a word, and the stylesheet sizes the two differently.
|
||||
if ( '' === $title && ! empty( $nav['image'] ) ) {
|
||||
$classes[] = $class . '--figure';
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'<a class="%s" href="%s"%s>%s</a>',
|
||||
esc_attr( implode( ' ', $classes ) ),
|
||||
esc_url( (string) $item->url ),
|
||||
$item->target ? sprintf( ' target="%s" rel="noopener"', esc_attr( $item->target ) ) : '',
|
||||
$inner
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The item's thumbnail, if it has one.
|
||||
*
|
||||
* @param array<string,mixed> $nav Item settings.
|
||||
* @return string
|
||||
*/
|
||||
private function figure( array $nav ): string {
|
||||
if ( empty( $nav['image'] ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$id = (int) $nav['image'];
|
||||
if ( ! $id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// `large`, not `medium`. Four of these are a column's whole content, and
|
||||
// two of them are 1200 and 1380 pixels wide in the library — asking for
|
||||
// `medium` handed the browser a 300px copy to stretch across a 730px
|
||||
// column. `large` carries a srcset, so a 56px thumbnail still costs a
|
||||
// small file.
|
||||
return wp_get_attachment_image(
|
||||
$id,
|
||||
'large',
|
||||
false,
|
||||
[
|
||||
'class' => 'site-nav__thumb',
|
||||
'loading' => 'lazy',
|
||||
'alt' => '',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Raw HTML attached to the item.
|
||||
*
|
||||
* 56 items carry one. They are not arbitrary — across every language only
|
||||
* five tags appear in them, and 14 of the 56 are the search form. They are
|
||||
* kept as an escape hatch, not as a design tool, and the content is filtered
|
||||
* so a menu cannot inject a stylesheet the way the Danish one does today.
|
||||
*
|
||||
* @param array<string,mixed> $nav Item settings.
|
||||
* @return string
|
||||
*/
|
||||
private function block( array $nav ): string {
|
||||
if ( empty( $nav['block'] ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$html = (string) $nav['block'];
|
||||
|
||||
if ( false !== strpos( $html, '[search-form]' ) ) {
|
||||
return '<div class="site-nav__block">' . get_search_form( [ 'echo' => false ] ) . '</div>';
|
||||
}
|
||||
|
||||
return '<div class="site-nav__block">' . wp_kses_post( do_shortcode( $html ) ) . '</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of child pages, generated rather than hand-maintained.
|
||||
*
|
||||
* This is the one setting that makes the menu content-driven in the literal
|
||||
* sense: publish a page under the named parent and it appears, in every
|
||||
* language, without anyone editing a menu.
|
||||
*
|
||||
* @param array<string,mixed> $nav Item settings.
|
||||
* @return string
|
||||
*/
|
||||
private function feed( array $nav ): string {
|
||||
if ( empty( $nav['feed'] ) || ! is_array( $nav['feed'] ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$feed = $nav['feed'];
|
||||
$parent = isset( $feed['parent'] ) ? (int) $feed['parent'] : 0;
|
||||
|
||||
if ( ! $parent ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$children = get_posts( [
|
||||
'post_type' => 'page',
|
||||
'post_parent' => $parent,
|
||||
'posts_per_page' => isset( $feed['count'] ) ? (int) $feed['count'] : -1,
|
||||
'orderby' => isset( $feed['orderby'] ) ? (string) $feed['orderby'] : 'title',
|
||||
'order' => 'ASC',
|
||||
'suppress_filters' => false,
|
||||
] );
|
||||
|
||||
if ( ! $children ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$out = '<ul class="site-nav__links site-nav__links--feed">';
|
||||
foreach ( $children as $child ) {
|
||||
$out .= sprintf(
|
||||
'<li class="site-nav__leaf"><a class="site-nav__link" href="%s"><span class="site-nav__label">%s</span></a></li>',
|
||||
esc_url( (string) get_permalink( $child ) ),
|
||||
esc_html( get_the_title( $child ) )
|
||||
);
|
||||
}
|
||||
|
||||
return $out . '</ul>';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
/**
|
||||
* Fallback template.
|
||||
*
|
||||
* @package TLIG
|
||||
*/
|
||||
|
||||
get_header();
|
||||
?>
|
||||
<div class="scroll">
|
||||
<div class="scroll__head"></div>
|
||||
<div class="scroll__body">
|
||||
<?php
|
||||
while ( have_posts() ) :
|
||||
the_post();
|
||||
?>
|
||||
<article <?php post_class(); ?>>
|
||||
<h1><?php the_title(); ?></h1>
|
||||
<div class="prose"><?php the_content(); ?></div>
|
||||
</article>
|
||||
<?php
|
||||
endwhile;
|
||||
?>
|
||||
</div>
|
||||
<div class="scroll__foot"></div>
|
||||
</div>
|
||||
<?php
|
||||
get_footer();
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
Theme Name: TLIG
|
||||
Theme URI: https://ww3.tlig.org
|
||||
Description: True Life in God — a standalone theme. No Avada, no menu plugins.
|
||||
The stylesheet is built from SASS in this project's src/ and emitted
|
||||
to assets/tlig.css; this file exists only to declare the theme to
|
||||
WordPress and is deliberately empty of rules.
|
||||
Version: 0.1.0
|
||||
Requires at least: 6.0
|
||||
Requires PHP: 8.1
|
||||
Text Domain: tlig
|
||||
*/
|
||||
Reference in New Issue
Block a user