From 7728104a7ce66a2e97e0a055e879c5d6ae6951a5 Mon Sep 17 00:00:00 2001 From: WPAuroraAdmin Date: Sun, 26 Jul 2026 16:15:24 -0500 Subject: [PATCH] Initial commit - Aurora Admin v1.0 Co-Authored-By: NixReaper --- .gitignore | 8 + CHANGELOG.md | 53 + admin/assets/native-dark.css | 563 +++++++ admin/assets/native-enhance.js | 71 + admin/src/ActivityLog/ActivityLog.php | 226 +++ admin/src/ActivityLog/ActivityLogData.php | 145 ++ admin/src/Analytics/Analytics.php | 199 +++ admin/src/App/AuroraAdmin.php | 118 ++ admin/src/Code/CodeInjection.php | 72 + admin/src/Editor/DisableGutenberg.php | 31 + admin/src/ImageFormats/ImageFormats.php | 346 +++++ admin/src/Login/LoginRedirect.php | 110 ++ admin/src/Login/LoginStyle.php | 137 ++ admin/src/Media/MediaFolders.php | 392 +++++ admin/src/Media/SvgUploads.php | 147 ++ admin/src/MenuCreator/MenuCreator.php | 192 +++ admin/src/MenuCreator/MenuCreatorData.php | 225 +++ admin/src/Notices/Notices.php | 143 ++ admin/src/Notices/NoticesData.php | 153 ++ admin/src/Options/Settings.php | 115 ++ admin/src/Pages/AppearancePages.php | 59 + admin/src/Pages/Dashboard.php | 71 + admin/src/Pages/DashboardTakeover.php | 93 ++ admin/src/Pages/ExportPage.php | 39 + admin/src/Pages/HijackedNativePages.php | 151 ++ admin/src/Pages/ImportPage.php | 137 ++ admin/src/Pages/NativeSettingsPages.php | 327 ++++ admin/src/Pages/ScreenHijack.php | 159 ++ admin/src/Pages/SettingsPage.php | 84 + admin/src/Pages/SetupWizard.php | 151 ++ admin/src/Pages/SubApps.php | 183 +++ admin/src/Pages/ToolsPage.php | 72 + admin/src/Posts/ScheduledDayFilter.php | 74 + admin/src/Rest/AnalyticsData.php | 136 ++ admin/src/Rest/BugReportData.php | 72 + admin/src/Rest/CommentsData.php | 209 +++ admin/src/Rest/CompanionPluginData.php | 157 ++ admin/src/Rest/DashboardData.php | 292 ++++ admin/src/Rest/DiscussionData.php | 142 ++ admin/src/Rest/MediaData.php | 237 +++ admin/src/Rest/MediaFolderData.php | 152 ++ admin/src/Rest/NavMenusData.php | 336 ++++ admin/src/Rest/PagesData.php | 230 +++ admin/src/Rest/PermalinksData.php | 64 + admin/src/Rest/PluginsData.php | 258 ++++ admin/src/Rest/PostsData.php | 231 +++ admin/src/Rest/ProfileData.php | 56 + admin/src/Rest/ThemesData.php | 108 ++ admin/src/Rest/UsersData.php | 142 ++ admin/src/Rest/WidgetsData.php | 196 +++ admin/src/Roles/RolesData.php | 174 +++ admin/src/Security/Security.php | 152 ++ admin/src/Shell/MenuSerializer.php | 255 +++ admin/src/Shell/Shell.php | 276 ++++ admin/src/Utility/Assets.php | 96 ++ admin/src/WhiteLabel/WhiteLabel.php | 100 ++ app/package-lock.json | 1363 +++++++++++++++++ app/package.json | 17 + app/src/components/ProgressBar.vue | 60 + app/src/components/Toggle.vue | 84 + app/src/composables/useJobPolling.js | 98 ++ app/src/entries/activity-log.js | 10 + app/src/entries/add-user.js | 10 + app/src/entries/admin-notices.js | 10 + app/src/entries/bug-report.js | 10 + app/src/entries/comments.js | 10 + app/src/entries/dashboard.js | 12 + app/src/entries/export.js | 10 + app/src/entries/import.js | 10 + app/src/entries/media-upload.js | 10 + app/src/entries/media.js | 10 + app/src/entries/menu-creator.js | 10 + app/src/entries/modules.js | 10 + app/src/entries/native-settings.js | 10 + app/src/entries/nav-menus.js | 10 + app/src/entries/pages.js | 10 + app/src/entries/plugins.js | 10 + app/src/entries/posts.js | 10 + app/src/entries/profile.js | 10 + app/src/entries/role-editor.js | 10 + app/src/entries/settings.js | 10 + app/src/entries/setup-wizard.js | 10 + app/src/entries/shell.js | 10 + app/src/entries/themes.js | 10 + app/src/entries/tools.js | 10 + app/src/entries/users.js | 10 + app/src/entries/widgets.js | 10 + app/src/pages/_shared/AppListItem.vue | 86 ++ app/src/pages/_shared/AppListShell.vue | 204 +++ app/src/pages/activity-log/ActivityLog.vue | 324 ++++ app/src/pages/admin-notices/AdminNotices.vue | 217 +++ app/src/pages/bug-report/BugReport.vue | 128 ++ app/src/pages/comments/Comments.vue | 701 +++++++++ app/src/pages/dashboard/Dashboard.vue | 259 ++++ app/src/pages/dashboard/cards/ActiveUsers.vue | 35 + app/src/pages/dashboard/cards/DeviceUsage.vue | 18 + app/src/pages/dashboard/cards/Engagement.vue | 18 + .../pages/dashboard/cards/MediaAnalytics.vue | 61 + app/src/pages/dashboard/cards/PageViews.vue | 26 + .../pages/dashboard/cards/RecentComments.vue | 43 + .../pages/dashboard/cards/RecentContent.vue | 72 + .../dashboard/cards/ScheduledContent.vue | 169 ++ .../pages/dashboard/cards/ServerHealth.vue | 71 + app/src/pages/dashboard/cards/TopList.vue | 17 + .../pages/dashboard/cards/UserAnalytics.vue | 27 + .../pages/dashboard/components/AreaChart.vue | 71 + .../pages/dashboard/components/AuroraCard.vue | 75 + .../pages/dashboard/components/BarList.vue | 44 + .../dashboard/components/ProgressBar.vue | 75 + .../pages/dashboard/components/StatRow.vue | 41 + app/src/pages/dashboard/useAnalyticsData.js | 34 + app/src/pages/dashboard/useDashboardData.js | 39 + app/src/pages/export/Export.vue | 222 +++ app/src/pages/import/Import.vue | 178 +++ app/src/pages/media-upload/MediaUpload.vue | 148 ++ app/src/pages/media/Media.vue | 838 ++++++++++ app/src/pages/media/MediaFolderTree.vue | 193 +++ app/src/pages/menu-creator/MenuCreator.vue | 313 ++++ app/src/pages/modules/Modules.vue | 144 ++ app/src/pages/modules/companionPlugins.js | 39 + .../pages/native-settings/NativeSettings.vue | 215 +++ app/src/pages/nav-menus/NavMenus.vue | 437 ++++++ app/src/pages/pages/Pages.vue | 622 ++++++++ app/src/pages/plugins/Plugins.vue | 698 +++++++++ app/src/pages/posts/Posts.vue | 624 ++++++++ app/src/pages/profile/Profile.vue | 205 +++ app/src/pages/role-editor/RoleEditor.vue | 255 +++ .../role-editor/capabilityDescriptions.js | 101 ++ app/src/pages/settings/Settings.vue | 402 +++++ app/src/pages/settings/applyLiveTheme.js | 26 + .../settings/components/SettingsField.vue | 409 +++++ app/src/pages/settings/googleFonts.js | 79 + app/src/pages/settings/settingsConfig.js | 560 +++++++ app/src/pages/settings/themePresets.js | 103 ++ app/src/pages/setup-wizard/SetupWizard.vue | 311 ++++ app/src/pages/themes/Themes.vue | 162 ++ app/src/pages/tools/Tools.vue | 76 + app/src/pages/users/AddUser.vue | 152 ++ app/src/pages/users/Users.vue | 569 +++++++ app/src/pages/widgets/Widgets.vue | 144 ++ app/src/shell/SiteShell.vue | 70 + app/src/shell/components/SearchModal.vue | 231 +++ app/src/shell/components/Sidebar.vue | 530 +++++++ app/src/shell/components/Toolbar.vue | 167 ++ app/src/shell/components/UserMenu.vue | 194 +++ app/src/shell/dashboardFrame.js | 35 + app/src/shell/shell-frame.css | 458 ++++++ app/vite.config.js | 70 + aurora-admin.php | 74 + readme.txt | 95 ++ 150 files changed, 24005 insertions(+) create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 admin/assets/native-dark.css create mode 100644 admin/assets/native-enhance.js create mode 100644 admin/src/ActivityLog/ActivityLog.php create mode 100644 admin/src/ActivityLog/ActivityLogData.php create mode 100644 admin/src/Analytics/Analytics.php create mode 100644 admin/src/App/AuroraAdmin.php create mode 100644 admin/src/Code/CodeInjection.php create mode 100644 admin/src/Editor/DisableGutenberg.php create mode 100644 admin/src/ImageFormats/ImageFormats.php create mode 100644 admin/src/Login/LoginRedirect.php create mode 100644 admin/src/Login/LoginStyle.php create mode 100644 admin/src/Media/MediaFolders.php create mode 100644 admin/src/Media/SvgUploads.php create mode 100644 admin/src/MenuCreator/MenuCreator.php create mode 100644 admin/src/MenuCreator/MenuCreatorData.php create mode 100644 admin/src/Notices/Notices.php create mode 100644 admin/src/Notices/NoticesData.php create mode 100644 admin/src/Options/Settings.php create mode 100644 admin/src/Pages/AppearancePages.php create mode 100644 admin/src/Pages/Dashboard.php create mode 100644 admin/src/Pages/DashboardTakeover.php create mode 100644 admin/src/Pages/ExportPage.php create mode 100644 admin/src/Pages/HijackedNativePages.php create mode 100644 admin/src/Pages/ImportPage.php create mode 100644 admin/src/Pages/NativeSettingsPages.php create mode 100644 admin/src/Pages/ScreenHijack.php create mode 100644 admin/src/Pages/SettingsPage.php create mode 100644 admin/src/Pages/SetupWizard.php create mode 100644 admin/src/Pages/SubApps.php create mode 100644 admin/src/Pages/ToolsPage.php create mode 100644 admin/src/Posts/ScheduledDayFilter.php create mode 100644 admin/src/Rest/AnalyticsData.php create mode 100644 admin/src/Rest/BugReportData.php create mode 100644 admin/src/Rest/CommentsData.php create mode 100644 admin/src/Rest/CompanionPluginData.php create mode 100644 admin/src/Rest/DashboardData.php create mode 100644 admin/src/Rest/DiscussionData.php create mode 100644 admin/src/Rest/MediaData.php create mode 100644 admin/src/Rest/MediaFolderData.php create mode 100644 admin/src/Rest/NavMenusData.php create mode 100644 admin/src/Rest/PagesData.php create mode 100644 admin/src/Rest/PermalinksData.php create mode 100644 admin/src/Rest/PluginsData.php create mode 100644 admin/src/Rest/PostsData.php create mode 100644 admin/src/Rest/ProfileData.php create mode 100644 admin/src/Rest/ThemesData.php create mode 100644 admin/src/Rest/UsersData.php create mode 100644 admin/src/Rest/WidgetsData.php create mode 100644 admin/src/Roles/RolesData.php create mode 100644 admin/src/Security/Security.php create mode 100644 admin/src/Shell/MenuSerializer.php create mode 100644 admin/src/Shell/Shell.php create mode 100644 admin/src/Utility/Assets.php create mode 100644 admin/src/WhiteLabel/WhiteLabel.php create mode 100644 app/package-lock.json create mode 100644 app/package.json create mode 100644 app/src/components/ProgressBar.vue create mode 100644 app/src/components/Toggle.vue create mode 100644 app/src/composables/useJobPolling.js create mode 100644 app/src/entries/activity-log.js create mode 100644 app/src/entries/add-user.js create mode 100644 app/src/entries/admin-notices.js create mode 100644 app/src/entries/bug-report.js create mode 100644 app/src/entries/comments.js create mode 100644 app/src/entries/dashboard.js create mode 100644 app/src/entries/export.js create mode 100644 app/src/entries/import.js create mode 100644 app/src/entries/media-upload.js create mode 100644 app/src/entries/media.js create mode 100644 app/src/entries/menu-creator.js create mode 100644 app/src/entries/modules.js create mode 100644 app/src/entries/native-settings.js create mode 100644 app/src/entries/nav-menus.js create mode 100644 app/src/entries/pages.js create mode 100644 app/src/entries/plugins.js create mode 100644 app/src/entries/posts.js create mode 100644 app/src/entries/profile.js create mode 100644 app/src/entries/role-editor.js create mode 100644 app/src/entries/settings.js create mode 100644 app/src/entries/setup-wizard.js create mode 100644 app/src/entries/shell.js create mode 100644 app/src/entries/themes.js create mode 100644 app/src/entries/tools.js create mode 100644 app/src/entries/users.js create mode 100644 app/src/entries/widgets.js create mode 100644 app/src/pages/_shared/AppListItem.vue create mode 100644 app/src/pages/_shared/AppListShell.vue create mode 100644 app/src/pages/activity-log/ActivityLog.vue create mode 100644 app/src/pages/admin-notices/AdminNotices.vue create mode 100644 app/src/pages/bug-report/BugReport.vue create mode 100644 app/src/pages/comments/Comments.vue create mode 100644 app/src/pages/dashboard/Dashboard.vue create mode 100644 app/src/pages/dashboard/cards/ActiveUsers.vue create mode 100644 app/src/pages/dashboard/cards/DeviceUsage.vue create mode 100644 app/src/pages/dashboard/cards/Engagement.vue create mode 100644 app/src/pages/dashboard/cards/MediaAnalytics.vue create mode 100644 app/src/pages/dashboard/cards/PageViews.vue create mode 100644 app/src/pages/dashboard/cards/RecentComments.vue create mode 100644 app/src/pages/dashboard/cards/RecentContent.vue create mode 100644 app/src/pages/dashboard/cards/ScheduledContent.vue create mode 100644 app/src/pages/dashboard/cards/ServerHealth.vue create mode 100644 app/src/pages/dashboard/cards/TopList.vue create mode 100644 app/src/pages/dashboard/cards/UserAnalytics.vue create mode 100644 app/src/pages/dashboard/components/AreaChart.vue create mode 100644 app/src/pages/dashboard/components/AuroraCard.vue create mode 100644 app/src/pages/dashboard/components/BarList.vue create mode 100644 app/src/pages/dashboard/components/ProgressBar.vue create mode 100644 app/src/pages/dashboard/components/StatRow.vue create mode 100644 app/src/pages/dashboard/useAnalyticsData.js create mode 100644 app/src/pages/dashboard/useDashboardData.js create mode 100644 app/src/pages/export/Export.vue create mode 100644 app/src/pages/import/Import.vue create mode 100644 app/src/pages/media-upload/MediaUpload.vue create mode 100644 app/src/pages/media/Media.vue create mode 100644 app/src/pages/media/MediaFolderTree.vue create mode 100644 app/src/pages/menu-creator/MenuCreator.vue create mode 100644 app/src/pages/modules/Modules.vue create mode 100644 app/src/pages/modules/companionPlugins.js create mode 100644 app/src/pages/native-settings/NativeSettings.vue create mode 100644 app/src/pages/nav-menus/NavMenus.vue create mode 100644 app/src/pages/pages/Pages.vue create mode 100644 app/src/pages/plugins/Plugins.vue create mode 100644 app/src/pages/posts/Posts.vue create mode 100644 app/src/pages/profile/Profile.vue create mode 100644 app/src/pages/role-editor/RoleEditor.vue create mode 100644 app/src/pages/role-editor/capabilityDescriptions.js create mode 100644 app/src/pages/settings/Settings.vue create mode 100644 app/src/pages/settings/applyLiveTheme.js create mode 100644 app/src/pages/settings/components/SettingsField.vue create mode 100644 app/src/pages/settings/googleFonts.js create mode 100644 app/src/pages/settings/settingsConfig.js create mode 100644 app/src/pages/settings/themePresets.js create mode 100644 app/src/pages/setup-wizard/SetupWizard.vue create mode 100644 app/src/pages/themes/Themes.vue create mode 100644 app/src/pages/tools/Tools.vue create mode 100644 app/src/pages/users/AddUser.vue create mode 100644 app/src/pages/users/Users.vue create mode 100644 app/src/pages/widgets/Widgets.vue create mode 100644 app/src/shell/SiteShell.vue create mode 100644 app/src/shell/components/SearchModal.vue create mode 100644 app/src/shell/components/Sidebar.vue create mode 100644 app/src/shell/components/Toolbar.vue create mode 100644 app/src/shell/components/UserMenu.vue create mode 100644 app/src/shell/dashboardFrame.js create mode 100644 app/src/shell/shell-frame.css create mode 100644 app/vite.config.js create mode 100644 aurora-admin.php create mode 100644 readme.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..98207ba --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +app/node_modules/ +app/dist/ +dist/ +wporg-submission/ +admin/src/Licensing/integrity-manifest.json +*.log +.DS_Store +Thumbs.db diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ece1cd8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,53 @@ +# Changelog + +All notable changes to Aurora Admin, newest first — one entry per version bump. + +## [1.0] — 2026-07-26 + +First public release. + +### Admin experience +- Full modern admin shell — fixed sidebar, toolbar, and global search — in + place of the standard WordPress admin chrome. +- Real, functional replacement screens (not visual reskins) for: Dashboard, + Posts, Pages, Media, Users, Comments, Add User, Media Upload, Profile, + Plugins, Nav Menus, Widgets, Themes, the five core Settings screens + (General, Writing, Reading, Discussion, Permalinks), Tools (Available + Tools), Export, and Import. +- Sub-apps: Aurora Settings, Setup Wizard, Menu Creator (with drag-to-reorder + and native menu-item preservation), Role Editor, Admin Notices, Activity + Log, and Report a Bug. +- **Modules**: a one-click addon marketplace for Aurora's standalone + companion plugins (File Manager, Database Explorer, Site Backup). +- **Login Redirect**: optional, off-by-default redirects — send logged-out + wp-admin visitors to a custom URL instead of the login screen, and/or send + specific roles to a custom URL after logging in instead of the dashboard. + Never touches wp-login.php itself. +- **Disable Gutenberg**: an opt-in toggle that switches every post type to + WordPress's classic editor instead of the block editor. +- **Activity Log**: a filterable table (Date, Author, IP Address, Type, + Action, Description) with separate Admin and User tabs; the Author column + shows the user's avatar, role, and email. +- Media Folders and White Label rebranding for client sites. +- Screens intentionally left native rather than rebuilt: the block editor + and the Theme/Plugin File Editor — both dark-themed for legibility, since + full file-editing/code-editor functionality isn't accepted into the + WordPress.org Plugin Directory. + +### Theming +- A real dark mode covering the entire admin, including every native screen + Aurora frames rather than rebuilds (Site Health, Export/Erase Personal + Data, Updates, the Theme/Plugin File Editor). +- Live theme presets — 10 color accents plus 2 flat neutrals — applied + across the whole interface, plus an optional (off-by-default) Google Font + for the interface typeface. +- Rounded, framed content cards with a soft shadow; scroll areas sit inside + the rounded frame instead of squaring off its corners, and scrollbars are + tinted with the active theme accent. + +### Under the hood +- REST API namespace `aurora-admin/v1` backing every takeover screen. +- Security & performance hardening module, custom admin CSS/JS injection, + and a login-screen customizer. + +Everything above is free — no license key, no locked features, no upsell. diff --git a/admin/assets/native-dark.css b/admin/assets/native-dark.css new file mode 100644 index 0000000..6f2a851 --- /dev/null +++ b/admin/assets/native-dark.css @@ -0,0 +1,563 @@ +/* + * Native WordPress admin dark theme. + * + * Loaded ONLY on native (non-Aurora) admin pages, and every rule is gated + * on html.aurora-dark so it is completely inert in light mode. It restyles + * the common WordPress admin elements to match Aurora's dark palette using + * the --aurora-* variables defined in the shell stylesheet. + * + * Scope is kept under #wpcontent / #wpbody-content so it never reaches the + * Aurora sidebar/toolbar (which live in their own root) and cannot leak to + * the front end. This is the one intentionally-large styling surface, kept + * isolated in a single file on purpose. + */ + +html.aurora-dark.aurora-ready #wpbody-content, +html.aurora-dark.aurora-ready #wpbody-content .wrap { + color: var(--aurora-text); +} + +/* Headings & text -------------------------------------------------------- */ +html.aurora-dark.aurora-ready #wpbody-content h1, +html.aurora-dark.aurora-ready #wpbody-content h2, +html.aurora-dark.aurora-ready #wpbody-content h3, +html.aurora-dark.aurora-ready #wpbody-content h4, +html.aurora-dark.aurora-ready #wpbody-content h5, +html.aurora-dark.aurora-ready #wpbody-content h6, +html.aurora-dark.aurora-ready #wpbody-content p, +html.aurora-dark.aurora-ready #wpbody-content li, +html.aurora-dark.aurora-ready #wpbody-content dt, +html.aurora-dark.aurora-ready #wpbody-content dd, +html.aurora-dark.aurora-ready #wpbody-content label, +html.aurora-dark.aurora-ready #wpbody-content th, +html.aurora-dark.aurora-ready #wpbody-content td, +html.aurora-dark.aurora-ready #wpbody-content strong { + color: var(--aurora-text); +} + +html.aurora-dark.aurora-ready #wpbody-content .description, +html.aurora-dark.aurora-ready #wpbody-content .howto, +html.aurora-dark.aurora-ready #wpbody-content .subtitle, +html.aurora-dark.aurora-ready #wpbody-content .wp-heading-inline + .subtitle { + color: var(--aurora-text-muted); +} + +html.aurora-dark.aurora-ready #wpbody-content a { + color: var(--aurora-accent); +} + +/* Postboxes, cards, metaboxes ------------------------------------------- */ +html.aurora-dark.aurora-ready #wpbody-content .postbox, +html.aurora-dark.aurora-ready #wpbody-content .card, +html.aurora-dark.aurora-ready #wpbody-content .welcome-panel, +html.aurora-dark.aurora-ready #wpbody-content .wp-list-table, +/* .widefat: WP's general "wide flat table" utility class, used beyond just + WP_List_Table screens — e.g. update-core.php's plugin/theme update + tables (class="widefat updates-table", no .wp-list-table and no + .striped either), which were completely uncovered without this. */ +html.aurora-dark.aurora-ready #wpbody-content .widefat, +html.aurora-dark.aurora-ready #wpbody-content .stuffbox { + background: var(--aurora-card-bg); + border-color: var(--aurora-card-border); + color: var(--aurora-text); + box-shadow: none; +} + +html.aurora-dark.aurora-ready #wpbody-content .postbox-header, +html.aurora-dark.aurora-ready #wpbody-content .postbox .hndle, +html.aurora-dark.aurora-ready #wpbody-content .postbox h2, +html.aurora-dark.aurora-ready #wpbody-content .postbox h3 { + border-color: var(--aurora-card-border); + color: var(--aurora-text); +} + +html.aurora-dark.aurora-ready #wpbody-content .inside, +html.aurora-dark.aurora-ready #wpbody-content .welcome-panel-content { + color: var(--aurora-text); +} + +html.aurora-dark.aurora-ready #wpbody-content .handlediv, +html.aurora-dark.aurora-ready #wpbody-content .postbox .handle-order-higher, +html.aurora-dark.aurora-ready #wpbody-content .postbox .handle-order-lower { + color: var(--aurora-text-muted); +} + +/* Tables ---------------------------------------------------------------- */ +html.aurora-dark.aurora-ready #wpbody-content .wp-list-table th, +html.aurora-dark.aurora-ready #wpbody-content .wp-list-table td, +html.aurora-dark.aurora-ready #wpbody-content .widefat th, +html.aurora-dark.aurora-ready #wpbody-content .widefat td { + border-color: var(--aurora-border); + /* Core sets an explicit white background directly on (not just the + row), which otherwise wins over .wp-list-table/.widefat's own dark + background above — transparent lets the row's actual background + (plain dark, or the striped-odd shade below) show through instead of a + white cell sitting on an otherwise-dark row. Seen first on Export/ + Erase Personal Data's request-queue table, but this is a generic + WP_List_Table/.widefat gap, not specific to any one screen. */ + background: transparent; +} + +html.aurora-dark.aurora-ready #wpbody-content .wp-list-table thead th, +html.aurora-dark.aurora-ready #wpbody-content .wp-list-table tfoot th, +html.aurora-dark.aurora-ready #wpbody-content .wp-list-table thead th a, +html.aurora-dark.aurora-ready #wpbody-content .wp-list-table tfoot th a, +html.aurora-dark.aurora-ready #wpbody-content .widefat thead th, +html.aurora-dark.aurora-ready #wpbody-content .widefat tfoot th, +html.aurora-dark.aurora-ready #wpbody-content .widefat thead th a, +html.aurora-dark.aurora-ready #wpbody-content .widefat tfoot th a { + color: var(--aurora-text); +} + +/* core's list-tables.css sets `.plugins tr { background: #fff; }` for the + native Plugins screen's list table — .plugins is a plain class name, not + scoped to that screen, so it also matches update-core.php's plugin-update + table (its reuses the same class). Aurora's own Plugins screen is + a full custom rebuild that never hits this native markup, which is why + this collision hadn't surfaced before update-core.php. */ +html.aurora-dark.aurora-ready #wpbody-content .plugins tr { + background: transparent; +} + +/* Theme File Editor / Plugin File Editor (theme-editor.php, plugin-editor.php) + share the same #templateside file-tree component. Core sets an explicit + light background directly on `#templateside > ul` (not just inherited from + the page), which nothing else here targets — combined with this file's own + light text/link colors (meant for a dark surface), that produced light text + on a light background, i.e. effectively unreadable rather than just + unstyled. Both screens are deliberately left as framed native content (not + rebuilt), same as Site Health/Updates, but still need to be legible. */ +html.aurora-dark.aurora-ready #wpbody-content #templateside > ul { + background: transparent; +} + +html.aurora-dark.aurora-ready #wpbody-content .wp-list-table.striped > tbody > :nth-child(odd), +html.aurora-dark.aurora-ready #wpbody-content .striped > tbody > :nth-child(odd) { + background: var(--aurora-bg-subtle); +} + +html.aurora-dark.aurora-ready #wpbody-content .wp-list-table tbody tr:hover { + background: var(--aurora-accent-soft); +} + +html.aurora-dark.aurora-ready #wpbody-content .row-actions { + color: var(--aurora-text-muted); +} + +/* Form fields ----------------------------------------------------------- */ +/* !important: some native screens style specific fields via a 2-ID + selector (e.g. core's `#titlediv #title { background: #fff }` on the + classic/attachment edit screen's title field) — that beats this rule's + single #wpbody-content id no matter how many classes we stack on it, + same specificity trap as the earlier block-editor-page background bug. + There's no legitimate case where a native admin form field should stay + white, so forcing it here is safe across every screen, not just the + one that surfaced it. */ +html.aurora-dark.aurora-ready #wpbody-content input[type='text'], +html.aurora-dark.aurora-ready #wpbody-content input[type='search'], +html.aurora-dark.aurora-ready #wpbody-content input[type='email'], +html.aurora-dark.aurora-ready #wpbody-content input[type='url'], +html.aurora-dark.aurora-ready #wpbody-content input[type='password'], +html.aurora-dark.aurora-ready #wpbody-content input[type='number'], +html.aurora-dark.aurora-ready #wpbody-content input[type='tel'], +html.aurora-dark.aurora-ready #wpbody-content input[type='date'], +html.aurora-dark.aurora-ready #wpbody-content textarea, +html.aurora-dark.aurora-ready #wpbody-content select { + background: var(--aurora-bg-subtle) !important; + border-color: var(--aurora-border) !important; + color: var(--aurora-text) !important; +} + +html.aurora-dark.aurora-ready #wpbody-content input::placeholder, +html.aurora-dark.aurora-ready #wpbody-content textarea::placeholder { + color: var(--aurora-text-muted); +} + +html.aurora-dark.aurora-ready #wpbody-content input[type='text']:focus, +html.aurora-dark.aurora-ready #wpbody-content input[type='search']:focus, +html.aurora-dark.aurora-ready #wpbody-content textarea:focus, +html.aurora-dark.aurora-ready #wpbody-content select:focus { + border-color: var(--aurora-accent); + box-shadow: 0 0 0 1px var(--aurora-accent); + outline: none; +} + +/* Buttons --------------------------------------------------------------- */ +html.aurora-dark.aurora-ready #wpbody-content .button, +html.aurora-dark.aurora-ready #wpbody-content .button-secondary { + background: var(--aurora-bg-subtle); + border-color: var(--aurora-border); + color: var(--aurora-text); +} + +html.aurora-dark.aurora-ready #wpbody-content .button:hover, +html.aurora-dark.aurora-ready #wpbody-content .button-secondary:hover { + background: var(--aurora-border); + border-color: var(--aurora-text-muted); + color: var(--aurora-text); +} + +html.aurora-dark.aurora-ready #wpbody-content .button-primary { + background: var(--aurora-accent); + border-color: var(--aurora-accent); + color: #fff; + text-shadow: none; + box-shadow: none; +} + +html.aurora-dark.aurora-ready #wpbody-content .button-primary:hover { + filter: brightness(1.1); +} + +/* Nav tabs -------------------------------------------------------------- */ +html.aurora-dark.aurora-ready #wpbody-content .nav-tab-wrapper, +html.aurora-dark.aurora-ready #wpbody-content .wp-filter { + background: var(--aurora-card-bg); + border-color: var(--aurora-border); +} + +html.aurora-dark.aurora-ready #wpbody-content .wp-filter .filter-links a { + color: var(--aurora-accent); +} + +html.aurora-dark.aurora-ready #wpbody-content .wp-filter .filter-links .current { + color: var(--aurora-text); +} + +html.aurora-dark.aurora-ready #wpbody-content .wp-filter .search-form input[type='search'] { + background: var(--aurora-bg-subtle); + border-color: var(--aurora-border); + color: var(--aurora-text); +} + +html.aurora-dark.aurora-ready #wpbody-content .nav-tab { + background: var(--aurora-bg-subtle); + border-color: var(--aurora-border); + color: var(--aurora-text-muted); +} + +html.aurora-dark.aurora-ready #wpbody-content .nav-tab-active { + background: var(--aurora-card-bg); + border-bottom-color: var(--aurora-card-bg); + color: var(--aurora-text); +} + +/* Notices --------------------------------------------------------------- */ +html.aurora-dark.aurora-ready #wpbody-content .notice, +html.aurora-dark.aurora-ready #wpbody-content div.updated, +html.aurora-dark.aurora-ready #wpbody-content div.error { + background: var(--aurora-card-bg); + color: var(--aurora-text); +} + +/* Code ------------------------------------------------------------------ */ +html.aurora-dark.aurora-ready #wpbody-content code, +html.aurora-dark.aurora-ready #wpbody-content kbd, +html.aurora-dark.aurora-ready #wpbody-content pre { + background: var(--aurora-bg-subtle); + color: var(--aurora-text); +} + +/* Tablenav / pagination / filters --------------------------------------- */ +html.aurora-dark.aurora-ready #wpbody-content .tablenav, +html.aurora-dark.aurora-ready #wpbody-content .subsubsub, +html.aurora-dark.aurora-ready #wpbody-content .tablenav .displaying-num { + color: var(--aurora-text-muted); +} + +html.aurora-dark.aurora-ready #wpbody-content .tablenav-pages a, +html.aurora-dark.aurora-ready #wpbody-content .pagination-links a { + background: var(--aurora-bg-subtle); + border-color: var(--aurora-border); + color: var(--aurora-text); +} + +/* Screen meta (Help / Screen Options pull-down tabs) -------------------- */ +html.aurora-dark.aurora-ready #screen-meta, +html.aurora-dark.aurora-ready #screen-meta-links .show-settings, +html.aurora-dark.aurora-ready #contextual-help-wrap, +html.aurora-dark.aurora-ready #screen-options-wrap { + background: var(--aurora-card-bg); + border-color: var(--aurora-border); + color: var(--aurora-text); +} + +/* Form tables (settings pages) ----------------------------------------- */ +html.aurora-dark.aurora-ready #wpbody-content .form-table th, +html.aurora-dark.aurora-ready #wpbody-content .form-table td { + border-color: var(--aurora-border); + color: var(--aurora-text); +} + +/* Horizontal rules & misc dividers ------------------------------------- */ +html.aurora-dark.aurora-ready #wpbody-content hr, +html.aurora-dark.aurora-ready #wpbody-content .wp-header-end { + border-color: var(--aurora-border); +} + +/* --------------------------------------------------------------------------- + * WordPress Updates page (update-core.php) — boxed "card" layout. + * native-enhance.js wraps each section (Current version, Plugins, Themes) + * into a .aurora-native-card div; these rules give those cards the same + * surface look as the dashboard cards, plus a clean title and accent button. + * Scoped to the updates page body class so it can't affect other screens. + * ------------------------------------------------------------------------ */ +html.aurora-dark.aurora-ready body.update-core-php #wpbody-content .wrap > h1 { + font-size: 1.5rem; + font-weight: 700; + margin: 0 0 6px; +} +html.aurora-dark.aurora-ready body.update-core-php #wpbody-content .wrap > p { + color: var(--aurora-text-muted); + font-size: 0.875rem; + margin: 0 0 20px; +} +html.aurora-dark.aurora-ready body.update-core-php #wpbody-content .aurora-native-card { + background: var(--aurora-card-bg); + border: 1px solid var(--aurora-card-border); + border-radius: 20px; + padding: 20px; + margin-bottom: 16px; +} +html.aurora-dark.aurora-ready body.update-core-php #wpbody-content .aurora-native-card h2 { + font-size: 1.0625rem; + font-weight: 600; + margin: 0 0 18px; + color: var(--aurora-text); + border: 0; + padding-top: 0; +} +html.aurora-dark.aurora-ready body.update-core-php #wpbody-content .aurora-native-card h2.response { + font-size: 1.15rem; +} +html.aurora-dark.aurora-ready body.update-core-php #wpbody-content .aurora-native-card .button { + background: transparent; + border: 1px solid var(--aurora-accent); + color: var(--aurora-accent); + border-radius: 8px; + padding: 7px 18px; + font-weight: 500; + height: auto; + box-shadow: none; + text-shadow: none; +} +html.aurora-dark.aurora-ready body.update-core-php #wpbody-content .aurora-native-card .button:hover { + background: var(--aurora-accent-soft); +} + +/* --------------------------------------------------------------------------- + * Modals & popups (media library, plugin/theme install details) — these are + * Backbone/thickbox views appended to , outside #wpbody-content, so + * every rule above misses them entirely. Scoped to html.aurora-dark only + * (no #wpbody-content ancestor requirement) since that's the only anchor + * available at body level. + * ------------------------------------------------------------------------ */ +html.aurora-dark.aurora-ready .media-modal, +html.aurora-dark.aurora-ready .media-frame, +html.aurora-dark.aurora-ready .media-frame-title, +html.aurora-dark.aurora-ready .media-frame-router, +html.aurora-dark.aurora-ready .media-frame-content, +html.aurora-dark.aurora-ready .media-frame-toolbar, +html.aurora-dark.aurora-ready .media-sidebar, +html.aurora-dark.aurora-ready #TB_window, +html.aurora-dark.aurora-ready #TB_ajaxContent, +html.aurora-dark.aurora-ready .plugin-details-modal, +html.aurora-dark.aurora-ready .plugin-install-php .plugin-card { + background: var(--aurora-card-bg); + color: var(--aurora-text); + border-color: var(--aurora-card-border); +} + +html.aurora-dark.aurora-ready .media-modal h1, +html.aurora-dark.aurora-ready .media-modal h2, +html.aurora-dark.aurora-ready .media-modal h3, +html.aurora-dark.aurora-ready .media-modal label, +html.aurora-dark.aurora-ready .media-frame-title h1, +html.aurora-dark.aurora-ready #TB_ajaxContent, +html.aurora-dark.aurora-ready #TB_ajaxContent p, +html.aurora-dark.aurora-ready #TB_ajaxContent h1, +html.aurora-dark.aurora-ready .plugin-card-top, +html.aurora-dark.aurora-ready .plugin-card .name, +html.aurora-dark.aurora-ready .plugin-card .desc { + color: var(--aurora-text); +} + +html.aurora-dark.aurora-ready .media-frame-menu, +html.aurora-dark.aurora-ready .media-menu, +html.aurora-dark.aurora-ready .attachments-browser .media-toolbar, +html.aurora-dark.aurora-ready .media-sidebar, +html.aurora-dark.aurora-ready .plugin-card-bottom { + background: var(--aurora-bg-subtle); + border-color: var(--aurora-border); +} + +html.aurora-dark.aurora-ready .attachments-browser .attachments, +html.aurora-dark.aurora-ready .media-frame-content { + background: var(--aurora-app-bg-solid); +} + +html.aurora-dark.aurora-ready .media-modal input, +html.aurora-dark.aurora-ready .media-modal textarea, +html.aurora-dark.aurora-ready .media-modal select, +html.aurora-dark.aurora-ready #TB_ajaxContent input, +html.aurora-dark.aurora-ready #TB_ajaxContent textarea, +html.aurora-dark.aurora-ready #TB_ajaxContent select { + background: var(--aurora-bg-subtle); + border-color: var(--aurora-border); + color: var(--aurora-text); +} + +html.aurora-dark.aurora-ready .media-modal-backdrop, +html.aurora-dark.aurora-ready #TB_overlay { + background: rgba(0, 0, 0, 0.7); +} + +/* Plugin/theme card grids on plugins.php / plugin-install.php / themes.php */ +html.aurora-dark.aurora-ready #wpbody-content .theme-browser .theme, +html.aurora-dark.aurora-ready #wpbody-content .plugin-card { + background: var(--aurora-card-bg); + border-color: var(--aurora-card-border); + color: var(--aurora-text); +} + +html.aurora-dark.aurora-ready #wpbody-content .theme-browser .theme .theme-name, +html.aurora-dark.aurora-ready #wpbody-content .theme-browser .theme .theme-actions { + background: var(--aurora-card-bg); + color: var(--aurora-text); + border-color: var(--aurora-card-border); +} + +/* --------------------------------------------------------------------------- + * Block editor (Gutenberg) chrome — the top toolbar/title bar and the + * right-hand Post/Block settings sidebar. The actual editing canvas + * (.interface-interface-skeleton__content, the white "page" itself) is + * deliberately left native/white — it's a WYSIWYG preview of the live, + * light-themed page, not admin chrome, so it shouldn't follow the theme. + * Everything surrounding it does, matching every other page's framing + * (shell-frame.css gives .interface-interface-skeleton itself the card + * background/border/radius; this just covers what's inside it). + * ------------------------------------------------------------------------ */ +html.aurora-dark.aurora-ready .editor-header { + background: var(--aurora-frame-bg); + border-bottom: 1px solid var(--aurora-frame-border); + color: var(--aurora-text); +} + +html.aurora-dark.aurora-ready .editor-header .components-button, +html.aurora-dark.aurora-ready .editor-header .editor-post-title__input, +html.aurora-dark.aurora-ready .editor-header .editor-document-bar, +html.aurora-dark.aurora-ready .editor-header .editor-document-bar__title { + color: var(--aurora-text); +} + +html.aurora-dark.aurora-ready .editor-header .components-button:hover { + background: var(--aurora-bg-subtle); +} + +html.aurora-dark.aurora-ready .interface-interface-skeleton__sidebar { + background: var(--aurora-frame-bg); + border-left: 1px solid var(--aurora-frame-border); + color: var(--aurora-text); +} + +/* .interface-complementary-area (the actual Post/Block settings panel) is + a nested child with its own explicit white background — the outer + __sidebar rule above doesn't reach it, so without this the whole panel + still shows through white regardless of theme. */ +html.aurora-dark.aurora-ready .interface-complementary-area { + background: var(--aurora-frame-bg); + color: var(--aurora-text); +} + +html.aurora-dark.aurora-ready .interface-interface-skeleton__sidebar .components-panel, +html.aurora-dark.aurora-ready .interface-interface-skeleton__sidebar .components-panel__body, +html.aurora-dark.aurora-ready .interface-interface-skeleton__sidebar .editor-post-status, +html.aurora-dark.aurora-ready .interface-interface-skeleton__sidebar .interface-complementary-area-header { + background: var(--aurora-frame-bg); + border-color: var(--aurora-frame-border); + color: var(--aurora-text); +} + +html.aurora-dark.aurora-ready .interface-interface-skeleton__sidebar .components-panel__body-title, +html.aurora-dark.aurora-ready .interface-interface-skeleton__sidebar label { + color: var(--aurora-text); +} + +html.aurora-dark.aurora-ready .interface-interface-skeleton__sidebar input, +html.aurora-dark.aurora-ready .interface-interface-skeleton__sidebar select, +html.aurora-dark.aurora-ready .interface-interface-skeleton__sidebar textarea, +html.aurora-dark.aurora-ready .interface-interface-skeleton__sidebar .components-select-control__input, +html.aurora-dark.aurora-ready .interface-interface-skeleton__sidebar .components-text-control__input { + background: var(--aurora-bg-subtle); + border-color: var(--aurora-border); + color: var(--aurora-text); +} + +html.aurora-dark.aurora-ready .interface-interface-skeleton__sidebar .components-button { + color: var(--aurora-text); +} + +/* Font Library (font-library.php) -------------------------------------- + * WP 6.5+ built this on @wordpress/components' Card/Modal primitives + * ("boot-layout" wrapper) instead of classic postbox markup — its own + * always-white background isn't reached by the postbox/card rules above, + * which left light text sitting on a white panel (invisible) inside an + * otherwise-dark-framed page. Emotion-generated hash classes on some of + * this screen's inner elements (e.g. "css-qi2azi") are unstable across WP + * builds and deliberately not targeted here — only the screen's own + * stable, hand-authored class names are. */ +html.aurora-dark.aurora-ready #wpbody-content .boot-layout-container, +html.aurora-dark.aurora-ready #wpbody-content .boot-layout__stage, +html.aurora-dark.aurora-ready #wpbody-content .admin-ui-page, +html.aurora-dark.aurora-ready #wpbody-content .admin-ui-navigable-region, +html.aurora-dark.aurora-ready #wpbody-content .admin-ui-page__header, +html.aurora-dark.aurora-ready #wpbody-content .font-library-page__tab-panel, +html.aurora-dark.aurora-ready #wpbody-content .font-library__tabpanel-layout, +html.aurora-dark.aurora-ready #wpbody-content .font-library__footer { + background: var(--aurora-card-bg); + color: var(--aurora-text); +} + +html.aurora-dark.aurora-ready #wpbody-content .admin-ui-page__header-title, +html.aurora-dark.aurora-ready #wpbody-content .font-library__fonts-title { + color: var(--aurora-text); +} + +html.aurora-dark.aurora-ready #wpbody-content .font-library-page__tablist { + border-color: var(--aurora-card-border); +} + +html.aurora-dark.aurora-ready #wpbody-content .font-library__fonts-list-item, +html.aurora-dark.aurora-ready #wpbody-content .font-library__font-card { + background: var(--aurora-card-bg); + border-color: var(--aurora-card-border); + color: var(--aurora-text); +} + +/* Site Health (site-health.php) is left running WordPress core's own test + * logic rather than reimplemented — its Status tab async checks hit core's + * own /wp-site-health/v1/tests/* REST routes, and third-party plugins can + * add their own checks via the site_status_tests filter, so reproducing it + * risks both drifting from core and silently dropping other plugins' + * checks. Only the framing/theming is Aurora's, same "frame native content" + * treatment as the block editor above. + * + * Three of its own elements paint a hardcoded white/off-white background + * that the generic #wpbody-content light-text cascade doesn't reach, + * leaving light text unreadable against it — the same "boot-layout" pattern + * fixed above, just a different screen. */ +html.aurora-dark.aurora-ready #wpbody-content .health-check-header, +html.aurora-dark.aurora-ready #wpbody-content .health-check-accordion-panel { + background: var(--aurora-card-bg); +} +html.aurora-dark.aurora-ready #wpbody-content .health-check-accordion-trigger { + background: var(--aurora-bg-subtle); + color: var(--aurora-text); +} +/* The Security/Performance category pill inside each trigger row keeps its + * own dark text color (readable against the trigger's original near-white + * background) — needs to flip to light along with the trigger's background + * above, or it becomes unreadable in the other direction. */ +html.aurora-dark.aurora-ready #wpbody-content .health-check-accordion-trigger .badge { + color: var(--aurora-text); +} diff --git a/admin/assets/native-enhance.js b/admin/assets/native-enhance.js new file mode 100644 index 0000000..c451cbe --- /dev/null +++ b/admin/assets/native-enhance.js @@ -0,0 +1,71 @@ +/** + * Progressive enhancement for native WordPress admin pages that benefit from + * Aurora's boxed "card" layout but ship as flat sibling markup. + * + * Currently targets the Updates page (update-core.php): it groups each + * section (Current version, Plugins, Themes) into a .aurora-native-card div + * so the dark stylesheet can give them the same surface as the dashboard + * cards. Purely visual and reversible — no native behaviour is altered. + */ +(function () { + 'use strict'; + + function enhanceUpdates() { + var wrap = document.querySelector('#wpbody-content .wrap'); + if (!wrap || wrap.querySelector('.aurora-native-card')) { + return; + } + + var children = Array.prototype.slice.call(wrap.children); + + // A new card starts at each major section heading. The "response" h2 + // ("You have the latest version") belongs inside the version section, + // so it is not treated as a boundary. + var isBoundary = function (el) { + return el.tagName === 'H2' && !el.classList.contains('response'); + }; + + var groups = []; + var current = null; + + children.forEach(function (el) { + if (el.tagName === 'H1') { + return; // page title stays outside the cards + } + if (!current && !isBoundary(el)) { + return; // intro paragraph(s) before the first section stay in place + } + if (isBoundary(el)) { + current = []; + groups.push(current); + } + if (current) { + current.push(el); + } + }); + + groups.forEach(function (group) { + if (!group.length) { + return; + } + var card = document.createElement('div'); + card.className = 'aurora-native-card'; + group[0].parentNode.insertBefore(card, group[0]); + group.forEach(function (el) { + card.appendChild(el); + }); + }); + } + + function run() { + if (document.body.classList.contains('update-core-php')) { + enhanceUpdates(); + } + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', run); + } else { + run(); + } +})(); diff --git a/admin/src/ActivityLog/ActivityLog.php b/admin/src/ActivityLog/ActivityLog.php new file mode 100644 index 0000000..07881e8 --- /dev/null +++ b/admin/src/ActivityLog/ActivityLog.php @@ -0,0 +1,226 @@ +prefix . self::TABLE; + } + + public static function maybe_install() + { + if (get_option(self::DB_OPTION) === self::DB_VERSION) { + return; + } + + global $wpdb; + $table = self::table_name(); + $charset = $wpdb->get_charset_collate(); + + $sql = "CREATE TABLE {$table} ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + action_type VARCHAR(60) NOT NULL DEFAULT '', + object_type VARCHAR(60) NOT NULL DEFAULT '', + object_id BIGINT UNSIGNED NULL, + object_label VARCHAR(191) NOT NULL DEFAULT '', + user_id BIGINT UNSIGNED NULL, + user_name VARCHAR(191) NOT NULL DEFAULT '', + ip_address VARCHAR(45) NULL, + details LONGTEXT NULL, + created_at DATETIME NOT NULL, + PRIMARY KEY (id), + KEY idx_action (action_type), + KEY idx_object (object_type), + KEY idx_created (created_at) + ) {$charset};"; + + require_once ABSPATH . "wp-admin/includes/upgrade.php"; + dbDelta($sql); + + update_option(self::DB_OPTION, self::DB_VERSION); + } + + /** + * @param array|null $actor Overrides the "current user" attribution with + * an explicit ["id" => int, "name" => string] — needed for wp_logout, + * where wp_get_current_user() has already been reset to the logged-out + * (id 0) user by the time this hook fires, which would otherwise record + * every logout as done by nobody. + */ + private static function record($action_type, $object_type, $object_id, $object_label, $details = [], $actor = null) + { + global $wpdb; + if ($actor === null) { + $current = wp_get_current_user(); + $actor = [ + "id" => $current && $current->exists() ? $current->ID : null, + "name" => $current && $current->exists() ? $current->display_name : "", + ]; + } + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- $wpdb->insert() is WordPress's own escaping-safe API for writing to a custom table (not raw SQL); there's no core post/postmeta abstraction for Aurora's own activity-log table, so any access to it is necessarily "direct" by this sniff's definition. + $wpdb->insert(self::table_name(), [ + "action_type" => $action_type, + "object_type" => $object_type, + "object_id" => $object_id, + "object_label" => substr((string) $object_label, 0, 191), + "user_id" => $actor["id"], + "user_name" => $actor["name"], + "ip_address" => self::client_ip(), + "details" => $details ? wp_json_encode($details) : null, + "created_at" => current_time("mysql"), + ]); + } + + /** + * REMOTE_ADDR only — the actual TCP peer, not spoofable by a request + * header. Behind a reverse proxy/load balancer this will read as the + * proxy's own address rather than the original visitor; deliberately not + * trusting X-Forwarded-For or similar instead, since blindly trusting a + * client-supplied header for anything (even just display, as here) is the + * standard IP-spoofing footgun, and this plugin has no reliable way to + * know whether a given host's proxy is trusted. + */ + private static function client_ip() + { + $ip = isset($_SERVER["REMOTE_ADDR"]) ? sanitize_text_field(wp_unslash($_SERVER["REMOTE_ADDR"])) : ""; + return filter_var($ip, FILTER_VALIDATE_IP) ? $ip : null; + } + + public static function on_save_post($post_id, $post, $update) + { + // save_post fires on autosaves, revisions, and attachments too — none + // represent a real user-visible content change (attachments get their + // own add_attachment/delete_attachment hooks below), so all three are + // skipped here. + if (wp_is_post_autosave($post_id) || wp_is_post_revision($post_id) || $post->post_type === "attachment") { + return; + } + if (!in_array($post->post_status, ["publish", "draft", "pending", "future", "private"], true)) { + return; + } + // object_type is the post's real type (post/page/a custom post type), + // not a hardcoded "post" — the Activity Log's Type column needs this to + // read correctly for anything that isn't the built-in Posts type. + self::record($update ? "updated" : "created", $post->post_type, $post_id, $post->post_title ?: "(no title)"); + } + + public static function on_delete_post($post_id) + { + $post = get_post($post_id); + if (!$post || $post->post_type === "revision" || $post->post_type === "attachment") { + return; + } + self::record("deleted", $post->post_type, $post_id, $post->post_title ?: "(no title)"); + } + + public static function on_add_attachment($post_id) + { + $post = get_post($post_id); + self::record("created", "attachment", $post_id, $post ? $post->post_title : "(no title)"); + } + + public static function on_delete_attachment($post_id) + { + $post = get_post($post_id); + self::record("deleted", "attachment", $post_id, $post ? $post->post_title : "(no title)"); + } + + public static function on_user_register($user_id) + { + $user = get_userdata($user_id); + // Self-registration means the visitor isn't authenticated yet at this + // point, so wp_get_current_user() would show "not logged in" rather + // than the account that was just created. Attributing explicitly to the + // new account instead — including when an admin creates the account — + // is a deliberate choice for this specific event: unlike profile edits + // (where the acting admin, shown via the default wp_get_current_user() + // attribution, is the more useful "who did this"), a registration event + // is overwhelmingly self-service, so "who this account belongs to" is + // the more useful read here, and treating both origins the same way + // keeps this one event type's Author column consistent regardless of + // which flow created the account. + self::record("registered", "user", $user_id, $user ? $user->user_login : "", [], [ + "id" => $user_id, + "name" => $user ? $user->display_name : "", + ]); + } + + public static function on_profile_update($user_id) + { + $user = get_userdata($user_id); + self::record("profile_updated", "user", $user_id, $user ? $user->user_login : ""); + } + + public static function on_login($user_login, $user) + { + // Not wp_get_current_user(): wp_login fires from inside wp_signon(), + // before wp-login.php's own form handler calls wp_set_current_user() — + // the same ordering issue on_logout() has, just in the other direction. + // $user is passed in for exactly this reason; use it directly instead. + self::record("login", "user", $user->ID, $user_login, [], [ + "id" => $user->ID, + "name" => $user->display_name, + ]); + } + + public static function on_logout($user_id) + { + $user = $user_id ? get_userdata($user_id) : false; + $login = $user ? $user->user_login : ""; + self::record("logout", "user", $user_id ?: null, $login, [], [ + "id" => $user_id ?: null, + "name" => $user ? $user->display_name : "", + ]); + } + + public static function on_plugin_activated($plugin) + { + self::record("activated", "plugin", null, $plugin); + } + + public static function on_plugin_deactivated($plugin) + { + self::record("deactivated", "plugin", null, $plugin); + } + + public static function on_theme_switch($new_name) + { + self::record("switched", "theme", null, $new_name); + } + + public static function on_settings_updated() + { + self::record("updated", "settings", null, "Aurora Admin settings"); + } +} diff --git a/admin/src/ActivityLog/ActivityLogData.php b/admin/src/ActivityLog/ActivityLogData.php new file mode 100644 index 0000000..98feb58 --- /dev/null +++ b/admin/src/ActivityLog/ActivityLogData.php @@ -0,0 +1,145 @@ + "GET", + "callback" => [self::class, "handle"], + "permission_callback" => function () { + return current_user_can("manage_options"); + }, + "args" => [ + // "admin" = site/content activity (everything except user-account + // events), "user" = account activity (login/logout/registration/ + // profile updates) — the Activity Log page's two tabs. Omit for + // unfiltered (both). + "scope" => ["type" => "string", "required" => false, "enum" => ["admin", "user"]], + "action_type" => ["type" => "string", "required" => false], + "object_type" => ["type" => "string", "required" => false], + "search" => ["type" => "string", "required" => false], + "page" => ["type" => "integer", "required" => false, "default" => 1], + "per_page" => ["type" => "integer", "required" => false, "default" => 20], + ], + ]); + } + + public static function handle($request) + { + global $wpdb; + $table = ActivityLog::table_name(); + + $where = ["1=1"]; + $params = []; + + $scope = $request->get_param("scope"); + if ($scope === "user") { + $where[] = "object_type = 'user'"; + } elseif ($scope === "admin") { + $where[] = "object_type != 'user'"; + } + + if ($request->get_param("action_type")) { + $where[] = "action_type = %s"; + $params[] = sanitize_text_field($request->get_param("action_type")); + } + if ($request->get_param("object_type")) { + $where[] = "object_type = %s"; + $params[] = sanitize_text_field($request->get_param("object_type")); + } + if ($request->get_param("search")) { + $where[] = "(object_label LIKE %s OR user_name LIKE %s)"; + $like = "%" . $wpdb->esc_like(sanitize_text_field($request->get_param("search"))) . "%"; + $params[] = $like; + $params[] = $like; + } + + $where_sql = implode(" AND ", $where); + + $per_page = max(1, min(100, (int) $request->get_param("per_page") ?: 20)); + $page = max(1, (int) $request->get_param("page") ?: 1); + $offset = ($page - 1) * $per_page; + + // $table is an internal, hardcoded {$wpdb->prefix}-based table name + // (ActivityLog::table_name()); $where_sql is built only from the fixed + // "1=1"/"col = %s" fragments above (never raw user input). + $count_sql = "SELECT COUNT(*) FROM {$table} WHERE {$where_sql}"; + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- reading Aurora's own custom activity-log table, no core API for that; $count_sql is prepare()'d whenever $params is non-empty (the only case with real values to bind) — when $params is empty, $count_sql is just the literal "SELECT COUNT(*) FROM {$table} WHERE 1=1" with nothing user-controlled in it at all. + $total = (int) ($params ? $wpdb->get_var($wpdb->prepare($count_sql, $params)) : $wpdb->get_var($count_sql)); + + // $table/$where_sql: same as $count_sql above. + $sql = "SELECT * FROM {$table} WHERE {$where_sql} ORDER BY created_at DESC LIMIT %d OFFSET %d"; + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- reading Aurora's own custom activity-log table; always passed through $wpdb->prepare() with $params plus the trailing $per_page/$offset bound via %d/%d. + $rows = $wpdb->get_results($wpdb->prepare($sql, array_merge($params, [$per_page, $offset]))); + + // Live user info (avatar/role/email) rather than freezing it into each + // log row at record time — reflects the account's CURRENT role/email + // (correct if either changes later) and avoids duplicating PII into + // every single event row when user_id is already a foreign key back to + // it. Batch-fetched once for every distinct user_id on this page, + // rather than one get_userdata() call per row. + $user_ids = array_values(array_unique(array_filter(array_map(function ($row) { + return $row->user_id ? (int) $row->user_id : 0; + }, $rows)))); + $users_by_id = []; + if ($user_ids) { + // $roles[0] is a role SLUG (e.g. "shop_manager") — its translated + // display name has to come from wp_roles()'s own registered name for + // that slug, not a guess like ucfirst($slug); that only happens to + // work for the single-word built-in roles. + $all_roles = wp_roles()->roles; + foreach (get_users(["include" => $user_ids, "fields" => "all_with_meta"]) as $u) { + $slug = $u->roles ? $u->roles[0] : ""; + $role_name = $slug && isset($all_roles[$slug]) ? translate_user_role($all_roles[$slug]["name"]) : ""; + $users_by_id[$u->ID] = [ + "avatar" => get_avatar_url($u->ID, ["size" => 64]), + "role" => $role_name, + "email" => $u->user_email, + "editUrl" => esc_url_raw(admin_url("user-edit.php?user_id=" . $u->ID)), + ]; + } + } + + $items = array_map(function ($row) use ($users_by_id) { + $uid = $row->user_id ? (int) $row->user_id : null; + $u = $uid && isset($users_by_id[$uid]) ? $users_by_id[$uid] : null; + return [ + "id" => (int) $row->id, + "actionType" => $row->action_type, + "objectType" => $row->object_type, + "objectId" => $row->object_id ? (int) $row->object_id : null, + "objectLabel" => $row->object_label, + "userId" => $uid, + "userName" => $row->user_name, + "userAvatar" => $u ? $u["avatar"] : null, + "userRole" => $u ? $u["role"] : "", + "userEmail" => $u ? $u["email"] : "", + "userEditUrl" => $u ? $u["editUrl"] : "", + "ipAddress" => $row->ip_address, + "details" => $row->details ? json_decode($row->details, true) : null, + "createdAt" => $row->created_at, + ]; + }, $rows); + + return new \WP_REST_Response([ + "items" => $items, + "total" => $total, + "page" => $page, + "perPage" => $per_page, + "totalPages" => (int) ceil($total / $per_page), + ], 200); + } +} diff --git a/admin/src/Analytics/Analytics.php b/admin/src/Analytics/Analytics.php new file mode 100644 index 0000000..ed59129 --- /dev/null +++ b/admin/src/Analytics/Analytics.php @@ -0,0 +1,199 @@ +prefix . self::TABLE; + } + + public static function maybe_install() + { + if (get_option(self::DB_OPTION) === self::DB_VERSION) { + return; + } + + global $wpdb; + $table = self::table_name(); + $charset = $wpdb->get_charset_collate(); + + $sql = "CREATE TABLE {$table} ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + session_id VARCHAR(32) NOT NULL DEFAULT '', + url VARCHAR(255) NOT NULL DEFAULT '', + referrer VARCHAR(255) NOT NULL DEFAULT '', + device VARCHAR(20) NOT NULL DEFAULT 'desktop', + browser VARCHAR(40) NOT NULL DEFAULT '', + country VARCHAR(2) NOT NULL DEFAULT '', + created_at DATETIME NOT NULL, + PRIMARY KEY (id), + KEY idx_created (created_at), + KEY idx_session (session_id) + ) {$charset};"; + + require_once ABSPATH . "wp-admin/includes/upgrade.php"; + dbDelta($sql); + + update_option(self::DB_OPTION, self::DB_VERSION); + } + + /** + * Records the current front-end request as a pageview, skipping admin, + * AJAX/REST/cron, bots, and logged-in administrators. + */ + public static function record() + { + // Tracking is on unless explicitly disabled in Settings > General. + $enabled = \AuroraAdmin\Options\Settings::get("enable_analytics", true); + if ($enabled === false || $enabled === 0 || $enabled === "0") { + return; + } + + if (is_admin() || wp_doing_ajax() || wp_doing_cron() || is_robots() || is_feed()) { + return; + } + if (defined("REST_REQUEST") && REST_REQUEST) { + return; + } + + // Admins browsing their own site are skipped unless Settings > + // Analytics > "Track logged-in admins" is enabled. + $track_admins = \AuroraAdmin\Options\Settings::get("track_admins", false); + if (!$track_admins && current_user_can("manage_options")) { + return; + } + + $ua = isset($_SERVER["HTTP_USER_AGENT"]) ? sanitize_text_field(wp_unslash($_SERVER["HTTP_USER_AGENT"])) : ""; + if (self::is_bot($ua)) { + return; + } + + global $wpdb; + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- $wpdb->insert() is WordPress's own escaping-safe API for writing to Aurora's custom pageview table. + $wpdb->insert(self::table_name(), [ + "session_id" => self::session_id(), + "url" => substr(self::current_path(), 0, 255), + "referrer" => substr(self::referrer(), 0, 255), + "device" => self::device($ua), + "browser" => self::browser($ua), + "country" => self::country(), + "created_at" => current_time("mysql"), + ]); + } + + private static function session_id() + { + if (!empty($_COOKIE[self::COOKIE])) { + $cookie = sanitize_text_field(wp_unslash($_COOKIE[self::COOKIE])); + return preg_replace("/[^a-f0-9]/", "", substr($cookie, 0, 32)); + } + $sid = md5(uniqid("", true)); + // 30-minute rolling session window + if (!headers_sent()) { + setcookie(self::COOKIE, $sid, time() + 1800, COOKIEPATH ?: "/", COOKIE_DOMAIN); + } + $_COOKIE[self::COOKIE] = $sid; + return $sid; + } + + private static function current_path() + { + $uri = isset($_SERVER["REQUEST_URI"]) ? sanitize_text_field(wp_unslash($_SERVER["REQUEST_URI"])) : "/"; + return wp_parse_url($uri, PHP_URL_PATH) ?: "/"; + } + + private static function referrer() + { + $ref = isset($_SERVER["HTTP_REFERER"]) ? sanitize_text_field(wp_unslash($_SERVER["HTTP_REFERER"])) : ""; + if (!$ref) { + return ""; + } + $host = wp_parse_url($ref, PHP_URL_HOST); + // Ignore self-referrals + if ($host && $host === wp_parse_url(home_url(), PHP_URL_HOST)) { + return ""; + } + return $host ?: ""; + } + + private static function device($ua) + { + $ua = strtolower($ua); + if (preg_match("/(ipad|tablet|playbook|silk)|(android(?!.*mobile))/", $ua)) { + return "tablet"; + } + if (preg_match("/(mobile|iphone|ipod|android|blackberry|opera mini|iemobile)/", $ua)) { + return "mobile"; + } + return "desktop"; + } + + private static function browser($ua) + { + $map = [ + "Edg" => "Edge", + "OPR" => "Opera", + "Chrome" => "Chrome", + "Firefox" => "Firefox", + "Safari" => "Safari", + ]; + foreach ($map as $needle => $name) { + if (stripos($ua, $needle) !== false) { + return $name; + } + } + return "Other"; + } + + private static function country() + { + // Use a CDN-provided country header if available; otherwise leave blank. + // Sanitized into $value before any check runs on it — the empty()/ + // strlen() checks below need the unslashed+sanitized copy too, not + // just the eventual return value, since reading the raw superglobal + // for any purpose (even just a length check) is what the sanitization + // rule actually flags. + foreach (["HTTP_CF_IPCOUNTRY", "GEOIP_COUNTRY_CODE", "HTTP_X_COUNTRY_CODE"] as $h) { + $value = isset($_SERVER[$h]) ? sanitize_text_field(wp_unslash($_SERVER[$h])) : ""; + if ($value !== "" && strlen($value) === 2) { + return strtoupper($value); + } + } + return ""; + } + + private static function is_bot($ua) + { + if (!$ua) { + return true; + } + return (bool) preg_match( + "/bot|crawl|slurp|spider|mediapartners|facebookexternalhit|embedly|quora|pingdom|monitor|headless|lighthouse|gtmetrix/i", + $ua + ); + } +} diff --git a/admin/src/App/AuroraAdmin.php b/admin/src/App/AuroraAdmin.php new file mode 100644 index 0000000..4df40ed --- /dev/null +++ b/admin/src/App/AuroraAdmin.php @@ -0,0 +1,118 @@ + Code). + * + * Four admin-authored fields, only settable by manage_options users on the + * settings page: + * - Admin CSS → wrapped in a ", "", $css); + echo "\n\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- admin-authored stylesheet, output raw inside a stripped above to prevent breakout. + } + + public static function admin_js() + { + $js = trim((string) Settings::get("custom_admin_js", "")); + if ($js === "") { + return; + } + $js = str_ireplace("", "<\\/script>", $js); + echo "\n\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- admin-authored script, output raw inside a + + esc_url_raw(get_rest_url()), + "restNonce" => wp_create_nonce("wp_rest"), + "adminUrl" => esc_url_raw(admin_url()), + "siteUrl" => esc_url_raw(home_url("/")), + "logoutUrl" => esc_url_raw(wp_logout_url()), + "userName" => $current_user->display_name, + "userEmail" => $current_user->user_email, + "avatar" => get_avatar_url($current_user->ID, ["size" => 64]), + "currentUrl" => esc_url_raw(self::current_admin_url()), + "activeParentFile" => is_string($parent_file) ? $parent_file : "", + // Secrets stripped — this is serialized for every admin user. + "settings" => Settings::get_safe(), + "menu" => MenuSerializer::serialize(), + "auroraNav" => self::aurora_nav(), + ]; + ?> +
+ "aurora-admin", "title" => __("Settings", "aurora-admin"), "icon" => "dashicons-admin-generic"], + ["id" => "aurora-admin-modules", "title" => __("Modules", "aurora-admin"), "icon" => "dashicons-screenoptions"], + ["id" => "aurora-admin-menu-creator", "title" => __("Menu Creator", "aurora-admin"), "icon" => "dashicons-menu-alt"], + ["id" => "aurora-admin-notices", "title" => __("Admin Notices", "aurora-admin"), "icon" => "dashicons-megaphone"], + ["id" => "aurora-admin-roles", "title" => __("Role Editor", "aurora-admin"), "icon" => "dashicons-groups"], + ["id" => "aurora-admin-activity-log", "title" => __("Activity Log", "aurora-admin"), "icon" => "dashicons-clock"], + ["id" => "aurora-admin-bug-report", "title" => __("Report a Bug", "aurora-admin"), "icon" => "dashicons-warning"], + ]; + + $nav = []; + foreach ($items as $item) { + if (!current_user_can("manage_options")) { + continue; + } + $nav[] = [ + "id" => $item["id"], + "title" => $item["title"], + "url" => esc_url_raw(admin_url("admin.php?page=" . $item["id"])), + "icon" => ["type" => "dashicon", "value" => $item["icon"]], + ]; + } + + return $nav; + } +} diff --git a/admin/src/Utility/Assets.php b/admin/src/Utility/Assets.php new file mode 100644 index 0000000..e5a213b --- /dev/null +++ b/admin/src/Utility/Assets.php @@ -0,0 +1,96 @@ +|null Cached find=>replace map for gettext. */ + private static $replacements = null; + + public function __construct() + { + add_action("admin_head", [self::class, "favicon"]); + add_filter("all_plugins", [self::class, "rename_plugin"]); + add_action("admin_menu", [self::class, "rename_menu"], 999); + add_filter("gettext", [self::class, "replace_text"], 20, 3); + } + + public static function favicon() + { + $url = Settings::get("admin_favicon", ""); + if (!$url) { + return; + } + printf('' . "\n", esc_url($url)); + } + + public static function rename_plugin($plugins) + { + $name = trim((string) Settings::get("plugin_name", "")); + if ($name === "") { + return $plugins; + } + + $basename = "aurora-admin/aurora-admin.php"; + if (isset($plugins[$basename])) { + $plugins[$basename]["Name"] = $name; + $plugins[$basename]["Title"] = $name; + } + + return $plugins; + } + + public static function rename_menu() + { + $name = trim((string) Settings::get("plugin_name", "")); + if ($name === "") { + return; + } + + global $menu; + if (!is_array($menu)) { + return; + } + foreach ($menu as $index => $item) { + if (isset($item[2]) && $item[2] === "aurora-admin") { + $menu[$index][0] = $name; + break; + } + } + } + + public static function replace_text($translation, $text, $domain) + { + // Front end and login screens stay untouched. + if (!is_admin()) { + return $translation; + } + + if (self::$replacements === null) { + self::$replacements = []; + $pairs = Settings::get("text_replacements", []); + if (is_array($pairs)) { + foreach ($pairs as $pair) { + if ( + is_array($pair) && + isset($pair["find"], $pair["replace"]) && + $pair["find"] !== "" + ) { + self::$replacements[$pair["find"]] = (string) $pair["replace"]; + } + } + } + } + + if (!self::$replacements) { + return $translation; + } + + return strtr($translation, self::$replacements); + } +} diff --git a/app/package-lock.json b/app/package-lock.json new file mode 100644 index 0000000..e5fbd7d --- /dev/null +++ b/app/package-lock.json @@ -0,0 +1,1363 @@ +{ + "name": "aurora-admin-app", + "version": "3.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "aurora-admin-app", + "version": "3.1.0", + "dependencies": { + "vue": "^3.5.26" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.2.1", + "vite": "^6.0.7" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.39.tgz", + "integrity": "sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/shared": "3.5.39", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.39.tgz", + "integrity": "sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.39.tgz", + "integrity": "sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/compiler-core": "3.5.39", + "@vue/compiler-dom": "3.5.39", + "@vue/compiler-ssr": "3.5.39", + "@vue/shared": "3.5.39", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.15", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.39.tgz", + "integrity": "sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.39.tgz", + "integrity": "sha512-TpsuBJ9gGlZa5d23XcM2y8EXanz9dZeVDQBXRwzy46ItgvM+rWpzs+UVM0wcRLxGvcav0HE5jz2gNL53xlRAog==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.39.tgz", + "integrity": "sha512-9GLtNyRvPAUMbX+7ono0RC2j0guo2LXVi8LvcmAooImACUKm0oFf0jjwbX8/H0AE/t1nxhAkn8RSl9PMCzzxZw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.39.tgz", + "integrity": "sha512-7Y6aAGboKcXAZ3ECuUy7RrS5yy2r47dhTp2SKaJmYxjopImaVFaNa5Ne66NwGovsrxVAl5S5rwc7m22UG7Lmww==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.39", + "@vue/runtime-core": "3.5.39", + "@vue/shared": "3.5.39", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.39.tgz", + "integrity": "sha512-yZSakiAGw85rZfG7UM8akMnIF+FmeiNk47uvHf2nVBBSe+dIKUhZuZq9+XgJhbV3nS5Z4ALH23/MpXofW+mbcw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.39", + "@vue/shared": "3.5.39" + }, + "peerDependencies": { + "vue": "3.5.39" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.39.tgz", + "integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.39.tgz", + "integrity": "sha512-xmZCYabFGcirU8r0fTuvl/LICc1OU620rnqepaJDL/a141ZigkG7AyaxQLdqJ02ZRYzWe6YPaDHeQx7MfknQfA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.39", + "@vue/compiler-sfc": "3.5.39", + "@vue/runtime-dom": "3.5.39", + "@vue/server-renderer": "3.5.39", + "@vue/shared": "3.5.39" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + } + } +} diff --git a/app/package.json b/app/package.json new file mode 100644 index 0000000..d81e2f6 --- /dev/null +++ b/app/package.json @@ -0,0 +1,17 @@ +{ + "name": "aurora-admin-app", + "version": "3.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build" + }, + "dependencies": { + "vue": "^3.5.26" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.2.1", + "vite": "^6.0.7" + } +} diff --git a/app/src/components/ProgressBar.vue b/app/src/components/ProgressBar.vue new file mode 100644 index 0000000..9aa1209 --- /dev/null +++ b/app/src/components/ProgressBar.vue @@ -0,0 +1,60 @@ + + + + + diff --git a/app/src/components/Toggle.vue b/app/src/components/Toggle.vue new file mode 100644 index 0000000..2a3131e --- /dev/null +++ b/app/src/components/Toggle.vue @@ -0,0 +1,84 @@ + + + + + diff --git a/app/src/composables/useJobPolling.js b/app/src/composables/useJobPolling.js new file mode 100644 index 0000000..d31f8cf --- /dev/null +++ b/app/src/composables/useJobPolling.js @@ -0,0 +1,98 @@ +import { ref, onUnmounted } from 'vue'; + +// Phase weights for a rough overall-progress percentage. Backup and +// restore jobs use different phase sets (see BackupEngine::effective_phases +// on the PHP side) — only the phases relevant to whichever job is active +// contribute, everything else is ignored. +const PHASE_WEIGHTS = { + db_dump: 40, + file_zip: 40, + upload_remote: 20, + download_remote: 20, + db_restore: 50, + file_restore: 50, +}; + +const PHASE_LABELS = { + pending: 'Starting…', + db_dump: 'Backing up the database…', + file_zip: 'Archiving files…', + upload_remote: 'Uploading to remote storage…', + download_remote: 'Downloading from remote storage…', + db_restore: 'Restoring the database…', + file_restore: 'Restoring files…', + done: 'Done', + failed: 'Failed', + cancelled: 'Cancelled', +}; + +/** + * Drives a backup/restore job to completion by repeatedly calling the + * REST "step" route — a setTimeout-recursion loop (not setInterval), so a + * slow step can never overlap the next poll for the same job. The + * server-side wp_cron safety net keeps the job moving even if this stops + * polling (e.g. the tab is closed mid-backup) — this composable is purely + * the foreground "watch it happen live" experience, not the only thing + * keeping a job alive. + */ +export function useJobPolling(api) { + const job = ref(null); + const isPolling = ref(false); + const error = ref(''); + let timer = null; + let stopped = false; + + const percent = () => { + if (!job.value) return null; + const weight = PHASE_WEIGHTS[job.value.phase]; + if (!weight) return job.value.status === 'done' ? 100 : null; + // No fine-grained progress within a phase is cheap to compute + // client-side (server tracks byte/row offsets, not percentages) — an + // indeterminate bar per phase, weighted by how much of the whole job + // that phase represents, is a reasonable approximation without a + // second round trip just to compute a number. + return null; + }; + + const label = () => { + if (!job.value) return ''; + if (job.value.status === 'failed') return job.value.error_message || 'Failed'; + return PHASE_LABELS[job.value.phase] || PHASE_LABELS[job.value.status] || ''; + }; + + const poll = async (jobId) => { + if (stopped) return; + try { + const res = await api(`/backup/jobs/${jobId}/step`, { method: 'POST' }); + job.value = res.job; + } catch (e) { + error.value = e.message || 'Request failed'; + isPolling.value = false; + return; + } + + if (!job.value || ['done', 'failed', 'cancelled'].includes(job.value.status)) { + isPolling.value = false; + return; + } + + timer = setTimeout(() => poll(jobId), 1500); + }; + + const start = (jobId) => { + stopped = false; + isPolling.value = true; + error.value = ''; + poll(jobId); + }; + + const stop = () => { + stopped = true; + isPolling.value = false; + if (timer) clearTimeout(timer); + }; + + onUnmounted(stop); + + return { job, isPolling, error, start, stop, percent, label }; +} diff --git a/app/src/entries/activity-log.js b/app/src/entries/activity-log.js new file mode 100644 index 0000000..4d2639f --- /dev/null +++ b/app/src/entries/activity-log.js @@ -0,0 +1,10 @@ +import { createApp } from 'vue'; +import '@/shell/shell-frame.css'; +import ActivityLog from '@/pages/activity-log/ActivityLog.vue'; + +const mountEl = document.getElementById('aurora-admin-activity-log-root'); + +if (mountEl) { + const data = JSON.parse(mountEl.dataset.auroraAdmin || '{}'); + createApp(ActivityLog, { serverData: data }).mount(mountEl); +} diff --git a/app/src/entries/add-user.js b/app/src/entries/add-user.js new file mode 100644 index 0000000..2d753ad --- /dev/null +++ b/app/src/entries/add-user.js @@ -0,0 +1,10 @@ +import { createApp } from 'vue'; +import '@/shell/shell-frame.css'; +import AddUser from '@/pages/users/AddUser.vue'; + +const mountEl = document.getElementById('aurora-admin-add-user-root'); + +if (mountEl) { + const data = JSON.parse(mountEl.dataset.auroraAdmin || '{}'); + createApp(AddUser, { serverData: data }).mount(mountEl); +} diff --git a/app/src/entries/admin-notices.js b/app/src/entries/admin-notices.js new file mode 100644 index 0000000..f492747 --- /dev/null +++ b/app/src/entries/admin-notices.js @@ -0,0 +1,10 @@ +import { createApp } from 'vue'; +import '@/shell/shell-frame.css'; +import AdminNotices from '@/pages/admin-notices/AdminNotices.vue'; + +const mountEl = document.getElementById('aurora-admin-admin-notices-root'); + +if (mountEl) { + const data = JSON.parse(mountEl.dataset.auroraAdmin || '{}'); + createApp(AdminNotices, { serverData: data }).mount(mountEl); +} diff --git a/app/src/entries/bug-report.js b/app/src/entries/bug-report.js new file mode 100644 index 0000000..ad56ae3 --- /dev/null +++ b/app/src/entries/bug-report.js @@ -0,0 +1,10 @@ +import { createApp } from 'vue'; +import '@/shell/shell-frame.css'; +import BugReport from '@/pages/bug-report/BugReport.vue'; + +const mountEl = document.getElementById('aurora-admin-bug-report-root'); + +if (mountEl) { + const data = JSON.parse(mountEl.dataset.auroraAdmin || '{}'); + createApp(BugReport, { serverData: data }).mount(mountEl); +} diff --git a/app/src/entries/comments.js b/app/src/entries/comments.js new file mode 100644 index 0000000..980af1d --- /dev/null +++ b/app/src/entries/comments.js @@ -0,0 +1,10 @@ +import { createApp } from 'vue'; +import '@/shell/shell-frame.css'; +import Comments from '@/pages/comments/Comments.vue'; + +const mountEl = document.getElementById('aurora-admin-comments-root'); + +if (mountEl) { + const data = JSON.parse(mountEl.dataset.auroraAdmin || '{}'); + createApp(Comments, { serverData: data }).mount(mountEl); +} diff --git a/app/src/entries/dashboard.js b/app/src/entries/dashboard.js new file mode 100644 index 0000000..3c631ca --- /dev/null +++ b/app/src/entries/dashboard.js @@ -0,0 +1,12 @@ +import { createApp } from 'vue'; +// Pulls in the shared --aurora-* theme variables. Harmless if the site-wide +// shell already loaded them (identical values); keeps this page self-sufficient. +import '@/shell/shell-frame.css'; +import DashboardPage from '@/pages/dashboard/Dashboard.vue'; + +const mountEl = document.getElementById('aurora-admin-dashboard-root'); + +if (mountEl) { + const data = JSON.parse(mountEl.dataset.auroraAdmin || '{}'); + createApp(DashboardPage, { serverData: data }).mount(mountEl); +} diff --git a/app/src/entries/export.js b/app/src/entries/export.js new file mode 100644 index 0000000..c2d3067 --- /dev/null +++ b/app/src/entries/export.js @@ -0,0 +1,10 @@ +import { createApp } from 'vue'; +import '@/shell/shell-frame.css'; +import Export from '@/pages/export/Export.vue'; + +const mountEl = document.getElementById('aurora-admin-export-root'); + +if (mountEl) { + const data = JSON.parse(mountEl.dataset.auroraAdmin || '{}'); + createApp(Export, { serverData: data }).mount(mountEl); +} diff --git a/app/src/entries/import.js b/app/src/entries/import.js new file mode 100644 index 0000000..ef492b5 --- /dev/null +++ b/app/src/entries/import.js @@ -0,0 +1,10 @@ +import { createApp } from 'vue'; +import '@/shell/shell-frame.css'; +import Import from '@/pages/import/Import.vue'; + +const mountEl = document.getElementById('aurora-admin-import-root'); + +if (mountEl) { + const data = JSON.parse(mountEl.dataset.auroraAdmin || '{}'); + createApp(Import, { serverData: data }).mount(mountEl); +} diff --git a/app/src/entries/media-upload.js b/app/src/entries/media-upload.js new file mode 100644 index 0000000..89c1b38 --- /dev/null +++ b/app/src/entries/media-upload.js @@ -0,0 +1,10 @@ +import { createApp } from 'vue'; +import '@/shell/shell-frame.css'; +import MediaUpload from '@/pages/media-upload/MediaUpload.vue'; + +const mountEl = document.getElementById('aurora-admin-media-upload-root'); + +if (mountEl) { + const data = JSON.parse(mountEl.dataset.auroraAdmin || '{}'); + createApp(MediaUpload, { serverData: data }).mount(mountEl); +} diff --git a/app/src/entries/media.js b/app/src/entries/media.js new file mode 100644 index 0000000..ca26cc5 --- /dev/null +++ b/app/src/entries/media.js @@ -0,0 +1,10 @@ +import { createApp } from 'vue'; +import '@/shell/shell-frame.css'; +import Media from '@/pages/media/Media.vue'; + +const mountEl = document.getElementById('aurora-admin-media-root'); + +if (mountEl) { + const data = JSON.parse(mountEl.dataset.auroraAdmin || '{}'); + createApp(Media, { serverData: data }).mount(mountEl); +} diff --git a/app/src/entries/menu-creator.js b/app/src/entries/menu-creator.js new file mode 100644 index 0000000..7e7d644 --- /dev/null +++ b/app/src/entries/menu-creator.js @@ -0,0 +1,10 @@ +import { createApp } from 'vue'; +import '@/shell/shell-frame.css'; +import MenuCreator from '@/pages/menu-creator/MenuCreator.vue'; + +const mountEl = document.getElementById('aurora-admin-menu-creator-root'); + +if (mountEl) { + const data = JSON.parse(mountEl.dataset.auroraAdmin || '{}'); + createApp(MenuCreator, { serverData: data }).mount(mountEl); +} diff --git a/app/src/entries/modules.js b/app/src/entries/modules.js new file mode 100644 index 0000000..fa5db7a --- /dev/null +++ b/app/src/entries/modules.js @@ -0,0 +1,10 @@ +import { createApp } from 'vue'; +import '@/shell/shell-frame.css'; +import Modules from '@/pages/modules/Modules.vue'; + +const mountEl = document.getElementById('aurora-admin-modules-root'); + +if (mountEl) { + const data = JSON.parse(mountEl.dataset.auroraAdmin || '{}'); + createApp(Modules, { serverData: data }).mount(mountEl); +} diff --git a/app/src/entries/native-settings.js b/app/src/entries/native-settings.js new file mode 100644 index 0000000..9d2d42a --- /dev/null +++ b/app/src/entries/native-settings.js @@ -0,0 +1,10 @@ +import { createApp } from 'vue'; +import '@/shell/shell-frame.css'; +import NativeSettings from '@/pages/native-settings/NativeSettings.vue'; + +const mountEl = document.getElementById('aurora-admin-native-settings-root'); + +if (mountEl) { + const data = JSON.parse(mountEl.dataset.auroraAdmin || '{}'); + createApp(NativeSettings, { serverData: data }).mount(mountEl); +} diff --git a/app/src/entries/nav-menus.js b/app/src/entries/nav-menus.js new file mode 100644 index 0000000..da6a771 --- /dev/null +++ b/app/src/entries/nav-menus.js @@ -0,0 +1,10 @@ +import { createApp } from 'vue'; +import '@/shell/shell-frame.css'; +import NavMenus from '@/pages/nav-menus/NavMenus.vue'; + +const mountEl = document.getElementById('aurora-admin-nav-menus-root'); + +if (mountEl) { + const data = JSON.parse(mountEl.dataset.auroraAdmin || '{}'); + createApp(NavMenus, { serverData: data }).mount(mountEl); +} diff --git a/app/src/entries/pages.js b/app/src/entries/pages.js new file mode 100644 index 0000000..3f4b6d7 --- /dev/null +++ b/app/src/entries/pages.js @@ -0,0 +1,10 @@ +import { createApp } from 'vue'; +import '@/shell/shell-frame.css'; +import Pages from '@/pages/pages/Pages.vue'; + +const mountEl = document.getElementById('aurora-admin-pages-root'); + +if (mountEl) { + const data = JSON.parse(mountEl.dataset.auroraAdmin || '{}'); + createApp(Pages, { serverData: data }).mount(mountEl); +} diff --git a/app/src/entries/plugins.js b/app/src/entries/plugins.js new file mode 100644 index 0000000..02d68d2 --- /dev/null +++ b/app/src/entries/plugins.js @@ -0,0 +1,10 @@ +import { createApp } from 'vue'; +import '@/shell/shell-frame.css'; +import Plugins from '@/pages/plugins/Plugins.vue'; + +const mountEl = document.getElementById('aurora-admin-plugins-root'); + +if (mountEl) { + const data = JSON.parse(mountEl.dataset.auroraAdmin || '{}'); + createApp(Plugins, { serverData: data }).mount(mountEl); +} diff --git a/app/src/entries/posts.js b/app/src/entries/posts.js new file mode 100644 index 0000000..f66b67f --- /dev/null +++ b/app/src/entries/posts.js @@ -0,0 +1,10 @@ +import { createApp } from 'vue'; +import '@/shell/shell-frame.css'; +import Posts from '@/pages/posts/Posts.vue'; + +const mountEl = document.getElementById('aurora-admin-posts-root'); + +if (mountEl) { + const data = JSON.parse(mountEl.dataset.auroraAdmin || '{}'); + createApp(Posts, { serverData: data }).mount(mountEl); +} diff --git a/app/src/entries/profile.js b/app/src/entries/profile.js new file mode 100644 index 0000000..436b1d9 --- /dev/null +++ b/app/src/entries/profile.js @@ -0,0 +1,10 @@ +import { createApp } from 'vue'; +import '@/shell/shell-frame.css'; +import Profile from '@/pages/profile/Profile.vue'; + +const mountEl = document.getElementById('aurora-admin-profile-root'); + +if (mountEl) { + const data = JSON.parse(mountEl.dataset.auroraAdmin || '{}'); + createApp(Profile, { serverData: data }).mount(mountEl); +} diff --git a/app/src/entries/role-editor.js b/app/src/entries/role-editor.js new file mode 100644 index 0000000..da56b68 --- /dev/null +++ b/app/src/entries/role-editor.js @@ -0,0 +1,10 @@ +import { createApp } from 'vue'; +import '@/shell/shell-frame.css'; +import RoleEditor from '@/pages/role-editor/RoleEditor.vue'; + +const mountEl = document.getElementById('aurora-admin-role-editor-root'); + +if (mountEl) { + const data = JSON.parse(mountEl.dataset.auroraAdmin || '{}'); + createApp(RoleEditor, { serverData: data }).mount(mountEl); +} diff --git a/app/src/entries/settings.js b/app/src/entries/settings.js new file mode 100644 index 0000000..f6a5139 --- /dev/null +++ b/app/src/entries/settings.js @@ -0,0 +1,10 @@ +import { createApp } from 'vue'; +import '@/shell/shell-frame.css'; +import SettingsPage from '@/pages/settings/Settings.vue'; + +const mountEl = document.getElementById('aurora-admin-settings-root'); + +if (mountEl) { + const data = JSON.parse(mountEl.dataset.auroraAdmin || '{}'); + createApp(SettingsPage, { serverData: data }).mount(mountEl); +} diff --git a/app/src/entries/setup-wizard.js b/app/src/entries/setup-wizard.js new file mode 100644 index 0000000..bd4b2e4 --- /dev/null +++ b/app/src/entries/setup-wizard.js @@ -0,0 +1,10 @@ +import { createApp } from 'vue'; +import '@/shell/shell-frame.css'; +import SetupWizard from '@/pages/setup-wizard/SetupWizard.vue'; + +const mountEl = document.getElementById('aurora-admin-setup-root'); + +if (mountEl) { + const data = JSON.parse(mountEl.dataset.auroraAdmin || '{}'); + createApp(SetupWizard, { serverData: data }).mount(mountEl); +} diff --git a/app/src/entries/shell.js b/app/src/entries/shell.js new file mode 100644 index 0000000..cca3f92 --- /dev/null +++ b/app/src/entries/shell.js @@ -0,0 +1,10 @@ +import { createApp } from 'vue'; +import '@/shell/shell-frame.css'; +import SiteShell from '@/shell/SiteShell.vue'; + +const mountEl = document.getElementById('aurora-admin-shell-root'); + +if (mountEl) { + const data = JSON.parse(mountEl.dataset.auroraAdmin || '{}'); + createApp(SiteShell, { serverData: data }).mount(mountEl); +} diff --git a/app/src/entries/themes.js b/app/src/entries/themes.js new file mode 100644 index 0000000..cbe192f --- /dev/null +++ b/app/src/entries/themes.js @@ -0,0 +1,10 @@ +import { createApp } from 'vue'; +import '@/shell/shell-frame.css'; +import Themes from '@/pages/themes/Themes.vue'; + +const mountEl = document.getElementById('aurora-admin-themes-root'); + +if (mountEl) { + const data = JSON.parse(mountEl.dataset.auroraAdmin || '{}'); + createApp(Themes, { serverData: data }).mount(mountEl); +} diff --git a/app/src/entries/tools.js b/app/src/entries/tools.js new file mode 100644 index 0000000..3d5d03e --- /dev/null +++ b/app/src/entries/tools.js @@ -0,0 +1,10 @@ +import { createApp } from 'vue'; +import '@/shell/shell-frame.css'; +import Tools from '@/pages/tools/Tools.vue'; + +const mountEl = document.getElementById('aurora-admin-tools-root'); + +if (mountEl) { + const data = JSON.parse(mountEl.dataset.auroraAdmin || '{}'); + createApp(Tools, { serverData: data }).mount(mountEl); +} diff --git a/app/src/entries/users.js b/app/src/entries/users.js new file mode 100644 index 0000000..999f82e --- /dev/null +++ b/app/src/entries/users.js @@ -0,0 +1,10 @@ +import { createApp } from 'vue'; +import '@/shell/shell-frame.css'; +import Users from '@/pages/users/Users.vue'; + +const mountEl = document.getElementById('aurora-admin-users-root'); + +if (mountEl) { + const data = JSON.parse(mountEl.dataset.auroraAdmin || '{}'); + createApp(Users, { serverData: data }).mount(mountEl); +} diff --git a/app/src/entries/widgets.js b/app/src/entries/widgets.js new file mode 100644 index 0000000..a460279 --- /dev/null +++ b/app/src/entries/widgets.js @@ -0,0 +1,10 @@ +import { createApp } from 'vue'; +import '@/shell/shell-frame.css'; +import Widgets from '@/pages/widgets/Widgets.vue'; + +const mountEl = document.getElementById('aurora-admin-widgets-root'); + +if (mountEl) { + const data = JSON.parse(mountEl.dataset.auroraAdmin || '{}'); + createApp(Widgets, { serverData: data }).mount(mountEl); +} diff --git a/app/src/pages/_shared/AppListItem.vue b/app/src/pages/_shared/AppListItem.vue new file mode 100644 index 0000000..859d726 --- /dev/null +++ b/app/src/pages/_shared/AppListItem.vue @@ -0,0 +1,86 @@ + + + + + diff --git a/app/src/pages/_shared/AppListShell.vue b/app/src/pages/_shared/AppListShell.vue new file mode 100644 index 0000000..1a28273 --- /dev/null +++ b/app/src/pages/_shared/AppListShell.vue @@ -0,0 +1,204 @@ + + + + + diff --git a/app/src/pages/activity-log/ActivityLog.vue b/app/src/pages/activity-log/ActivityLog.vue new file mode 100644 index 0000000..e6d789e --- /dev/null +++ b/app/src/pages/activity-log/ActivityLog.vue @@ -0,0 +1,324 @@ + + + + + diff --git a/app/src/pages/admin-notices/AdminNotices.vue b/app/src/pages/admin-notices/AdminNotices.vue new file mode 100644 index 0000000..4363b71 --- /dev/null +++ b/app/src/pages/admin-notices/AdminNotices.vue @@ -0,0 +1,217 @@ + + +