-
+
+
+
+
+
+
diff --git a/package.json b/package.json
index 53229bd78d..8e38efd730 100644
--- a/package.json
+++ b/package.json
@@ -36,6 +36,7 @@
"devDependencies": {
"@eslint/js": "^9.15.0",
"@faker-js/faker": "^9.5.0",
+ "@fullhuman/postcss-purgecss": "^6.0.0",
"@rsbuild/plugin-react": "^1.3.4",
"@rsdoctor/rspack-plugin": "^1.1.8",
"@rspack/cli": "^1.6.2",
@@ -71,6 +72,8 @@
"husky": "^9.1.7",
"lint-staged": "^16.1.0",
"path": "^0.12.7",
+ "postcss": "^8.4.38",
+ "postcss-loader": "^8.1.1",
"prettier": "^3.8.1",
"sass": "^1.62.1",
"sass-loader": "^16.0.5",
diff --git a/purgecss.config.mjs b/purgecss.config.mjs
new file mode 100644
index 0000000000..36291afb74
--- /dev/null
+++ b/purgecss.config.mjs
@@ -0,0 +1,140 @@
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+// Breakpoint first segment for responsive utilities — keep in sync with
+// assets/core/scss/tokens/_utility-config.scss ($tutor-responsive-breakpoints).
+const tutorUtilityBreakpoints = ['xl', 'lg', 'md', 'sm'];
+const tutorResponsiveUtilityPrefix = `(?:${tutorUtilityBreakpoints.join('|')})-`;
+
+// Prefixes after `tutor-` (or after `tutor-{bp}-`) from assets/core/scss/utilities.
+// Used by PurgeCSS so real utilities are not treated as component safelist entries.
+const utilityPrefixes = [
+ 'm[trblxy]?',
+ 'p[trblxy]?',
+ 'p[1-3]',
+ 'w',
+ 'h',
+ 'min-w',
+ 'min-h',
+ 'max-w',
+ 'max-h',
+ 'bg',
+ 'text',
+ 'surface',
+ 'icon',
+ 'actions',
+ 'shadow',
+ 'opacity',
+ 'border',
+ 'rounded',
+ 'block',
+ 'inline',
+ 'flex',
+ 'grid',
+ 'hidden',
+ 'justify',
+ 'items',
+ 'content',
+ 'self',
+ 'gap',
+ 'gap-x',
+ 'gap-y',
+ 'col',
+ 'static',
+ 'fixed',
+ 'absolute',
+ 'relative',
+ 'sticky',
+ 'top',
+ 'bottom',
+ 'left',
+ 'right',
+ 'inset',
+ 'z',
+ 'overflow',
+ 'float',
+ 'ratio',
+ 'h[1-5]',
+ 'medium',
+ 'small',
+ 'tiny',
+ 'font',
+ 'underline',
+ 'line-through',
+ 'no-underline',
+ 'uppercase',
+ 'lowercase',
+ 'capitalize',
+ 'normal-case',
+ 'truncate',
+ 'whitespace',
+ 'break',
+ 'list',
+ 'hover',
+ 'focus',
+ 'transition',
+ 'duration',
+ 'delay',
+ 'animate',
+ 'origin',
+ 'scale',
+ 'rotate',
+ 'translate',
+ 'skew',
+ 'transform',
+ 'backface',
+];
+
+export const tutorComponentsRegex = new RegExp(
+ `^tutor-(?!(${tutorResponsiveUtilityPrefix})?(${utilityPrefixes.join('|')})(-|$))`,
+);
+
+export const purgecssContent = [
+ // Tutor LMS paths
+ path.resolve(__dirname, './components/**/*.php'),
+ path.resolve(__dirname, './templates/**/*.php'),
+ path.resolve(__dirname, './views/**/*.php'),
+ path.resolve(__dirname, './classes/**/*.php'),
+ path.resolve(__dirname, './assets/src/js/**/*.{js,ts,jsx,tsx}'),
+ path.resolve(__dirname, './assets/core/ts/**/*.{ts,tsx}'),
+ path.resolve(__dirname, './includes/**/*.php'),
+ path.resolve(__dirname, './ecommerce/**/*.php'),
+ path.resolve(__dirname, './tutor.php'),
+ // Third Party Scripts
+ path.resolve(__dirname, './node_modules/vanilla-calendar-pro/**/*.js'),
+ // Tutor LMS Pro paths
+ path.resolve(__dirname, '../tutor-pro/templates/**/*.php'),
+ path.resolve(__dirname, '../tutor-pro/classes/**/*.php'),
+ path.resolve(__dirname, '../tutor-pro/views/**/*.php'),
+ path.resolve(__dirname, '../tutor-pro/assets/src/js/**/*.{js,ts,jsx,tsx}'),
+ path.resolve(__dirname, '../tutor-pro/includes/**/*.php'),
+ path.resolve(__dirname, '../tutor-pro/addons/**/*.php'),
+ path.resolve(__dirname, '../tutor-pro/addons/**/*.{js,ts,jsx,tsx}'),
+ path.resolve(__dirname, '../tutor-pro/ecommerce/**/*.php'),
+ path.resolve(__dirname, '../tutor-pro/gift-course/**/*.php'),
+ path.resolve(__dirname, '../tutor-pro/tutor-pro.php'),
+];
+
+export const purgecssSafelist = {
+ standard: [
+ /^is-/,
+ /^has-/,
+ /^show-/,
+ /^tutor-theme-/,
+ /^vc-/,
+ /^wp-editor-/,
+ /^mce-/,
+ /^quicktags-/,
+ /^arrow-/,
+ tutorComponentsRegex,
+ 'active',
+ 'disabled',
+ 'failed',
+ 'passed',
+ 'pending',
+ ],
+ deep: [/^vc-/, /^tutor-vc-/, /^tutor-range-calendar/],
+ greedy: [/data-vc/, /data-active/, /data-tutor-theme/, /data-tutor-contrast/],
+};
diff --git a/rspack.config.mjs b/rspack.config.mjs
index bfc76e5f62..329a31597a 100644
--- a/rspack.config.mjs
+++ b/rspack.config.mjs
@@ -1,3 +1,4 @@
+import purgecss from '@fullhuman/postcss-purgecss';
import { RsdoctorRspackPlugin } from '@rsdoctor/rspack-plugin';
import { rspack } from '@rspack/core';
import fs from 'node:fs';
@@ -5,6 +6,7 @@ import path from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
import nodeExternals from 'webpack-node-externals';
+import { purgecssContent, purgecssSafelist } from './purgecss.config.mjs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -59,6 +61,28 @@ const createConfig = (env, options) => {
const isDevelopment = mode === 'development';
const isMakePot = env?.['make-pot'];
+ const cssLoaderConfig = {
+ loader: 'css-loader',
+ options: {
+ url: {
+ filter: (url) => {
+ return /\.(woff2?|woff|ttf|otf|eot)(\?.*)?$/i.test(url);
+ },
+ },
+ },
+ };
+
+ const sassLoaderConfig = {
+ loader: 'sass-loader',
+ options: {
+ implementation: 'sass',
+ sassOptions: {
+ outputStyle: isDevelopment ? 'expanded' : 'compressed',
+ silenceDeprecations: ['abs-percent', 'color-functions', 'global-builtin', 'import', 'legacy-js-api'],
+ },
+ },
+ };
+
const baseConfig = {
mode,
cache: false,
@@ -66,30 +90,33 @@ const createConfig = (env, options) => {
rules: [
{
test: /\.s[ac]ss$/i,
+ include: [path.resolve(__dirname, 'assets/core/scss')],
use: [
rspack.CssExtractRspackPlugin.loader,
+ cssLoaderConfig,
{
- loader: 'css-loader',
+ loader: 'postcss-loader',
options: {
- url: {
- filter: (url) => {
- return /\.(woff2?|woff|ttf|otf|eot)(\?.*)?$/i.test(url);
- },
- },
- },
- },
- {
- loader: 'sass-loader',
- options: {
- implementation: 'sass',
- sassOptions: {
- outputStyle: isDevelopment ? 'expanded' : 'compressed',
- silenceDeprecations: ['abs-percent', 'color-functions', 'global-builtin', 'import', 'legacy-js-api'],
+ postcssOptions: {
+ plugins: [
+ !isDevelopment &&
+ purgecss({
+ content: purgecssContent,
+ defaultExtractor: (content) => content.match(/[\w-/:]+(?
-
\ No newline at end of file
+
diff --git a/templates/dashboard/account/settings/preferences.php b/templates/dashboard/account/settings/preferences.php
index 3b603e6703..ae20953f97 100644
--- a/templates/dashboard/account/settings/preferences.php
+++ b/templates/dashboard/account/settings/preferences.php
@@ -14,6 +14,7 @@
use Tutor\Components\SvgIcon;
use TUTOR\UserPreference;
use Tutor\Components\ConfirmationModal;
+use Tutor\Components\Constants\Color;
use Tutor\Components\InputField;
use Tutor\Components\Constants\InputType;
use Tutor\Components\Constants\Size;
@@ -21,11 +22,11 @@
use Tutor\Options_V2;
use TUTOR\User;
-$theme_options = UserPreference::get_theme_options();
-
-$learning_mood_options = UserPreference::get_learning_mood_options();
-
-$font_scale_options = UserPreference::get_font_scale_options();
+$theme_options = UserPreference::get_theme_options();
+$vision_options = UserPreference::get_vision_options();
+$motion_effects_options = UserPreference::get_motion_effects_options();
+$learning_mood_options = UserPreference::get_learning_mood_options();
+$font_scale_options = UserPreference::get_font_scale_options();
// Load current user preferences to seed the form.
$user_preferences = UserPreference::get_preferences();
@@ -38,7 +39,6 @@