diff --git a/assets/core/scss/components/_select.scss b/assets/core/scss/components/_select.scss index 1048bb7159..226f362bba 100644 --- a/assets/core/scss/components/_select.scss +++ b/assets/core/scss/components/_select.scss @@ -287,6 +287,10 @@ background: $tutor-surface-l2; } + &[data-selected='true'] { + background: $tutor-surface-l2; + } + &[data-disabled='true'] { color: $tutor-text-subdued; cursor: not-allowed; diff --git a/assets/core/scss/main.scss b/assets/core/scss/main.scss index 22dd0babcf..51078b8f6a 100644 --- a/assets/core/scss/main.scss +++ b/assets/core/scss/main.scss @@ -66,43 +66,63 @@ body { display: none !important; } -// View Transitions for Theme Toggle -::view-transition-group(root) { - animation-duration: 0.7s; - animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); -} - ::view-transition-new(root) { - animation-name: tutor-theme-reveal-light; - animation-fill-mode: both; + animation: tutor-theme-fade-in 220ms ease-out both; } -::view-transition-old(root), -[data-tutor-theme="dark"]::view-transition-old(root) { - animation: none; - animation-fill-mode: both; - z-index: -1; +::view-transition-old(root) { + animation: tutor-theme-fade-out 220ms ease-out both; } -[data-tutor-theme="dark"]::view-transition-new(root) { - animation-name: tutor-theme-reveal-dark; - animation-fill-mode: both; -} - -@keyframes tutor-theme-reveal-dark { +@keyframes tutor-theme-fade-in { from { - clip-path: polygon(50% -71%, -50% 71%, -50% 71%, 50% -71%); + opacity: 0; } to { - clip-path: polygon(50% -71%, -50% 71%, 50% 171%, 171% 50%); + opacity: 1; } } -@keyframes tutor-theme-reveal-light { +@keyframes tutor-theme-fade-out { from { - clip-path: polygon(171% 50%, 50% 171%, 50% 171%, 171% 50%); + opacity: 1; } to { - clip-path: polygon(171% 50%, 50% 171%, -50% 71%, 50% -71%); + opacity: 0; + } +} + +// Reduce Motion +[data-tutor-motion='reduce'] *, +[data-tutor-motion='reduce'] *::before, +[data-tutor-motion='reduce'] *::after { + animation: none !important; + scroll-behavior: auto !important; +} + +[data-tutor-motion='reduce'] * { + transition-duration: 0.1s !important; +} + +[data-tutor-motion='reduce']::view-transition-new(root), +[data-tutor-motion='reduce']::view-transition-old(root) { + animation: none !important; +} + +@media (prefers-reduced-motion: reduce) { + [data-tutor-motion='auto'] *, + [data-tutor-motion='auto'] *::before, + [data-tutor-motion='auto'] *::after { + animation: none !important; + scroll-behavior: auto !important; + } + + [data-tutor-motion='auto'] * { + transition-duration: 0.1s !important; + } + + [data-tutor-motion='auto']::view-transition-new(root), + [data-tutor-motion='auto']::view-transition-old(root) { + animation: none !important; } } diff --git a/assets/core/scss/mixins/_avatars.scss b/assets/core/scss/mixins/_avatars.scss index e38a14f55c..dff702f286 100644 --- a/assets/core/scss/mixins/_avatars.scss +++ b/assets/core/scss/mixins/_avatars.scss @@ -8,7 +8,7 @@ position: relative; border-radius: $tutor-radius-full; overflow: hidden; - background-color: $tutor-button-primary-soft; + background-color: $tutor-surface-brand-quaternary; color: $tutor-text-primary; flex-shrink: 0; user-select: none; diff --git a/assets/core/scss/mixins/_badges.scss b/assets/core/scss/mixins/_badges.scss index 3ac7bf698b..3f838097aa 100644 --- a/assets/core/scss/mixins/_badges.scss +++ b/assets/core/scss/mixins/_badges.scss @@ -6,7 +6,7 @@ @include tutor-typography('tiny', 'medium', 'secondary'); border-radius: $tutor-radius-sm; padding-inline: $tutor-spacing-4; - display: flex; + display: inline-flex; align-items: center; gap: $tutor-spacing-3; background-color: $tutor-actions-gray-secondary; diff --git a/assets/core/scss/mixins/_buttons.scss b/assets/core/scss/mixins/_buttons.scss index 1450c2f3eb..de350cef0f 100644 --- a/assets/core/scss/mixins/_buttons.scss +++ b/assets/core/scss/mixins/_buttons.scss @@ -391,15 +391,29 @@ position: absolute; left: 50%; top: 50%; + transform: translate(-50%, -50%); width: 16px; height: 16px; border: 2px solid currentColor; border-radius: 50%; border-top-color: transparent; animation: tutor-button-spin 1s linear infinite; + + [data-tutor-motion='reduce'] & { + animation: tutor-button-pulse 1.5s ease-in-out infinite !important; + border-top-color: currentColor; + } + + @media (prefers-reduced-motion: reduce) { + [data-tutor-motion='auto'] & { + animation: tutor-button-pulse 1.5s ease-in-out infinite !important; + border-top-color: currentColor; + } + } } } +// Keyframes for the standard spin @keyframes tutor-button-spin { 0% { transform: translate(-50%, -50%) rotate(0deg); @@ -409,6 +423,19 @@ } } +// Keyframes for the reduced motion "Pulse" +@keyframes tutor-button-pulse { + 0%, + 100% { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } + 50% { + opacity: 0.4; + transform: translate(-50%, -50%) scale(0.9); + } +} + @mixin tutor-button-reset { background: none; border: none; diff --git a/assets/core/scss/themes/_dark.scss b/assets/core/scss/themes/_dark.scss index 9c0ce86e14..514731271f 100644 --- a/assets/core/scss/themes/_dark.scss +++ b/assets/core/scss/themes/_dark.scss @@ -9,14 +9,15 @@ // ============================================================================= --tutor-surface-base: #{$tutor-gray-900}; - --tutor-surface-l1: #{$tutor-gray-950}; - --tutor-surface-l1-hover: #{$tutor-gray-800}; - --tutor-surface-l2: #{$tutor-gray-900}; + --tutor-surface-l1: #{$tutor-gray-800}; + --tutor-surface-l1-hover: #{$tutor-gray-750}; + --tutor-surface-l2: #{$tutor-gray-700}; --tutor-surface-l2-hover: #{$tutor-gray-800}; --tutor-surface-brand-dark: #{$tutor-brand-950}; --tutor-surface-brand-primary: #{$tutor-brand-600}; + --tutor-surface-brand-primary-2: #{$tutor-brand-800}; --tutor-surface-brand-secondary: #{$tutor-brand-300}; - --tutor-surface-brand-tertiary: #{$tutor-gray-900}; + --tutor-surface-brand-tertiary: #{$tutor-gray-750}; --tutor-surface-brand-quaternary: #{$tutor-brand-900}; --tutor-surface-sidebar-l1: #{$tutor-gray-950}; --tutor-surface-exception2-secondary: #{$tutor-exception-2-tertiary}; @@ -24,7 +25,7 @@ --tutor-surface-dark: #{$tutor-gray-800}; --tutor-surface-exception6: #{$tutor-exception-6}; --tutor-surface-exception7: #{$tutor-brand-900}; - --tutor-surface-warning: #{$tutor-warning-950}; + --tutor-surface-warning: #{$tutor-gray-750}; --tutor-surface-warning-hover: #{$tutor-warning-900}; --tutor-surface-success: #{$tutor-success-950}; --tutor-surface-critical: #{$tutor-error-950}; @@ -33,12 +34,12 @@ // TEXT COLORS // ============================================================================= - --tutor-text-primary: #{$tutor-gray-1}; + --tutor-text-primary: #{$tutor-gray-100}; --tutor-text-primary-inverse: #{$tutor-gray-1}; --tutor-text-secondary: #{$tutor-gray-300}; --tutor-text-subdued: #{$tutor-gray-500}; --tutor-text-disabled: #{$tutor-gray-600}; - --tutor-text-brand: #{$tutor-brand-600}; + --tutor-text-brand: #{$tutor-brand-500}; --tutor-text-brand-hover: #{$tutor-brand-700}; --tutor-text-brand-secondary: #{$tutor-brand-400}; --tutor-text-light: #{$tutor-gray-25}; @@ -52,7 +53,6 @@ --tutor-text-exception4: #{$tutor-warning-400}; --tutor-text-exception5: #{$tutor-exception-5}; --tutor-text-highlighted-hover: #{$tutor-gray-700}; - --tutor-text-disabled: #{$tutor-gray-300}; // ============================================================================= // ICON COLORS @@ -64,12 +64,13 @@ --tutor-icon-secondary: #{$tutor-gray-500}; --tutor-icon-subdued: #{$tutor-gray-600}; --tutor-icon-disabled: #{$tutor-gray-600}; - --tutor-icon-brand: #{$tutor-brand-600}; + --tutor-icon-brand: #{$tutor-brand-500}; --tutor-icon-brand-hover: #{$tutor-brand-700}; --tutor-icon-brand-secondary: #{$tutor-brand-300}; --tutor-icon-exception1: #{$tutor-exception-1}; --tutor-icon-exception2: #{$tutor-exception-2}; - --tutor-icon-success-primary: #{$tutor-success-600}; + --tutor-icon-success-primary: #{$tutor-success-700}; + --tutor-icon-success-secondary: #{$tutor-success-500}; --tutor-icon-exception4: #{$tutor-warning-400}; --tutor-icon-exception5: #{$tutor-exception-5}; --tutor-icon-caution: #{$tutor-yellow-400}; @@ -86,9 +87,9 @@ --tutor-button-primary-hover: #{$tutor-brand-700}; --tutor-button-primary-focused: #{$tutor-brand-600}; --tutor-button-primary-disabled: #{$tutor-brand-400}; - --tutor-button-primary-soft: #{$tutor-brand-200}; - --tutor-button-primary-soft-hover: #{$tutor-brand-300}; - --tutor-button-primary-soft-focused: #{$tutor-brand-200}; + --tutor-button-primary-soft: #{$tutor-brand-300}; + --tutor-button-primary-soft-hover: #{$tutor-gray-700}; + --tutor-button-primary-soft-focused: #{$tutor-brand-300}; --tutor-button-disabled: #{$tutor-gray-750}; --tutor-button-destructive: #{$tutor-error-600}; --tutor-button-destructive-hover: #{$tutor-error-700}; @@ -96,39 +97,39 @@ --tutor-button-destructive-soft: #{$tutor-error-100}; --tutor-button-destructive-soft-hover: #{$tutor-error-200}; --tutor-button-destructive-soft-focused: #{$tutor-error-100}; - --tutor-button-secondary: #{$tutor-gray-600}; - --tutor-button-secondary-hover: #{$tutor-gray-700}; - --tutor-button-secondary-focused: #{$tutor-gray-600}; + --tutor-button-success: #{$tutor-success-500}; + --tutor-button-success-hover: #{$tutor-success-600}; + --tutor-button-success-focused: #{$tutor-success-500}; + --tutor-button-secondary: #{$tutor-gray-700}; + --tutor-button-secondary-hover: #{$tutor-gray-600}; + --tutor-button-secondary-focused: #{$tutor-gray-700}; --tutor-button-outline-inverse: #{$tutor-gray-950}; --tutor-button-outline-hover: #{$tutor-gray-700}; --tutor-button-outline-focused-inverse: #{$tutor-gray-950}; --tutor-button-ghost-hover: #{$tutor-gray-700}; --tutor-button-caution: #{$tutor-yellow-400}; - --tutor-button-success: #{$tutor-success-600}; - --tutor-button-success-hover: #{$tutor-success-700}; - --tutor-button-success-focused: #{$tutor-success-600}; // ============================================================================= // BORDER COLORS // ============================================================================= - --tutor-border-idle: #{$tutor-gray-800}; + --tutor-border-idle: #{$tutor-gray-750}; --tutor-border-hover: #{$tutor-gray-700}; + --tutor-border-tertiary: #{$tutor-gray-600}; --tutor-border-inverse: #{$tutor-gray-700}; --tutor-border-brand: #{$tutor-brand-600}; --tutor-border-brand-secondary: #{$tutor-gray-750}; - --tutor-border-brand-tertiary: #{$tutor-gray-700}; + --tutor-border-brand-tertiary: #{$tutor-brand-700}; --tutor-border-dark: #{$tutor-gray-700}; --tutor-border-success: #{$tutor-success-300}; - --tutor-border-success-secondary: #{$tutor-success-700}; + --tutor-border-success-secondary: #{$tutor-success-600}; --tutor-border-warning: #{$tutor-warning-200}; --tutor-border-warning-secondary: #{$tutor-warning-600}; --tutor-border-warning-tertiary: #{$tutor-warning-400}; --tutor-border-error: #{$tutor-error-300}; - --tutor-border-error-secondary: #{$tutor-error-700}; - --tutor-border-exception6: #{$tutor-exception-5}; + --tutor-border-error-secondary: #{$tutor-error-600}; --tutor-border-error-tertiary: #{$tutor-error-400}; - --tutor-border-tertiary: #{$tutor-gray-400}; + --tutor-border-exception6: #{$tutor-exception-5}; // ============================================================================= // TAB COLORS @@ -136,11 +137,13 @@ --tutor-tab-sidebar-l2: #{$tutor-gray-1}; --tutor-tab-sidebar-l2-hover: #{$tutor-gray-750}; - --tutor-tab-sidebar-l2-active: #{$tutor-gray-900}; + --tutor-tab-sidebar-l2-active: #{$tutor-gray-800}; --tutor-tab-l3-active: #{$tutor-gray-900}; - --tutor-tab-l3-active-hover: #{$tutor-gray-800}; - --tutor-tab-l3: #{$tutor-brand-950}; + --tutor-tab-l3-active-hover: #{$tutor-gray-750}; + --tutor-tab-l3: #{$tutor-gray-750}; --tutor-tab-l3-hover: #{$tutor-gray-700}; + --tutor-tab-sidebar-l4-hover: #{$tutor-gray-750}; + --tutor-tab-sidebar-l4-active: #{$tutor-gray-750}; // ============================================================================= // ACTION COLORS @@ -148,23 +151,25 @@ --tutor-actions-success-primary: #{$tutor-success-600}; --tutor-actions-success-secondary: #{$tutor-success-200}; - --tutor-actions-success-tertiary: #{$tutor-success-950}; + --tutor-actions-success-tertiary: #{$tutor-gray-700}; + --tutor-actions-success-exception: #{$tutor-cyan-950}; --tutor-actions-warning-primary: #{$tutor-warning-400}; - --tutor-actions-warning-secondary: #{$tutor-warning-950}; + --tutor-actions-warning-secondary: #{$tutor-gray-700}; --tutor-actions-warning-tertiary: #{$tutor-warning-50}; - --tutor-actions-brand-primary: #{$tutor-brand-600}; + --tutor-actions-warning-exception: #{$tutor-orange-50}; + --tutor-actions-brand-primary: #{$tutor-brand-500}; --tutor-actions-brand-secondary: #{$tutor-brand-950}; --tutor-actions-brand-tertiary: #{$tutor-brand-950}; - --tutor-actions-gray-empty: #{$tutor-gray-750}; + --tutor-actions-gray-empty: #{$tutor-gray-800}; --tutor-actions-gray-secondary: #{$tutor-gray-900}; --tutor-actions-gray-tertiary: #{$tutor-gray-800}; --tutor-actions-critical-primary: #{$tutor-error-500}; - --tutor-actions-critical-secondary: #{$tutor-error-950}; + --tutor-actions-critical-secondary: #{$tutor-gray-700}; --tutor-actions-exception3-highlight: #{$tutor-exception-3}; --tutor-actions-caution: #{$tutor-yellow-400}; --tutor-actions-caution-secondary: #{$tutor-yellow-100}; --tutor-actions-inverse: #{$tutor-gray-1}; - --tutor-actions-exception5: #{$tutor-exception-5}; + --tutor-actions-exception5: #{$tutor-exception-6}; --tutor-actions-exception6: #{$tutor-exception-6}; // ============================================================================= diff --git a/assets/core/scss/themes/_deuteranomaly.scss b/assets/core/scss/themes/_deuteranomaly.scss new file mode 100644 index 0000000000..4ba867ce5a --- /dev/null +++ b/assets/core/scss/themes/_deuteranomaly.scss @@ -0,0 +1,132 @@ +// Deuteranomaly Theme +// CVD variant for users with Deuteranomaly (shifted green cone cells — most common form). +// Replaces red/error tokens with Orange palette; success tokens use Cyan where needed. +// Applies on top of the active light or dark base theme via data-tutor-vision. + +@use '../tokens' as *; + +// ============================================================================= +// SHARED — identical values in both light and dark +// ============================================================================= + +[data-tutor-vision='deuteranomaly'] { + // ICON COLORS + --tutor-icon-caution: #{$tutor-yellow-500}; + --tutor-icon-critical: #{$tutor-orange-600}; + --tutor-icon-critical-hover: #{$tutor-orange-700}; + --tutor-icon-warning: #{$tutor-orange-700}; + --tutor-icon-warning-secondary: #{$tutor-yellow-500}; + + // BUTTON COLORS + --tutor-button-destructive: #{$tutor-orange-600}; + --tutor-button-destructive-hover: #{$tutor-orange-700}; + --tutor-button-destructive-focused: #{$tutor-orange-600}; + --tutor-button-destructive-soft: #{$tutor-orange-100}; + --tutor-button-destructive-soft-hover: #{$tutor-orange-200}; + --tutor-button-destructive-soft-focused: #{$tutor-orange-100}; + + // BORDER COLORS + --tutor-border-warning: #{$tutor-yellow-300}; + --tutor-border-warning-secondary: #{$tutor-orange-700}; + --tutor-border-success: #{$tutor-cyan-400}; + --tutor-border-success-secondary: #{$tutor-cyan-600}; + --tutor-border-error-secondary: #{$tutor-orange-700}; + --tutor-border-error-tertiary: #{$tutor-orange-400}; + + // ACTION COLORS + --tutor-actions-success-primary: #{$tutor-cyan-500}; + --tutor-actions-success-secondary: #{$tutor-cyan-400}; + --tutor-actions-warning-primary: #{$tutor-orange-300}; + --tutor-actions-warning-tertiary: #{$tutor-orange-50}; + --tutor-actions-critical-primary: #{$tutor-orange-600}; + --tutor-actions-caution: #{$tutor-yellow-500}; + --tutor-actions-caution-secondary: #{$tutor-yellow-100}; + + // TEXT COLORS + --tutor-text-critical: #{$tutor-orange-600}; + --tutor-text-critical-hover: #{$tutor-orange-700}; + --tutor-text-warning: #{$tutor-orange-700}; + --tutor-text-caution: #{$tutor-yellow-700}; +} + +// ============================================================================= +// LIGHT — overrides specific to the light base theme +// ============================================================================= + +[data-tutor-theme='light'][data-tutor-vision='deuteranomaly'] { + // SURFACE COLORS + --tutor-surface-warning: #{$tutor-warning-50}; + --tutor-surface-warning-hover: #{$tutor-warning-100}; + --tutor-surface-success: #{$tutor-cyan-50}; + --tutor-surface-critical: #{$tutor-orange-100}; + + // TEXT COLORS + --tutor-text-success: #{$tutor-success-700}; + + // ICON COLORS + --tutor-icon-success-primary: #{$tutor-success-600}; + --tutor-icon-success-secondary: #{$tutor-success-700}; + + // BUTTON COLORS + --tutor-button-success: #{$tutor-cyan-500}; + --tutor-button-success-hover: #{$tutor-cyan-600}; + --tutor-button-success-focused: #{$tutor-cyan-600}; + + // BORDER COLORS + --tutor-border-warning-tertiary: #{$tutor-orange-400}; + --tutor-border-error: #{$tutor-orange-300}; + + // ACTION COLORS + --tutor-actions-success-tertiary: #{$tutor-cyan-100}; + --tutor-actions-success-exception: #{$tutor-cyan-400}; + --tutor-actions-warning-secondary: #{$tutor-orange-100}; + --tutor-actions-warning-exception: #{$tutor-orange-600}; + --tutor-actions-critical-secondary: #{$tutor-orange-100}; +} + +// ============================================================================= +// DARK — overrides specific to the dark base theme +// ============================================================================= + +[data-tutor-theme='dark'][data-tutor-vision='deuteranomaly'] { + // SURFACE COLORS + --tutor-surface-warning: #{$tutor-gray-750}; + --tutor-surface-warning-hover: #{$tutor-warning-900}; + --tutor-surface-success: #{$tutor-cyan-900}; + --tutor-surface-critical: #{$tutor-orange-900}; + + // TEXT COLORS + --tutor-text-success: #{$tutor-cyan-400}; + + // ICON COLORS + --tutor-icon-success-primary: #{$tutor-cyan-500}; + --tutor-icon-success-secondary: #{$tutor-cyan-600}; + --tutor-icon-exception4: #{$tutor-warning-400}; + + // BUTTON COLORS + --tutor-button-success: #{$tutor-cyan-500}; + --tutor-button-success-hover: #{$tutor-cyan-600}; + --tutor-button-success-focused: #{$tutor-cyan-500}; + + // BORDER COLORS + --tutor-border-warning-tertiary: #{$tutor-orange-300}; + --tutor-border-success: #{$tutor-cyan-300}; + --tutor-border-error: #{$tutor-orange-300}; + + // ACTION COLORS + --tutor-actions-success-tertiary: #{$tutor-gray-700}; + --tutor-actions-success-exception: #{$tutor-cyan-950}; + --tutor-actions-warning-secondary: #{$tutor-gray-700}; + --tutor-actions-warning-exception: #{$tutor-orange-50}; + --tutor-actions-critical-secondary: #{$tutor-gray-700}; +} + +// ============================================================================= +// HIGH CONTRAST — applies on top of either light or dark + deuteranomaly +// ============================================================================= + +[data-tutor-vision='deuteranomaly'][data-tutor-contrast='high'] { + --tutor-actions-success-tertiary: #{$tutor-gray-700}; + --tutor-actions-warning-secondary: #{$tutor-gray-700}; + --tutor-actions-critical-secondary: #{$tutor-gray-700}; +} diff --git a/assets/core/scss/themes/_deuteranopia.scss b/assets/core/scss/themes/_deuteranopia.scss new file mode 100644 index 0000000000..f9fd33b64b --- /dev/null +++ b/assets/core/scss/themes/_deuteranopia.scss @@ -0,0 +1,125 @@ +// Deuteranopia Theme +// CVD variant for users with Deuteranopia (absent green cone cells). +// Replaces green/success tokens with Cyan palette and red/error tokens with Orange palette. +// Applies on top of the active light or dark base theme via data-tutor-vision. + +@use '../tokens' as *; + +// ============================================================================= +// SHARED — identical values in both light and dark +// ============================================================================= + +[data-tutor-vision='deuteranopia'] { + // ICON COLORS + --tutor-icon-success-primary: #{$tutor-cyan-600}; + --tutor-icon-success-secondary: #{$tutor-cyan-700}; + --tutor-icon-caution: #{$tutor-yellow-500}; + --tutor-icon-critical: #{$tutor-orange-500}; + --tutor-icon-critical-hover: #{$tutor-orange-600}; + --tutor-icon-warning: #{$tutor-orange-700}; + --tutor-icon-warning-secondary: #{$tutor-yellow-500}; + + // BUTTON COLORS + --tutor-button-destructive: #{$tutor-orange-600}; + --tutor-button-destructive-hover: #{$tutor-orange-700}; + --tutor-button-destructive-focused: #{$tutor-orange-600}; + --tutor-button-destructive-soft: #{$tutor-orange-100}; + --tutor-button-destructive-soft-hover: #{$tutor-orange-200}; + --tutor-button-destructive-soft-focused: #{$tutor-orange-100}; + --tutor-button-success: #{$tutor-cyan-600}; + --tutor-button-success-hover: #{$tutor-cyan-700}; + --tutor-button-success-focused: #{$tutor-cyan-600}; + + // BORDER COLORS + --tutor-border-warning-tertiary: #{$tutor-orange-300}; + --tutor-border-success: #{$tutor-cyan-400}; + --tutor-border-success-secondary: #{$tutor-cyan-600}; + --tutor-border-error: #{$tutor-orange-400}; + --tutor-border-error-secondary: #{$tutor-orange-600}; + --tutor-border-error-tertiary: #{$tutor-orange-300}; + + // ACTION COLORS + --tutor-actions-warning-primary: #{$tutor-orange-300}; + --tutor-actions-warning-tertiary: #{$tutor-orange-50}; + --tutor-actions-caution: #{$tutor-yellow-400}; + --tutor-actions-caution-secondary: #{$tutor-yellow-100}; + + // TEXT COLORS + --tutor-text-critical: #{$tutor-orange-500}; + --tutor-text-warning: #{$tutor-orange-600}; + --tutor-text-caution: #{$tutor-yellow-600}; +} + +// ============================================================================= +// LIGHT — overrides specific to the light base theme +// ============================================================================= + +[data-tutor-theme='light'][data-tutor-vision='deuteranopia'] { + // SURFACE COLORS + --tutor-surface-warning: #{$tutor-warning-50}; + --tutor-surface-warning-hover: #{$tutor-warning-100}; + --tutor-surface-success: #{$tutor-cyan-50}; + --tutor-surface-critical: #{$tutor-orange-100}; + + // TEXT COLORS + --tutor-text-success: #{$tutor-cyan-700}; + --tutor-text-critical-hover: #{$tutor-warning-600}; + + // BORDER COLORS + --tutor-border-warning: #{$tutor-yellow-400}; + --tutor-border-warning-secondary: #{$tutor-orange-600}; + + // ACTION COLORS + --tutor-actions-success-primary: #{$tutor-cyan-600}; + --tutor-actions-success-secondary: #{$tutor-cyan-500}; + --tutor-actions-success-tertiary: #{$tutor-cyan-100}; + --tutor-actions-success-exception: #{$tutor-cyan-400}; + --tutor-actions-warning-secondary: #{$tutor-orange-100}; + --tutor-actions-warning-exception: #{$tutor-orange-500}; + --tutor-actions-critical-primary: #{$tutor-orange-500}; + --tutor-actions-critical-secondary: #{$tutor-orange-100}; +} + +// ============================================================================= +// DARK — overrides specific to the dark base theme +// ============================================================================= + +[data-tutor-theme='dark'][data-tutor-vision='deuteranopia'] { + // SURFACE COLORS + --tutor-surface-warning: #{$tutor-gray-750}; + --tutor-surface-warning-hover: #{$tutor-warning-900}; + --tutor-surface-success: #{$tutor-cyan-900}; + --tutor-surface-critical: #{$tutor-orange-900}; + + // TEXT COLORS + --tutor-text-success: #{$tutor-cyan-400}; + --tutor-text-critical-hover: #{$tutor-orange-600}; + + // ICON COLORS + --tutor-icon-exception4: #{$tutor-warning-400}; + + // BORDER COLORS + --tutor-border-warning: #{$tutor-yellow-300}; + --tutor-border-warning-secondary: #{$tutor-orange-700}; + + // ACTION COLORS + --tutor-actions-success-primary: #{$tutor-cyan-500}; + --tutor-actions-success-secondary: #{$tutor-cyan-400}; + --tutor-actions-success-tertiary: #{$tutor-gray-700}; + --tutor-actions-success-exception: #{$tutor-cyan-950}; + --tutor-actions-warning-secondary: #{$tutor-gray-700}; + --tutor-actions-warning-exception: #{$tutor-orange-50}; + --tutor-actions-critical-primary: #{$tutor-orange-600}; + --tutor-actions-critical-secondary: #{$tutor-gray-700}; + --tutor-actions-caution: #{$tutor-yellow-500}; +} + +// ============================================================================= +// HIGH CONTRAST — applies on top of either light or dark + deuteranopia +// ============================================================================= + +[data-tutor-vision='deuteranopia'][data-tutor-contrast='high'] { + --tutor-actions-success-tertiary: #{$tutor-gray-700}; + --tutor-actions-warning-secondary: #{$tutor-gray-700}; + --tutor-actions-critical-secondary: #{$tutor-gray-700}; +} diff --git a/assets/core/scss/themes/_high-contrast.scss b/assets/core/scss/themes/_high-contrast.scss new file mode 100644 index 0000000000..9b84a02560 --- /dev/null +++ b/assets/core/scss/themes/_high-contrast.scss @@ -0,0 +1,68 @@ +// High Contrast Theme +// Accessibility variant with increased contrast ratios for users who need +// higher foreground/background differentiation. +// Applies on top of the active light or dark base theme via data-tutor-contrast. + +@use '../tokens' as *; + +// ============================================================================= +// LIGHT — overrides specific to the light base theme +// ============================================================================= + +[data-tutor-theme='light'][data-tutor-contrast='high'] { + // TEXT COLORS + --tutor-text-primary: #{$tutor-gray-950}; + --tutor-text-secondary: #{$tutor-gray-950}; + --tutor-text-subdued: #{$tutor-gray-950}; + --tutor-text-disabled: #{$tutor-gray-500}; + + // ICON COLORS + --tutor-icon-idle: #{$tutor-gray-750}; + --tutor-icon-hover: #{$tutor-gray-900}; + --tutor-icon-secondary: #{$tutor-gray-700}; + --tutor-icon-subdued: #{$tutor-gray-700}; + --tutor-icon-disabled: #{$tutor-gray-400}; + + // BORDER COLORS + --tutor-border-idle: #{$tutor-gray-800}; + --tutor-border-hover: #{$tutor-gray-900}; + --tutor-border-tertiary: #{$tutor-gray-800}; + --tutor-border-brand: #{$tutor-brand-600}; + --tutor-border-brand-secondary: #{$tutor-brand-800}; + --tutor-border-brand-tertiary: #{$tutor-brand-800}; +} + +// ============================================================================= +// DARK — overrides specific to the dark base theme +// ============================================================================= + +[data-tutor-theme='dark'][data-tutor-contrast='high'] { + // SURFACE COLORS + --tutor-surface-warning: #{$tutor-gray-750}; + + // TEXT COLORS + --tutor-text-primary: #{$tutor-gray-100}; + --tutor-text-secondary: #{$tutor-gray-50}; + --tutor-text-subdued: #{$tutor-gray-50}; + --tutor-text-disabled: #{$tutor-gray-300}; + + // ICON COLORS + --tutor-icon-idle: #{$tutor-gray-300}; + --tutor-icon-hover: #{$tutor-gray-1}; + --tutor-icon-secondary: #{$tutor-gray-500}; + --tutor-icon-subdued: #{$tutor-gray-600}; + --tutor-icon-disabled: #{$tutor-gray-600}; + + // BORDER COLORS + --tutor-border-idle: #{$tutor-gray-400}; + --tutor-border-hover: #{$tutor-gray-300}; + --tutor-border-tertiary: #{$tutor-gray-300}; + --tutor-border-brand: #{$tutor-brand-200}; + --tutor-border-brand-secondary: #{$tutor-brand-800}; + --tutor-border-brand-tertiary: #{$tutor-brand-700}; + + // ACTION COLORS + --tutor-actions-success-tertiary: #{$tutor-gray-700}; + --tutor-actions-warning-secondary: #{$tutor-gray-700}; + --tutor-actions-critical-secondary: #{$tutor-gray-700}; +} diff --git a/assets/core/scss/themes/_index.scss b/assets/core/scss/themes/_index.scss index 07cf2ab474..313e109e95 100644 --- a/assets/core/scss/themes/_index.scss +++ b/assets/core/scss/themes/_index.scss @@ -1,2 +1,6 @@ @forward "light"; @forward "dark"; +@forward "high-contrast"; +@forward "protanopia"; +@forward "deuteranopia"; +@forward "deuteranomaly"; diff --git a/assets/core/scss/themes/_light.scss b/assets/core/scss/themes/_light.scss index a24a7410ba..1db7cbc9cd 100644 --- a/assets/core/scss/themes/_light.scss +++ b/assets/core/scss/themes/_light.scss @@ -4,7 +4,7 @@ @use '../tokens' as *; :root, -[data-tutor-theme="light"] { +[data-tutor-theme='light'] { // ============================================================================= // SURFACE COLORS // ============================================================================= @@ -16,6 +16,7 @@ --tutor-surface-l2-hover: #{$tutor-gray-300}; --tutor-surface-brand-dark: #{$tutor-brand-950}; --tutor-surface-brand-primary: #{$tutor-brand-600}; + --tutor-surface-brand-primary-2: #{$tutor-brand-800}; --tutor-surface-brand-secondary: #{$tutor-brand-300}; --tutor-surface-brand-tertiary: #{$tutor-brand-100}; --tutor-surface-brand-quaternary: #{$tutor-brand-200}; @@ -37,8 +38,8 @@ --tutor-text-primary: #{$tutor-gray-950}; --tutor-text-primary-inverse: #{$tutor-gray-1}; --tutor-text-secondary: #{$tutor-gray-700}; - --tutor-text-disabled: #{$tutor-gray-500}; --tutor-text-subdued: #{$tutor-gray-500}; + --tutor-text-disabled: #{$tutor-gray-300}; --tutor-text-brand: #{$tutor-brand-600}; --tutor-text-brand-hover: #{$tutor-brand-700}; --tutor-text-brand-secondary: #{$tutor-brand-400}; @@ -53,7 +54,6 @@ --tutor-text-exception4: #{$tutor-warning-400}; --tutor-text-exception5: #{$tutor-exception-5}; --tutor-text-highlighted-hover: #{$tutor-gray-700}; - --tutor-text-disabled: #{$tutor-gray-300}; // ============================================================================= // ICON COLORS @@ -71,6 +71,7 @@ --tutor-icon-exception1: #{$tutor-exception-1}; --tutor-icon-exception2: #{$tutor-exception-2}; --tutor-icon-success-primary: #{$tutor-success-700}; + --tutor-icon-success-secondary: #{$tutor-success-600}; --tutor-icon-exception4: #{$tutor-warning-400}; --tutor-icon-exception5: #{$tutor-exception-5}; --tutor-icon-caution: #{$tutor-yellow-400}; @@ -97,6 +98,9 @@ --tutor-button-destructive-soft: #{$tutor-error-100}; --tutor-button-destructive-soft-hover: #{$tutor-error-200}; --tutor-button-destructive-soft-focused: #{$tutor-error-100}; + --tutor-button-success: #{$tutor-success-600}; + --tutor-button-success-hover: #{$tutor-success-700}; + --tutor-button-success-focused: #{$tutor-success-600}; --tutor-button-secondary: #{$tutor-gray-200}; --tutor-button-secondary-hover: #{$tutor-gray-300}; --tutor-button-secondary-focused: #{$tutor-gray-200}; @@ -105,9 +109,6 @@ --tutor-button-outline-focused-inverse: #{$tutor-gray-1}; --tutor-button-ghost-hover: #{$tutor-gray-200}; --tutor-button-caution: #{$tutor-yellow-400}; - --tutor-button-success: #{$tutor-success-600}; - --tutor-button-success-hover: #{$tutor-success-700}; - --tutor-button-success-focused: #{$tutor-success-600}; // ============================================================================= // BORDER COLORS @@ -115,6 +116,7 @@ --tutor-border-idle: #{$tutor-gray-200}; --tutor-border-hover: #{$tutor-gray-300}; + --tutor-border-tertiary: #{$tutor-gray-400}; --tutor-border-inverse: #{$tutor-gray-1}; --tutor-border-brand: #{$tutor-brand-600}; --tutor-border-brand-secondary: #{$tutor-brand-300}; @@ -127,9 +129,8 @@ --tutor-border-warning-tertiary: #{$tutor-warning-400}; --tutor-border-error: #{$tutor-error-300}; --tutor-border-error-secondary: #{$tutor-error-700}; - --tutor-border-exception6: #{$tutor-exception-5}; --tutor-border-error-tertiary: #{$tutor-error-400}; - --tutor-border-tertiary: #{$tutor-gray-400}; + --tutor-border-exception6: #{$tutor-exception-5}; // ============================================================================= // TAB COLORS @@ -142,6 +143,8 @@ --tutor-tab-l3-active-hover: #{$tutor-gray-100}; --tutor-tab-l3: #{$tutor-brand-200}; --tutor-tab-l3-hover: #{$tutor-brand-300}; + --tutor-tab-sidebar-l4-hover: #{$tutor-gray-200}; + --tutor-tab-sidebar-l4-active: #{$tutor-brand-200}; // ============================================================================= // ACTION COLORS @@ -150,9 +153,11 @@ --tutor-actions-success-primary: #{$tutor-success-600}; --tutor-actions-success-secondary: #{$tutor-success-200}; --tutor-actions-success-tertiary: #{$tutor-success-50}; + --tutor-actions-success-exception: #{$tutor-success-500}; --tutor-actions-warning-primary: #{$tutor-warning-400}; --tutor-actions-warning-secondary: #{$tutor-warning-100}; --tutor-actions-warning-tertiary: #{$tutor-warning-50}; + --tutor-actions-warning-exception: #{$tutor-warning-500}; --tutor-actions-brand-primary: #{$tutor-brand-600}; --tutor-actions-brand-secondary: #{$tutor-brand-300}; --tutor-actions-brand-tertiary: #{$tutor-brand-200}; @@ -188,4 +193,4 @@ --tutor-shadow-xl: #{$tutor-shadow-xl}; --tutor-shadow-2xl: #{$tutor-shadow-2xl}; --tutor-shadow-3xl: #{$tutor-shadow-3xl}; -} \ No newline at end of file +} diff --git a/assets/core/scss/themes/_protanopia.scss b/assets/core/scss/themes/_protanopia.scss new file mode 100644 index 0000000000..475a0e8ddd --- /dev/null +++ b/assets/core/scss/themes/_protanopia.scss @@ -0,0 +1,114 @@ +// Protanopia Theme +// CVD variant for users with Protanopia (absent red cone cells). +// Replaces green/success tokens with Cyan palette and red/error tokens with Orange palette. +// Applies on top of the active light or dark base theme via data-tutor-vision. + +@use '../tokens' as *; + +// ============================================================================= +// SHARED — identical values in both light and dark +// ============================================================================= + +[data-tutor-vision='protanopia'] { + // ICON COLORS + --tutor-icon-success-primary: #{$tutor-cyan-500}; + --tutor-icon-success-secondary: #{$tutor-cyan-600}; + --tutor-icon-caution: #{$tutor-yellow-500}; + --tutor-icon-critical: #{$tutor-orange-600}; + --tutor-icon-critical-hover: #{$tutor-orange-700}; + --tutor-icon-warning: #{$tutor-orange-700}; + --tutor-icon-warning-secondary: #{$tutor-yellow-500}; + + // BUTTON COLORS + --tutor-button-destructive: #{$tutor-orange-600}; + --tutor-button-destructive-hover: #{$tutor-orange-700}; + --tutor-button-destructive-focused: #{$tutor-orange-600}; + --tutor-button-destructive-soft: #{$tutor-orange-100}; + --tutor-button-destructive-soft-hover: #{$tutor-orange-200}; + --tutor-button-destructive-soft-focused: #{$tutor-orange-100}; + --tutor-button-success: #{$tutor-cyan-500}; + --tutor-button-success-hover: #{$tutor-cyan-600}; + --tutor-button-success-focused: #{$tutor-cyan-500}; + + // BORDER COLORS + --tutor-border-warning: #{$tutor-yellow-300}; + --tutor-border-warning-secondary: #{$tutor-orange-700}; + --tutor-border-warning-tertiary: #{$tutor-orange-300}; + --tutor-border-success: #{$tutor-cyan-300}; + --tutor-border-success-secondary: #{$tutor-cyan-600}; + --tutor-border-error: #{$tutor-orange-300}; + --tutor-border-error-secondary: #{$tutor-orange-700}; + --tutor-border-error-tertiary: #{$tutor-orange-400}; + + // ACTION COLORS + --tutor-actions-success-primary: #{$tutor-cyan-500}; + --tutor-actions-success-secondary: #{$tutor-cyan-400}; + --tutor-actions-success-exception: #{$tutor-cyan-400}; + --tutor-actions-warning-primary: #{$tutor-orange-300}; + --tutor-actions-warning-tertiary: #{$tutor-orange-50}; + --tutor-actions-critical-primary: #{$tutor-orange-600}; + --tutor-actions-caution: #{$tutor-yellow-500}; + --tutor-actions-caution-secondary: #{$tutor-yellow-100}; + + // TEXT COLORS + --tutor-text-critical: #{$tutor-orange-600}; + --tutor-text-critical-hover: #{$tutor-orange-700}; + --tutor-text-warning: #{$tutor-orange-700}; + --tutor-text-caution: #{$tutor-yellow-700}; +} + +// ============================================================================= +// LIGHT — overrides specific to the light base theme +// ============================================================================= + +[data-tutor-theme='light'][data-tutor-vision='protanopia'] { + // SURFACE COLORS + --tutor-surface-warning: #{$tutor-warning-50}; + --tutor-surface-warning-hover: #{$tutor-warning-100}; + --tutor-surface-success: #{$tutor-cyan-50}; + --tutor-surface-critical: #{$tutor-orange-100}; + + // TEXT COLORS + --tutor-text-success: #{$tutor-cyan-700}; + + // ACTION COLORS + --tutor-actions-success-tertiary: #{$tutor-cyan-100}; + --tutor-actions-warning-secondary: #{$tutor-orange-100}; + --tutor-actions-warning-exception: #{$tutor-orange-600}; + --tutor-actions-critical-secondary: #{$tutor-orange-100}; +} + +// ============================================================================= +// DARK — overrides specific to the dark base theme +// ============================================================================= + +[data-tutor-theme='dark'][data-tutor-vision='protanopia'] { + // SURFACE COLORS + --tutor-surface-warning: #{$tutor-gray-750}; + --tutor-surface-warning-hover: #{$tutor-warning-900}; + --tutor-surface-success: #{$tutor-cyan-900}; + --tutor-surface-critical: #{$tutor-orange-900}; + + // TEXT COLORS + --tutor-text-success: #{$tutor-cyan-400}; + + // ICON COLORS + --tutor-icon-exception4: #{$tutor-warning-400}; + + // ACTION COLORS + --tutor-actions-success-tertiary: #{$tutor-gray-700}; + --tutor-actions-success-exception: #{$tutor-cyan-950}; + --tutor-actions-warning-secondary: #{$tutor-gray-700}; + --tutor-actions-warning-exception: #{$tutor-orange-50}; + --tutor-actions-critical-secondary: #{$tutor-gray-700}; +} + +// ============================================================================= +// HIGH CONTRAST — applies on top of either light or dark + protanopia +// ============================================================================= + +[data-tutor-vision='protanopia'][data-tutor-contrast='high'] { + --tutor-actions-success-tertiary: #{$tutor-gray-700}; + --tutor-actions-warning-secondary: #{$tutor-gray-700}; + --tutor-actions-critical-secondary: #{$tutor-gray-700}; +} diff --git a/assets/core/scss/tokens/_actions.scss b/assets/core/scss/tokens/_actions.scss index c9a09cbc9b..072b014303 100644 --- a/assets/core/scss/tokens/_actions.scss +++ b/assets/core/scss/tokens/_actions.scss @@ -8,15 +8,17 @@ $tutor-actions-success-primary: var(--tutor-actions-success-primary); $tutor-actions-success-secondary: var(--tutor-actions-success-secondary); $tutor-actions-success-tertiary: var(--tutor-actions-success-tertiary); +$tutor-actions-success-exception: var(--tutor-actions-success-exception); $tutor-actions-warning-primary: var(--tutor-actions-warning-primary); $tutor-actions-warning-secondary: var(--tutor-actions-warning-secondary); $tutor-actions-warning-tertiary: var(--tutor-actions-warning-tertiary); +$tutor-actions-warning-exception: var(--tutor-actions-warning-exception); $tutor-actions-brand-primary: var(--tutor-actions-brand-primary); $tutor-actions-brand-secondary: var(--tutor-actions-brand-secondary); $tutor-actions-brand-tertiary: var(--tutor-actions-brand-tertiary); $tutor-actions-gray-empty: var(--tutor-actions-gray-empty); $tutor-actions-gray-secondary: var(--tutor-actions-gray-secondary); -$tutor-actions-gray-tertiary: var(--tutor-actions-gray-secondary-tertiary); +$tutor-actions-gray-tertiary: var(--tutor-actions-gray-tertiary); $tutor-actions-critical-primary: var(--tutor-actions-critical-primary); $tutor-actions-critical-secondary: var(--tutor-actions-critical-secondary); $tutor-actions-caution: var(--tutor-actions-caution); @@ -34,9 +36,11 @@ $tutor-actions: ( success-primary: $tutor-actions-success-primary, success-secondary: $tutor-actions-success-secondary, success-tertiary: $tutor-actions-success-tertiary, + success-exception: $tutor-actions-success-exception, warning-primary: $tutor-actions-warning-primary, warning-secondary: $tutor-actions-warning-secondary, warning-tertiary: $tutor-actions-warning-tertiary, + warning-exception: $tutor-actions-warning-exception, brand-primary: $tutor-actions-brand-primary, brand-secondary: $tutor-actions-brand-secondary, brand-tertiary: $tutor-actions-brand-tertiary, diff --git a/assets/core/scss/tokens/_borders.scss b/assets/core/scss/tokens/_borders.scss index 13b319ef2c..34a35e87a9 100644 --- a/assets/core/scss/tokens/_borders.scss +++ b/assets/core/scss/tokens/_borders.scss @@ -19,8 +19,8 @@ $tutor-border-warning-secondary: var(--tutor-border-warning-secondary); $tutor-border-warning-tertiary: var(--tutor-border-warning-tertiary); $tutor-border-error: var(--tutor-border-error); $tutor-border-error-secondary: var(--tutor-border-error-secondary); -$tutor-border-exception6: var(--tutor-border-exception6); $tutor-border-error-tertiary: var(--tutor-border-error-tertiary); +$tutor-border-exception6: var(--tutor-border-exception6); $tutor-border-tertiary: var(--tutor-border-tertiary); // ============================================================================= @@ -58,7 +58,9 @@ $tutor-border-colors: ( warning-tertiary: $tutor-border-warning-tertiary, error: $tutor-border-error, error-secondary: $tutor-border-error-secondary, + error-tertiary: $tutor-border-error-tertiary, exception6: $tutor-border-exception6, + tertiary: $tutor-border-tertiary, ); // ============================================================================= diff --git a/assets/core/scss/tokens/_colors.scss b/assets/core/scss/tokens/_colors.scss index d1087fa0b9..96d886d494 100644 --- a/assets/core/scss/tokens/_colors.scss +++ b/assets/core/scss/tokens/_colors.scss @@ -90,6 +90,31 @@ $tutor-yellow-800: #854a0e; $tutor-yellow-900: #713b12; $tutor-yellow-950: #542c0d; +// Cyan Colors (CVD accessibility - replaces green/success) +$tutor-cyan-50: #ecfeff; +$tutor-cyan-100: #cefafe; +$tutor-cyan-200: #a2f4fd; +$tutor-cyan-300: #53eafd; +$tutor-cyan-400: #00d3f2; +$tutor-cyan-500: #00b8db; +$tutor-cyan-600: #0092b8; +$tutor-cyan-700: #007595; +$tutor-cyan-800: #005f78; +$tutor-cyan-900: #104e64; +$tutor-cyan-950: #053345; + +// Orange Colors (CVD accessibility - replaces red/error) +$tutor-orange-50: #fefbe8; +$tutor-orange-100: #ffedd4; +$tutor-orange-200: #ffd6a7; +$tutor-orange-300: #ffb86a; +$tutor-orange-400: #ff8904; +$tutor-orange-500: #ff6900; +$tutor-orange-600: #f54900; +$tutor-orange-700: #ca3500; +$tutor-orange-800: #9f2d00; +$tutor-orange-900: #7e2a0c; + // Exception Colors $tutor-exception-1: #00acc2; $tutor-exception-2: #ee0097; @@ -203,3 +228,30 @@ $tutor-exception-colors: ( 5: $tutor-exception-5, 6: $tutor-exception-6, ); + +$tutor-cyan-colors: ( + 50: $tutor-cyan-50, + 100: $tutor-cyan-100, + 200: $tutor-cyan-200, + 300: $tutor-cyan-300, + 400: $tutor-cyan-400, + 500: $tutor-cyan-500, + 600: $tutor-cyan-600, + 700: $tutor-cyan-700, + 800: $tutor-cyan-800, + 900: $tutor-cyan-900, + 950: $tutor-cyan-950, +); + +$tutor-orange-colors: ( + 50: $tutor-orange-50, + 100: $tutor-orange-100, + 200: $tutor-orange-200, + 300: $tutor-orange-300, + 400: $tutor-orange-400, + 500: $tutor-orange-500, + 600: $tutor-orange-600, + 700: $tutor-orange-700, + 800: $tutor-orange-800, + 900: $tutor-orange-900, +); diff --git a/assets/core/scss/tokens/_icons.scss b/assets/core/scss/tokens/_icons.scss index 6c2fa2ab16..45967dad93 100644 --- a/assets/core/scss/tokens/_icons.scss +++ b/assets/core/scss/tokens/_icons.scss @@ -14,6 +14,7 @@ $tutor-icon-brand: var(--tutor-icon-brand); $tutor-icon-brand-hover: var(--tutor-icon-brand-hover); $tutor-icon-brand-secondary: var(--tutor-icon-brand-secondary); $tutor-icon-success-primary: var(--tutor-icon-success-primary); +$tutor-icon-success-secondary: var(--tutor-icon-success-secondary); $tutor-icon-critical: var(--tutor-icon-critical); $tutor-icon-critical-hover: var(--tutor-icon-critical-hover); $tutor-icon-warning: var(--tutor-icon-warning); @@ -39,6 +40,7 @@ $tutor-icons: ( brand-hover: $tutor-icon-brand-hover, brand-secondary: $tutor-icon-brand-secondary, success-primary: $tutor-icon-success-primary, + success-secondary: $tutor-icon-success-secondary, critical: $tutor-icon-critical, critical-hover: $tutor-icon-critical-hover, warning: $tutor-icon-warning, diff --git a/assets/core/scss/tokens/_index.scss b/assets/core/scss/tokens/_index.scss index 3445e6a7ed..854d4395d0 100644 --- a/assets/core/scss/tokens/_index.scss +++ b/assets/core/scss/tokens/_index.scss @@ -1,18 +1,19 @@ -@forward "actions"; -@forward "buttons"; -@forward "borders"; -@forward "breakpoints"; -@forward "colors"; -@forward "file-uploader"; -@forward "icons"; -@forward "inputs"; -@forward "shadows"; -@forward "spacing"; -@forward "surfaces"; -@forward "tabs"; -@forward "text-colors"; -@forward "typography"; -@forward "zIndex"; -@forward "popover"; -@forward "modal"; -@forward "progress"; +@forward 'utility-config'; +@forward 'actions'; +@forward 'buttons'; +@forward 'borders'; +@forward 'breakpoints'; +@forward 'colors'; +@forward 'file-uploader'; +@forward 'icons'; +@forward 'inputs'; +@forward 'shadows'; +@forward 'spacing'; +@forward 'surfaces'; +@forward 'tabs'; +@forward 'text-colors'; +@forward 'typography'; +@forward 'zIndex'; +@forward 'popover'; +@forward 'modal'; +@forward 'progress'; diff --git a/assets/core/scss/tokens/_surfaces.scss b/assets/core/scss/tokens/_surfaces.scss index 9192e21699..747c586ccd 100644 --- a/assets/core/scss/tokens/_surfaces.scss +++ b/assets/core/scss/tokens/_surfaces.scss @@ -12,6 +12,7 @@ $tutor-surface-l2: var(--tutor-surface-l2); $tutor-surface-l2-hover: var(--tutor-surface-l2-hover); $tutor-surface-brand-dark: var(--tutor-surface-brand-dark); $tutor-surface-brand-primary: var(--tutor-surface-brand-primary); +$tutor-surface-brand-primary-2: var(--tutor-surface-brand-primary-2); $tutor-surface-brand-secondary: var(--tutor-surface-brand-secondary); $tutor-surface-brand-tertiary: var(--tutor-surface-brand-tertiary); $tutor-surface-brand-quaternary: var(--tutor-surface-brand-quaternary); @@ -38,6 +39,7 @@ $tutor-surfaces: ( l2-hover: $tutor-surface-l2-hover, brand-dark: $tutor-surface-brand-dark, brand-primary: $tutor-surface-brand-primary, + brand-primary-2: $tutor-surface-brand-primary-2, brand-secondary: $tutor-surface-brand-secondary, brand-tertiary: $tutor-surface-brand-tertiary, brand-quaternary: $tutor-surface-brand-quaternary, diff --git a/assets/core/scss/tokens/_tabs.scss b/assets/core/scss/tokens/_tabs.scss index 12aa5afdc1..889bbee25a 100644 --- a/assets/core/scss/tokens/_tabs.scss +++ b/assets/core/scss/tokens/_tabs.scss @@ -18,6 +18,8 @@ $tutor-tab-height-lg: var(--tutor-tab-height-lg, 44px); $tutor-tab-sidebar-l2: var(--tutor-tab-sidebar-l2); $tutor-tab-sidebar-l2-hover: var(--tutor-tab-sidebar-l2-hover); $tutor-tab-sidebar-l2-active: var(--tutor-tab-sidebar-l2-active); +$tutor-tab-sidebar-l4-hover: var(--tutor-tab-sidebar-l4-hover); +$tutor-tab-sidebar-l4-active: var(--tutor-tab-sidebar-l4-active); $tutor-tab-l3: var(--tutor-tab-l3); $tutor-tab-l3-hover: var(--tutor-tab-l3-hover); $tutor-tab-l3-active: var(--tutor-tab-l3-active); @@ -31,6 +33,8 @@ $tutor-tabs: ( sidebar-l2: $tutor-tab-sidebar-l2, sidebar-l2-hover: $tutor-tab-sidebar-l2-hover, sidebar-l2-active: $tutor-tab-sidebar-l2-active, + sidebar-l4-hover: $tutor-tab-sidebar-l4-hover, + sidebar-l4-active: $tutor-tab-sidebar-l4-active, l3: $tutor-tab-l3, l3-hover: $tutor-tab-l3-hover, l3-active: $tutor-tab-l3-active, diff --git a/assets/core/scss/tokens/_text-colors.scss b/assets/core/scss/tokens/_text-colors.scss index 1cf3dadf6f..feaff10071 100644 --- a/assets/core/scss/tokens/_text-colors.scss +++ b/assets/core/scss/tokens/_text-colors.scss @@ -47,5 +47,6 @@ $tutor-text-colors: ( exception2: $tutor-text-exception2, exception4: $tutor-text-exception4, exception5: $tutor-text-exception5, - highlighted-hover: $tutor-text-highlighted-hover + highlighted-hover: $tutor-text-highlighted-hover, + disabled: $tutor-text-disabled, ); \ No newline at end of file diff --git a/assets/core/scss/tokens/_utility-config.scss b/assets/core/scss/tokens/_utility-config.scss new file mode 100644 index 0000000000..5e4aa8aacf --- /dev/null +++ b/assets/core/scss/tokens/_utility-config.scss @@ -0,0 +1,42 @@ +// Utility Generation Configuration +// Use these maps to limit which utilities are generated for responsive breakpoints +// to keep the build size small. + +@use 'sass:map'; +@use 'breakpoints' as b; +@use 'spacing' as s; +@use 'borders' as bor; + +// Breakpoints to include in responsive utility generation +$tutor-responsive-breakpoints: ( + xl: map.get(b.$tutor-breakpoints, xl), + lg: map.get(b.$tutor-breakpoints, lg), + md: map.get(b.$tutor-breakpoints, md), + sm: map.get(b.$tutor-breakpoints, sm), +); + +// Spacing values to include in responsive utility generation (subset of full scale) +$tutor-responsive-spacing: ( + none: map.get(s.$tutor-spacing, none), + 1: map.get(s.$tutor-spacing, 1), + 2: map.get(s.$tutor-spacing, 2), + 3: map.get(s.$tutor-spacing, 3), + 4: map.get(s.$tutor-spacing, 4), + 5: map.get(s.$tutor-spacing, 5), + 6: map.get(s.$tutor-spacing, 6), + 7: map.get(s.$tutor-spacing, 7), + 8: map.get(s.$tutor-spacing, 8), + 9: map.get(s.$tutor-spacing, 9), + 10: map.get(s.$tutor-spacing, 10), +); + +// Radius values to include in responsive utility generation +$tutor-responsive-radius: ( + none: map.get(bor.$tutor-radius, none), + sm: map.get(bor.$tutor-radius, sm), + md: map.get(bor.$tutor-radius, md), + lg: map.get(bor.$tutor-radius, lg), + 2xl: map.get(bor.$tutor-radius, 2xl), + full: map.get(bor.$tutor-radius, full), +); + diff --git a/assets/core/scss/utilities/_borders.scss b/assets/core/scss/utilities/_borders.scss index b5e531d7e7..2fb0e98a9c 100644 --- a/assets/core/scss/utilities/_borders.scss +++ b/assets/core/scss/utilities/_borders.scss @@ -1,6 +1,7 @@ // Border Utilities // DRY, RTL-aware, width, style, color, radius, responsive +@use 'sass:map'; @use '../tokens' as *; @use '../mixins' as *; @@ -147,10 +148,30 @@ $radius-directions: ( // ============================================================================= // RESPONSIVE BORDER UTILITIES // ============================================================================= -@each $breakpoint, $min-width in $tutor-breakpoints { +$responsive-border-dirs: ( + border: border, + border-t: border-top, + border-b: border-bottom, +); + +$responsive-radius-dirs: ( + '': ( + border-radius, + ), + 't': ( + border-start-start-radius, + border-start-end-radius, + ), + 'b': ( + border-end-start-radius, + border-end-end-radius, + ), +); + +@each $breakpoint, $min-width in $tutor-responsive-breakpoints { @include tutor-breakpoint-down($breakpoint) { - // Border widths & directional borders - @each $dir, $property in $border-directions { + // Border widths & directional borders (Restricted to All, Top, Bottom) + @each $dir, $property in $responsive-border-dirs { .tutor-#{$breakpoint}-#{$dir} { #{$property}: 1px solid $tutor-border-idle; } @@ -165,14 +186,17 @@ $radius-directions: ( } } - // Border radius - @each $key, $value in $tutor-radius { + // Border radius (Restricted to All, Top, Bottom + Subset of values) + @each $key, $value in $tutor-responsive-radius { @each $abbr, $props in $radius-directions { - $prefix: if($abbr == '', '', $abbr + '-'); - - .tutor-#{$breakpoint}-rounded-#{$prefix}#{$key} { - @each $prop in $props { - #{$prop}: $value; + // We only generate for specific abbreviations to save space + @if map.has-key($responsive-radius-dirs, $abbr) { + $prefix: if($abbr == '', '', $abbr + '-'); + + .tutor-#{$breakpoint}-rounded-#{$prefix}#{$key} { + @each $prop in $props { + #{$prop}: $value; + } } } } diff --git a/assets/core/scss/utilities/_colors.scss b/assets/core/scss/utilities/_colors.scss index 4c87137aff..e0cf3e4121 100644 --- a/assets/core/scss/utilities/_colors.scss +++ b/assets/core/scss/utilities/_colors.scss @@ -195,38 +195,3 @@ $opacities: ( .tutor-focus-outline-none:focus { outline: none; } - -// ------------------------------------------------------------ -// Responsive Color Utilities -// ------------------------------------------------------------ -@each $breakpoint, $min-width in $tutor-breakpoints { - @include tutor-breakpoint-down($breakpoint) { - // Surface Colors - @each $key, $surface-color in $tutor-surfaces { - .tutor-#{$breakpoint}-surface-#{$key} { - background-color: $surface-color; - } - } - - // Text Colors - @each $key, $text-color in $tutor-text-colors { - .tutor-#{$breakpoint}-text-#{$key} { - color: $text-color; - } - } - - // Icon Colors - @each $key, $icon-color in $tutor-icons { - .tutor-#{$breakpoint}-icon-#{$key} { - color: $icon-color; - } - } - - // Actions Colors - @each $key, $action-color in $tutor-actions { - .tutor-#{$breakpoint}-actions-#{$key} { - color: $action-color; - } - } - } -} diff --git a/assets/core/scss/utilities/_layout.scss b/assets/core/scss/utilities/_layout.scss index 3ed8743d57..eb523854e5 100644 --- a/assets/core/scss/utilities/_layout.scss +++ b/assets/core/scss/utilities/_layout.scss @@ -361,7 +361,7 @@ $overflow-types: auto, hidden, visible, scroll; // Responsive Utilities // ------------------------ -@each $breakpoint, $min-width in $tutor-breakpoints { +@each $breakpoint, $min-width in $tutor-responsive-breakpoints { @include tutor-breakpoint-down($breakpoint) { // Display utilities @each $name, $value in $display-utils { @@ -392,16 +392,10 @@ $overflow-types: auto, hidden, visible, scroll; } // Gap utilities - @each $name, $value in $tutor-spacing { + @each $name, $value in $tutor-responsive-spacing { .tutor-#{$breakpoint}-gap-#{$name} { gap: $value; } - .tutor-#{$breakpoint}-gap-x-#{$name} { - column-gap: $value; - } - .tutor-#{$breakpoint}-gap-y-#{$name} { - row-gap: $value; - } } // Grid column utilities @@ -426,3 +420,4 @@ $overflow-types: auto, hidden, visible, scroll; } } } + diff --git a/assets/core/scss/utilities/_sizing.scss b/assets/core/scss/utilities/_sizing.scss index c8ef1ba60c..d2e6846baa 100644 --- a/assets/core/scss/utilities/_sizing.scss +++ b/assets/core/scss/utilities/_sizing.scss @@ -170,7 +170,7 @@ $max-heights: ( $responsive-widths: map-merge($fixed-widths, $fractional-widths); $responsive-heights: $fixed-heights; -@each $breakpoint, $min-width in $tutor-breakpoints { +@each $breakpoint, $min-width in $tutor-responsive-breakpoints { @include tutor-breakpoint-down($breakpoint) { // Width @each $key, $value in $responsive-widths { diff --git a/assets/core/scss/utilities/_spacing.scss b/assets/core/scss/utilities/_spacing.scss index 41fe3d3ee9..ee986d7df1 100644 --- a/assets/core/scss/utilities/_spacing.scss +++ b/assets/core/scss/utilities/_spacing.scss @@ -5,6 +5,7 @@ @use '../mixins' as *; // Margin utilities + @each $key, $value in $tutor-spacing { .tutor-m-#{$key} { margin: $value; @@ -133,9 +134,9 @@ } // Responsive spacing utilities -@each $breakpoint, $min-width in $tutor-breakpoints { +@each $breakpoint, $min-width in $tutor-responsive-breakpoints { @include tutor-breakpoint-down($breakpoint) { - @each $space-key, $space-value in $tutor-spacing { + @each $space-key, $space-value in $tutor-responsive-spacing { .tutor-#{$breakpoint}-m-#{$space-key} { margin: $space-value; } diff --git a/assets/core/scss/utilities/_typography.scss b/assets/core/scss/utilities/_typography.scss index 40fb25e9fd..f4d7cc7f77 100644 --- a/assets/core/scss/utilities/_typography.scss +++ b/assets/core/scss/utilities/_typography.scss @@ -206,7 +206,7 @@ $text-align-utils: ( } // Responsive typography utilities -@each $breakpoint, $min-width in $tutor-breakpoints { +@each $breakpoint, $min-width in $tutor-responsive-breakpoints { @include tutor-breakpoint-down($breakpoint) { @each $size-key, $size-value in $tutor-font-sizes { .tutor-#{$breakpoint}-text-#{$size-key} { @@ -228,3 +228,4 @@ $text-align-utils: ( } } } + diff --git a/assets/core/ts/components/select.ts b/assets/core/ts/components/select.ts index f1e7dd4399..f2d5ec5759 100644 --- a/assets/core/ts/components/select.ts +++ b/assets/core/ts/components/select.ts @@ -15,6 +15,7 @@ export interface SelectOption { icon?: string; description?: string; group?: string; + display_label?: string; } export interface SelectGroup { @@ -152,13 +153,13 @@ export const select = (props: SelectProps = {}) => { const count = this.selectedValues.size; if (count === 1) { const opt = this.selectedOptions[0]; - return opt ? opt.label : this.placeholder; + return opt ? (opt.display_label ?? opt.label) : this.placeholder; } return `${count} selected`; } const opt = this.selectedOptions[0]; - return opt ? opt.label : this.placeholder; + return opt ? (opt.display_label ?? opt.label) : this.placeholder; }, get canClear(): boolean { diff --git a/assets/core/ts/services/Preference.ts b/assets/core/ts/services/Preference.ts index 452f09d98b..b3481a4d95 100644 --- a/assets/core/ts/services/Preference.ts +++ b/assets/core/ts/services/Preference.ts @@ -1,5 +1,8 @@ import { type ServiceMeta } from '@Core/ts/types'; type Theme = 'dark' | 'light' | 'system'; +type Vision = 'normal' | 'protanopia' | 'deuteranopia' | 'deuteranomaly'; +type Contrast = '' | 'high'; +type Motion = '' | 'auto' | 'reduce' | 'standard'; class PreferenceService { private readonly THEME = { DARK: 'dark', LIGHT: 'light', SYSTEM: 'system' } as const; @@ -10,6 +13,9 @@ class PreferenceService { private readonly SCALE_PERCENTAGE_BASE = 100; private readonly STYLE_ID = 'tutor-font-scale'; private readonly DATA_THEME_ATTR = 'data-tutor-theme'; + private readonly DATA_VISION_ATTR = 'data-tutor-vision'; + private readonly DATA_CONTRAST_ATTR = 'data-tutor-contrast'; + private readonly DATA_MOTION_ATTR = 'data-tutor-motion'; constructor() { this.mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); @@ -17,17 +23,44 @@ class PreferenceService { this.initialize(); } + private getWrapper(): HTMLElement { + return (document.querySelector(`[${this.DATA_THEME_ATTR}]`) as HTMLElement | null) || document.documentElement; + } + + private inferBase(themeAttr: string | null): 'dark' | 'light' { + return themeAttr?.startsWith('dark') ? 'dark' : 'light'; + } + initialize(): void { - const wrapper = document.querySelector(`[${this.DATA_THEME_ATTR}]`) || document.documentElement; - const attrTheme = wrapper.getAttribute(this.DATA_THEME_ATTR); - if (attrTheme === this.THEME.SYSTEM) { - this.applyTheme(this.THEME.SYSTEM, false); + const wrapper = this.getWrapper(); + const attrTheme = wrapper.getAttribute(this.DATA_THEME_ATTR) as Theme | null; + + // If the saved preference is "system", re-apply to attach listener and compute correct attr. + if (attrTheme === this.THEME.SYSTEM) this.applyTheme(this.THEME.SYSTEM, false); + + const contrast = (wrapper.getAttribute(this.DATA_CONTRAST_ATTR) as Contrast | null) ?? ''; + if (contrast) { + this.applyContrast(contrast); } + + const motion = (wrapper.getAttribute(this.DATA_MOTION_ATTR) as Motion | null) ?? ''; + this.applyMotionEffects(motion as Motion); } applyTheme(theme: Theme, withTransition: boolean = true): void { if (!theme) return; - const wrapper = document.querySelector(`[${this.DATA_THEME_ATTR}]`) || document.documentElement; + const wrapper = this.getWrapper(); + + // Resolve what the new effective theme would be. + const incomingEffectiveTheme = + theme === this.THEME.SYSTEM ? (this.mediaQuery.matches ? this.THEME.DARK : this.THEME.LIGHT) : theme; + + // Skip transition if the effective theme hasn't changed. + const currentAttr = wrapper.getAttribute(this.DATA_THEME_ATTR); + const effectiveCurrent = this.inferBase(currentAttr); + if (incomingEffectiveTheme === effectiveCurrent && theme !== this.THEME.SYSTEM) { + return; + } if (this.systemThemeListener) { this.mediaQuery.removeEventListener('change', this.systemThemeListener); @@ -36,7 +69,8 @@ class PreferenceService { const updateTheme = () => { if (theme === this.THEME.SYSTEM) { - wrapper.setAttribute(this.DATA_THEME_ATTR, this.mediaQuery.matches ? this.THEME.DARK : this.THEME.LIGHT); + const base = this.mediaQuery.matches ? this.THEME.DARK : this.THEME.LIGHT; + wrapper.setAttribute(this.DATA_THEME_ATTR, base); } else { wrapper.setAttribute(this.DATA_THEME_ATTR, theme); } @@ -51,9 +85,7 @@ class PreferenceService { } const applied = wrapper.getAttribute(this.DATA_THEME_ATTR); - if (applied === this.THEME.DARK || applied === this.THEME.LIGHT) { - this.activeTheme = applied; - } + this.activeTheme = this.inferBase(applied); }; if (withTransition && document.startViewTransition) { @@ -64,6 +96,39 @@ class PreferenceService { applyLogic(); } } + + applyContrast(contrast: Contrast): void { + const wrapper = this.getWrapper(); + if (contrast === 'high') { + wrapper.setAttribute(this.DATA_CONTRAST_ATTR, 'high'); + } else { + wrapper.removeAttribute(this.DATA_CONTRAST_ATTR); + } + } + + applyVision(vision: Vision): void { + const wrapper = this.getWrapper(); + const safeVision: Vision = + vision === 'protanopia' || vision === 'deuteranopia' || vision === 'deuteranomaly' ? vision : 'normal'; + + if (safeVision === 'normal') { + wrapper.removeAttribute(this.DATA_VISION_ATTR); + } else { + wrapper.setAttribute(this.DATA_VISION_ATTR, safeVision); + } + } + + applyMotionEffects(motion: Motion): void { + const wrapper = this.getWrapper(); + if (motion === 'reduce') { + wrapper.setAttribute(this.DATA_MOTION_ATTR, 'reduce'); + } else if (motion === 'auto') { + wrapper.setAttribute(this.DATA_MOTION_ATTR, 'auto'); + } else { + wrapper.removeAttribute(this.DATA_MOTION_ATTR); + } + } + applyFontScale(fontScale: string | number): void { if (!fontScale) return; diff --git a/assets/icons/archive-2.svg b/assets/icons/archive-2.svg index 54bbcb1a95..fdb8a78b4d 100644 --- a/assets/icons/archive-2.svg +++ b/assets/icons/archive-2.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/assets/icons/grab-handle.svg b/assets/icons/grab-handle.svg index c371badd54..97f95492b6 100644 --- a/assets/icons/grab-handle.svg +++ b/assets/icons/grab-handle.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/gradebook.svg b/assets/icons/gradebook.svg index d1cf49f802..dfb473bf65 100644 --- a/assets/icons/gradebook.svg +++ b/assets/icons/gradebook.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/interface.svg b/assets/icons/interface.svg index 7f4f116a41..d70b0305c3 100644 --- a/assets/icons/interface.svg +++ b/assets/icons/interface.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/assets/icons/publish.svg b/assets/icons/publish.svg index 417158c796..eb1fdeb76e 100644 --- a/assets/icons/publish.svg +++ b/assets/icons/publish.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/vision.svg b/assets/icons/vision.svg new file mode 100644 index 0000000000..dc90f45785 --- /dev/null +++ b/assets/icons/vision.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/wallet.svg b/assets/icons/wallet.svg index b68ea59963..15c261ce06 100644 --- a/assets/icons/wallet.svg +++ b/assets/icons/wallet.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/icons/warning-line.svg b/assets/icons/warning-line.svg index f2927a3588..574743127e 100644 --- a/assets/icons/warning-line.svg +++ b/assets/icons/warning-line.svg @@ -1,9 +1 @@ - - - - - - - - - + \ No newline at end of file diff --git a/assets/icons/x.svg b/assets/icons/x.svg index 6c59f729c1..89f85920af 100644 --- a/assets/icons/x.svg +++ b/assets/icons/x.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/vision/deuteranomaly.webp b/assets/images/vision/deuteranomaly.webp new file mode 100644 index 0000000000..64e3b90100 Binary files /dev/null and b/assets/images/vision/deuteranomaly.webp differ diff --git a/assets/images/vision/deuteranopia.webp b/assets/images/vision/deuteranopia.webp new file mode 100644 index 0000000000..cfebb3dbcd Binary files /dev/null and b/assets/images/vision/deuteranopia.webp differ diff --git a/assets/images/vision/normal.webp b/assets/images/vision/normal.webp new file mode 100644 index 0000000000..8bd0e0c5d2 Binary files /dev/null and b/assets/images/vision/normal.webp differ diff --git a/assets/images/vision/protanopia.webp b/assets/images/vision/protanopia.webp new file mode 100644 index 0000000000..cfc82a2842 Binary files /dev/null and b/assets/images/vision/protanopia.webp differ diff --git a/assets/src/js/frontend/dashboard/pages/settings.ts b/assets/src/js/frontend/dashboard/pages/settings.ts index 90cc6cdcdb..c406c87655 100644 --- a/assets/src/js/frontend/dashboard/pages/settings.ts +++ b/assets/src/js/frontend/dashboard/pages/settings.ts @@ -191,9 +191,15 @@ const settings = () => { this.savePreferencesMutation = query.useMutation(this.updatePreferences, { onSuccess: (data: TutorMutationResponse, payload: PreferencesFormProps) => { + const previousLearningMood = form.getValue(payload?.formId || '', 'learning_mood'); + const learningMoodChanged = previousLearningMood !== payload.learning_mood; + form.reset(payload?.formId || '', payload as unknown as Record); toast.success(data?.message ?? __('Preferences saved successfully', 'tutor')); - window.location.reload(); + + if (learningMoodChanged) { + window.location.reload(); + } }, onError: (error: Error) => { toast.error(convertToErrorMessage(error)); diff --git a/assets/src/js/v3/shared/icons/types.ts b/assets/src/js/v3/shared/icons/types.ts index 60fcea6c00..85bbdfbcd1 100644 --- a/assets/src/js/v3/shared/icons/types.ts +++ b/assets/src/js/v3/shared/icons/types.ts @@ -409,6 +409,7 @@ export const icons = [ 'videoFill', 'videoQuality', 'vimeo', + 'vision', 'visited', 'wallet', 'warning', diff --git a/assets/src/scss/frontend/dashboard/settings/_preferences.scss b/assets/src/scss/frontend/dashboard/settings/_preferences.scss index 98be40c795..f1c3f96d81 100644 --- a/assets/src/scss/frontend/dashboard/settings/_preferences.scss +++ b/assets/src/scss/frontend/dashboard/settings/_preferences.scss @@ -11,10 +11,10 @@ &-setting-item { @include tutor-flex-between; - margin-bottom: $tutor-spacing-6; - - &:last-child { - margin-bottom: $tutor-spacing-none; + gap: $tutor-spacing-10; + + &:not(:last-child) { + margin-bottom: $tutor-spacing-6; } .tutor-preferences-setting-content { @@ -23,11 +23,15 @@ .tutor-preferences-setting-icon { @include tutor-flex-center; + color: $tutor-icon-idle; } .tutor-preferences-setting-title { @include tutor-typography('small', 'medium', 'primary'); - flex: 1 1 0%; + } + + .tutor-preferences-setting-subtitle { + @include tutor-typography(tiny); } .tutor-preferences-setting-text { @@ -38,6 +42,14 @@ @include tutor-typography('small', 'regular', 'subdued'); } } + + &:has(.tutor-preferences-setting-subtitle) { + align-items: start; + + .tutor-preferences-setting-icon { + margin-top: 3px; + } + } } .tutor-preferences-setting-action { @@ -45,4 +57,17 @@ flex-shrink: 0; } } -} \ No newline at end of file + + &-vision-preview { + @include tutor-flex-center(); + background-color: $tutor-surface-l1-hover; + border-radius: $tutor-radius-md; + padding-top: $tutor-spacing-5; + margin-bottom: $tutor-spacing-5; + + img { + width: 100%; + max-width: 65%; + } + } +} diff --git a/classes/Icon.php b/classes/Icon.php index 30b025630c..573016dd29 100644 --- a/classes/Icon.php +++ b/classes/Icon.php @@ -425,6 +425,7 @@ final class Icon { const VIDEO_FILL = 'video-fill'; const VIDEO_QUALITY = 'video-quality'; const VIMEO = 'vimeo'; + const VISION = 'vision'; const VISITED = 'visited'; const WALLET = 'wallet'; const WARNING = 'warning'; diff --git a/classes/UserPreference.php b/classes/UserPreference.php index 7b95813d0f..2672bcc347 100644 --- a/classes/UserPreference.php +++ b/classes/UserPreference.php @@ -65,6 +65,105 @@ class UserPreference { */ const THEME_SYSTEM = 'system'; + /** + * Vision option: normal. + * + * @since 4.0.0 + * + * @var string + */ + const VISION_NORMAL = 'normal'; + + /** + * Vision option: protanopia. + * + * @since 4.0.0 + * + * @var string + */ + const VISION_PROTANOPIA = 'protanopia'; + + /** + * Vision option: deuteranopia. + * + * @since 4.0.0 + * + * @var string + */ + const VISION_DEUTERANOPIA = 'deuteranopia'; + + /** + * Vision option: deuteranomaly. + * + * @since 4.0.0 + * + * @var string + */ + const VISION_DEUTERANOMALY = 'deuteranomaly'; + + /** + * Available vision options. + * + * @since 4.0.0 + * + * @var array + */ + const VISIONS = array( + self::VISION_NORMAL, + self::VISION_PROTANOPIA, + self::VISION_DEUTERANOPIA, + self::VISION_DEUTERANOMALY, + ); + + /** + * Contrast option: high. + * + * @since 4.0.0 + * + * @var string + */ + const CONTRAST_HIGH = 'high'; + + /** + * Motion effects option: auto (follow system). + * + * @since 4.0.0 + * + * @var string + */ + const MOTION_EFFECTS_AUTO = 'auto'; + + /** + * Motion effects option: reduce. + * + * @since 4.0.0 + * + * @var string + */ + const MOTION_EFFECTS_REDUCE = 'reduce'; + + /** + * Motion effects option: standard (no reduction). + * + * @since 4.0.0 + * + * @var string + */ + const MOTION_EFFECTS_STANDARD = 'standard'; + + /** + * Available motion effects options. + * + * @since 4.0.0 + * + * @var array + */ + const MOTION_EFFECTS_OPTIONS = array( + self::MOTION_EFFECTS_AUTO, + self::MOTION_EFFECTS_REDUCE, + self::MOTION_EFFECTS_STANDARD, + ); + /** * Default theme value. * @@ -99,7 +198,7 @@ class UserPreference { * * @var array */ - const FONT_SCALE_OPTIONS = array( 70, 80, 90, 100, 110, 120 ); + const FONT_SCALE_OPTIONS = array( 60, 80, 100, 120, 140, 160, 180, 200 ); /** * Register hooks. @@ -152,7 +251,38 @@ public function add_theme_attribute( $output ) { if ( ! in_array( $theme, self::THEMES, true ) ) { $theme = self::DEFAULT_THEME; } - return $output . ' data-tutor-theme="' . esc_attr( $theme ) . '"'; + + $vision = isset( $prefs['vision'] ) ? (string) $prefs['vision'] : self::VISION_NORMAL; + if ( ! in_array( $vision, self::VISIONS, true ) ) { + $vision = self::VISION_NORMAL; + } + + // Resolve actual theme for 'system' (PHP fallback is light; JS applies real system preference). + $resolved_theme = self::THEME_SYSTEM === $theme ? self::THEME_LIGHT : $theme; + + $contrast = isset( $prefs['contrast'] ) ? (string) $prefs['contrast'] : ''; + $contrast_attr = ''; + if ( self::CONTRAST_HIGH === $contrast ) { + $contrast_attr = ' data-tutor-contrast="' . esc_attr( self::CONTRAST_HIGH ) . '"'; + } + + $motion_effects = isset( $prefs['motion_effects'] ) ? (string) $prefs['motion_effects'] : self::MOTION_EFFECTS_AUTO; + if ( ! in_array( $motion_effects, self::MOTION_EFFECTS_OPTIONS, true ) ) { + $motion_effects = self::MOTION_EFFECTS_AUTO; + } + + $motion_effects_attr = ''; + if ( self::MOTION_EFFECTS_REDUCE === $motion_effects ) { + $motion_effects_attr = ' data-tutor-motion="reduce"'; + } elseif ( self::MOTION_EFFECTS_AUTO === $motion_effects ) { + $motion_effects_attr = ' data-tutor-motion="auto"'; + } + + $vision_attr = self::VISION_NORMAL !== $vision + ? ' data-tutor-vision="' . esc_attr( $vision ) . '"' + : ''; + + return $output . ' data-tutor-theme="' . esc_attr( $resolved_theme ) . '"' . $vision_attr . $contrast_attr . $motion_effects_attr; } /** @@ -254,6 +384,9 @@ public function ajax_save_user_preferences() { $auto_play_next = Input::post( 'auto_play_next', null ); $theme = Input::post( 'theme', null ); + $vision = Input::post( 'vision', null ); + $contrast = Input::post( 'contrast', null ); + $motion_effects = Input::post( 'motion_effects', null ); $font_scale = Input::post( 'font_scale', null ); $learning_mood = Input::post( 'learning_mood', null ); @@ -266,9 +399,40 @@ public function ajax_save_user_preferences() { } if ( null !== $theme ) { + if ( ! in_array( $theme, self::THEMES, true ) ) { + $theme = self::DEFAULT_THEME; + } $preferences_settings['theme'] = $theme; } + if ( null !== $vision ) { + $vision = (string) $vision; + if ( ! in_array( $vision, self::VISIONS, true ) ) { + $vision = self::VISION_NORMAL; + } + $preferences_settings = array_merge( $preferences_settings, self::prepare_preference_setting( 'vision', $vision, self::VISION_NORMAL ) ); + } + + if ( null !== $contrast ) { + $contrast = (string) $contrast; + // Switch inputs often post "true" when checked. + if ( 'true' === $contrast ) { + $contrast = self::CONTRAST_HIGH; + } + if ( self::CONTRAST_HIGH !== $contrast ) { + $contrast = ''; + } + $preferences_settings = array_merge( $preferences_settings, self::prepare_preference_setting( 'contrast', $contrast, '' ) ); + } + + if ( null !== $motion_effects ) { + $motion_effects = (string) $motion_effects; + if ( ! in_array( $motion_effects, self::MOTION_EFFECTS_OPTIONS, true ) ) { + $motion_effects = self::MOTION_EFFECTS_AUTO; + } + $preferences_settings = array_merge( $preferences_settings, self::prepare_preference_setting( 'motion_effects', $motion_effects, self::MOTION_EFFECTS_AUTO ) ); + } + if ( null !== $font_scale ) { $preferences_settings['font_scale'] = (int) $font_scale; } @@ -394,6 +558,9 @@ private static function get_default_preferences() { array( 'auto_play_next' => (bool) tutor_utils()->get_option( 'autoload_next_course_content' ), 'theme' => self::DEFAULT_THEME, + 'vision' => self::VISION_NORMAL, + 'contrast' => '', + 'motion_effects' => self::MOTION_EFFECTS_AUTO, 'font_scale' => self::DEFAULT_FONT_SCALE, 'learning_mood' => tutor_utils()->get_option( 'learning_mode', Options_V2::LEARNING_MODE_MODERN ), ) @@ -426,6 +593,34 @@ public static function get_theme_options() { ); } + /** + * Get vision options for UI selects. + * + * @since 4.0.0 + * + * @return array + */ + public static function get_vision_options() { + return array( + array( + 'label' => __( 'Normal', 'tutor' ), + 'value' => self::VISION_NORMAL, + ), + array( + 'label' => __( 'Protanopia', 'tutor' ), + 'value' => self::VISION_PROTANOPIA, + ), + array( + 'label' => __( 'Deuteranopia', 'tutor' ), + 'value' => self::VISION_DEUTERANOPIA, + ), + array( + 'label' => __( 'Deuteranomaly', 'tutor' ), + 'value' => self::VISION_DEUTERANOMALY, + ), + ); + } + /** * Get learning mood options for UI selects. * @@ -446,6 +641,30 @@ public static function get_learning_mood_options() { ); } + /** + * Get motion effects options for UI selects. + * + * @since 4.0.0 + * + * @return array + */ + public static function get_motion_effects_options() { + return array( + array( + 'label' => __( 'Auto (System Default)', 'tutor' ), + 'value' => self::MOTION_EFFECTS_AUTO, + ), + array( + 'label' => __( 'Reduced Motion', 'tutor' ), + 'value' => self::MOTION_EFFECTS_REDUCE, + ), + array( + 'label' => __( 'Standard Motion', 'tutor' ), + 'value' => self::MOTION_EFFECTS_STANDARD, + ), + ); + } + /** * Get font scale options for UI selects. * @@ -459,11 +678,21 @@ public static function get_font_scale_options() { $values = apply_filters( 'tutor_user_preference_font_scale_values', self::FONT_SCALE_OPTIONS ); $options = array(); foreach ( $values as $value ) { - $value = (int) $value; - $options[] = array( + $value = (int) $value; + $option = array( 'label' => $value . '%', 'value' => $value, ); + + if ( self::DEFAULT_FONT_SCALE === $value ) { + $option['html_label'] = $value . '% ' . esc_html__( 'Default', 'tutor' ) . ''; + $option['display_label'] = __( 'Default', 'tutor' ); + } elseif ( 140 === $value ) { + $option['html_label'] = $value . '% ' . esc_html__( 'Large', 'tutor' ) . ''; + $option['display_label'] = __( 'Large', 'tutor' ); + } + + $options[] = $option; } return $options; } diff --git a/components/InputField.php b/components/InputField.php index fc61684c7f..d2297eff16 100644 --- a/components/InputField.php +++ b/components/InputField.php @@ -1137,7 +1137,12 @@ class="tutor-select-option"
-
+ + 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 @@
'}); })($event)" > -
-
- +
+
+
- name( Icon::RELOAD_3 )->size( 16 )->render(); ?> - + name( Icon::RELOAD_3 )->color( Color::SUBDUED )->render(); ?> +
name( Icon::PLAY_LINE )->size( 20 )->render(); ?>
- +
+ +
+ + +
+ +
+
+
+
+
+ name( Icon::ANIMATION )->size( 20 )->render(); ?> +
+
+
+ +
+
+ +
+
+
+
+ type( InputType::SELECT ) + ->size( Size::SM ) + ->name( 'motion_effects' ) + ->options( $motion_effects_options ) + ->attr( 'x-bind', "register('motion_effects')" ) + ->attr( 'x-effect', 'TutorCore.preference.applyMotionEffects(watch("motion_effects"))' ) + ->attr( 'style', 'min-width: 140px;' ) + ->render(); + ?> +
+
+
+ +
+ +
+
name( Icon::LIGHT )->size( 20 )->render(); ?>
- +
+ +
+ +
+
+
+ name( Icon::INTERFACE )->size( 20 )->render(); ?> +
+
+ +
+
+
+ type( InputType::SELECT ) + ->size( Size::SM ) + ->name( 'learning_mood' ) + ->options( $learning_mood_options ) + ->value( $user_preferences['learning_mood'] ?? Options_V2::LEARNING_MODE_MODERN ) + ->placeholder( __( 'Select mode', 'tutor' ) ) + ->attr( 'x-bind', "register('learning_mood')" ) + ->attr( 'style', 'min-width: 140px;' ) + ->render(); + ?> +
+
+ +
+ +
+ +
+
name( Icon::FONT )->size( 20 )->render(); ?>
- +
+
+ +
+
+ +
+
-
- name( Icon::INTERFACE )->size( 20 )->render(); ?> + name( Icon::CONTRAST )->size( 20 )->render(); ?> +
+
+
+ +
+
+ +
-
type( InputType::SWITCH ) + ->size( Size::SM ) + ->name( 'contrast' ) + ->value( 'high' ) + ->checked( isset( $user_preferences['contrast'] ) && 'high' === $user_preferences['contrast'] ) + ->attr( 'x-bind', "register('contrast')" ) + ->attr( 'x-effect', 'TutorCore.preference.applyContrast(watch("contrast") ? "high" : "")' ) + ->render(); + ?> +
+
+
+ +
+
+
+
+ name( Icon::VISION )->size( 20 )->render(); ?> +
+
+
+ +
+
+ +
+
+
+
+ type( InputType::SELECT ) ->size( Size::SM ) - ->name( 'learning_mood' ) - ->options( $learning_mood_options ) - ->value( $user_preferences['learning_mood'] ?? Options_V2::LEARNING_MODE_MODERN ) - ->placeholder( __( 'Select mode', 'tutor' ) ) - ->attr( 'x-bind', "register('learning_mood')" ) + ->name( 'vision' ) + ->options( $vision_options ) + ->placeholder( __( 'Select vision...', 'tutor' ) ) + ->attr( 'x-bind', "register('vision')" ) + ->attr( 'x-effect', 'TutorCore.preference.applyVision(watch("vision"))' ) ->attr( 'style', 'min-width: 140px;' ) ->render(); ?>
-