<?php
/**
 * Mobile bottom menu settings schema.
 *
 * See the wording note in the mobile menu schema: labels are written for
 * someone describing what they want, not for someone who already knows the
 * CSS term for it. `ui.advanced` collapses a property into "Fine tuning", and
 * `ui.showIf` keeps a setting out of sight until it can do anything.
 *
 * @package MenuVerse
 */

defined( 'ABSPATH' ) || exit;

/**
 * Ready made looks, loaded only where they can be shown.
 *
 * This schema is rebuilt on every request, so anything declared here is
 * translated on every request too. The presets are several hundred strings the
 * frontend never reads, and `ui` is invisible to the sanitizer, the validator
 * and the REST arguments alike — so leaving them out of a page view changes
 * nothing except the work done.
 */
$menuverse_bottom_presets = ( is_admin() || wp_is_json_request() )
	? require __DIR__ . '/presets.php'
	: array();

return array(
	'type'       => 'object',
	'properties' => array(
		'items'                   => array(
			'type'     => 'array',
			'default'  => array(),
			'minItems' => 0,
			// The bar itself is only usable with a handful of items, and the
			// dashboard says so. This higher ceiling is deliberate: settings
			// are sanitized on every read, so a lower number here would throw
			// away items an existing site had already saved.
			'maxItems' => 12,
			'items'    => array(
				'type'       => 'object',
				'properties' => array(
					'icon_source'            => array(
						'type'    => 'string',
						'default' => 'library',
						'enum'    => array( 'library', 'image' ),
						'ui'      => array(
							'control' => 'segmented',
							'label'   => __( 'Which kind of icon', 'menuverse' ),
							'options' => array(
								'library' => __( 'Built in', 'menuverse' ),
								'image'   => __( 'My own image', 'menuverse' ),
							),
						),
					),
					'icon'                   => array(
						'type'    => 'string',
						'format'  => 'icon',
						'default' => 'home',
						'ui'      => array(
							'label'  => __( 'Icon', 'menuverse' ),
							'showIf' => array( 'icon_source' => 'library' ),
						),
					),

					/*
					 * A second built in icon, used only while this item points
					 * at the page being viewed. Empty means the one above is
					 * kept and only its colour changes, which is what every
					 * bar did before this existed.
					 */
					'active_icon'            => array(
						'type'    => 'string',
						'format'  => 'icon',
						'default' => '',
						'ui'      => array(
							'advanced' => true,
							'label'    => __( 'Icon when this is the current page', 'menuverse' ),
							'help'     => __( 'Optional. A filled version of the same icon reads well here.', 'menuverse' ),
							'showIf'   => array( 'icon_source' => 'library' ),
						),
					),

					/*
					 * Image icons are addressed by URL rather than through the
					 * media modal on purpose: `wp_enqueue_media()` costs this
					 * screen the whole Backbone media stack, which the
					 * dashboard drops deliberately (see Admin_Page). Anything
					 * already in the library still has an address to paste.
					 */
					'icon_image'             => array(
						'type'    => 'string',
						'format'  => 'url',
						'default' => '',
						'ui'      => array(
							'label'    => __( 'Image address', 'menuverse' ),
							'help'     => __( 'Paste the address of an image already uploaded to this site. A square PNG or SVG works best.', 'menuverse' ),
							'required' => true,
							'showIf'   => array( 'icon_source' => 'image' ),
						),
					),
					'icon_image_active'      => array(
						'type'    => 'string',
						'format'  => 'url',
						'default' => '',
						'ui'      => array(
							'advanced' => true,
							'label'    => __( 'Image when this is the current page', 'menuverse' ),
							'help'     => __( 'Optional. Leave empty to keep the same image.', 'menuverse' ),
							'showIf'   => array( 'icon_source' => 'image' ),
						),
					),
					'icon_image_width'       => array(
						'type'    => 'integer',
						'default' => 0,
						'minimum' => 0,
						'maximum' => 64,
						'ui'      => array(
							'control'  => 'range',
							'advanced' => true,
							'unit'     => 'px',
							'label'    => __( 'Image size', 'menuverse' ),
							'help'     => __( 'Leave at zero to match the icon size set for the whole bar.', 'menuverse' ),
							'showIf'   => array( 'icon_source' => 'image' ),
						),
					),
					'label'                  => array(
						'type'      => 'string',
						'default'   => '',
						'maxLength' => 24,
						'ui'        => array(
							'label'       => __( 'Wording underneath', 'menuverse' ),
							'placeholder' => __( 'Home', 'menuverse' ),
						),
					),
					'show_label'             => array(
						'type'    => 'boolean',
						'default' => true,
						'ui'      => array(
							'label' => __( 'Show the wording for this one', 'menuverse' ),
							'help'  => __( 'Hidden wording is still read out by screen readers.', 'menuverse' ),
						),
					),
					'label_position'         => array(
						'type'    => 'string',
						'default' => 'bottom',
						'enum'    => array( 'top', 'bottom' ),
						'ui'      => array(
							'label'   => __( 'Where the wording sits', 'menuverse' ),
							'showIf'  => array( 'show_label' => true ),
							'options' => array(
								'top'    => __( 'Above the icon', 'menuverse' ),
								'bottom' => __( 'Below the icon', 'menuverse' ),
							),
						),
					),
					'link_type'              => array(
						'type'    => 'string',
						'default' => 'url',
						'enum'    => array( 'url', 'page', 'search', 'whatsapp', 'call', 'popup', 'panel' ),
						'ui'      => array(
							'control' => 'select',
							'label'   => __( 'What this button does', 'menuverse' ),
							'options' => array(
								'url'      => __( 'Go to a web address', 'menuverse' ),
								'page'     => __( 'Go to a page on this site', 'menuverse' ),
								'search'   => __( 'Open the search box', 'menuverse' ),
								'whatsapp' => __( 'Start a WhatsApp chat', 'menuverse' ),
								'call'     => __( 'Start a phone call', 'menuverse' ),
								'popup'    => __( 'Open a popup', 'menuverse' ),
								'panel'    => __( 'Slide open a panel', 'menuverse' ),
							),
						),
					),

					/*
					 * A drop up panel belonging to one button. Shortcodes are
					 * run, which is what lets a form, a product list or a
					 * builder's own block reach the bar without this module
					 * knowing anything about them.
					 */
					'panel_content'          => array(
						'type'    => 'string',
						'format'  => 'html',
						'default' => '',
						'ui'      => array(
							'control'     => 'textarea',
							'label'       => __( 'What is inside the panel', 'menuverse' ),
							'help'        => __( 'Wording, links or a shortcode. Shortcodes are run when the panel is drawn.', 'menuverse' ),
							'placeholder' => '[contact-form-7 id="12"]',
							'required'    => true,
							'showIf'      => array( 'link_type' => 'panel' ),
						),
					),
					'url'                    => array(
						'type'    => 'string',
						'format'  => 'url',
						'default' => '',
						'ui'      => array(
							'label'       => __( 'Where it goes', 'menuverse' ),
							'help'        => __( 'A full web address, or a path such as /contact.', 'menuverse' ),
							'placeholder' => '/contact',
							'required'    => true,
							'showIf'      => array( 'link_type' => 'url' ),
						),
					),
					'page_id'                => array(
						'type'    => 'integer',
						'default' => 0,
						'minimum' => 0,
						'ui'      => array(
							'control'  => 'post-picker',
							'postType' => 'page',
							'label'    => __( 'Which page', 'menuverse' ),
							'required' => true,
							'showIf'   => array( 'link_type' => 'page' ),
						),
					),
					'popup_id'               => array(
						'type'    => 'integer',
						'default' => 0,
						'minimum' => 0,
						'ui'      => array(
							'control'  => 'post-picker',
							'postType' => 'popup',
							'requires' => 'popup_maker',
							'label'    => __( 'Which popup', 'menuverse' ),
							'help'     => __( 'The popup’s own display rules still apply, so it will only open on pages it is allowed on.', 'menuverse' ),
							'required' => true,
							'showIf'   => array( 'link_type' => 'popup' ),
						),
					),
					'whatsapp_number'        => array(
						'type'    => 'string',
						'format'  => 'phone',
						'default' => '',
						'ui'      => array(
							'control'  => 'phone',
							'label'    => __( 'WhatsApp number', 'menuverse' ),
							'help'     => __( 'Pick the country, then type the rest of the number.', 'menuverse' ),
							'required' => true,
							'showIf'   => array( 'link_type' => 'whatsapp' ),
						),
					),
					'whatsapp_message'       => array(
						'type'      => 'string',
						'default'   => '',
						'maxLength' => 200,
						'ui'        => array(
							'control'     => 'textarea',
							'label'       => __( 'Message to start with', 'menuverse' ),
							'help'        => __( 'Optional. This is typed into the chat for them, ready to send.', 'menuverse' ),
							'placeholder' => __( 'Hello, I have a question about…', 'menuverse' ),
							'showIf'      => array( 'link_type' => 'whatsapp' ),
						),
					),
					'call_number'            => array(
						'type'    => 'string',
						'format'  => 'phone',
						'default' => '',
						'ui'      => array(
							'control'  => 'phone',
							'label'    => __( 'Phone number', 'menuverse' ),
							'required' => true,
							'showIf'   => array( 'link_type' => 'call' ),
						),
					),
					// Which store a search button looks in. One field rather
					// than a scope plus a provider, because the two only ever
					// vary together: naming the shop *is* the scope. A shop
					// that is not installed falls back to searching the whole
					// site at render time, so switching plugins never leaves
					// a button that finds nothing.
					'search_scope'           => array(
						'type'    => 'string',
						'default' => 'site',
						'enum'    => array( 'site', 'woocommerce', 'edd' ),
						'ui'      => array(
							'control' => 'select',
							'label'   => __( 'What it searches', 'menuverse' ),
							'showIf'  => array( 'link_type' => 'search' ),
							'options' => array(
								'site'        => __( 'Everything on the site', 'menuverse' ),
								'woocommerce' => __( 'WooCommerce products only', 'menuverse' ),
								'edd'         => __( 'Easy Digital Downloads only', 'menuverse' ),
							),
						),
					),
					'search_placeholder'     => array(
						'type'      => 'string',
						'default'   => '',
						'maxLength' => 40,
						'ui'        => array(
							'label'       => __( 'Wording inside the search box', 'menuverse' ),
							'help'        => __( 'Optional. Leave empty for “Search…”.', 'menuverse' ),
							'placeholder' => __( 'What are you looking for?', 'menuverse' ),
							'showIf'      => array( 'link_type' => 'search' ),
						),
					),
					'target'                 => array(
						'type'    => 'string',
						'default' => '',
						'enum'    => array( '', '_blank' ),
						'ui'      => array(
							'control'  => 'toggle',
							'advanced' => true,
							'label'    => __( 'Open in a new tab', 'menuverse' ),
							'on'       => '_blank',
							'off'      => '',
							'showIf'   => array( 'link_type' => array( 'url', 'page' ) ),
						),
					),
					'badge'                  => array(
						'type'      => 'string',
						'default'   => '',
						'maxLength' => 6,
						'ui'        => array(
							'advanced' => true,
							'label'    => __( 'Little number or dot', 'menuverse' ),
							'help'     => __( 'Optional, and fixed. For a live count, use the cart or wishlist option below instead.', 'menuverse' ),
						),
					),

					/*
					 * Four optional colours for one item on its own. Empty is
					 * the inherit signal throughout, matching every other
					 * "leave it empty" colour in this schema — a per item
					 * "use the bar's colour" switch alongside the colour would
					 * be a second way of saying the same thing, and the two
					 * can disagree.
					 *
					 * These have no dark mode twin on purpose. A colour picked
					 * for one button is picked against that button's own
					 * meaning — a red "Sale", a brand green — which is the same
					 * intent in either scheme, and four more pickers per row
					 * would cost more than it returns.
					 */
					'icon_color'             => array(
						'type'    => 'string',
						'format'  => 'color',
						'default' => '',
						'ui'      => array(
							'advanced' => true,
							'label'    => __( 'Icon colour for this one', 'menuverse' ),
							'help'     => __( 'Leave empty to use the colour set for the whole bar.', 'menuverse' ),
						),
					),
					'icon_active_color'      => array(
						'type'    => 'string',
						'format'  => 'color',
						'default' => '',
						'ui'      => array(
							'advanced' => true,
							'label'    => __( 'Icon colour when this is the current page', 'menuverse' ),
							'help'     => __( 'Leave empty to use the colour set for the whole bar.', 'menuverse' ),
						),
					),
					'text_color'             => array(
						'type'    => 'string',
						'format'  => 'color',
						'default' => '',
						'ui'      => array(
							'advanced' => true,
							'label'    => __( 'Wording colour for this one', 'menuverse' ),
							'help'     => __( 'Leave empty to use the colour set for the whole bar.', 'menuverse' ),
							'showIf'   => array( 'show_label' => true ),
						),
					),
					'text_active_color'      => array(
						'type'    => 'string',
						'format'  => 'color',
						'default' => '',
						'ui'      => array(
							'advanced' => true,
							'label'    => __( 'Wording colour when this is the current page', 'menuverse' ),
							'help'     => __( 'Leave empty to use the colour set for the whole bar.', 'menuverse' ),
							'showIf'   => array( 'show_label' => true ),
						),
					),
					'icon_bg_enabled'        => array(
						'type'    => 'boolean',
						'default' => false,
						'ui'      => array(
							'advanced' => true,
							'label'    => __( 'Circle behind the icon', 'menuverse' ),
							'help'     => __( 'Makes one button stand out. The icon turns white inside it.', 'menuverse' ),
						),
					),
					'icon_bg_type'           => array(
						'type'    => 'string',
						'default' => 'color',
						'enum'    => array( 'color', 'gradient' ),
						'ui'      => array(
							'advanced' => true,
							'label'    => __( 'Circle fill', 'menuverse' ),
							'showIf'   => array( 'icon_bg_enabled' => true ),
							'options'  => array(
								'color'    => __( 'One colour', 'menuverse' ),
								'gradient' => __( 'Blend of two', 'menuverse' ),
							),
						),
					),
					'icon_bg_color'          => array(
						'type'    => 'string',
						'format'  => 'color',
						'default' => '#d462ff',
						'ui'      => array(
							'advanced' => true,
							'label'    => __( 'Circle colour', 'menuverse' ),
							'showIf'   => array(
								'icon_bg_enabled' => true,
								'icon_bg_type'    => 'color',
							),
						),
					),
					'icon_bg_gradient_start' => array(
						'type'    => 'string',
						'format'  => 'color',
						'default' => '#16e2f5',
						'ui'      => array(
							'advanced' => true,
							'label'    => __( 'Blend from', 'menuverse' ),
							'showIf'   => array(
								'icon_bg_enabled' => true,
								'icon_bg_type'    => 'gradient',
							),
						),
					),
					'icon_bg_gradient_end'   => array(
						'type'    => 'string',
						'format'  => 'color',
						'default' => '#d462ff',
						'ui'      => array(
							'advanced' => true,
							'label'    => __( 'Blend to', 'menuverse' ),
							'showIf'   => array(
								'icon_bg_enabled' => true,
								'icon_bg_type'    => 'gradient',
							),
						),
					),
					'show_cart_count'        => array(
						'type'    => 'boolean',
						'default' => false,
						'ui'      => array(
							'advanced' => true,
							'requires' => 'woocommerce',
							'label'    => __( 'Show how many things are in the cart', 'menuverse' ),
						),
					),
					'show_wishlist_count'    => array(
						'type'    => 'boolean',
						'default' => false,
						'ui'      => array(
							'advanced' => true,
							'requires' => 'yith_wishlist',
							'label'    => __( 'Show how many things are on the wishlist', 'menuverse' ),
						),
					),

					/*
					 * Who this one button is for. The bar as a whole already
					 * has display rules; this is the same decision one item at
					 * a time — a "My account" button for signed in visitors, a
					 * "Sign in" button for everybody else.
					 */
					'visibility'             => array(
						'type'    => 'string',
						'default' => 'everyone',
						'enum'    => array( 'everyone', 'logged_in', 'logged_out', 'roles' ),
						'ui'      => array(
							'advanced' => true,
							'control'  => 'select',
							'label'    => __( 'Who can see this one', 'menuverse' ),
							'options'  => array(
								'everyone'   => __( 'Everybody', 'menuverse' ),
								'logged_in'  => __( 'Only people signed in', 'menuverse' ),
								'logged_out' => __( 'Only people signed out', 'menuverse' ),
								'roles'      => __( 'Only certain kinds of user', 'menuverse' ),
							),
						),
					),
					'visibility_roles'       => array(
						'type'    => 'array',
						'default' => array(),
						'items'   => array( 'type' => 'string' ),
						'ui'      => array(
							'advanced' => true,
							'control'  => 'roles',
							'label'    => __( 'Which kinds of user', 'menuverse' ),
							'help'     => __( 'Leave empty and everybody signed in sees it.', 'menuverse' ),
							'showIf'   => array( 'visibility' => 'roles' ),
						),
					),
				),
			),
			'ui'       => array(
				'control'        => 'repeater',
				'group'          => 'items',
				'label'          => __( 'What goes in the bar', 'menuverse' ),
				'help'           => __( 'Between two and six. Three to five read best on a phone, and anything past what fits moves into a “More” drawer.', 'menuverse' ),
				'addLabel'       => __( 'Add an item', 'menuverse' ),
				'titleKey'       => 'label',
				'iconKey'        => 'icon',
				'minItems'       => 2,
				'maxRecommended' => 6,
				// Names the setting that decides how many of these rows reach
				// the bar itself, so the dashboard can mark the ones that end
				// up behind the “More” button instead of leaving someone to
				// count the list against a slider two sections down.
				'overflowKey'    => 'max_visible',
			),
		),
		// What happens to the items past `max_visible`. The drawer is the
		// default because it keeps every item the same size and reachable with
		// one tap; sideways scrolling suits a bar built as a strip of many
		// small shortcuts, where the row itself is the affordance.
		'overflow_mode'           => array(
			'type'    => 'string',
			'default' => 'drawer',
			'enum'    => array( 'drawer', 'scroll' ),
			'ui'      => array(
				'control' => 'segmented',
				'group'   => 'items',
				'label'   => __( 'What happens to the extra items', 'menuverse' ),
				'options' => array(
					'drawer' => __( 'Behind a “More” button', 'menuverse' ),
					'scroll' => __( 'Scroll the bar sideways', 'menuverse' ),
				),
			),
		),
		'max_visible'             => array(
			'type'    => 'integer',
			'default' => 5,
			'minimum' => 2,
			'maximum' => 6,
			'ui'      => array(
				'control' => 'range',
				'group'   => 'items',
				'label'   => __( 'How many to show at once', 'menuverse' ),
				'help'    => __( 'The rest go into the “More” drawer.', 'menuverse' ),
				'showIf'  => array( 'overflow_mode' => 'drawer' ),
			),
		),
		'more_label'              => array(
			'type'      => 'string',
			'default'   => 'More',
			'maxLength' => 24,
			'ui'        => array(
				'control'  => 'text',
				'group'    => 'items',
				'advanced' => true,
				'label'    => __( 'Wording on the “More” button', 'menuverse' ),
				'showIf'   => array( 'overflow_mode' => 'drawer' ),
			),
		),
		'more_icon'               => array(
			'type'    => 'string',
			'format'  => 'icon',
			'default' => 'dots-horizontal',
			'ui'      => array(
				'control'  => 'icon',
				'group'    => 'items',
				'advanced' => true,
				'label'    => __( 'Icon on the “More” button', 'menuverse' ),
				'showIf'   => array( 'overflow_mode' => 'drawer' ),
			),
		),

		'auto_insert'             => array(
			'type'    => 'boolean',
			'default' => false,
			'ui'      => array(
				'control' => 'toggle',
				'group'   => 'general',
				'label'   => __( 'Show it on every page automatically', 'menuverse' ),
				'help'    => __( 'Pins the bar to the bottom of every page for you.', 'menuverse' ),
			),
		),
		'sticky'                  => array(
			'type'    => 'boolean',
			'default' => true,
			'ui'      => array(
				'control' => 'toggle',
				'group'   => 'general',
				'label'   => __( 'Keep the bar on screen while scrolling', 'menuverse' ),
				'help'    => __( 'Turn this off and the bar sits at the end of the page instead of following along.', 'menuverse' ),
			),
		),
		'scroll_hide_enabled'     => array(
			'type'    => 'boolean',
			'default' => false,
			'ui'      => array(
				'control' => 'toggle',
				'group'   => 'general',
				'label'   => __( 'Slide it away when reading down the page', 'menuverse' ),
				'help'    => __( 'The bar returns as soon as they scroll back up.', 'menuverse' ),
				'showIf'  => array( 'sticky' => true ),
			),
		),
		'breakpoint'              => array(
			'type'    => 'integer',
			'default' => 782,
			'minimum' => 320,
			'maximum' => 1600,
			'ui'      => array(
				'control' => 'range',
				'group'   => 'general',
				'unit'    => 'px',
				'label'   => __( 'Show the bar on screens narrower than', 'menuverse' ),
				'help'    => __( 'A bottom bar is a phone pattern, so it is normally hidden on desktop.', 'menuverse' ),
			),
		),
		'show_on'                 => array(
			'type'    => 'string',
			'default' => 'all',
			'enum'    => array( 'all', 'specific', 'exclude' ),
			'ui'      => array(
				'control' => 'segmented',
				'group'   => 'general',
				'label'   => __( 'Which pages it appears on', 'menuverse' ),
				'help'    => __( 'This applies to the automatic placement. A bar you place yourself, with the block or the shortcode, always shows.', 'menuverse' ),
				'options' => array(
					'all'      => __( 'Everywhere', 'menuverse' ),
					'specific' => __( 'Only some pages', 'menuverse' ),
					'exclude'  => __( 'Everywhere except', 'menuverse' ),
				),
			),
		),
		'show_on_pages'           => array(
			'type'    => 'array',
			'default' => array(),
			// Clamped at 0 rather than 1: an unrecognised or negative value
			// then lands on 0 and is dropped when the rules are read, instead
			// of being pulled up into a real page id and quietly targeting a
			// page nobody chose.
			'items'   => array(
				'type'    => 'integer',
				'minimum' => 0,
			),
			'ui'      => array(
				'control'  => 'post-picker-multi',
				'postType' => 'page',
				'group'    => 'general',
				'label'    => __( 'Show it only on these', 'menuverse' ),
				'showIf'   => array( 'show_on' => 'specific' ),
			),
		),
		'show_on_exclude_pages'   => array(
			'type'    => 'array',
			'default' => array(),
			// Clamped at 0 rather than 1: an unrecognised or negative value
			// then lands on 0 and is dropped when the rules are read, instead
			// of being pulled up into a real page id and quietly targeting a
			// page nobody chose.
			'items'   => array(
				'type'    => 'integer',
				'minimum' => 0,
			),
			'ui'      => array(
				'control'  => 'post-picker-multi',
				'postType' => 'page',
				'group'    => 'general',
				'label'    => __( 'Hide it on these', 'menuverse' ),
				'showIf'   => array( 'show_on' => 'exclude' ),
			),
		),
		'hide_for_admins'         => array(
			'type'    => 'boolean',
			'default' => false,
			'ui'      => array(
				'control' => 'toggle',
				'group'   => 'general',
				'label'   => __( 'Hide it from administrators', 'menuverse' ),
				'help'    => __( 'Handy while you are still working on the site.', 'menuverse' ),
			),
		),
		'active_match'            => array(
			'type'    => 'string',
			'default' => 'prefix',
			'enum'    => array( 'exact', 'prefix' ),
			'ui'      => array(
				'control'  => 'segmented',
				'group'    => 'general',
				'advanced' => true,
				'label'    => __( 'Highlight the item for the current page when', 'menuverse' ),
				'options'  => array(
					'exact'  => __( 'It is exactly that page', 'menuverse' ),
					'prefix' => __( 'It is that page or below it', 'menuverse' ),
				),
			),
		),
		'body_padding'            => array(
			'type'    => 'boolean',
			'default' => true,
			'ui'      => array(
				'control'  => 'toggle',
				'group'    => 'general',
				'advanced' => true,
				'label'    => __( 'Leave room at the bottom of the page', 'menuverse' ),
				'help'     => __( 'Recommended. Without it the bar sits on top of the last of your content.', 'menuverse' ),
			),
		),
		'safe_area'               => array(
			'type'    => 'boolean',
			'default' => true,
			'ui'      => array(
				'control'  => 'toggle',
				'group'    => 'general',
				'advanced' => true,
				'label'    => __( 'Sit above the home bar on newer phones', 'menuverse' ),
				'help'     => __( 'Recommended. Keeps the bar clear of the rounded corners and the swipe bar on an iPhone.', 'menuverse' ),
			),
		),

		/*
		 * Stacking order is deliberately not a per module setting. It lives
		 * once, globally, under Tools, and `--menuverse-bar-z` falls back to
		 * the token that sets. A module level copy defaulting to 0 emitted
		 * that 0 as a real inline token, which beat the global and dropped
		 * the bar behind anything else on the page.
		 */

		'bg_type'                 => array(
			'type'    => 'string',
			'default' => 'color',
			'enum'    => array( 'color', 'gradient', 'glass', 'image' ),
			'ui'      => array(
				'control' => 'segmented',
				'group'   => 'appearance',
				'label'   => __( 'Bar background', 'menuverse' ),
				'options' => array(
					'color'    => __( 'One colour', 'menuverse' ),
					'gradient' => __( 'Blend of two', 'menuverse' ),
					'glass'    => __( 'Frosted glass', 'menuverse' ),
					'image'    => __( 'A picture', 'menuverse' ),
				),
			),
		),

		/*
		 * Addressed by URL for the same reason the item image icons are: the
		 * dashboard drops the media modal on purpose, and pasting an address
		 * costs nothing next to loading it back.
		 */
		'bg_image'                => array(
			'type'    => 'string',
			'format'  => 'url',
			'default' => '',
			'ui'      => array(
				'group'    => 'appearance',
				'label'    => __( 'Picture address', 'menuverse' ),
				'help'     => __( 'The picture is scaled to cover the bar. A wide, quiet image reads best behind icons.', 'menuverse' ),
				'required' => true,
				'showIf'   => array( 'bg_type' => 'image' ),
			),
		),
		'bg_image_overlay'        => array(
			'type'    => 'string',
			'format'  => 'color-alpha',
			'default' => '',
			'ui'      => array(
				'control' => 'color',
				'group'   => 'appearance',
				'label'   => __( 'Wash over the picture', 'menuverse' ),
				'help'    => __( 'A see through colour laid over the picture so icons stay readable. Leave empty for none.', 'menuverse' ),
				'showIf'  => array( 'bg_type' => 'image' ),
			),
		),
		'bar_background'          => array(
			'type'    => 'string',
			'format'  => 'color',
			'default' => '',
			'ui'      => array(
				'control' => 'color',
				'group'   => 'appearance',
				'label'   => __( 'Background colour', 'menuverse' ),
				'help'    => __( 'Leave empty to inherit your theme’s colours.', 'menuverse' ),
				'token'   => '--menuverse-bar-background',
				'showIf'  => array( 'bg_type' => 'color' ),
			),
		),
		'bg_gradient_start'       => array(
			'type'    => 'string',
			'format'  => 'color',
			'default' => '#ffffff',
			'ui'      => array(
				'control' => 'color',
				'group'   => 'appearance',
				'label'   => __( 'Blend from', 'menuverse' ),
				'token'   => '--menuverse-bar-gradient-start',
				'showIf'  => array( 'bg_type' => 'gradient' ),
			),
		),
		'bg_gradient_end'         => array(
			'type'    => 'string',
			'format'  => 'color',
			'default' => '#f1f5f9',
			'ui'      => array(
				'control' => 'color',
				'group'   => 'appearance',
				'label'   => __( 'Blend to', 'menuverse' ),
				'token'   => '--menuverse-bar-gradient-end',
				'showIf'  => array( 'bg_type' => 'gradient' ),
			),
		),
		'glass_tint'              => array(
			'type'    => 'string',
			'format'  => 'color-alpha',
			'default' => '#0f172a80',
			'ui'      => array(
				'control' => 'color',
				'group'   => 'appearance',
				'label'   => __( 'Glass tint', 'menuverse' ),
				'help'    => __( 'The colour laid over whatever is behind the bar. Use the opacity slider to decide how much shows through.', 'menuverse' ),
				'token'   => '--menuverse-glass-tint',
				'showIf'  => array( 'bg_type' => 'glass' ),
			),
		),
		'glass_blur'              => array(
			'type'    => 'integer',
			'default' => 20,
			'minimum' => 4,
			// Heavy enough for the darkest of the frosted looks, which needs 26
			// before the page behind it stops showing through as detail. A value
			// past the ceiling is clamped rather than rejected, so a look asking
			// for more than this would quietly be stored as something else.
			'maximum' => 28,
			'ui'      => array(
				'control' => 'range',
				'group'   => 'appearance',
				'unit'    => 'px',
				'label'   => __( 'How frosted', 'menuverse' ),
				'help'    => __( 'The effect only shows while content scrolls underneath the bar.', 'menuverse' ),
				'token'   => '--menuverse-glass-blur',
				'showIf'  => array( 'bg_type' => 'glass' ),
			),
		),
		'glass_border'            => array(
			'type'    => 'boolean',
			'default' => true,
			'ui'      => array(
				'control' => 'toggle',
				'group'   => 'appearance',
				'label'   => __( 'Bright edge along the top', 'menuverse' ),
				'showIf'  => array( 'bg_type' => 'glass' ),
			),
		),
		'bar_color'               => array(
			'type'    => 'string',
			'format'  => 'color',
			'default' => '',
			'ui'      => array(
				'control' => 'color',
				'group'   => 'appearance',
				'label'   => __( 'Icon and text colour', 'menuverse' ),
				'token'   => '--menuverse-bar-color',
			),
		),
		'bar_active_color'        => array(
			'type'    => 'string',
			'format'  => 'color',
			'default' => '',
			'ui'      => array(
				'control' => 'color',
				'group'   => 'appearance',
				'label'   => __( 'Colour for the current page', 'menuverse' ),
				'help'    => __( 'Leave empty and the current page uses the same colour as the rest.', 'menuverse' ),
				'token'   => '--menuverse-bar-active-color',
			),
		),

		/*
		 * Wording follows the icon until one of these says otherwise, which is
		 * why they are tucked into fine tuning rather than sitting beside the
		 * pair above. Most bars want one colour for both — a separate wording
		 * colour is for the case where the labels need to sit back from icons
		 * that are carrying the brand.
		 */
		'label_color'             => array(
			'type'    => 'string',
			'format'  => 'color',
			'default' => '',
			'ui'      => array(
				'control'  => 'color',
				'group'    => 'appearance',
				'advanced' => true,
				'label'    => __( 'Wording colour', 'menuverse' ),
				'help'     => __( 'Leave empty and the wording matches the icons.', 'menuverse' ),
				'token'    => '--menuverse-bar-label-color',
				'showIf'   => array( 'show_labels' => true ),
			),
		),
		'label_active_color'      => array(
			'type'    => 'string',
			'format'  => 'color',
			'default' => '',
			'ui'      => array(
				'control'  => 'color',
				'group'    => 'appearance',
				'advanced' => true,
				'label'    => __( 'Wording colour for the current page', 'menuverse' ),
				'help'     => __( 'Leave empty and it matches the icon for the current page.', 'menuverse' ),
				'token'    => '--menuverse-bar-label-active-color',
				'showIf'   => array( 'show_labels' => true ),
			),
		),
		'show_labels'             => array(
			'type'    => 'boolean',
			'default' => true,
			'ui'      => array(
				'control' => 'toggle',
				'group'   => 'appearance',
				'label'   => __( 'Show wording under the icons', 'menuverse' ),
				'help'    => __( 'This is the switch for the whole bar. Each item can also be set on its own.', 'menuverse' ),
			),
		),
		// Rounding the corners is only half of it: a rounded bar flush against
		// the bottom of the screen has its bottom two corners cut off by the
		// viewport, so anything above zero also lifts the bar clear of the
		// edges. That second half is a class the module adds at render time,
		// because CSS cannot branch on the value of a custom property.
		'bar_radius'              => array(
			'type'    => 'integer',
			'default' => 0,
			'minimum' => 0,
			'maximum' => 28,
			'ui'      => array(
				'control' => 'range',
				'group'   => 'appearance',
				'unit'    => 'px',
				'label'   => __( 'Corner shape', 'menuverse' ),
				'help'    => __( 'Past zero the bar becomes a rounded island floating just above the bottom of the screen.', 'menuverse' ),
				'token'   => '--menuverse-bar-radius',
			),
		),
		'bar_height'              => array(
			'type'    => 'string',
			'format'  => 'css-size',
			'default' => '58px',
			'ui'      => array(
				'control'  => 'unit',
				'group'    => 'appearance',
				'advanced' => true,
				'label'    => __( 'Bar height', 'menuverse' ),
				'token'    => '--menuverse-bar-height',
			),
		),
		'icon_size'               => array(
			'type'    => 'string',
			'format'  => 'css-size',
			'default' => '24px',
			'ui'      => array(
				'control'  => 'unit',
				'group'    => 'appearance',
				'advanced' => true,
				'label'    => __( 'Icon size', 'menuverse' ),
				'token'    => '--menuverse-bar-icon-size',
			),
		),
		// A plain bounded number rather than the `css-size` string its two
		// neighbours use. Wording this small only ever makes sense in pixels,
		// and a slider with ends on it is easier to get right than a free
		// text size. The neighbours keep their type so no stored value moves.
		'font_size'               => array(
			'type'    => 'integer',
			'default' => 11,
			'minimum' => 8,
			'maximum' => 16,
			'ui'      => array(
				'control'  => 'range',
				'group'    => 'appearance',
				'advanced' => true,
				'unit'     => 'px',
				'label'    => __( 'Wording size', 'menuverse' ),
				'token'    => '--menuverse-bar-font-size',
			),
		),
		'show_border'             => array(
			'type'    => 'boolean',
			'default' => true,
			'ui'      => array(
				'control'  => 'toggle',
				'group'    => 'appearance',
				'advanced' => true,
				'label'    => __( 'Thin line along the top of the bar', 'menuverse' ),
			),
		),
		'show_icons'              => array(
			'type'    => 'boolean',
			'default' => true,
			'ui'      => array(
				'control' => 'toggle',
				'group'   => 'appearance',
				'label'   => __( 'Show the icons', 'menuverse' ),
				'help'    => __( 'Off leaves a bar of wording only. The wording has to be on for that to be worth doing.', 'menuverse' ),
			),
		),
		// Named shapes rather than the five numbers a shadow really has:
		// offset, blur and spread are a way of saying "how far off the page",
		// and the person setting it is choosing between flat and lifted.
		'bar_shadow'              => array(
			'type'    => 'string',
			'default' => 'none',
			'enum'    => array( 'none', 'soft', 'medium', 'strong' ),
			'ui'      => array(
				'control'  => 'segmented',
				'group'    => 'appearance',
				'advanced' => true,
				'label'    => __( 'Shadow under the bar', 'menuverse' ),
				'help'     => __( 'A rounded island already casts one; this is the control for a bar that sits flush too.', 'menuverse' ),
				'options'  => array(
					'none'   => __( 'None', 'menuverse' ),
					'soft'   => __( 'Soft', 'menuverse' ),
					'medium' => __( 'Medium', 'menuverse' ),
					'strong' => __( 'Strong', 'menuverse' ),
				),
			),
		),
		'shadow_color'            => array(
			'type'    => 'string',
			'format'  => 'color-alpha',
			'default' => '',
			'ui'      => array(
				'control'  => 'color',
				'group'    => 'appearance',
				'advanced' => true,
				'label'    => __( 'Shadow colour', 'menuverse' ),
				'help'     => __( 'Leave empty for a plain grey shadow.', 'menuverse' ),
				'token'    => '--menuverse-bar-shadow-color',
				'showIf'   => array( 'bar_shadow' => array( 'soft', 'medium', 'strong' ) ),
			),
		),
		'bar_padding'             => array(
			'type'    => 'integer',
			'default' => 0,
			'minimum' => 0,
			'maximum' => 40,
			'ui'      => array(
				'control'  => 'range',
				'group'    => 'appearance',
				'advanced' => true,
				'unit'     => 'px',
				'label'    => __( 'Space at the ends of the bar', 'menuverse' ),
				'token'    => '--menuverse-bar-padding-inline',
			),
		),
		'item_align'              => array(
			'type'    => 'string',
			'default' => 'spread',
			'enum'    => array( 'spread', 'start', 'center', 'end' ),
			'ui'      => array(
				'control'  => 'select',
				'group'    => 'appearance',
				'advanced' => true,
				'label'    => __( 'How the items sit across the bar', 'menuverse' ),
				'help'     => __( 'Spread gives every item the same share of the width, which is what a phone bar usually wants.', 'menuverse' ),
				'options'  => array(
					'spread' => __( 'Spread evenly', 'menuverse' ),
					'start'  => __( 'Grouped at the start', 'menuverse' ),
					'center' => __( 'Grouped in the middle', 'menuverse' ),
					'end'    => __( 'Grouped at the end', 'menuverse' ),
				),
			),
		),
		// Left alone the bar sits on whatever layer the shared token names.
		// Anything above zero is someone answering a theme that puts a sticky
		// header, a cookie bar or a chat bubble over the top of it.
		'z_index'                 => array(
			'type'    => 'integer',
			'default' => 0,
			'minimum' => 0,
			'maximum' => 100000,
			'ui'      => array(
				'control'  => 'number',
				'group'    => 'appearance',
				'advanced' => true,
				'label'    => __( 'Stacking order', 'menuverse' ),
				'help'     => __( 'Only worth setting if something on your site covers the bar. Leave at zero otherwise.', 'menuverse' ),
				'token'    => '--menuverse-bar-z',
			),
		),

		'active_indicator'        => array(
			'type'    => 'string',
			'default' => 'color',
			'enum'    => array( 'color', 'pill', 'line', 'underline', 'raised' ),
			'ui'      => array(
				'control' => 'segmented',
				'group'   => 'active',
				'label'   => __( 'How to mark the current page', 'menuverse' ),
				'options' => array(
					'color'     => __( 'Colour only', 'menuverse' ),
					'pill'      => __( 'Pill', 'menuverse' ),
					'line'      => __( 'Line on top', 'menuverse' ),
					'underline' => __( 'Underline', 'menuverse' ),
					'raised'    => __( 'Lifted above the bar', 'menuverse' ),
				),
			),
		),
		'underline_color'         => array(
			'type'    => 'string',
			'format'  => 'color',
			'default' => '',
			'ui'      => array(
				'control' => 'color',
				'group'   => 'active',
				'label'   => __( 'Underline colour', 'menuverse' ),
				'help'    => __( 'Leave empty to use the colour for the current page.', 'menuverse' ),
				'token'   => '--menuverse-bar-underline-color',
				'showIf'  => array( 'active_indicator' => 'underline' ),
			),
		),
		// Deliberately not offered alongside the lifted indicator: that one
		// already draws a filled disc where the dot would go, and the two
		// stacked read as a rendering fault rather than as emphasis.
		'active_pulse'            => array(
			'type'    => 'boolean',
			'default' => false,
			'ui'      => array(
				'control' => 'toggle',
				'group'   => 'active',
				'label'   => __( 'Small pulsing dot on the current item', 'menuverse' ),
				'help'    => __( 'A live, app like touch. It holds still for visitors who have asked their device to reduce motion.', 'menuverse' ),
				'showIf'  => array( 'active_indicator' => array( 'color', 'pill', 'line', 'underline' ) ),
			),
		),
		'pulse_color'             => array(
			'type'    => 'string',
			'format'  => 'color',
			'default' => '',
			'ui'      => array(
				'control' => 'color',
				'group'   => 'active',
				'label'   => __( 'Dot colour', 'menuverse' ),
				'help'    => __( 'Leave empty to use the colour for the current page.', 'menuverse' ),
				'token'   => '--menuverse-bar-pulse-color',
				'showIf'  => array( 'active_pulse' => true ),
			),
		),

		'auto_dark_mode_enabled'  => array(
			'type'    => 'boolean',
			'default' => false,
			'ui'      => array(
				'control' => 'toggle',
				'group'   => 'dark',
				'label'   => __( 'Use different colours in dark mode', 'menuverse' ),
				'help'    => __( 'Follows the setting on the visitor’s own phone or computer.', 'menuverse' ),
			),
		),
		'dark_bg_type'            => array(
			'type'    => 'string',
			'default' => 'color',
			'enum'    => array( 'color', 'gradient' ),
			'ui'      => array(
				'control' => 'segmented',
				'group'   => 'dark',
				'label'   => __( 'Dark background', 'menuverse' ),
				'showIf'  => array( 'auto_dark_mode_enabled' => true ),
				'options' => array(
					'color'    => __( 'One colour', 'menuverse' ),
					'gradient' => __( 'Blend of two', 'menuverse' ),
				),
			),
		),
		'dark_bg_color'           => array(
			'type'    => 'string',
			'format'  => 'color',
			'default' => '#0b0f19',
			'ui'      => array(
				'control' => 'color',
				'group'   => 'dark',
				'label'   => __( 'Background colour', 'menuverse' ),
				'showIf'  => array(
					'auto_dark_mode_enabled' => true,
					'dark_bg_type'           => 'color',
				),
			),
		),
		'dark_bg_gradient_start'  => array(
			'type'    => 'string',
			'format'  => 'color',
			'default' => '#0b0f19',
			'ui'      => array(
				'control' => 'color',
				'group'   => 'dark',
				'label'   => __( 'Blend from', 'menuverse' ),
				'showIf'  => array(
					'auto_dark_mode_enabled' => true,
					'dark_bg_type'           => 'gradient',
				),
			),
		),
		'dark_bg_gradient_end'    => array(
			'type'    => 'string',
			'format'  => 'color',
			'default' => '#141b2e',
			'ui'      => array(
				'control' => 'color',
				'group'   => 'dark',
				'label'   => __( 'Blend to', 'menuverse' ),
				'showIf'  => array(
					'auto_dark_mode_enabled' => true,
					'dark_bg_type'           => 'gradient',
				),
			),
		),
		'dark_bar_color'          => array(
			'type'    => 'string',
			'format'  => 'color',
			'default' => '#e7eaf0',
			'ui'      => array(
				'control' => 'color',
				'group'   => 'dark',
				'label'   => __( 'Icon and text colour', 'menuverse' ),
				'showIf'  => array( 'auto_dark_mode_enabled' => true ),
			),
		),
		'dark_bar_active_color'   => array(
			'type'    => 'string',
			'format'  => 'color',
			'default' => '#16e2f5',
			'ui'      => array(
				'control' => 'color',
				'group'   => 'dark',
				'label'   => __( 'Colour for the current page', 'menuverse' ),
				'showIf'  => array( 'auto_dark_mode_enabled' => true ),
			),
		),
		// The dark twins of the two wording colours. Without them a wording
		// colour chosen for a light bar would carry straight into dark mode
		// and be the one thing on the bar that never adapted.
		'dark_label_color'        => array(
			'type'    => 'string',
			'format'  => 'color',
			'default' => '',
			'ui'      => array(
				'control'  => 'color',
				'group'    => 'dark',
				'advanced' => true,
				'label'    => __( 'Wording colour', 'menuverse' ),
				'help'     => __( 'Leave empty and the wording matches the icons.', 'menuverse' ),
				'showIf'   => array( 'auto_dark_mode_enabled' => true ),
			),
		),
		'dark_label_active_color' => array(
			'type'    => 'string',
			'format'  => 'color',
			'default' => '',
			'ui'      => array(
				'control'  => 'color',
				'group'    => 'dark',
				'advanced' => true,
				'label'    => __( 'Wording colour for the current page', 'menuverse' ),
				'help'     => __( 'Leave empty and it matches the icon for the current page.', 'menuverse' ),
				'showIf'   => array( 'auto_dark_mode_enabled' => true ),
			),
		),
	),
	'ui'         => array(
		'preview' => array(
			'viewport' => 360,
		),
		'presets' => $menuverse_bottom_presets,
		'groups'  => array(
			'items'      => array(
				'label'       => __( 'What is in the bar', 'menuverse' ),
				'description' => __( 'The bottom bar has its own short list of destinations rather than reusing a whole menu, because only a few fit across a phone.', 'menuverse' ),
				'order'       => 10,
			),
			'general'    => array(
				'label'       => __( 'When and where it shows', 'menuverse' ),
				'description' => __( 'Which visitors see the bar, on which pages, and how it behaves as they scroll.', 'menuverse' ),
				// The heading says both halves; the step above it has room for
				// only one, and where it shows is the decision being made.
				'tabLabel'    => __( 'Where it shows', 'menuverse' ),
				'order'       => 20,
			),
			// These three are all about appearance, so they share one tab and
			// stay separate headings within it. Splitting them across three
			// tabs made each one look thinner than it is.
			'appearance' => array(
				'label'    => __( 'The bar itself', 'menuverse' ),
				'tab'      => 'appearance',
				'tabLabel' => __( 'How it looks', 'menuverse' ),
				'order'    => 30,
			),
			'active'     => array(
				'label'       => __( 'Marking the current page', 'menuverse' ),
				'description' => __( 'How the item for the page someone is on is picked out from the rest.', 'menuverse' ),
				'tab'         => 'appearance',
				'order'       => 40,
			),
			'dark'       => array(
				'label'       => __( 'Dark mode', 'menuverse' ),
				'description' => __( 'Optional second set of colours, used when the visitor’s device is set to dark.', 'menuverse' ),
				'tab'         => 'appearance',
				'order'       => 50,
			),
		),
	),
);
