does not * help there: JavaScript is ON, it just never ran bfields. * * So the escape hatch is ordinary server HTML INSIDE the root. The bundle * removes it when it mounts; if the bundle never runs, it stays visible * (review 3.6). The same URL rides on `data-fallback` for the error boundary, * which covers a bundle that loads and then fails to render. * * It first shows a skeleton of the screen, and turns into the notice only once the bundle has * had its chance: when it gave up, or `bfields_fallback_delay` (5 s) after the * page's scripts have run. A slow page therefore never looks broken while it * is still loading (3D Viewer integration demo, 27 Sep 2026). * * @package BFields */ namespace BFields; if (!defined('ABSPATH')) { exit; } /** * Prints `.bfields-root` with its fallback notice. */ final class Mount { /** * Print the root for one screen. * * @param string $unique Storage key. * @param string $kind 'options' | 'metabox'. * @param array $schema Normalised schema. * @param array $data Extra data-* attributes (name => value). */ public static function render(string $unique, string $kind, array $schema, array $data = array()): void { $style = ''; foreach ($schema['args']['brand'] as $token => $value) { if ('logo' === $token) { continue; } $style .= '--bfields-' . sanitize_key($token) . ':' . $value . ';'; } $fallback = self::fallback_url($unique); $attrs = ''; $data = array_merge(array('unique' => $unique, 'kind' => $kind, 'fallback' => $fallback), $data); foreach ($data as $name => $value) { if ('' === (string) $value) { continue; } $attrs .= sprintf(' data-%s="%s"', sanitize_key($name), esc_attr((string) $value)); } // `bfields-app` is the token + reset scope every bfields stylesheet is // written under (theme/base.css rule 1); `bfields-root` is the mount // hook the bundle looks for. The root element carries both. printf( '
', $attrs, // Escaped above. '' === $style ? '' : ' style="' . esc_attr($style) . '"' ); // The escape hatch has two states. The bundle removes the whole // `.bfields-fallback` when it mounts. Until then it shows a skeleton, because // the bundle mounts only when the page's scripts have all run, which on a // heavy admin page or a slow host can take a while, and a failure message // there reads as a broken screen. Only when the bundle has had its chance // does it say so and offer the host's switch. Both states are plain HTML // plus an inline style and script: they have to work when the bundle, and // bfields' own stylesheet, never arrive. With JavaScript off, the second // state shows at once. self::fallback_assets(); echo '
'; echo '
'; echo '' . esc_html('metabox' === $kind ? __('Loading these settings…', 'bfields') : __('Loading the settings…', 'bfields')) . ''; echo self::skeleton($kind, $schema, $data); // Static markup. echo '
'; echo '

'; echo esc_html( 'metabox' === $kind ? __('These settings are taking longer than usual to load, or they could not load on this site. Nothing has changed: saving the post will not change them.', 'bfields') : __('This screen is taking longer than usual to load, or it could not load on this site. Your saved settings are safe and unchanged.', 'bfields') ); if ('' !== $fallback) { echo ' ' . esc_html__('Switch to the classic interface', 'bfields') . ''; } echo '

'; } /** * Grey placeholder blocks in the screen's shape: the page editor's title, * tabs, cards and side column; an options page's head, tabs and rows; a * meta box's tabs and rows. */ private static function skeleton(string $kind, array $schema, array $data): string { $frame = (string) ($data['frame'] ?? ''); $sections = isset($schema['sections']) && is_array($schema['sections']) ? $schema['sections'] : array(); $tabs = count($sections) > 1 ? min(6, count($sections)) : 0; $first = isset($sections[0]['fields']) && is_array($sections[0]['fields']) ? count($sections[0]['fields']) : 0; $left = 'left' === ($schema['args']['tabsPosition'] ?? 'top'); $bar = static function (string $class, string $style = ''): string { return ''; }; $strip = ''; for ($i = 0; $i < $tabs; $i++) { $strip .= $bar('bfields-skel__tab', 'width:' . (96 + ($i * 37) % 70) . 'px'); } $strip = '' === $strip ? '' : '
' . $strip . '
'; $rows = static function (int $count) use ($bar): string { $html = ''; for ($i = 0; $i < $count; $i++) { $html .= '
' . $bar('bfields-skel__icon') . '' . $bar('bfields-skel__title', 'width:' . (22 + ($i * 13) % 18) . '%') . $bar('bfields-skel__text', 'width:' . (40 + ($i * 17) % 25) . '%') . '' . $bar('bfields-skel__control', 'width:' . (0 === $i % 2 ? 72 : 120) . 'px') . '
'; } return $html; }; $count = max(3, min(7, $first)); if ('page' === $frame) { $editor = json_decode((string) ($data['editor'] ?? ''), true); $editor = is_array($editor) ? $editor : array(); $chip = !empty($editor['shortcode']) ? $bar('bfields-skel__chip') : ''; $cards = ''; for ($i = 0; $i < 3; $i++) { $cards .= '
' . $bar('bfields-skel__title', 'width:' . (18 + $i * 6) . '%') . $bar('bfields-skel__text', 'width:' . (34 + $i * 9) . '%') . '
'; } // The side column as EditorShell draws it: preview, Publish, Save Draft on a draft, the shortcode bar. $side = $bar('bfields-skel__preview') . $bar('bfields-skel__button bfields-skel__button--primary') . (empty($editor['published']) ? $bar('bfields-skel__button') : '') . ('' !== $chip ? $bar('bfields-skel__button') : ''); $body = '
' . $cards . '
' . $side . '
'; // The real column goes full width by setting this on ; the rule leaves with the fallback. $width = 'full' === ($schema['args']['pageWidth'] ?? '') ? '' : ''; return $width . ''; } $head = 'options' === $kind ? '
' . $bar('bfields-skel__heading') . $bar('bfields-skel__search') . '
' : ''; return ''; } /** * What switches the fallback's two states, once per page, inline. * * The bundle is a footer script and mounts on DOMContentLoaded, so right * after that event it has either mounted a root (the fallback is gone), * given up on it (the root is marked but the fallback is still there), or * not run at all. The script shows the notice at once in the second case, * and after `bfields_fallback_delay` in the third, which leaves room for a * bundle an optimisation plugin made load late. * * Without the script (JavaScript off, inline scripts blocked, a script * earlier on the page that never finishes loading) CSS does it: