* @license GPL-2.0+ * @link https://aumcreate.com * @copyright 2025 AumCreate * @since 1.0.0 */ // ========= Security Check ========= if (!defined('ABSPATH')) { exit; } // ========= Core Theme Setup ========= add_action('after_setup_theme', 'aumcreate_setup'); function aumcreate_setup() { unload_textdomain('aumcreate'); load_textdomain('aumcreate', get_template_directory() . '/languages/aumcreate-' . determine_locale() . '.mo'); add_theme_support('aumcreate'); add_theme_support('title-tag'); /* Theme Check 要求:没有它,订阅源的 不会出现在 里。 */ add_theme_support('automatic-feed-links'); /* * ── 下面三个是 2026-09-30 上架 wordpress.org 时补的 ────────────────────────────────── * * ⚠️ 它们**只作用于核心区块和核心生成的标记**,而我们的页面是 aumframe 烘焙的 —— * 所以对我们自己的 24 份模板影响是 **0**(数过:0/24 用到对齐类,orgcheck 107 篇 * 已发布内容里 0 篇用到)。 * * 真正的理由是**审核员拿 WordPress 自己的 theme unit test 内容来测**, * 那份数据故意塞满了对齐图片、图片说明、置顶文章、嵌套评论。买家自己写一篇普通博客 * 也会走到这条路。判据:**这不是"我们的用户在受苦",是"这一族标记我们没答应过却收下了"。** */ add_theme_support('html5', [ 'search-form', 'comment-form', 'comment-list', 'gallery', 'caption', 'style', 'script', ]); add_theme_support('responsive-embeds'); // 没有它,核心嵌入区块在手机上会溢出 add_theme_support('align-wide'); // 没有它,宽 / 全宽区块的对齐不生效 add_theme_support('post-thumbnails'); /* * **One nav menu location, and the layout actually reads it** (`source: 'menu'`, Aumframe 1.7.23-M6). * * The history is worth keeping because it is the shape of a whole class of mistake: * * - Until 2026-09-04 three locations were registered (`primary` / `footer` / `user-menu`) and * `wp_nav_menu()` was **never called once**. A buyer could build a menu, assign it, save — and * nothing happened, with no explanation. Registering a capability and ignoring it is not "not * supporting menus", it is pretending to. * - The first fix was to drop all three and hide the screen. That was honest but it **gave up a real * capability**: submenus, external links, categories as items, drag-reordering without opening a * layout editor — and, decisively, the multilingual plugins (WPML / Polylang / GTranslate, and our * own AumLang) all hook menus. A site that needs a second language has nowhere to hang it. * * So: one location, and the site shell reads it through a Repeater. **The container stays the * designer's, the items become the site owner's** — the same split every builder in this class uses. * * ⚠️ Only `primary` is registered. `footer` and `user-menu` went away and are not coming back on * spec: the footer's columns are grouped, titled and hand-ordered — a flat menu cannot express that, * and registering a location no layout reads is the exact mistake above. * * ⚠️ **The shell must not depend on this.** A site with no menu assigned gets an empty list, and the * shell falls back to the links authored in the layout (`when` on the query's `count`). Navigation * that can go blank is worse than navigation that cannot be edited. */ register_nav_menus([ 'primary' => __('Primary Menu', 'aumcreate'), ]); add_theme_support('woocommerce'); add_theme_support('wc-product-gallery-zoom'); add_theme_support('wc-product-gallery-lightbox'); add_theme_support('wc-product-gallery-slider'); } // ========= Block Editor ========= // Gutenberg remains available for pages and posts. Visual page building is // handled by Aum Builder. /* * ⚠️ **不要再把前台工具条关掉。** * * 原来这里是 `add_filter('show_admin_bar', '__return_false');`。 * wordpress.org 的主题审核明确禁止主题隐藏管理工具条 —— Theme Check 的原话是 * *Themes are not allowed to hide the admin bar*,而且标着「必须人工复核」。 * 理由是它属于**站点级偏好**:每个用户在「个人资料」里自己就有一个开关, * 主题替所有人关掉,等于把用户的选择拿走,而且没有任何界面能改回来。 * * 影响面:只有**登录用户**看得见工具条,访客从来看不见。所以去掉这一行对 * 演示站的对外观感没有变化,只是我们自己登录时会多出那一条。 * * 真要在某个版面里让开它,用 CSS 读 `body.admin-bar` 的偏移,别关掉它。 */ // ========= Helper Functions ========= // Load order matters: color must precede dynamic CSS. require_once get_template_directory() . '/inc/helpers/admin-url.php'; require_once get_template_directory() . '/inc/helpers/i18n.php'; require_once get_template_directory() . '/inc/helpers/icons.php'; require_once get_template_directory() . '/inc/helpers/color.php'; require_once get_template_directory() . '/inc/helpers/comment.php'; require_once get_template_directory() . '/inc/helpers/post-cover.php'; require_once get_template_directory() . '/inc/helpers/post.php'; require_once get_template_directory() . '/inc/helpers/post-archive-router.php'; require_once get_template_directory() . '/inc/helpers/search.php'; /* * Loaded right after search.php: it is the other half of the same feature — search.php decides *which* * types are searched, this decides whether the words on a taken-over page are searchable at all. */ require_once get_template_directory() . '/inc/helpers/search-index.php'; require_once get_template_directory() . '/inc/fonts.php'; require_once get_template_directory() . '/inc/helpers/user-avatar.php'; require_once get_template_directory() . '/inc/helpers/post-stats.php'; require_once get_template_directory() . '/inc/helpers/user-display-policy.php'; require_once get_template_directory() . '/inc/helpers/mobile-menu-walker.php'; require_once get_template_directory() . '/inc/helpers/block-editor.php'; // ========= AJAX Handlers ========= require_once get_template_directory() . '/inc/ajax/video-cover.php'; require_once get_template_directory() . '/inc/ajax/posts-list.php'; require_once get_template_directory() . '/inc/ajax/user-profile.php'; require_once get_template_directory() . '/inc/ajax/avatar-ajax.php'; // ========= Login Page ========= require_once get_template_directory() . '/inc/helpers/login-page.php'; // ========= Admin & Frontend Classes ========= require_once get_template_directory() . '/inc/admin/class-page-template-binder.php'; new AumCreate_Page_Template_Binder(); if (is_admin()) { /* 每份模板各自需要的插件 —— 挂进上面那份清单的 filter,见文件头 */ require_once get_theme_file_path('inc/admin/class-admin-menu.php'); /* 2.11.0 把菜单挪进「外观」之后,还挂在老 slug 上的插件页会悄悄消失 —— 这个把它念出来 */ require_once get_theme_file_path('inc/admin/class-orphaned-submenus.php'); AumCreate_Orphaned_Submenus::boot(); require_once get_theme_file_path('inc/admin/class-settings.php'); require_once get_template_directory() . '/inc/admin/class-post-template-metabox.php'; require_once get_theme_file_path('/inc/admin/class-avatar-handler.php'); /* * Kit import and export. * * This replaced `class-demo-manager.php` (WXR plus One Click Demo Import) — that path could not carry * Aumframe's layouts (they live in the `wp_aumframe_templates` table, a concept WXR does not have) and it * depended on a third-party plugin: a buyer who does not install it loses the import button entirely. * It now goes through `Aumframe\Kit\Package`, **exactly the same path as our own delivery and the * seeder**. */ /* 「模板与插件」标签页 —— 装的是什么、有没有新版、还能装什么 */ } /* * Outside is_admin() on purpose: settings can also be written by WP-CLI and the REST API, where `is_admin()` * is false — and a cache left stale by those paths looks exactly as broken as one left stale by the form. */ require_once get_template_directory() . '/inc/admin/class-cache-purge.php'; new AumCreate_Cache_Purge(); // Loaded outside is_admin() — both have frontend hooks (wp_head, wp_footer, wp_enqueue_scripts). require_once get_template_directory() . '/inc/admin/class-performance.php'; /* * 🔴 Cookie 同意横幅不在主题里了(2026-10-06)。 * * 两个理由,缺一个都不够: * · wordpress.org 主题审核把它算**插件领地** —— 同意管理不是设计和呈现, * 而且它读写 cookie、决定别的脚本能不能跑。 * · 自己养一个同意横幅=**永远自己扛 GDPR 的正确性**。换来的是一个 * 25 个演示站里一个都没开着的功能(2026-10-06 两条路各量一次: * option 端 `cc_enabled` 这个键根本不存在;行为端 25 个首页抓 HTML * 命中 0;而且那个探测器本身被验过真能抓到渲染出来的 `aum-cc`)。 * * 真需要同意闸的站装一个专门的同意插件,让它发 `aumframe:consent` 事件 —— * aumframe 的延迟嵌入(地图/视频)听的就是这个通用事件,不认任何一家的 UI。 * * ⚠️ 已经存过的 `cc_*` 选项**原样留在库里,没有迁移、没有删除**: * 万一有站开着过,那份配置还在,不是被我们销毁了。 */ /* * 🔴 浏览量统计不在主题里了(2026-10-06,随 aumframe 1.28.0 搬走)。 * * wordpress.org 的主题审核把「Analytics or tracking support」和 * 「Non-design related meta boxes」列为插件领地,Required §5 还有一条总则: * 不许包含与设计和呈现无关的功能。这一套两条都占。 * 实现和数据现在归 `Aumframe\Stats\Views`,而且**默认是关的**(插件指引第 7 条: * 未经用户明确确认不得自动收集用户数据)。 * * 主题这边只剩 `aumcreate_get_post_views()` 一个转发(见 inc/helpers/post-stats.php), * 插件不在或功能没打开时返回 0。 */ require_once get_template_directory() . '/inc/admin/class-user-fields.php'; /* * 🔴 **点赞 / 收藏已搬进 aumframe(主题 2.15.0 / aumframe 1.19.0)。** * * 原来这里 require `class-post-interactions.php`(326 行)和 `inc/ajax/post-{like,favorite, * interaction-state}.php`(140 行)。存储、AJAX、按钮标记全部搬走,主题只留**作者关注**。 * * 顺带删掉三个死端点(`aumcreate_toggle_like` / `aumcreate_toggle_favorite` / `aumframe_add_comment`)—— * 数过,注册之外零个调用方,和前端真正在用的那三个重复。 */ require_once get_template_directory() . '/inc/frontend/class-author-follow.php'; require_once get_template_directory() . '/inc/frontend/comment-ajax-handler.php'; require_once get_template_directory() . '/inc/frontend/class-aum-lang-orbit.php'; require_once get_template_directory() . '/inc/frontend/class-aum-floating-actions.php'; /* * ══ 搬进 aumframe 的短代码 ═══════════════════════════════════════════════════ * * wordpress.org 不允许主题用 `add_shortcode()` —— Theme Check 把每一处都判成阻断级错误, * 原话是 *plugin-territory functionality and must not be used in themes*。所以它们逐个搬进 * 插件,代码一字不改(只换 text domain)。搬走的列在这里,别让这份清单散成一处一段注释: * * [aumframe_pagination] → aumframe/src/Shortcodes/Pagination.php (主题 2.2.0 / aumframe 1.9.0) * [aumframe_audio] → aumframe/src/Shortcodes/Audio.php (主题 2.2.1 / aumframe 1.9.1) * [aumframe_theme_toggle] → aumframe/src/Shortcodes/ThemeToggle.php (主题 2.3.0 / aumframe 1.10.0) * [aumframe_search] → aumframe/src/Shortcodes/Search.php (主题 2.5.0 / aumframe 1.11.0) * [aumframe_form] → aumframe/src/Shortcodes/Form.php (主题 2.6.0 / aumframe 1.12.0) * ⚠️ **只搬了注册。** 字段 / 校验 / 存储 / 通知 / 防垃圾 / 后台编辑器 / * 收件箱一共 1548 行,一行都没搬 —— 那些在主题里是允许的。 * [aumframe_cart_link] · [aumframe_product_action] → Shortcodes/Catalog.php (2.9.0 / 1.14.0) * [aumframe_like_button] · [aumframe_favorite_button] → Interactions/(**实现也搬走了** 2.15.0 / 1.19.0) * [aumframe_comments_section] → **删掉**,它从来没有实现过 * * ⚠️ 到这里主题里 `add_shortcode()` **清零**。五个子系统各自答同一条 * `aumframe/shortcode_html` 通道(按 $tag 分派),而不是一个短代码一条过滤器。 * * AumViso 短代码的空处理器 → 不再需要(主题 2.8.0 / aumframe 1.13.0) * 引擎改成通用处理:`Renderer::drop_unhandled()` 把**任何** * 没有处理方的 `aum*`/`arb*` 标签去掉,不用维护名字表。 * * 内容模型的类型/分类法 → 同一条 `aumframe/register_types` 通道(主题 2.7.0) * ⚠️ **插件侧一行没改** —— 通道上一刀就建好了。字段 * (`register_post_meta`)留在主题,那个不违规且跟着定义走。 * * `aumframe_form` **这个类型**也由插件 register(参数还是主题给的, * 走 `aumframe/register_types`),渲染走 `aumframe/shortcode_html`。 * ⚠️ **只搬了注册。** 表单本身还在主题里(`inc/helpers/search.php` 的 * `aumcreate_get_search_form()`)—— 下拉框的类型来自主题设置、标记带着 * `aum-search.js` 认的类名、样式是主题的,而且主题自己的 `search.php` * 模板也在调它。改成插件问、主题答:`aumframe/shortcode_html` 通道。 * ⚠️ 这个不是复制:它有三处耦合,其中一处是循环。 * 「开关开没开」留在主题(`aumcreate_theme_toggle_enabled()`,在 * `inc/helpers/color.php`),插件通过 `aumframe/site_appearance` 的 * `dayNight` 拿答案。悬浮列改用 `do_shortcode()`,并先 `shortcode_exists()`。 * * ⚠️ **样式留在这个主题里**,而插件那边的**标记一字未改** —— 例如分页的 `.nav-links` * (`style.css` 里 6 个规则块、25 条声明,前景色按各自底色算过对比度)继续生效, * 接管页和主题页看起来仍然是一套。**改插件那边的类名,会静默地把现存站点的样式打掉。** * * 🔴 **这条风险现在没有任何一端在拦,写在这里是为了让下一个人知道它还在。** * * 原来的做法是:每搬一个短代码过去,就把 `class-plugin-dependencies.php` 里 aumframe 的 * `min_version` 抬一级,主题在后台提示「你的插件太旧了」。2026-10-07 那整套提示从主题里 * 删掉了(提示属于 aumkit,不属于主题),`min_version` 也随之消失。 * * 剩下的事实没变:**只升主题、没升插件的站,这些短代码没有任何一处注册它们, * 会当字面文本印在页面上。** 插件走自建更新端点、是推送的,所以实际落后的窗口很窄, * 但窄不等于零。真要补的话,该补的位置是 aumframe 自己(它知道自己注册了哪些短代码), * 或者 aumkit 的 `Requirements::check()`(它已经有 `too_old` 这一档)—— **不是主题。** */ require_once get_template_directory() . '/inc/frontend/woo-integration.php'; /* * 🔴 **表单系统已整体搬进 aumframe(主题 2.13.0 / aumframe 1.17.0)。** * * 原来这里是 `require_once inc/forms/class-aum-forms.php; AumCreate_Forms::boot();`, * `inc/forms/` 一共 1592 行 + 32KB 资源,全部搬走,主题这边一行不留。 * * 为什么整体搬而不是只搬注册:wordpress.org 主题手册 Required 里除了 * *Custom post types* / *Shortcodes*,还有一句 **"Functionality that is not related to * design and presentation"** —— 存储、通知、防垃圾、收件箱本身就不是设计和展示。 * 更要紧的是产品目标:**每一件东西单独装也要能用**。只搬注册的话, * aumframe 单独装 = 短代码渲染出空;主题单独装 = 短代码根本不存在。 * * ⚠️ 搬走之后主题里**不要再出现 `aumframe_form` 相关的任何东西** —— * 想改表单就去 `plugins/aumframe/src/Forms/`。在这里再建一个「轻量表单」 * 会得到两套字段定义和两张收件箱表,而它们都叫 aumframe_form。 */ // ========= Content Model ========= // Content types, taxonomies and fields are defined in the admin and stored in the database — no more // hand-written register_post_type per site. The engine is universal and every site difference is data. /* * 🔴 **内容模型已整体搬进 aumframe(主题 2.14.0 / aumframe 1.18.0)。** * * 原来这里 require 并 boot `inc/content-types/`,一共 2950 行 + 28KB 资源,全部搬走。 * * 那个目录的文件头上曾经用三段论证「内容模型该留在主题里」,结尾一句是 * 「哪天主题要单独卖、或者要支持第三方主题,这个决定就得重来」—— 那一天到了。 * * ⚠️ 主题里**仍然读**内容模型的地方有两处(catalog 适配器、设置页的「不可搜索类型」提示), * 它们现在读 `\Aumframe\Model\Store`,并且都用 `class_exists()` 守着 —— * 主题的 `min_version` 要求 aumframe 1.18.0,所以正常情况一定在; * 守卫是为了「插件被停用」那一瞬间不白屏。 */ require_once get_template_directory() . '/inc/aumframe-integration.php'; require_once get_template_directory() . '/inc/aumlang-bridge.php'; require_once get_template_directory() . '/inc/multilingual-bridge.php'; // ========= Catalog ========= // 目录抽象层已在 aumframe(见下面那段说明)。这里只剩 Woo / Reserva / NexCart 的 // 表现层和 kit 播种 —— 它们通过 aumframe/catalog/register 之外的普通钩子接进去。 require_once get_template_directory() . '/inc/frontend/class-aum-viso.php'; AumCreate_Viso::boot(); /* * Self-hosted updates. Admin-only: an update check has nothing to do on a visitor's page load, and * loading it there would put a network call in the path of every uncached request. */ /* * ⚠️ **There is deliberately no self-hosted updater and no licence gate any more.** * * Both used to load here. The theme is going to wordpress.org, where updates come from .org — a theme * that ships its own updater fights core for the same slug, and a licence gate is not allowed at all * ("Do not place WordPress features behind a paywall"). Removed rather than made conditional: a gate * that is switched off still has to be read by everyone who comes after it. * * A copy installed before 2.1.0 still carries the old updater and will keep asking aumcreate.com, so it * has to be replaced by hand once. As of 2.1.0 that is only our own demo sites — nothing was ever sold. */ foreach (glob(get_template_directory() . '/inc/catalog/adapters/*.php') as $aumcreate_adapter) { require_once $aumcreate_adapter;// Each adapter decides whether its engine is present and does not register when it is not } /* * 🔴 **商品目录的抽象层已搬进 aumframe(主题 2.16.0 / aumframe 1.20.0)。** * * 搬走的是 `AumCreate_Catalog` / `Adapter` / `Bag` 和四个引擎适配器(Woo / NexCart / * Reserva / CPT)以及 AumViso 桥 —— 一共 1434 行,**它们调主题函数 0 次**。 * * 搬它的直接原因不是「放在主题不合适」,而是**方向已经反了**:内容模型搬走之后, * `Aumframe\Model\Fields` 就在调 `\AumCreate_Catalog`,也就是插件依赖主题。 * 后果是 aumframe 单独装时,kit 版面里绑 `post.product.*` 的字段全部渲染成空, * 而且被 `class_exists()` 守着,一声不响。 * * ⚠️ **留在主题的是表现层和 kit 播种**:`woo-kit.php` / `reserva-kit.php` / * `nexcart-kit.php` / `woo-buttons.php` / `woo-chrome.php`,它们一共调 16 个主题函数, * 是这个主题怎么把 Woo 页面画成我们的样子。下面那个 glob 仍然把它们加载进来。 */ // ========= Theme Activation Redirect ========= add_action('after_switch_theme', 'aumcreate_redirect_to_settings'); function aumcreate_redirect_to_settings() { if (current_user_can('manage_options') && isset($_GET['activated'])) { if (!get_transient('aumcreate_theme_activated')) { set_transient('aumcreate_theme_activated', true, 30); wp_safe_redirect(aumcreate_settings_url()); exit; } } } // ========= Plugin Activation via AJAX ========= add_action('wp_ajax_aumcreate_activate_plugin', 'aumcreate_activate_plugin_callback'); function aumcreate_activate_plugin_callback() { check_ajax_referer('activate-plugin', '_wpnonce'); if (!current_user_can('activate_plugins')) { wp_send_json_error(__('You do not have permission to perform this action', 'aumcreate')); } if (empty($_POST['plugin'])) { wp_send_json_error(__('Missing plugin parameter', 'aumcreate')); } $plugin = sanitize_text_field($_POST['plugin']); if (!function_exists('is_plugin_active')) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } // Plugin file may not be registered yet right after install; refresh the cache. wp_clean_plugins_cache(); // Already active counts as success (idempotent activation). if (!is_plugin_active($plugin)) { $result = activate_plugin($plugin); if (is_wp_error($result)) { wp_send_json_error($result->get_error_message()); } } $is_bulk = isset($_POST['bulk_activate']) && $_POST['bulk_activate']; $response_data = []; if (strpos($plugin, 'woocommerce') !== false) { $response_data['setup_url'] = admin_url('admin.php?page=wc-admin'); } if ($is_bulk && !empty($response_data['setup_url'])) { wp_send_json_success([ 'message' => __('Plugin activated successfully', 'aumcreate'), 'data' => $response_data, ]); } else { wp_send_json_success(__('Plugin activated successfully', 'aumcreate')); } } // ========= Enqueue Styles ========= add_action('wp_enqueue_scripts', 'aumcreate_theme_enqueue_styles'); function aumcreate_theme_enqueue_styles() { wp_enqueue_style( 'aum-theme-style', get_template_directory_uri() . '/style.css', [], wp_get_theme()->get('Version') ); if (class_exists('WooCommerce')) { wp_enqueue_style( 'aum-woo-style', get_template_directory_uri() . '/assets/frontend/css/aum-woo.css', ['aum-theme-style'], filemtime(get_template_directory() . '/assets/frontend/css/aum-woo.css') ); } /* * The `.posts-grid` / `.post-card` sheet. It used to be enqueued from inside archive.php, which meant * only that template could have cards: index.php rendered the same kind of list and had no way to ask * for the styles without copying the enqueue — a second place that would have to stay in step. * * The condition reproduces exactly what archive.php's inline enqueue covered, plus the blog index: * author archives are drawn by author.php, which carries its own `author-profile.css` (and that sheet * holds **its own copy** of these card rules — a real duplicate, left alone here because merging the * two is a visual change to two pages, not a side effect of this one). * * `aumframe_is_taking_over()` is the same guard the archive's AJAX assets use: on a taken-over page * none of this markup is rendered, so the sheet is dead weight. */ $aumcreate_cards_page = is_home() || is_search() || (is_archive() && !is_author()); $aumcreate_taken_over = function_exists('aumframe_is_taking_over') && aumframe_is_taking_over(); if ($aumcreate_cards_page && !$aumcreate_taken_over) { wp_enqueue_style( 'aumcreate-archive-cards', get_template_directory_uri() . '/assets/frontend/css/archive-cards.css', ['aum-theme-style'], filemtime(get_template_directory() . '/assets/frontend/css/archive-cards.css') ); } } /** * Dark controls for WooCommerce's block checkout. * * Woo ships `.has-dark-controls` (260 rules covering inputs, textareas, checkboxes, radios and validation * errors). Attaching that class is far sturdier than rewriting those 260 — Woo's base rules have a specificity * of (0,3,1), hand-written overrides have to keep escalating, and the moment Woo updates they rot. * * ⚠️ This only covers **first paint**: with `theme_mode` set to `system` the server cannot know the outcome, and * pressing the toggle only changes `data-theme` — both of those are synced by `theme-mode.js`. Without the * server half, a dark site flashes a screen of white boxes on entering checkout. * * (Origin: 2026-08-22, white inputs with invisible placeholders on the checkout page.) */ add_filter('body_class', 'aumcreate_woo_dark_controls_class'); function aumcreate_woo_dark_controls_class($classes) { $options = get_option('aumcreate_settings'); $mode = is_array($options) ? ($options['theme_mode'] ?? 'system') : 'system'; if ($mode === 'dark') { $classes[] = 'has-dark-controls'; } /* * Hand Woo's block buttons **to the theme**. * * Woo's entire button styling is wrapped in `body:not(.woocommerce-block-theme-has-button-styles)` (56 * places) — that is the opt-out it leaves for themes: add the class and it stops styling buttons, letting * our `--primary` and `--text-on-primary` take effect. * * Without it, "Place Order" is fluorescent green with **white text** and the label is unreadable — on the * last button of the entire checkout flow. * * ⚠️ This is not light/dark specific; buttons follow the theme in both modes. */ $classes[] = 'woocommerce-block-theme-has-button-styles'; return $classes; } // ========= AJAX Localization ========= add_action('wp_enqueue_scripts', 'aumcreate_author_ajax_localize'); function aumcreate_author_ajax_localize() { wp_localize_script('jquery', 'aumcreate_ajax', [ 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('aumframe_interaction_nonce'), 'posts_nonce' => wp_create_nonce('aumcreate_posts_nonce'), 'profile_nonce' => wp_create_nonce('aumcreate_edit_profile'), 'is_logged_in' => is_user_logged_in(), 'login_url' => wp_login_url(get_permalink()), 'comment_nonce' => wp_create_nonce('aumcreate_comment_nonce'), 'i18n' => [ 'need_login_confirm' => __('You need to log in to follow users. Go to login?', 'aumcreate'), 'action_failed' => __('Action or request failed, please try again', 'aumcreate'), 'following' => __('Following', 'aumcreate'), 'follow' => __('Follow', 'aumcreate'), 'action_success' => __('Action succeeded', 'aumcreate'), 'network_error' => __('Network error, please check your connection and try again', 'aumcreate'), 'saving' => __('Saving...', 'aumcreate'), 'profile_updated' => __('Profile updated', 'aumcreate'), 'update_failed' => __('Update failed, please try again', 'aumcreate'), 'network_error_long' => __('Network error. Please check your connection and try again', 'aumcreate'), 'save_changes' => __('Save Changes', 'aumcreate'), 'loading_comments' => __('Loading comments...', 'aumcreate'), 'comments' => __('comments', 'aumcreate'), 'submit_failed' => __('Submission failed, please try again later', 'aumcreate'), 'replying_to' => __('Replying to', 'aumcreate'), 'cancel_reply' => __('Cancel reply', 'aumcreate'), 'load_failed' => __('Failed to load content, please refresh and try again', 'aumcreate'), 'no_content_found' => __('No content found', 'aumcreate'), 'request_failed' => __('Request failed, please try again', 'aumcreate'), 'prev_page' => __('Previous page', 'aumcreate'), 'next_page' => __('Next page', 'aumcreate'), 'login_required' => __('Please log in first before proceeding with the operation.', 'aumcreate'), 'loading' => __('Loading', 'aumcreate'), 'retry' => __('Retry', 'aumcreate'), 'select_layout' => __('Please select a layout.', 'aumcreate'), 'no_value' => __('—', 'aumcreate'), 'save_failed' => __('An error occurred while saving the template type.', 'aumcreate'), 'save_error' => __('An error occurred while saving the template type.', 'aumcreate'), 'installationNotAvailable' => __('Installation feature not available, please refresh the page', 'aumcreate'), 'activateNow' => __('Activate Now', 'aumcreate'), 'installationComplete' => __('Installation complete, preparing to activate...', 'aumcreate'), 'installationFailed' => __('Installation failed', 'aumcreate'), 'unknownError' => __('Unknown error', 'aumcreate'), 'activated' => __('Activated', 'aumcreate'), 'activationFailed' => __('Activation failed', 'aumcreate'), 'activationComplete' => __('Activation complete', 'aumcreate'), 'processing' => __('Processing', 'aumcreate'), 'installAllPlugins' => __('Install All Plugins', 'aumcreate'), 'goToSetup' => __('Go to Setup', 'aumcreate'), 'activatedPleaseSetup' => __('Activated, please click to setup', 'aumcreate'), 'processCompleted' => __('Process completed', 'aumcreate'), 'succeeded' => __('succeeded', 'aumcreate'), 'failed' => __('failed', 'aumcreate'), 'pluginsFailed' => __('Some plugins failed to process, please check and try again', 'aumcreate'), 'allPluginsProcessed' => __('All plugins processed, please refresh manually', 'aumcreate'), 'refreshPage' => __('Refresh Page', 'aumcreate'), 'processingComplete' => __('Processing complete, please check errors and refresh manually', 'aumcreate'), 'remove' => __('Remove', 'aumcreate'), 'selectImage' => __('Select Image', 'aumcreate'), 'useThisImage' => __('Use this image', 'aumcreate'), 'selectFile' => __('Please select a file', 'aumcreate'), 'importing' => __('Importing...', 'aumcreate'), 'importError' => __('Import error', 'aumcreate'), 'themeModeDisabled' => __('Enable Day/Night Mode Switch to use this option', 'aumcreate'), ], ]); } // Enqueue ajax-comment script on singular posts/pages with open comments. add_action('wp_enqueue_scripts', function () { if (is_singular() && comments_open()) { wp_enqueue_script( 'aum-ajax-comment', get_template_directory_uri() . '/assets/frontend/js/ajax-comment.js', ['jquery'], '1.0.0', true ); } }); // ========= Critical & Dynamic CSS ========= function aumcreate_critical_css() { $options = get_option('aumcreate_settings'); $light_bg = $options['light_bg'] ?? '#ffffff'; $dark_bg = $options['dark_bg'] ?? '#1a1a1a'; echo ''; } add_action('wp_head', 'aumcreate_critical_css', 0); function aumcreate_dynamic_css() { $options = get_option('aumcreate_settings'); $container_width = $options['container_width'] ?? '1600px'; $woo_container_width_raw = $options['woo_container_width'] ?? ''; $woo_container_width = !empty(trim($woo_container_width_raw)) ? $woo_container_width_raw : $container_width; $theme_mode = $options['theme_mode'] ?? 'system'; $image_aspect_ratio = $options['image_aspect_ratio'] ?? '4x3'; $light_bg = $options['light_bg'] ?? '#ffffff'; $dark_bg = $options['dark_bg'] ?? '#1a1a1a'; $light_text_on_bg = aumcreate_get_soft_contrast_color($light_bg); $dark_text_on_bg = aumcreate_get_soft_contrast_color($dark_bg); /* * **Text on the primary colour** — a different thing from `--text-on-bg`; do not conflate them. * * The `--text-on-color` variable answers "what colour of text goes on the page's ground" and follows **the * mode**: white in dark mode. But a fluorescent green brand colour **is light** in dark mode, and white * text on it disappears — which is exactly how the back-to-top button went blind * (`background: var(--primary); color: #fff`): an invisible white arrow inside a green circle. * * So this is computed from **the primary colour's own luminance** rather than the mode. * Things in Woo's blocks whose ground is the primary colour (the order button, the quantity badge) use it * too. (Found on the checkout page on 2026-08-22.) */ $light_primary = $options['light_primary'] ?? '#3366cc'; $dark_primary = $options['dark_primary'] ?? '#6699ff'; // ⚠️ ink_on rather than get_soft_contrast_color — the latter does not guarantee contrast, and a // fluorescent green would produce another pale green (see the note in inc/helpers/color.php). // It is **the same function** as Aumframe's {color.brand-ink}: a site can only have one answer. $light_on_primary = aumcreate_aumframe_ink_on($light_primary); $dark_on_primary = aumcreate_aumframe_ink_on($dark_primary); /* * **Text on the secondary colour** — the same argument as `--text-on-primary`, for the token that had * no answer at all. * * `--secondary` is used as a *ground* in five places (the tag pills and the pagination chips), while the * foreground on those grounds was `--text-on-bg` — the colour computed for the *page* ground. The two * tokens are unrelated, so the contrast of a pagination chip was whatever the buyer's two colour picks * happened to produce. Measured on 2026-09-04 with `workflow/tools/contrast-audit.js`: a brass secondary * with a deep-indigo text-on-bg gives **2.86:1**, and the current-page chip (ground `--primary`) gives * **2.52:1**. Both need 4.5. * * Whatever token paints the ground, the foreground has to be the one computed **from that token's own * luminance**. Same function as `{color.brand-ink}` — a site can only have one answer. */ $light_secondary = $options['light_secondary'] ?? '#eeeeee'; $dark_secondary = $options['dark_secondary'] ?? '#666666'; $light_on_secondary = aumcreate_aumframe_ink_on($light_secondary); $dark_on_secondary = aumcreate_aumframe_ink_on($dark_secondary); $aspect_ratio_value = ($image_aspect_ratio === '3x4') ? '3 / 4' : '4 / 3'; echo ''; echo ''; } // Priority 1 ensures dynamic CSS loads before other wp_head output. function aumcreate_force_css_priority() { remove_action('wp_head', 'aumcreate_dynamic_css'); add_action('wp_head', 'aumcreate_dynamic_css', 1); } add_action('init', 'aumcreate_force_css_priority'); // ========= Theme Mode Script ========= add_action('wp_enqueue_scripts', 'aumcreate_enqueue_theme_mode_script', 5); function aumcreate_enqueue_theme_mode_script() { if (!is_admin()) { wp_enqueue_script( 'aumcreate-theme-mode', get_theme_file_uri('assets/common/js/theme-mode.js'), [], filemtime(get_theme_file_path('assets/common/js/theme-mode.js')), true ); wp_enqueue_script('aum-animate', get_template_directory_uri() . '/assets/common/js/aum-animate.js', [], null, true); } } /** * Synchronously resolve the effective theme (light/dark) before first paint. * * Output a tiny inline script at the very top of so the browser knows * the active theme on the first style calculation. Without this, CSS variables * scoped to [data-theme="..."] resolve to undefined until theme-mode.js runs * in the footer, causing a visible style swap (FOUC). * * Reads the same sources as theme-mode.js (localStorage > server default > * "system") so the result is identical and no second pass is needed. */ add_action('wp_head', 'aumcreate_inline_theme_resolver', 0); function aumcreate_inline_theme_resolver() { $options = get_option('aumcreate_settings'); $default_mode = $options['theme_mode'] ?? 'system'; ?> admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('aumcreate_posts_nonce'), 'is_logged_in' => is_user_logged_in(), 'i18n' => aumcreate_get_frontend_i18n(), ]); } /* * Registered outside the takeover guard below, because the floating column it belongs to renders on * every page — taken over or not. Enqueued by whoever draws the button. */ add_action('wp_enqueue_scripts', 'aumcreate_register_floating_script'); function aumcreate_register_floating_script() { wp_register_script( 'aum-floating', get_theme_file_uri('assets/frontend/js/aum-floating.js'), [], filemtime(get_theme_file_path('assets/frontend/js/aum-floating.js')), true ); } // ========= Frontend Scripts ========= add_action('wp_enqueue_scripts', 'aumcreate_enqueue_frontend_scripts'); function aumcreate_enqueue_frontend_scripts() { /* * On a page Aumframe takes over, none of the theme's templates render — the DOM these scripts operate on * simply does not exist. Without skipping them, a taken-over page downloads aum.js, imagesloaded and the * jQuery plus jquery-migrate they drag along: roughly 130KB of dead code. * * The test comes from the plugin (aumframe_is_taking_over), while **which scripts to skip is the theme's * decision** — only the theme knows what each one does. theme-mode.js, for instance, cannot be skipped: it * writes data-theme onto , and the editor's entire palette depends on that attribute. */ if (function_exists('aumframe_is_taking_over') && aumframe_is_taking_over()) { return; } wp_enqueue_script('aum-custom-js', get_template_directory_uri() . '/assets/frontend/js/aum.js', ['jquery'], '1.0.0', true); wp_enqueue_script('imagesloaded'); } add_action('wp_enqueue_scripts', 'aumcreate_enqueue_search_scripts'); /** * Register the search script; let whoever renders a form ask for it. * * This used to bail out early on a taken-over page, with the comment "the search box is a component of * the theme's header and does not exist on a taken-over page". That was true right up until * `[aumframe_search]` was added **precisely so a taken-over layout could hold a search box** — and the guard * was never revisited. The result: on the only pages the shortcode exists for, its script never loaded. * * Registering rather than enqueueing unconditionally keeps it off pages with no search box at all; * the shortcode enqueues it, and `canvas.php` calls `wp_footer()`, so a footer script asked for during * rendering still prints. */ function aumcreate_enqueue_search_scripts() { wp_register_script( 'aum-search', get_theme_file_uri('assets/frontend/js/aum-search.js'), [], filemtime(get_theme_file_path('assets/frontend/js/aum-search.js')), true ); } add_action('wp_enqueue_scripts', 'aumcreate_enqueue_gallery_scripts'); function aumcreate_enqueue_gallery_scripts() { if (!is_singular('post')) return; $template = get_post_meta(get_the_ID(), '_aumcreate_post_template', true); if ($template !== 'gallery') return; wp_enqueue_style( 'swiper-bundle-css', get_template_directory_uri() . '/assets/frontend/css/swiper-bundle.min.css', [], '1.0.0' ); wp_enqueue_script( 'swiper-bundle-js', get_template_directory_uri() . '/assets/frontend/js/swiper-bundle.min.js', ['jquery'], '1.0.0', true ); } // ========= Comment Form Nonce ========= add_action('comment_form', function () { if (empty(get_post())) return; wp_nonce_field('comment_form_' . get_the_ID(), '_wpnonce'); }); // ========= Async Stats Script ========= add_filter('script_loader_tag', 'async_stats_script', 10, 3); function async_stats_script($tag, $handle, $src) { if (strpos($src, 'stats.wp.com/w.js') !== false) { return str_replace('