bake in the browser -> save. * * Once M2 brought the editor, this page is superseded by it — but **the save pipeline itself (bake in * the browser, submit over REST) is the final form** and will not be redone. */ class Admin { public static function boot(): void { add_action('admin_menu', [self::class, 'menu']); add_action('admin_enqueue_scripts', [self::class, 'assets']); add_action('admin_post_aumframe_new_template', [self::class, 'create_blank']); add_filter( 'plugin_action_links_' . plugin_basename(AUMFRAME_FILE), [Docs::class, 'action_link'] ); } public static function menu(): void { add_menu_page( 'Aumframe', 'Aumframe', 'edit_theme_options', 'aumframe', [self::class, 'page'], 'dashicons-layout', 58 ); } public static function assets(string $hook): void { if ($hook !== 'toplevel_page_aumframe') { return; } wp_enqueue_style( 'aumframe-admin', AUMFRAME_URL . 'assets/admin.css', ['dashicons'], aumframe_asset_ver('assets/admin.css') ); wp_enqueue_script( 'aumframe-admin', AUMFRAME_URL . 'assets/admin.js', // wp-i18n: status copy is translated through WordPress's own runtime, the same path the // editor (Editor.php) uses. ['wp-i18n'], aumframe_asset_ver('assets/admin.js'), true ); Plugin::as_module('aumframe-admin'); wp_set_script_translations('aumframe-admin', 'aumframe', AUMFRAME_DIR . 'languages'); wp_add_inline_script( 'aumframe-admin', 'window.AUMFRAME = ' . wp_json_encode([ 'restUrl' => esc_url_raw(rest_url(Rest::NS . '/templates')), 'nonce' => wp_create_nonce('wp_rest'), // Carry a version: if the compiler or the samples change while the URL does not, the browser // bakes with the old copy and the output does not match the source — exceptionally tedious to // trace. 'coreUrl' => self::v(AUMFRAME_URL . 'packages/core/src/index.js'), 'restBase' => esc_url_raw(rest_url(Rest::NS)), 'engine' => AUMFRAME_VERSION, 'bakedWith' => (string) get_option('aumframe_baked_with', ''), 'samples' => [ 'showcase' => self::v(AUMFRAME_URL . 'packages/core/test/showcase.json'), 'hero' => self::v(AUMFRAME_URL . 'packages/core/test/hero.json'), ], ]) . ';', 'before' ); } /** * Attach a version to same-origin static assets. * * The version is the newest mtime across the whole module graph (the editor's output plus * packages/core). * * It used to look at `assets/editor.js` alone: `compile.js` and `render.js` in core are bare-URL * ESM, so changing them without rebuilding editor.js left the version unchanged — the browser kept * using the old compiler, the canvas disagreed with the front end, and the files on the server * genuinely were the new ones, so no amount of looking would find it. * (The front end's runtime chain hit the same trap; see the note on `aumframe_module_ver`.) */ private static function v(string $url): string { return add_query_arg('ver', aumframe_module_ver(['assets/editor.js', 'packages/core/src']), $url); } /** * This template's front-end address. Empty when there is none — the site shell has no address of * its own, and a link that 404s is worse than no link. */ private static function preview_link(array $t): string { $rules = json_decode((string) ($t['rules'] ?? ''), true); if (!is_array($rules)) { return ''; } $kind = $rules['kind'] ?? ''; if ($kind === 'front_page') { return home_url('/'); } if ($kind === 'singular') { if (!empty($rules['post_ids'][0])) { return (string) (get_permalink((int) $rules['post_ids'][0]) ?: ''); } $latest = get_posts([ 'post_type' => $rules['post_type'] ?? 'post', 'numberposts' => 1, 'post_status' => 'publish', 'fields' => 'ids', ]); return $latest ? (string) (get_permalink((int) $latest[0]) ?: '') : ''; } if ($kind === 'archive') { if (!empty($rules['taxonomy'])) { $terms = get_terms([ 'taxonomy' => $rules['taxonomy'], 'number' => 1, 'hide_empty' => false, 'fields' => 'ids', ]); if (!is_wp_error($terms) && $terms) { $link = get_term_link((int) $terms[0]); if (!is_wp_error($link)) { return (string) $link; } } } if (!empty($rules['post_type'])) { return (string) (get_post_type_archive_link($rules['post_type']) ?: ''); } } return ''; } /** * Create a blank template and jump into the editor. * * The root container is given explicit size and padding — a genuinely empty Frame computes to zero * height, nothing appears on the canvas, and a new user's first impression is that the tool is * broken. */ public static function create_blank(): void { if (!current_user_can('edit_theme_options') || !isset($_GET['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'aumframe_new_template')) { wp_die(esc_html__('You do not have permission to do that.', 'aumframe')); } $scene = [ 'schemaVersion' => 1, 'root' => 'root', 'breakpoints' => [ ['id' => 'base', 'max' => null], ['id' => 'tablet', 'max' => 1024], ['id' => 'mobile', 'max' => 640], ], 'nodes' => [ 'root' => [ 'id' => 'root', 'type' => 'Frame', 'name' => 'Page', 'children' => [], 'style' => [ 'layout' => 'stack', 'direction' => 'vertical', 'gap' => 24, 'width' => ['mode' => 'fill'], 'height' => ['mode' => 'fit', 'min' => 400], 'padding' => ['top' => 80, 'right' => 28, 'bottom' => 80, 'left' => 28], ], ], ], ]; $n = count(Storage::published()) + 1; $id = Storage::save([ /* translators: %d: sequential number of the new design. */ 'title' => sprintf(__('New design %d', 'aumframe'), $n), 'slug' => 'design-' . $n . '-' . wp_generate_password(4, false), 'kind' => 'page', 'status' => 'publish', // Empty rules mean it takes over no URL. Deliberate: a new template going live before it // is designed would displace real pages outright, the most alarming class of accident. Fill in // the rules once the design is done. 'rules' => wp_json_encode([]), 'scene' => wp_json_encode($scene), 'baked_html' => '', 'baked_css' => '', ]); wp_safe_redirect(Editor::url((int) $id)); exit; } public static function page(): void { $templates = Storage::published(); /* * The site shell goes first. * * It is the most consequential template on the site — it wraps every page — and it was the least * findable row in this list: no front-end address, so no "View on site" link, and a title its * author chose freely. A buyer looking for "where do I change the logo or the menu" searched the * Pages list (it is deliberately not there — it belongs to no page), then scanned this list for * something called "header" and gave up. Pinning it costs one sort. */ usort($templates, function ($a, $b) { $sa = ($a['kind'] ?? '') === 'shell' ? 0 : 1; $sb = ($b['kind'] ?? '') === 'shell' ? 0 : 1; return $sa === $sb ? 0 : ($sa <=> $sb); }); ?>

Aumframe v


|

packages/core' ); ?>

{"kind":"front_page"} {"kind":"singular","post_type":"post"} {"kind":"archive","taxonomy":"category"} {"kind":"any"} —