refactor: Modularize CSS and fix theme-aware text colors

CSS Restructuring:
- Reorganize monolithic main.css into modular architecture
- Create foundation/ (reset, variables, typography, themes)
- Create layout/ (container, page, grid, paper)
- Create components/ (8 component files)
- Create interactive/ (toggles, remaining for future split)
- Create effects/ (skeleton loading)
- Create contexts/ (print styles)

Theme Support Fixes:
- Replace all hardcoded text colors with CSS variables
- Fix .section-title: rgb(51,51,51) → var(--text-primary)
- Fix .cv-name, .intro-text: hardcoded → theme-aware
- Fix .experience-period, .duration-text: #555/#aaa → variables
- Fix course/project/experience text colors
- Support proper light/dark theme text contrast

Icon & Layout Fixes:
- Standardize all icon sizes to 80×80px
- Change all icon backgrounds to transparent
- Fix award section layout (missing flexbox)
- Update HTML templates (experience.html, awards.html) to width='80'
- Fix default icon sizing conflicts

View Switcher Fix:
- Fix toggleTheme() to target .cv-container instead of body
- Ensures clean/default theme toggle works correctly

Files: 40+ CSS files modularized, 3 templates updated, 7 tests added
This commit is contained in:
juanatsap
2025-11-19 14:31:17 +00:00
parent f8948413bc
commit f7cda5dba3
41 changed files with 12804 additions and 4740 deletions
+3
View File
@@ -53,3 +53,6 @@ coverage.out
*.coverprofile *.coverprofile
.claude .claude
playwright.config.js playwright.config.js
# Test artifacts
tests/screenshots/
+36
View File
@@ -0,0 +1,36 @@
/* ============================================================================
CSS RESET - Normalize & Base Styles
============================================================================ */
/* Box sizing reset */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Body base */
body {
background-color: var(--page-bg);
background-image:
linear-gradient(90deg, rgba(0, 0, 0, 0.05) 1px, transparent 1px),
linear-gradient(180deg, rgba(0, 0, 0, 0.05) 1px, transparent 1px),
linear-gradient(90deg, rgba(0, 0, 0, 0.02) 1px, transparent 1px),
linear-gradient(180deg, rgba(0, 0, 0, 0.02) 1px, transparent 1px);
background-size: 50px 50px, 50px 50px, 10px 10px, 10px 10px;
background-attachment: fixed;
overflow-x: auto;
}
/* Smooth scrolling */
html {
scroll-behavior: smooth;
scroll-padding-top: 70px; /* Account for fixed header */
}
/* Ensure Iconify icons display properly */
.iconify,
iconify-icon {
display: inline-block;
vertical-align: middle;
}
+285
View File
@@ -0,0 +1,285 @@
/* ==============================================================================
COLOR THEME SYSTEM
============================================================================== */
/*
IMPORTANT: This is the COLOR theme system (light/dark/auto)
This is SEPARATE from the LAYOUT theme (.theme-clean)
- COLOR theme: Controls backgrounds, text colors, shadows
- LAYOUT theme (.theme-clean): Controls sidebars, layout structure
Both systems work independently and can be combined.
*/
/* ==============================================================================
LIGHT THEME (DEFAULT)
============================================================================== */
:root {
/* Page Background - Softer version of dark theme */
--page-bg: #b8bbbe;
/* Paper/Card Backgrounds */
--paper-bg: #ffffff;
--paper-secondary-bg: #f5f5f5;
/* Text Colors */
--text-primary: #1a1a1a;
--text-secondary: #333333;
--text-muted: #666666;
--text-light: #999999;
/* Action Bar & Navigation */
--action-bar-bg: #2b2b2b;
--action-bar-text: #ffffff;
--action-bar-text-muted: rgba(255, 255, 255, 0.85);
/* Borders & Dividers */
--border-color: #333333;
--border-light: #e0e0e0;
/* Shadows */
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1);
--shadow-md: 0 2px 8px rgba(0, 0, 0, 0.15);
--shadow-lg: 2px 2px 9px rgba(0, 0, 0, 0.5);
/* Interactive Elements */
--button-bg: transparent;
--button-bg-hover: rgba(0, 0, 0, 0.05);
--button-bg-active: rgba(0, 0, 0, 0.1);
/* Accent Colors (unchanged in dark mode) */
--accent-blue: #0066cc;
--accent-green: #27ae60;
/* Sidebar (for non-clean theme) */
--sidebar-bg: #d1d4d2;
/* Legacy CV content variables - theme-aware overrides */
--text-dark: #1a1a1a; /* Dark text for light background */
--text-gray: #333333; /* Secondary text for light background */
}
/* ==============================================================================
DARK THEME
============================================================================== */
[data-color-theme="dark"] {
/* Page Background - Original background */
--page-bg: rgb(82, 86, 89);
/* Paper/Card Backgrounds */
--paper-bg: #1a1a1a;
--paper-secondary-bg: #2a2a2a;
/* Text Colors */
--text-primary: #e0e0e0;
--text-secondary: #d0d0d0;
--text-muted: #b0b0b0;
--text-light: #808080;
/* Action Bar & Navigation */
--action-bar-bg: #1a1a1a;
--action-bar-text: #e0e0e0;
--action-bar-text-muted: rgba(224, 224, 224, 0.85);
/* Borders & Dividers */
--border-color: #404040;
--border-light: #333333;
/* Shadows */
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
--shadow-md: 0 2px 8px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.6);
/* Interactive Elements */
--button-bg: transparent;
--button-bg-hover: rgba(255, 255, 255, 0.05);
--button-bg-active: rgba(255, 255, 255, 0.1);
/* Accent Colors - slightly brighter in dark mode */
--accent-blue: #3399ff;
--accent-green: #2ecc71;
/* Sidebar (for non-clean theme) */
--sidebar-bg: #2a2a2a;
/* Legacy CV content variables - theme-aware overrides */
--text-dark: #e0e0e0; /* Light text for dark background */
--text-gray: #d0d0d0; /* Secondary text for dark background */
}
/* ==============================================================================
AUTO THEME - Follows System Preference
============================================================================== */
@media (prefers-color-scheme: dark) {
[data-color-theme="auto"] {
/* Page Background - Original background */
--page-bg: rgb(82, 86, 89);
/* Paper/Card Backgrounds */
--paper-bg: #1a1a1a;
--paper-secondary-bg: #2a2a2a;
/* Text Colors */
--text-primary: #e0e0e0;
--text-secondary: #d0d0d0;
--text-muted: #b0b0b0;
--text-light: #808080;
/* Action Bar & Navigation */
--action-bar-bg: #1a1a1a;
--action-bar-text: #e0e0e0;
--action-bar-text-muted: rgba(224, 224, 224, 0.85);
/* Borders & Dividers */
--border-color: #404040;
--border-light: #333333;
/* Shadows */
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
--shadow-md: 0 2px 8px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.6);
/* Interactive Elements */
--button-bg: transparent;
--button-bg-hover: rgba(255, 255, 255, 0.05);
--button-bg-active: rgba(255, 255, 255, 0.1);
/* Accent Colors - slightly brighter in dark mode */
--accent-blue: #3399ff;
--accent-green: #2ecc71;
/* Sidebar (for non-clean theme) */
--sidebar-bg: #2a2a2a;
/* Legacy CV content variables - theme-aware overrides */
--text-dark: #e0e0e0; /* Light text for dark background */
--text-gray: #d0d0d0; /* Secondary text for dark background */
}
}
/* ==============================================================================
THEME SWITCHER BUTTON STYLES - Dynamic colors based on theme mode
============================================================================== */
.color-theme-switcher {
position: fixed;
bottom: 14rem; /* Middle position - between print (18rem) and shortcuts (10rem) */
left: 2rem;
width: 50px;
height: 50px;
background: var(--black-bar);
color: white;
border: none;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
transition: all 0.3s ease;
z-index: 999;
opacity: 0.6;
}
/* Dynamic colors ONLY on hover based on active theme mode */
.color-theme-switcher:hover[data-theme-mode="light"] {
background: #d4b200 !important; /* Bright sun yellow (gold) for light mode */
}
.color-theme-switcher:hover[data-theme-mode="dark"] {
background: #013c77 !important; /* Dark nighty blue for dark mode */
}
.color-theme-switcher:hover[data-theme-mode="auto"] {
background: #9b59b6 !important; /* Purple for auto mode (mix of both) */
}
.color-theme-switcher:hover {
opacity: 1 !important;
transform: translateY(-3px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4);
}
/* At-bottom state - dynamic colors based on theme mode (matches hover) */
.color-theme-switcher.at-bottom[data-theme-mode="light"] {
opacity: 1;
background: #d4b200 !important; /* Bright sun yellow (gold) for light mode */
}
.color-theme-switcher.at-bottom[data-theme-mode="dark"] {
opacity: 1;
background: #013c77 !important; /* Dark nighty blue for dark mode */
}
.color-theme-switcher.at-bottom[data-theme-mode="auto"] {
opacity: 1;
background: #9b59b6 !important; /* Purple for auto mode */
}
.color-theme-switcher iconify-icon {
color: white !important;
transition: color 0.3s ease;
}
.color-theme-switcher:hover iconify-icon {
color: white !important;
}
/* Hide the internal theme buttons - we'll cycle through on click */
.theme-option-btn {
display: none;
}
/* ==============================================================================
ICON COLOR PRESERVATION
============================================================================== */
/* Ensure all iconify icons keep their intended colors across themes */
/* Section icons - keep their brand colors */
.section-icon iconify-icon,
.project-icon iconify-icon,
.course-icon iconify-icon,
.default-project-icon iconify-icon {
color: inherit !important;
}
/* Toggle switch icons - keep their state-specific colors */
/* Note: Already defined in main.css with !important - just ensure they're not overridden */
/* Hamburger menu and site icons */
.site-icon iconify-icon,
.site-icon-mobile iconify-icon {
color: white !important;
}
/* CV content icons */
.cv-paper iconify-icon {
color: inherit !important;
}
/* Error toast icon */
.error-icon iconify-icon {
color: #dc3545 !important;
}
/* Mobile adjustments */
@media (max-width: 900px) {
.color-theme-switcher {
position: fixed !important;
bottom: 1.5rem !important;
left: auto !important;
right: auto !important;
width: 50px !important;
height: 50px !important;
opacity: 0.7 !important;
transform: none !important;
/* Position before info button: 5 buttons total */
/* Download, Print, Shortcuts, Theme, Info */
/* Total width: 5 * 50px + 4 * 10px = 290px */
left: calc(50% + 35px) !important; /* Fourth button */
}
.color-theme-switcher:hover {
opacity: 1 !important;
transform: translateY(-3px) !important;
}
}
+28
View File
@@ -0,0 +1,28 @@
/* ============================================================================
TYPOGRAPHY - Fonts & Text Styles
============================================================================ */
/* Font Imports */
@import url('https://fonts.googleapis.com/css2?family=Quicksand:wght@400;500;600;700&family=Source+Sans+Pro:wght@400;600&family=Inter:wght@400;500;600;700&display=swap');
/* Base Typography */
body {
font-family: 'Quicksand', 'Source Sans Pro', -apple-system, system-ui, sans-serif;
color: var(--text-secondary);
line-height: 1.5;
font-size: 16px;
font-weight: 400;
font-smoothing: antialiased;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Links */
a {
color: var(--accent-blue);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
+55
View File
@@ -0,0 +1,55 @@
/* ============================================================================
CSS CUSTOM PROPERTIES - Design Tokens
============================================================================ */
:root {
/* Brand Colors */
--bg-gray: rgb(82, 86, 89);
--sidebar-gray: #d1d4d2;
--black-bar: #2b2b2b;
--paper-white: #ffffff;
--text-dark: rgb(0, 0, 0);
--text-gray: rgb(51, 51, 51);
--accent-blue: #0066cc;
--border-gray: #dddddd;
/* Theme System - These get overridden by color-theme.css */
/* Page Background */
--page-bg: #b8bbbe;
/* Paper/Card Backgrounds */
--paper-bg: #ffffff;
--paper-secondary-bg: #f5f5f5;
/* Text Colors */
--text-primary: #1a1a1a;
--text-secondary: #333333;
--text-muted: #666666;
--text-light: #999999;
/* Action Bar & Navigation */
--action-bar-bg: #2b2b2b;
--action-bar-text: #ffffff;
--action-bar-text-muted: rgba(255, 255, 255, 0.85);
/* Borders & Dividers */
--border-color: #333333;
--border-light: #e0e0e0;
/* Shadows */
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1);
--shadow-md: 0 2px 8px rgba(0, 0, 0, 0.15);
--shadow-lg: 2px 2px 9px rgba(0, 0, 0, 0.5);
/* Interactive Elements */
--button-bg: transparent;
--button-bg-hover: rgba(0, 0, 0, 0.05);
--button-bg-active: rgba(0, 0, 0, 0.1);
/* Accent Colors */
--accent-blue: #0066cc;
--accent-green: #27ae60;
/* Sidebar */
--sidebar-bg: #d1d4d2;
}
+58
View File
@@ -0,0 +1,58 @@
/* ============================================================================
CV CONTAINER & ZOOM WRAPPER
============================================================================ */
/* Zoom Wrapper - wraps cv-container for zoom functionality */
.zoom-wrapper {
/* CSS zoom property changes actual layout space (not just visual) */
/* This allows footer to naturally position right after zoomed content */
}
/* Main CV Container */
.cv-container {
width: 100%;
max-width: 100%; /* Full width to accommodate pages */
margin: 0 auto;
padding: 20px 0 0 0; /* Top padding to prevent sticky action bar overlap */
display: block;
/* Clean theme - no sidebars, centered content */
&.theme-clean {
padding: 20px 0 0 0;
transition: all 0.3s ease-in-out;
.cv-page {
box-shadow: var(--shadow-lg);
border: 1px solid var(--border-color);
margin: 0 auto;
max-width: 900px;
transition: all 0.3s ease-in-out;
}
.cv-sidebar,
.cv-title-badges-header,
.cv-footer {
display: none !important;
animation: fadeOutShrink 0.3s ease-in-out;
}
.page-content {
grid-template-columns: 1fr !important;
transition: grid-template-columns 0.3s ease-in-out;
}
.cv-main {
grid-column: 1 !important;
padding: 2rem 3rem!important;
transition: all 0.3s ease-in-out;
}
}
}
/* Animate sidebar, header, footer when hiding/showing */
.cv-sidebar,
.cv-title-badges-header,
.cv-footer {
overflow: hidden;
transition: all 0.3s ease-in-out;
}
+38
View File
@@ -0,0 +1,38 @@
/* ============================================================================
GRID LAYOUT - Page Content Grid
============================================================================ */
/* Professional Title Badges - Spans Both Columns */
.cv-title-badges-header {
grid-column: 1 / -1; /* Span all columns */
background: #303030 !important; /* Elegant dark gray */
padding: 10px 20px;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
gap: 0;
border-bottom: 2px solid #34495e;
}
.title-badge {
font-size: 0.9em;
font-weight: normal;
color: #ccc;
text-transform: uppercase;
white-space: nowrap;
}
.badge-separator {
color: #ccc;
font-weight: normal;
padding: 0 15px;
position: relative;
top: -1px;
}
/* Main Content Area */
.cv-main {
background: var(--paper-white);
padding: 3rem 2.5rem 8rem 2.5rem; /* Bottom padding for footer and zoom control clearance */
}
+112
View File
@@ -0,0 +1,112 @@
.cv-page {
background: var(--paper-bg);
max-width: 1200px;
margin: 2rem auto;
box-shadow: var(--shadow-lg);
border: 1px solid var(--border-color);
transform: scale(0.95);
transform-origin: top center;
transition: transform 0.3s ease;
}
/* Page Content Grid */
.page-content {
display: grid;
}
/* Page 1: Left sidebar + Main content */
.page-1 .page-content {
grid-template-columns: 300px 1fr;
}
/* Page 2: Main content + Right sidebar */
.page-2 .page-content {
grid-template-columns: 1fr 300px;
}
/* Sidebar positioning */
.cv-sidebar-left {
grid-column: 1;
grid-row: 1;
}
.cv-sidebar-right {
grid-column: 2;
grid-row: 1;
text-align: right;
}
/* Main content positioning */
.page-1 .cv-main {
grid-column: 2;
grid-row: 1;
}
.page-2 .cv-main {
grid-column: 1;
grid-row: 1;
}
/* ===============================================
FOOTER STYLES
=============================================== */
.cv-footer {
background: #303030;
color: #ccc;
padding: 20px 0;
margin: 0;
grid-column: 1 / -1; /* Span all columns */
}
.footer-content {
list-style: none;
text-align: center;
margin: 0;
padding: 0;
}
.footer-content li {
display: inline-block;
margin: 0;
}
.footer-content li > div {
display: inline-block;
margin: 0 20px;
text-align: left;
}
.footer-label {
width: 200px;
font-size: 1.7em;
}
.footer-value {
width: 450px;
font-size: 1em;
}
.footer-value b {
font-weight: normal;
font-size: 1.7em;
}
.footer-separator {
position: relative;
left: -4%;
font-size: 0.6em;
}
.footer-separator i {
opacity: 0.3;
}
.cv-footer a {
color: inherit;
}
.cv-footer a:hover {
color: #0275d8;
text-decoration: none;
}
+29
View File
@@ -0,0 +1,29 @@
/* ============================================================================
CV PAPER - Container for two-page layout
============================================================================ */
.cv-paper {
width: 100%;
background: transparent; /* Remove white background - each page has its own */
box-shadow: none; /* Remove shadow - each page has its own */
margin: 0;
position: relative;
display: block; /* Changed from grid to block for stacking pages */
min-height: auto;
/* Zoom transform properties */
transform-origin: top center; /* Scale from top center - page stays anchored at top */
transition: transform 0.08s linear; /* Smooth, immediate zoom response */
will-change: transform; /* Hint browser to optimize for transforms */
}
/* Page break helpers */
.page-break {
page-break-after: always;
break-after: page;
}
.avoid-break {
page-break-inside: avoid;
break-inside: avoid;
}
+602
View File
@@ -0,0 +1,602 @@
/* Single Black Top Bar */
.action-bar {
background: var(--action-bar-bg);
color: var(--action-bar-text);
position: sticky;
top: 0;
z-index: 100;
box-shadow: var(--shadow-md);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
}
.action-bar-content {
max-width: 100%;
margin: 0 auto;
padding: 0;
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: stretch;
gap: 2rem;
height: 50px;
}
/* Left: Site Title */
.site-title {
display: flex;
align-items: center;
gap: 0.75rem;
justify-self: start;
white-space: nowrap;
padding: 0;
height: 100%;
}
.site-title-left {
display: flex;
align-items: center;
gap: 0.75rem;
}
.site-icon {
color: #fff;
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
height: 36px;
padding: 0 .5rem 0 1.5rem;
}
/* Mobile icon hidden by default, shown only on mobile */
.site-icon-mobile {
display: none;
color: #fff;
flex-shrink: 0;
margin-right: 0.5rem;
}
/* Site logo and title links */
.site-logo-link,
.site-title-link {
text-decoration: none;
color: inherit;
display: flex;
align-items: center;
height: 36px;
transition: opacity 0.2s ease;
}
.site-logo-link:hover,
.site-title-link:hover {
opacity: 0.8;
text-decoration: none;
}
.site-logo-link {
padding: 0;
}
/* Ensure Iconify icons display properly */
.iconify,
iconify-icon {
display: inline-block;
vertical-align: middle;
}
.site-title-text {
font-size: 1.05rem;
font-weight: 500;
color: #fff;
letter-spacing: -0.01em;
line-height: 1;
display: flex;
align-items: center;
height: 36px;
padding: 0 1rem 0 0rem;
}
/* Center: View controls with labels */
.view-controls-center {
display: flex;
flex-direction: row;
align-items: center;
gap: 2.5rem;
justify-self: center;
flex-shrink: 0;
white-space: nowrap;
height: 100%;
}
.selector-group {
display: flex;
align-items: center;
gap: 0.75rem;
}
.selector-label {
font-size: 0.875rem;
color: rgba(255,255,255,0.85);
font-weight: 500;
white-space: nowrap;
letter-spacing: -0.01em;
line-height: 1;
display: flex;
align-items: center;
height: 36px;
}
.selector-label span {
color: #27ae60;
font-weight: 600;
}
.language-toggle,
.cv-length-toggle,
.logo-toggle {
flex-shrink: 0;
}
/* Right: Action buttons */
.action-buttons {
justify-self: end;
flex-shrink: 0;
}
.htmx-indicator {
flex-shrink: 0;
}
.lang-btn {
padding: 0.4rem 1rem;
border: 1px solid rgba(255,255,255,0.3);
background: transparent;
color: white;
border-radius: 3px;
cursor: pointer;
font-size: 1rem;
font-weight: 400;
text-transform: capitalize;
transition: all 0.2s ease;
}
.lang-btn:hover {
background: rgba(255,255,255,0.1);
border-color: rgba(255,255,255,0.5);
}
.lang-btn.active {
background: #27ae60 !important;
border-color: #27ae60 !important;
font-weight: 500;
}
/* Icon Toggle Switches */
.icon-toggle {
position: relative;
display: flex;
cursor: pointer;
}
.icon-toggle input[type="checkbox"] {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.icon-toggle-slider {
position: relative;
display: inline-flex;
align-items: center;
justify-content: space-between;
width: 75px;
height: 30px;
background: #e0e0e0;
border: 2px solid #d0d0d0;
border-radius: 15px;
padding: 0 6px;
transition: all 0.3s ease;
}
.icon-toggle-slider::before {
content: '';
position: absolute;
width: 24px;
height: 24px;
left: 2px;
background: white;
border-radius: 50%;
transition: transform 0.3s ease;
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
z-index: 2;
pointer-events: none;
}
.icon-toggle input:checked + .icon-toggle-slider::before {
transform: translateX(43px);
}
.icon-toggle input:checked + .icon-toggle-slider {
background: #27ae60;
border-color: #229954;
}
.icon-toggle-slider .icon-left,
.icon-toggle-slider .icon-right {
position: absolute;
z-index: 3;
transition: all 0.3s ease;
flex-shrink: 0;
pointer-events: none;
}
.icon-toggle-slider .icon-left {
left: 6px;
}
.icon-toggle-slider .icon-right {
right: 6px;
}
.icon-toggle input:not(:checked) + .icon-toggle-slider .icon-left {
color: #333 !important;
font-weight: bold;
}
.icon-toggle input:not(:checked) + .icon-toggle-slider .icon-right {
color: #999 !important;
opacity: 0.5;
}
.icon-toggle input:checked + .icon-toggle-slider .icon-left {
color: rgba(255,255,255,0.4) !important;
opacity: 0.5;
}
.icon-toggle input:checked + .icon-toggle-slider .icon-right {
color: white !important;
font-weight: bold;
}
.icon-toggle input:focus + .icon-toggle-slider {
box-shadow: 0 0 0 3px rgba(39, 174, 96, 0.2);
}
/* Language selector wrapper - contains indicators outside swap target */
.language-selector-wrapper {
position: relative;
display: inline-flex;
height: 100%;
/* Ensure wrapper doesn't create extra spacing */
width: fit-content;
}
/* Language selector - matching action button style */
.language-selector {
display: inline-flex;
gap: 0;
padding: 0;
padding-left: 1rem; /* Space after the title */
margin-right: 0;
background: transparent;
border-radius: 0;
height: 100%;
align-items: stretch;
}
/* Position language indicators next to their respective buttons */
#lang-indicator-en,
#lang-indicator-es {
position: absolute;
top: 50%;
transform: translateY(-50%);
pointer-events: none;
z-index: 10;
}
/* Position indicators inside the button visual area */
#lang-indicator-en {
left: calc(1rem + 50px); /* Inside first button */
}
#lang-indicator-es {
left: calc(1rem + 135px); /* Inside second button */
}
.selector-btn {
padding: 0 1.5rem;
background: transparent;
color: white;
border: none;
border-radius: 0;
cursor: pointer;
font-size: 1rem;
font-weight: 500;
display: inline-flex;
align-items: center;
justify-content: center;
/* gap: 0.5rem; */
gap: 0rem;
text-decoration: none;
white-space: nowrap;
letter-spacing: -0.01em;
height: 100%;
line-height: 1;
transition: all 0.2s ease;
outline: none !important;
box-shadow: none !important;
min-width: 50px!important;
}
.selector-btn:focus,
.selector-btn:focus-visible,
.selector-btn:active {
outline: none !important;
box-shadow: none !important;
}
.selector-btn:hover {
background: #666;
}
.selector-btn:hover iconify-icon {
color: #27ae60;
}
.selector-btn.active {
background: #27ae60;
color: white;
}
.selector-btn:not(.active) {
background: transparent;
color: white;
}
/* Language selector buttons - no global animations (applied in responsive range only) */
/* Action buttons - transparent with white text */
.action-btn {
padding: 0 1.5rem;
background: transparent;
color: white;
border: none;
border-radius: 0;
cursor: pointer;
font-size: 1rem;
font-weight: 500;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
text-decoration: none;
white-space: nowrap;
letter-spacing: -0.01em;
height: 100%;
line-height: 1;
transition: background-color 0.3s ease, color 0.3s ease; /* Smooth color transitions */
}
.action-btn iconify-icon {
color: white;
transition: color 0.3s ease; /* Smooth icon color transition */
}
.action-btn:hover {
background: #ddd;
color: #333;
text-decoration: none;
}
.action-btn:hover iconify-icon {
color: #27ae60;
}
/* PDF Download button - gray by default, red on hover */
.pdf-btn {
background: transparent !important; /* Transparent like other buttons */
color: white !important;
}
.pdf-btn:hover,
.pdf-btn.pdf-hover-sync {
background: #cd6060 !important; /* PDF red on hover */
color: white !important;
}
.pdf-btn iconify-icon {
color: white !important;
filter: brightness(0) invert(1); /* Always white */
transition: filter 0.3s ease;
}
.pdf-btn:hover iconify-icon {
color: white !important;
filter: brightness(0) invert(1); /* Keep white on hover */
}
/* Print Friendly button - white bg with green icon on hover */
.print-btn {
background: transparent !important;
color: white !important;
}
.print-btn:hover,
.print-btn.print-hover-sync {
background: white !important; /* White background on hover */
color: #27ae60 !important; /* Green icon on hover */
}
.print-btn iconify-icon {
color: white; /* White icon by default */
}
.print-btn:hover iconify-icon,
.print-btn.print-hover-sync iconify-icon {
color: #27ae60; /* Green icon on hover */
}
/* CV Length Toggle - Center of action bar */
.cv-length-toggle {
display: flex;
gap: 0.5rem;
justify-self: center;
}
.length-btn {
padding: 0.4rem 1rem;
border: 1px solid rgba(255,255,255,0.4);
background: rgba(255,255,255,0.1);
color: white;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 500;
transition: all 0.2s ease;
}
.length-btn:hover {
background: rgba(255,255,255,0.2);
border-color: rgba(255,255,255,0.6);
}
.length-btn.active {
background: white;
color: #1a1a1a;
border-color: white;
font-weight: 600;
}
/* Action buttons styling (already positioned by grid) */
.action-buttons,
.action-buttons-right {
display: flex;
gap: 0;
align-items: stretch;
height: 100%;
}
.action-buttons-right {
justify-self: end;
margin-left: auto;
}
/* ============================================================================
HTMX Loading Indicators
========================================================================= */
/* Base indicator styles - hidden by default with opacity for smooth transitions */
.htmx-indicator {
opacity: 0; /* Hidden by default */
transition: opacity 200ms ease-in-out;
pointer-events: none;
display: inline-flex;
align-items: center;
justify-content: center;
position: absolute; /* Remove from layout flow to prevent spacing issues */
}
/* Override for when request is active - must come AFTER base rule */
.htmx-indicator.htmx-request,
#lang-indicator-en.htmx-request,
#lang-indicator-es.htmx-request {
opacity: 1 !important; /* Force visible state */
}
/* Ensure iconify-icon indicators override global iconify-icon display style */
iconify-icon.htmx-indicator {
display: inline-flex;
align-items: center;
justify-content: center;
}
/* Show indicators during HTMX requests */
/* Using span wrapper, so target span.htmx-request specifically */
span.htmx-request.htmx-indicator,
.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator {
opacity: 1 !important;
}
/* Spinning animation for loading icons */
.htmx-indicator.spinning {
animation: htmx-spin 1s linear infinite;
}
@keyframes htmx-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Indicator size variants */
.htmx-indicator.small {
width: 14px;
height: 14px;
font-size: 14px;
}
.htmx-indicator.medium {
width: 18px;
height: 18px;
font-size: 18px;
}
.htmx-indicator.large {
width: 24px;
height: 24px;
font-size: 24px;
}
/* Positioning variants */
.htmx-indicator.inline {
display: inline-flex;
margin-left: 8px;
vertical-align: middle;
}
.htmx-indicator.inline-start {
display: inline-flex;
margin-right: 8px;
vertical-align: middle;
}
/* Color variants for different contexts */
.htmx-indicator.light {
color: rgba(255, 255, 255, 0.9);
}
.htmx-indicator.dark {
color: rgba(0, 0, 0, 0.7);
}
.htmx-indicator.accent {
color: #27ae60;
}
/* Respect reduced motion preference */
@media (prefers-reduced-motion: reduce) {
.htmx-indicator.spinning {
animation: none;
}
.htmx-indicator {
transition: none;
}
}
/* Legacy loader class for backward compatibility */
.loader {
border: 2px solid #f3f3f3;
border-top: 2px solid white;
border-radius: 50%;
width: 20px;
height: 20px;
animation: htmx-spin 1s linear infinite;
}
/* ============================================================================
Inline Loading States for HTMX Transitions
========================================================================= */
/* Inline loading states - no blocking overlay, smooth transitions only */
/* Language selector buttons already have htmx-indicator spinners */
/* CV content areas show subtle fade during swap */
+90
View File
@@ -0,0 +1,90 @@
/* Courses */
.course-item {
display: flex;
gap: 1.2rem;
align-items: flex-start;
margin-bottom: 2.5rem;
padding-bottom: 2rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
/* Keep border on all course items including last one */
.course-icon {
flex-shrink: 0;
width: 80px;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
}
.course-icon img {
width: 80px;
height: 80px;
object-fit: contain;
border-radius: 4px;
border: 1px solid #ddd;
background: transparent;
padding: 4px;
}
.default-course-icon {
width: 80px;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
border: 1px solid #ddd;
background: transparent;
color: var(--text-light);
padding: 10px;
}
.course-content {
flex: 1;
}
.course-header {
margin-bottom: 0.5rem;
}
.course-title {
font-size: 1em;
font-weight: 600;
margin: 0 0 0.3rem 0;
line-height: 1.4;
color: var(--text-dark);
}
.course-title-text {
display: inline;
}
.course-institution {
display: inline;
margin-left: 0.5em;
font-weight: normal;
}
.course-period,
.course-separator,
.course-location,
.course-duration {
color: var(--text-muted);
font-size: 0.9em;
}
.course-separator {
color: var(--text-light);
}
.course-desc {
font-size: 0.85em;
color: var(--text-gray);
margin-top: 0.4rem;
line-height: 1.4;
text-align: justify;
}
+80
View File
@@ -0,0 +1,80 @@
/* Header with photo and name */
.cv-header {
margin-bottom: 2rem;
}
.cv-header-content {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 2rem;
}
.cv-header-left {
flex: 1;
position: relative;
/* Desktop: Add right padding to make room for the photo */
padding-right: 185px; /* Photo width (150px) + gap (35px) */
}
.cv-photo {
width: 150px;
height: 200px;
flex-shrink: 0;
overflow: hidden;
border: 3px solid white;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
/* Desktop: Position photo in the right padding area */
position: absolute;
top: 15px;
right: 15px; /* Margin from the right edge */
}
.cv-photo img {
width: 100%;
height: 100%;
object-fit: cover;
}
.cv-name {
font-family: 'Quicksand', sans-serif;
font-size: 2.2em;
font-weight: 400;
{{/* font-style: italic; */}}
line-height: 1.1;
margin-bottom: 8px;
color: var(--text-primary);
text-align: right;
}
.cv-experience-years {
font-family: 'Quicksand', sans-serif;
font-size: 0.9em;
font-weight: 500;
line-height: 1.5;
color: var(--text-primary);
margin: 0;
}
.years-experience {
font-family: 'Quicksand', sans-serif;
font-size: 1.25em;
font-weight: 400;
color: var(--text-muted);
margin: 4px 0 0 0;
line-height: 1.4;
text-align: right;
}
/* Intro/Excerpt Text - Positioned inside header, matching old React CV */
.intro-text {
font-family: 'Quicksand', sans-serif;
font-size: 1.0em;
line-height: 1.6;
color: var(--text-secondary);
margin-top: 20px;
text-align: justify;
font-style: italic;
}
+309
View File
@@ -0,0 +1,309 @@
/* Sections */
.cv-section {
margin-bottom: 3rem;
page-break-inside: avoid;
}
/* Remove margin when section is collapsed */
.cv-section:has(details:not([open])) {
margin-bottom: 0;
}
.section-title {
font-family: 'Quicksand', sans-serif;
font-size: 1.4em;
font-weight: 500;
line-height: 1.2em;
margin: 20px 0 25px 0;
padding: 0;
color: var(--text-primary);
}
/* Collapsible Section Styles */
.cv-section details {
margin: 0;
}
.cv-section details summary ~ * {
overflow: hidden;
max-height: 0;
opacity: 0;
transform: translateY(-8px);
transition: max-height 0.5s ease-in-out,
opacity 0.3s ease-in-out,
transform 0.3s ease-in-out;
}
.cv-section details[open] summary ~ * {
max-height: 3000px;
opacity: 1;
transform: translateY(0);
}
.cv-section summary {
cursor: pointer;
list-style: none;
user-select: none;
position: relative;
}
/* Remove default triangle marker in all browsers */
.cv-section summary::-webkit-details-marker,
.cv-section summary::marker {
display: none;
}
/* Add custom collapse indicator after the title */
.cv-section summary .section-title {
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.cv-section summary .section-title::after {
content: '▼';
font-size: 0.8em;
color: var(--text-muted);
transition: transform 0.2s ease, opacity 0.2s ease;
opacity: 0;
margin-left: 0.5rem;
}
/* Show indicator on hover or when closed */
.cv-section summary:hover .section-title::after,
.cv-section details:not([open]) summary .section-title::after {
opacity: 1;
}
/* Rotate indicator when closed */
.cv-section details:not([open]) summary .section-title::after {
transform: rotate(-90deg);
}
/* Hover effect on summary */
.cv-section summary:hover .section-title {
color: var(--accent-blue);
}
.summary-text {
font-family: 'Quicksand', sans-serif;
line-height: 1.5;
text-align: justify;
font-size: 0.9em;
font-weight: 400;
color: var(--text-primary);
}
/* Experience */
/* Experience item layout moved to logo-toggle.css */
.experience-header {
margin-bottom: 0.6rem;
}
.experience-title-line {
margin-bottom: 0.3em;
}
.position {
font-size: 1rem;
font-weight: 500;
margin: 0;
color: var(--text-dark);
margin-bottom: 4px;
}
.position .position-title {
display: inline-block;
margin-right: 0.3em;
}
.position .company-name {
display: inline-block;
}
.current-badge {
display: inline-block;
background: #27ae60;
color: white;
font-weight: 700;
font-size: 0.7em;
padding: 0.2em 0.5em;
border-radius: 3px;
margin-left: 0.5em;
vertical-align: middle;
letter-spacing: 0.5px;
}
.live-badge {
display: inline-flex;
align-items: center;
gap: 0.3em;
background: #27ae60;
color: white;
font-weight: 700;
font-size: 0.7em;
padding: 0.2em 0.5em;
border-radius: 3px;
margin-left: 0.5em;
vertical-align: middle;
letter-spacing: 0.5px;
}
.live-badge iconify-icon {
font-size: 1.2em;
}
.expired-badge {
display: inline-block;
background: #e74c3c;
color: white;
font-weight: 700;
font-size: 0.7em;
padding: 0.2em 0.5em;
border-radius: 3px;
margin-left: 0.5em;
vertical-align: middle;
letter-spacing: 0.5px;
}
.maintained-badge {
display: inline-block;
background: #3498db;
color: white;
font-weight: 700;
font-size: 0.7em;
padding: 0.2em 0.5em;
border-radius: 3px;
margin-left: 0.5em;
vertical-align: middle;
letter-spacing: 0.5px;
}
.experience-period,
.experience-separator,
.experience-location,
.experience-duration {
color: var(--text-muted);
font-weight: 600;
display: inline-block;
font-size: 1.05rem;
}
.experience-duration {
font-style: italic;
}
.short-desc {
color: var(--text-dark);
font-size: 0.95rem;
line-height: 1.6;
margin-top: 0.5rem;
}
.duration-text {
color: var(--text-light);
font-weight: 500;
}
.responsibilities {
list-style: none;
margin-top: 1rem;
padding-left: 0;
}
.responsibilities li {
padding-left: 1.2rem;
margin-bottom: 0.4rem;
position: relative;
font-size: 0.95rem;
color: var(--text-dark);
line-height: 1.5;
}
.responsibilities li:before {
content: "•";
position: absolute;
left: 0;
color: var(--text-gray);
}
/* Responsibilities with company icons (similar to main experience layout) */
.responsibilities li:has(img),
.responsibilities li:has(iconify-icon) {
display: grid;
grid-template-columns: 60px 1fr;
gap: 1rem;
padding-left: 0;
margin-bottom: 1rem;
align-items: start;
}
.responsibilities li:has(img):before,
.responsibilities li:has(iconify-icon):before {
display: none;
}
.responsibilities li img {
width: 60px;
height: 60px;
object-fit: contain;
border-radius: 4px;
border: 1px solid #ddd;
background: transparent;
padding: 4px;
}
.responsibilities li iconify-icon.default-company-icon {
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
border: 1px solid #ddd;
background: transparent;
color: var(--text-light);
padding: 8px;
}
/* Education */
.education-item {
margin-bottom: 1rem;
font-size: 0.95rem;
line-height: 1.6;
color: var(--text-dark);
}
/* Languages */
.languages-list {
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
}
.language-item {
font-size: 0.95rem!important;
color: var(--text-dark);
margin-bottom: 0.3rem!important;
line-height: 1.4!important;
margin-left: 2rem!important;
}
.language-item small {
display: block;
font-size: 0.8em;
margin-top: 0.2rem;
font-style: italic;
}
/* Experience Items */
.experience-item {
margin-bottom: 2.5rem;
padding-bottom: 2rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
/* Keep border on all experience items including last one */
+40
View File
@@ -0,0 +1,40 @@
/* Education */
.education-item {
margin-bottom: 1rem;
font-size: 0.95rem;
line-height: 1.6;
color: var(--text-dark);
}
/* Languages */
.languages-list {
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
}
.language-item {
font-size: 0.95rem!important;
color: var(--text-dark);
margin-bottom: 0.3rem!important;
line-height: 1.4!important;
margin-left: 2rem!important;
}
.language-item small {
display: block;
font-size: 0.8em;
margin-top: 0.2rem;
font-style: italic;
}
/* Experience Items */
.experience-item {
margin-bottom: 2.5rem;
padding-bottom: 2rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
/* Keep border on all experience items including last one */
+533
View File
@@ -0,0 +1,533 @@
/* Experience */
/* Experience item layout moved to logo-toggle.css */
.experience-header {
margin-bottom: 0.6rem;
}
.experience-title-line {
margin-bottom: 0.3em;
}
.position {
font-size: 1rem;
font-weight: 500;
margin: 0;
color: var(--text-dark);
margin-bottom: 4px;
}
.position .position-title {
display: inline-block;
margin-right: 0.3em;
}
.position .company-name {
display: inline-block;
}
.current-badge {
display: inline-block;
background: #27ae60;
color: white;
font-weight: 700;
font-size: 0.7em;
padding: 0.2em 0.5em;
border-radius: 3px;
margin-left: 0.5em;
vertical-align: middle;
letter-spacing: 0.5px;
}
.live-badge {
display: inline-flex;
align-items: center;
gap: 0.3em;
background: #27ae60;
color: white;
font-weight: 700;
font-size: 0.7em;
padding: 0.2em 0.5em;
border-radius: 3px;
margin-left: 0.5em;
vertical-align: middle;
letter-spacing: 0.5px;
}
.live-badge iconify-icon {
font-size: 1.2em;
}
.expired-badge {
display: inline-block;
background: #e74c3c;
color: white;
font-weight: 700;
font-size: 0.7em;
padding: 0.2em 0.5em;
border-radius: 3px;
margin-left: 0.5em;
vertical-align: middle;
letter-spacing: 0.5px;
}
.maintained-badge {
display: inline-block;
background: #3498db;
color: white;
font-weight: 700;
font-size: 0.7em;
padding: 0.2em 0.5em;
border-radius: 3px;
margin-left: 0.5em;
vertical-align: middle;
letter-spacing: 0.5px;
}
.experience-period,
.experience-separator,
.experience-location,
.experience-duration {
color: var(--text-muted);
font-weight: 600;
display: inline-block;
font-size: 1.05rem;
}
.experience-duration {
font-style: italic;
}
.short-desc {
color: var(--text-dark);
font-size: 0.95rem;
line-height: 1.6;
margin-top: 0.5rem;
}
.duration-text {
color: var(--text-light);
font-weight: 500;
}
.responsibilities {
list-style: none;
margin-top: 1rem;
padding-left: 0;
}
.responsibilities li {
padding-left: 1.2rem;
margin-bottom: 0.4rem;
position: relative;
font-size: 0.95rem;
color: var(--text-dark);
line-height: 1.5;
}
.responsibilities li:before {
content: "•";
position: absolute;
left: 0;
color: var(--text-gray);
}
/* Responsibilities with company icons (similar to main experience layout) */
.responsibilities li:has(img),
.responsibilities li:has(iconify-icon) {
display: grid;
grid-template-columns: 60px 1fr;
gap: 1rem;
padding-left: 0;
margin-bottom: 1rem;
align-items: start;
}
.responsibilities li:has(img):before,
.responsibilities li:has(iconify-icon):before {
display: none;
}
.responsibilities li img {
width: 60px;
height: 60px;
object-fit: contain;
border-radius: 4px;
border: 1px solid #ddd;
background: transparent;
padding: 4px;
}
.responsibilities li iconify-icon.default-company-icon {
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
border: 1px solid #ddd;
background: transparent;
color: var(--text-light);
padding: 8px;
}
/* Education */
.education-item {
margin-bottom: 1rem;
font-size: 0.95rem;
line-height: 1.6;
color: var(--text-dark);
}
/* Languages */
.languages-list {
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
}
.language-item {
font-size: 0.95rem!important;
color: var(--text-dark);
margin-bottom: 0.3rem!important;
line-height: 1.4!important;
margin-left: 2rem!important;
}
.language-item small {
display: block;
font-size: 0.8em;
margin-top: 0.2rem;
font-style: italic;
}
/* Experience Items */
.experience-item {
margin-bottom: 2.5rem;
padding-bottom: 2rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
/* Keep border on all experience items including last one */
/* Courses */
.course-item {
display: flex;
gap: 1.2rem;
align-items: flex-start;
margin-bottom: 2.5rem;
padding-bottom: 2rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
/* Keep border on all course items including last one */
.course-icon {
flex-shrink: 0;
width: 80px;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
}
.course-icon img {
width: 80px;
height: 80px;
object-fit: contain;
border-radius: 4px;
border: 1px solid #ddd;
background: transparent;
padding: 4px;
}
.default-course-icon {
width: 80px;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
border: 1px solid #ddd;
background: transparent;
color: var(--text-light);
padding: 10px;
}
.course-content {
flex: 1;
}
.course-header {
margin-bottom: 0.5rem;
}
.course-title {
font-size: 1em;
font-weight: 600;
margin: 0 0 0.3rem 0;
line-height: 1.4;
color: var(--text-dark);
}
.course-title-text {
display: inline;
}
.course-institution {
display: inline;
margin-left: 0.5em;
font-weight: normal;
}
.course-period,
.course-separator,
.course-location,
.course-duration {
color: var(--text-muted);
font-size: 0.9em;
}
.course-separator {
color: var(--text-light);
}
.course-desc {
font-size: 0.85em;
color: var(--text-gray);
margin-top: 0.4rem;
line-height: 1.4;
text-align: justify;
}
/* Projects */
.project-item {
display: flex;
gap: 1.2rem;
align-items: flex-start;
margin-bottom: 2.5rem;
padding-bottom: 2rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
.project-icon {
flex-shrink: 0;
width: 80px;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
}
.project-icon img {
width: 80px;
height: 80px;
object-fit: contain;
border-radius: 4px;
border: 1px solid #ddd;
background: transparent;
padding: 4px;
}
.default-project-icon {
width: 80px;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
border: 1px solid #ddd;
background: transparent;
color: var(--text-light);
padding: 10px;
}
.project-content {
flex: 1;
}
.project-header {
margin-bottom: 0.5rem;
}
.project-title {
font-size: 1em;
font-weight: 600;
margin: 0 0 0.3rem 0;
line-height: 1.4;
color: var(--text-dark);
}
.project-title-text {
display: inline;
}
.project-title-text a {
color: var(--accent-blue);
text-decoration: none;
}
.project-title-text a:hover {
text-decoration: underline;
}
.project-period,
.project-separator,
.project-location {
color: var(--text-muted);
font-size: 0.9em;
font-weight: 600;
}
.project-separator {
color: var(--text-light);
}
.project-desc {
font-size: 0.95rem;
color: var(--text-dark);
margin-top: 0.5rem;
line-height: 1.6;
text-align: justify;
}
.project-technologies {
font-size: 0.85em;
color: var(--text-gray);
margin-top: 0.5rem;
line-height: 1.4;
}
.projects-footer {
margin-top: -1.5rem;
padding-top: 0rem;
text-align: center;
font-size: 0.95rem;
color: var(--text-gray);
}
.projects-footer p {
margin: 0;
}
.projects-footer a {
color: var(--accent-blue);
text-decoration: none;
}
.projects-footer a:hover {
text-decoration: underline;
}
/* References */
.reference-item {
margin-bottom: 0!important;
line-height: 1.4!important;
margin-left: 2rem!important;
font-size: 0.95rem!important;
}
.reference-item a {
color: var(--accent-blue);
text-decoration: none;
word-break: break-word;
}
.reference-item a:hover {
text-decoration: underline;
}
.ref-type {
display: block;
font-size: 0.8em;
color: var(--text-gray);
font-style: italic;
margin-top: 0.2rem;
}
/* Footer */
footer {
text-align: center;
padding: 2rem;
color: rgba(255,255,255,0.7);
font-size: 0.85rem;
}
/* GitHub repository link styling */
.github-repo-link {
color: whitesmoke !important;
transition: color 0.2s ease-in-out;
}
.github-repo-link:hover {
color: #66B3FF !important;
}
/* CV Version Toggle Animations */
@keyframes fadeInGrow {
from {
opacity: 0;
max-height: 0;
transform: scaleY(0.8);
transform-origin: top;
}
to {
opacity: 1;
max-height: 5000px;
transform: scaleY(1);
}
}
@keyframes fadeOutShrink {
from {
opacity: 1;
max-height: 5000px;
transform: scaleY(1);
}
to {
opacity: 0;
max-height: 0;
transform: scaleY(0.8);
transform-origin: top;
}
}
/* Elements that appear/disappear */
.long-only,
.short-desc {
overflow: hidden;
transition: all 0.3s ease-in-out;
}
/* Short CV - Hide detailed content with animation */
.cv-short .long-only {
display: none;
animation: fadeOutShrink 0.3s ease-in-out;
}
.cv-short .short-desc {
display: block;
animation: fadeInGrow 0.3s ease-in-out;
}
/* Long CV - Hide short descriptions with animation */
.cv-long .short-desc,
.short-desc {
display: none;
animation: fadeOutShrink 0.3s ease-in-out;
}
.cv-long .long-only {
display: block;
animation: fadeInGrow 0.3s ease-in-out;
}
.cv-long .responsibilities {
display: block;
animation: fadeInGrow 0.3s ease-in-out;
}
+31
View File
@@ -0,0 +1,31 @@
/* Languages */
.languages-list {
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
}
.language-item {
font-size: 0.95rem!important;
color: var(--text-dark);
margin-bottom: 0.3rem!important;
line-height: 1.4!important;
margin-left: 2rem!important;
}
.language-item small {
display: block;
font-size: 0.8em;
margin-top: 0.2rem;
font-style: italic;
}
/* Experience Items */
.experience-item {
margin-bottom: 2.5rem;
padding-bottom: 2rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
/* Keep border on all experience items including last one */
+230
View File
@@ -0,0 +1,230 @@
/* Projects */
.project-item {
display: flex;
gap: 1.2rem;
align-items: flex-start;
margin-bottom: 2.5rem;
padding-bottom: 2rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
.project-icon {
flex-shrink: 0;
width: 80px;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
}
.project-icon img {
width: 80px;
height: 80px;
object-fit: contain;
border-radius: 4px;
border: 1px solid #ddd;
background: transparent;
padding: 4px;
}
.default-project-icon {
width: 80px;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
border: 1px solid #ddd;
background: transparent;
color: var(--text-light);
padding: 10px;
}
.project-content {
flex: 1;
}
.project-header {
margin-bottom: 0.5rem;
}
.project-title {
font-size: 1em;
font-weight: 600;
margin: 0 0 0.3rem 0;
line-height: 1.4;
color: var(--text-dark);
}
.project-title-text {
display: inline;
}
.project-title-text a {
color: var(--accent-blue);
text-decoration: none;
}
.project-title-text a:hover {
text-decoration: underline;
}
.project-period,
.project-separator,
.project-location {
color: var(--text-muted);
font-size: 0.9em;
font-weight: 600;
}
.project-separator {
color: var(--text-light);
}
.project-desc {
font-size: 0.95rem;
color: var(--text-dark);
margin-top: 0.5rem;
line-height: 1.6;
text-align: justify;
}
.project-technologies {
font-size: 0.85em;
color: var(--text-gray);
margin-top: 0.5rem;
line-height: 1.4;
}
.projects-footer {
margin-top: -1.5rem;
padding-top: 0rem;
text-align: center;
font-size: 0.95rem;
color: var(--text-gray);
}
.projects-footer p {
margin: 0;
}
.projects-footer a {
color: var(--accent-blue);
text-decoration: none;
}
.projects-footer a:hover {
text-decoration: underline;
}
/* References */
.reference-item {
margin-bottom: 0!important;
line-height: 1.4!important;
margin-left: 2rem!important;
font-size: 0.95rem!important;
}
.reference-item a {
color: var(--accent-blue);
text-decoration: none;
word-break: break-word;
}
.reference-item a:hover {
text-decoration: underline;
}
.ref-type {
display: block;
font-size: 0.8em;
color: var(--text-gray);
font-style: italic;
margin-top: 0.2rem;
}
/* Footer */
footer {
text-align: center;
padding: 2rem;
color: rgba(255,255,255,0.7);
font-size: 0.85rem;
}
/* GitHub repository link styling */
.github-repo-link {
color: whitesmoke !important;
transition: color 0.2s ease-in-out;
}
.github-repo-link:hover {
color: #66B3FF !important;
}
/* CV Version Toggle Animations */
@keyframes fadeInGrow {
from {
opacity: 0;
max-height: 0;
transform: scaleY(0.8);
transform-origin: top;
}
to {
opacity: 1;
max-height: 5000px;
transform: scaleY(1);
}
}
@keyframes fadeOutShrink {
from {
opacity: 1;
max-height: 5000px;
transform: scaleY(1);
}
to {
opacity: 0;
max-height: 0;
transform: scaleY(0.8);
transform-origin: top;
}
}
/* Elements that appear/disappear */
.long-only,
.short-desc {
overflow: hidden;
transition: all 0.3s ease-in-out;
}
/* Short CV - Hide detailed content with animation */
.cv-short .long-only {
display: none;
animation: fadeOutShrink 0.3s ease-in-out;
}
.cv-short .short-desc {
display: block;
animation: fadeInGrow 0.3s ease-in-out;
}
/* Long CV - Hide short descriptions with animation */
.cv-long .short-desc,
.short-desc {
display: none;
animation: fadeOutShrink 0.3s ease-in-out;
}
.cv-long .long-only {
display: block;
animation: fadeInGrow 0.3s ease-in-out;
}
.cv-long .responsibilities {
display: block;
animation: fadeInGrow 0.3s ease-in-out;
}
+157
View File
@@ -0,0 +1,157 @@
/* ============================================================================
SIDEBAR COMPONENT
============================================================================ */
/* Sidebar - Left/Right columns */
.cv-sidebar {
background: var(--sidebar-bg);
padding: 4rem 1.5rem;
font-size: 0.9rem;
}
/* Sidebar Accordion - Hidden on desktop, visible on mobile */
.sidebar-accordion-header {
display: none;
}
.sidebar-section {
margin-bottom: 2rem;
/* Add margin when section is collapsed */
&:has(details:not([open])) {
margin-bottom: 3rem;
margin-top: 0rem;
}
/* Collapsible Details */
details {
margin: 0;
summary ~ * {
overflow: hidden;
max-height: 0;
opacity: 0;
transform: translateY(-8px);
transition: max-height 0.5s ease-in-out,
opacity 0.3s ease-in-out,
transform 0.3s ease-in-out;
}
&[open] summary ~ * {
max-height: 1500px;
opacity: 1;
transform: translateY(0);
}
&[open] .sidebar-content {
margin-top: 0.5rem;
}
}
summary {
cursor: pointer;
list-style: none;
user-select: none;
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
/* Remove default triangle marker */
&::-webkit-details-marker,
&::marker {
display: none;
}
.sidebar-title {
margin-bottom: 0;
}
&:hover .sidebar-title {
color: var(--accent-blue);
}
&:hover::after,
details:not([open]) &::after {
opacity: 1;
}
}
}
.sidebar-title {
font-family: 'Quicksand', sans-serif;
font-size: 1.4em;
font-weight: 700;
line-height: 1.3em;
margin-bottom: 10px;
padding: 0;
color: rgb(51, 51, 51);
text-align: left;
}
.sidebar-content {
font-family: 'Quicksand', sans-serif;
font-size: 0.95rem;
font-weight: 400;
line-height: 1.5;
}
.skill-item {
margin-bottom: 0.15rem;
color: rgb(0, 0, 0);
font-weight: 400;
}
/* Left Sidebar Specific */
.cv-sidebar-left {
.sidebar-section summary::after {
content: '▶';
font-size: 0.8em;
color: rgb(100, 100, 100);
transition: transform 0.2s ease, opacity 0.2s ease;
opacity: 0;
margin-left: 15px;
flex-shrink: 0;
}
.sidebar-section details[open] summary::after {
transform: rotate(90deg);
}
.sidebar-content,
.skill-item {
text-align: left;
}
}
/* Right Sidebar Specific */
.cv-sidebar-right {
.sidebar-section summary {
flex-direction: row-reverse;
justify-content: space-between;
.sidebar-title {
text-align: right;
width: 100%;
}
&::after {
content: '▶';
font-size: 0.8em;
color: rgb(100, 100, 100);
transition: transform 0.2s ease, opacity 0.2s ease;
opacity: 0;
margin-right: 15px;
flex-shrink: 0;
}
}
.sidebar-section details[open] summary::after {
transform: rotate(90deg);
}
.sidebar-content,
.skill-item {
text-align: right;
}
}
+391
View File
@@ -0,0 +1,391 @@
/* Hamburger button */
.hamburger-btn {
background: transparent;
border: none;
color: #fff;
cursor: pointer;
padding: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s ease;
border-radius: 4px;
margin: 0 0.5rem;
position: relative; /* For CSS-only hover trigger */
}
.hamburger-btn:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.hamburger-btn:active {
background-color: rgba(255, 255, 255, 0.2);
}
/* Navigation Menu */
.navigation-menu {
position: fixed;
top: 50px; /* Height of action bar */
left: 0;
width: 280px;
max-height: 0;
background: #ffffff;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.15);
transition: max-height 0.5s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease;
overflow-y: auto;
z-index: 1000; /* Above fixed buttons (z-index: 999) */
pointer-events: none; /* Disable pointer events when hidden */
opacity: 0;
}
/* Pure CSS Menu Activation - Show menu when hovering hamburger OR menu */
/* Show when hovering the hamburger button (adjacent in DOM after site-title-left) */
.hamburger-btn:hover ~ .navigation-menu,
.hamburger-btn:focus ~ .navigation-menu,
/* Show when hovering the menu itself */
.navigation-menu:hover,
/* Legacy class for backward compatibility */
.navigation-menu.menu-hover,
.navigation-menu.menu-open {
max-height: calc(100vh - 60px); /* Viewport height minus header + some spacing */
pointer-events: auto; /* Enable pointer events when visible */
opacity: 1;
}
.menu-content {
padding: 1rem 0;
}
.menu-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.875rem 1.5rem;
color: var(--text-dark);
text-decoration: none;
transition: background-color 0.2s ease, color 0.2s ease;
font-size: 0.95rem;
font-weight: 500;
border-left: 3px solid transparent;
}
.menu-item:hover {
background-color: rgba(0, 102, 204, 0.08);
color: var(--accent-blue);
border-left-color: var(--accent-blue);
text-decoration: none;
}
.menu-item iconify-icon {
color: var(--text-gray);
flex-shrink: 0;
transition: color 0.2s ease;
}
.menu-item:hover iconify-icon {
color: var(--accent-blue);
}
/* Menu item action controls (Expand All, Collapse All) */
/* Removed centered text styling - action items now behave like regular menu items */
/* Remove extra padding - all menu items should align consistently */
/* Submenu styles - hover triggered, opens to the right */
.menu-item-submenu {
position: relative;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
padding: 0 0 1rem 0;
}
.menu-item.has-submenu {
justify-content: space-between;
position: relative;
}
.submenu-arrow {
transition: transform 0.2s ease;
margin-left: auto;
}
/* Rotate arrow slightly on hover */
.menu-item-submenu:hover .submenu-arrow {
transform: translateX(3px);
}
.submenu-content {
position: fixed; /* Changed from absolute to fixed to break out of parent overflow */
left: 232px; /* Slight overlap with menu to eliminate any gap */
background: #ffffff;
box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.15);
border-radius: 8px;
min-width: 250px;
max-width: 300px;
opacity: 0;
visibility: hidden;
transform: translateX(-3px);
transition: all 0.3s ease;
z-index: 1000; /* Higher z-index to appear above everything */
padding: 0.5rem 0;
max-height: calc(100vh - 100px); /* Ensure it fits viewport */
overflow-y: auto; /* Scroll if content is too long */
}
/* Show submenu when hovering the submenu container OR the submenu itself */
.menu-item-submenu:hover .submenu-content,
.submenu-content:hover {
opacity: 1;
visibility: visible;
transform: translateX(0);
}
/* Legacy class for JS compatibility */
.menu-item-submenu.submenu-open .submenu-arrow {
transform: translateX(3px);
}
.menu-item-submenu.submenu-open .submenu-content {
opacity: 1;
visibility: visible;
transform: translateX(0);
}
.submenu-content .menu-item {
padding: 0.875rem 1.5rem;
font-size: 0.9rem;
border-left: 3px solid transparent;
border-radius: 0;
}
.submenu-content .menu-item:first-child {
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
.submenu-content .menu-item:last-child {
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
}
/* ========== Menu Sections with Separators ========== */
/* Quick Actions section - always visible */
.menu-section-wrapper {
padding: 0.5rem 1.5rem 1rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
/* Remove border from last visible section */
.menu-content > *:last-child,
.menu-content > div:last-child {
border-bottom: none !important;
}
/* ========== Menu Controls & Actions (Always Visible) ========== */
/* Always visible in hamburger menu at all screen sizes */
.menu-controls-section,
.menu-actions-section {
display: block;
padding: 0.5rem 1.5rem 1rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
.menu-item-header {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.875rem 0 0.875rem 0;
color: var(--text-dark);
font-size: 0.85rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
cursor: default;
}
/* Disable hover effect for headers */
.menu-item-header:hover {
background-color: transparent !important;
color: var(--text-dark) !important;
border-left-color: transparent !important;
}
.menu-item-header iconify-icon {
color: var(--text-gray);
flex-shrink: 0;
}
.menu-item-header:hover iconify-icon {
color: var(--text-gray) !important;
}
.menu-item-header span {
flex: 1;
}
.menu-control-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 0;
}
.menu-control-label {
display: flex;
align-items: center;
gap: 0.75rem;
color: var(--text-dark);
font-size: 0.9rem;
font-weight: 500;
}
.menu-control-label iconify-icon {
color: var(--text-gray);
}
.menu-action-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
padding: 0.875rem 1rem;
margin: 0.25rem 0;
background: rgba(0, 0, 0, 0.03);
border: none;
border-radius: 8px;
color: var(--text-dark);
text-decoration: none;
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
width: 100%;
}
.menu-action-btn:hover {
background: rgba(0, 102, 204, 0.08);
color: var(--accent-blue);
text-decoration: none;
}
.menu-action-btn iconify-icon {
color: var(--text-gray);
flex-shrink: 0;
transition: color 0.2s ease;
}
.menu-action-btn:hover iconify-icon {
color: var(--accent-blue);
}
/* PDF button in menu - White bg with red icon on hover */
.menu-pdf-btn:hover,
.menu-pdf-btn.pdf-hover-sync {
background: white !important;
color: #e74c3c !important;
}
.menu-pdf-btn:hover iconify-icon,
.menu-pdf-btn.pdf-hover-sync iconify-icon {
color: #e74c3c !important;
}
/* Print button in menu - White bg with green icon on hover */
.menu-print-btn:hover,
.menu-print-btn.print-hover-sync {
background: white !important;
color: #27ae60 !important;
}
.menu-print-btn:hover iconify-icon,
.menu-print-btn.print-hover-sync iconify-icon {
color: #27ae60 !important;
}
/* Section icons in titles */
.section-icon {
vertical-align: middle;
margin-right: 0.5rem;
color: #7d7d7d;
}
/* Add invisible separator (blank space) below section titles */
#experience .section-title,
#awards .section-title,
#courses .section-title,
#projects .section-title {
margin-bottom: 40px !important;
}
/* Smooth scrolling */
html {
scroll-behavior: smooth;
}
/* Add scroll padding to account for fixed header */
html {
scroll-padding-top: 70px; /* Action bar height + some spacing */
}
/* Mobile responsive */
@media (max-width: 768px) {
.navigation-menu {
width: 240px;
}
.menu-item {
padding: 0.75rem 1rem;
font-size: 0.9rem;
}
.site-title {
justify-content: space-between;
width: 100%;
}
}
/* Hide menu overlay on print */
@media print {
.navigation-menu {
display: none !important;
}
.hamburger-btn {
display: none !important;
}
}
/* ========================================
Scroll Direction - Hide/Show Header
======================================== */
/* Add smooth transition to header elements */
.action-bar,
.navigation-menu {
transition: transform 0.3s ease-in-out;
}
/* Hide header when scrolling down */
.action-bar.header-hidden {
transform: translateY(-100%);
}
.navigation-menu.header-hidden {
transform: translateY(-100%);
}
/* ========================================
Back to Top Button
======================================== */
.back-to-top {
position: fixed;
bottom: 2rem;
right: 2rem;
width: 50px;
height: 50px;
background: var(--black-bar);
color: white;
border: none;
border-radius: 50%;
cursor: pointer;
File diff suppressed because it is too large Load Diff
+260
View File
@@ -0,0 +1,260 @@
/* Toggle Components - Unified Design */
.language-toggle,
.cv-length-toggle,
.logo-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
white-space: nowrap;
}
.toggle-switch {
display: inline-block;
cursor: pointer;
user-select: none;
position: relative;
}
.toggle-switch input[type="checkbox"] {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: relative;
display: inline-block;
width: 50px;
height: 26px;
background-color: #555;
border-radius: 26px;
transition: background-color 0.3s ease;
}
.toggle-slider::after {
content: '';
position: absolute;
width: 20px;
height: 20px;
border-radius: 50%;
background-color: white;
top: 3px;
left: 3px;
transition: transform 0.3s ease;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.toggle-switch input[type="checkbox"]:checked + .toggle-slider {
background-color: var(--accent-blue);
}
.toggle-switch input[type="checkbox"]:checked + .toggle-slider::after {
transform: translateX(24px);
}
.toggle-switch input[type="checkbox"]:focus + .toggle-slider {
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.2);
}
.toggle-label-left,
.toggle-label-right {
font-size: 0.8rem;
font-weight: 500;
color: #999;
transition: all 0.3s ease;
white-space: nowrap;
display: flex;
align-items: center;
justify-content: center;
height: 28px;
}
/* Flag icons - special styling */
.flag-icon {
border-radius: 50%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
/* Highlight active label/icon based on parent container state */
.language-toggle:has(#langToggle:not(:checked)) .toggle-label-left,
.cv-length-toggle:has(#lengthToggle:not(:checked)) .toggle-label-left,
.logo-toggle:has(#logoToggle:not(:checked)) .toggle-label-left {
color: #fff;
opacity: 1;
}
.language-toggle:has(#langToggle:checked) .toggle-label-right,
.cv-length-toggle:has(#lengthToggle:checked) .toggle-label-right,
.logo-toggle:has(#logoToggle:checked) .toggle-label-right {
color: #fff;
opacity: 1;
}
/* Dim inactive icons */
.language-toggle:has(#langToggle:not(:checked)) .toggle-label-right,
.cv-length-toggle:has(#lengthToggle:not(:checked)) .toggle-label-right,
.logo-toggle:has(#logoToggle:not(:checked)) .toggle-label-right {
opacity: 0.4;
}
.language-toggle:has(#langToggle:checked) .toggle-label-left,
.cv-length-toggle:has(#lengthToggle:checked) .toggle-label-left,
.logo-toggle:has(#logoToggle:checked) .toggle-label-left {
opacity: 0.4;
}
/* Experience Item with Logo Support */
.experience-item,
.award-item {
margin-bottom: 2.5rem;
padding-bottom: 2rem;
border-bottom: 2px solid #ddd;
page-break-inside: avoid;
display: flex;
gap: 1.2rem;
position: relative;
transition: gap 0.3s ease-in-out;
}
.experience-item:last-child,
.award-item:last-child {
border-bottom: none;
padding-bottom: 0;
}
/* Adjust gap when icons are hidden */
.cv-paper:not(.show-icons) .experience-item,
.cv-paper:not(.show-icons) .award-item {
gap: 0;
}
.company-logo,
.award-logo,
.project-icon,
.course-icon {
display: block;
flex-shrink: 0;
}
.company-logo img,
.award-logo img,
.project-icon img,
.course-icon img {
width: 80px;
height: 80px;
object-fit: contain;
border-radius: 4px;
border: 1px solid #ddd;
background: transparent;
padding: 10px;
}
.default-company-icon,
.default-award-icon,
.default-project-icon,
.default-course-icon {
width: 80px !important;
height: 80px !important;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
border: 1px solid #ddd;
background: transparent;
color: #999;
padding: 10px;
}
.experience-content,
.award-content {
flex: 1;
min-width: 0; /* Prevents flex item from overflowing */
}
/* Animate icons with fade and scale */
.company-logo,
.award-logo,
.section-icon,
.default-company-icon,
.project-icon,
.default-project-icon,
.course-icon,
.default-course-icon {
overflow: hidden;
transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out, width 0.3s ease-in-out, height 0.3s ease-in-out, margin 0.3s ease-in-out;
opacity: 1;
transform: scale(1);
width: auto;
height: auto;
}
/* Hide icons when toggle is OFF - with animation */
.cv-paper:not(.show-icons) .company-logo,
.cv-paper:not(.show-icons) .award-logo,
.cv-paper:not(.show-icons) .section-icon,
.cv-paper:not(.show-icons) .default-company-icon,
.cv-paper:not(.show-icons) .project-icon,
.cv-paper:not(.show-icons) .default-project-icon,
.cv-paper:not(.show-icons) .course-icon,
.cv-paper:not(.show-icons) .default-course-icon {
opacity: 0;
transform: scale(0.8);
width: 0;
height: 0;
margin: 0;
padding: 0;
pointer-events: none;
overflow: hidden;
}
/* Show icons when toggle is ON (default) - with animation */
.show-icons .company-logo,
.show-icons .award-logo,
.show-icons .section-icon,
.show-icons .default-company-icon,
.show-icons .project-icon,
.show-icons .default-project-icon,
.show-icons .course-icon,
.show-icons .default-course-icon {
opacity: 1;
transform: scale(1);
width: auto;
height: auto;
}
/* Company icons visible in print - styling controlled by print.css */
/* Mobile responsiveness */
@media (max-width: 768px) {
.logo-toggle {
order: 3; /* Move to bottom on mobile */
}
.toggle-label {
font-size: 0.85rem;
}
.toggle-slider {
width: 38px;
height: 20px;
}
.toggle-slider::after {
width: 14px;
height: 14px;
}
.toggle-switch input[type="checkbox"]:checked + .toggle-slider::after {
transform: translateX(18px);
}
.company-logo img {
width: 40px;
height: 40px;
}
}
+603
View File
@@ -0,0 +1,603 @@
/**
* Component-Level Skeleton Loaders for Language Transitions
* ==========================================================
* Each CV component has dual-state structure:
* - .actual-content (real CV content)
* - .skeleton-content (gray pulsing placeholders)
*
* Loading state controlled via .loading class on component wrapper
*/
/* ========================================================================
BASE SKELETON STYLES
======================================================================== */
.skeleton {
background: linear-gradient(
90deg,
#f0f0f0 0%,
#e8e8e8 20%,
#f0f0f0 40%,
#f0f0f0 100%
);
background-size: 200% 100%;
animation: skeleton-shimmer 1.8s ease-in-out infinite;
border-radius: 4px;
will-change: background-position;
}
@keyframes skeleton-shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
/* ========================================================================
COMPONENT WRAPPER STATE TOGGLING
======================================================================== */
/* Default state: Show actual content, hide skeleton */
.component-wrapper {
position: relative;
}
.component-wrapper .actual-content {
opacity: 1;
transition: opacity 250ms ease-out;
}
.component-wrapper .skeleton-content {
position: absolute;
top: 0;
left: 0;
right: 0;
opacity: 0;
pointer-events: none;
transition: opacity 250ms ease-out;
}
/* Loading state: Hide actual content, show skeleton */
/* Triggered by manual .loading class OR when parent page container has .loading */
.component-wrapper.loading .actual-content,
.loading .component-wrapper .actual-content {
opacity: 0;
pointer-events: none;
}
.component-wrapper.loading .skeleton-content,
.loading .component-wrapper .skeleton-content {
opacity: 1;
pointer-events: all;
}
/* ========================================================================
SKELETON SHAPE DEFINITIONS
======================================================================== */
/* Header Section Skeleton */
/* Matches actual header layout with photo absolutely positioned on right */
.skeleton-header {
position: relative;
padding-right: 185px; /* Match .cv-header-left padding */
min-height: 200px; /* Ensure space for photo */
}
.skeleton-header-text {
position: relative;
z-index: 1;
}
.skeleton-name {
height: 40px; /* Larger to match h1 */
width: 75%;
margin-bottom: 12px;
}
.skeleton-experience-years {
height: 24px; /* Larger subtitle */
width: 55%;
margin-bottom: 24px;
}
.skeleton-photo {
width: 150px; /* Match actual photo */
height: 200px; /* Match actual photo */
border-radius: 0; /* No border-radius on actual photo */
border: 3px solid #e8e8e8; /* Match photo border style */
/* Absolute positioning to match actual layout */
position: absolute;
top: 15px;
right: 15px;
flex-shrink: 0;
}
.skeleton-intro {
height: 90px; /* Taller for 3-4 lines of text */
width: 100%;
margin-top: 12px;
}
/* Section Title Skeleton */
.skeleton-section-title {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
}
.skeleton-icon {
width: 24px;
height: 24px;
border-radius: 4px;
flex-shrink: 0;
}
.skeleton-title-text {
height: 24px;
width: 40%;
}
/* Skill Item Skeleton (Sidebar) */
.skeleton-skill-category {
margin-bottom: 20px;
}
.skeleton-skill-title {
height: 20px;
width: 60%;
margin-bottom: 12px;
}
.skeleton-skill-items {
display: flex;
flex-direction: column;
gap: 8px;
}
.skeleton-skill-item {
height: 32px;
width: 100%;
}
.skeleton-skill-item:nth-child(2) {
width: 85%;
}
.skeleton-skill-item:nth-child(3) {
width: 90%;
}
.skeleton-skill-item:nth-child(4) {
width: 75%;
}
/* Experience Entry Skeleton */
.skeleton-experience-item {
display: flex;
gap: 16px;
margin-bottom: 24px;
}
.skeleton-company-logo {
width: 60px;
height: 60px;
border-radius: 8px;
flex-shrink: 0;
}
.skeleton-experience-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
/* NEW: Structural skeleton lines for experience */
.skeleton-position-line {
height: 20px;
width: 80%;
}
.skeleton-date-line {
height: 14px;
width: 50%;
}
.skeleton-description-line {
height: 16px;
width: 100%;
margin-top: 4px;
}
.skeleton-responsibility-line {
height: 14px;
width: 100%;
margin-left: 16px; /* Indent like list items */
}
/* Legacy styles (keeping for backward compatibility) */
.skeleton-position {
height: 20px;
width: 80%;
}
.skeleton-company-info {
height: 16px;
width: 60%;
}
.skeleton-description {
height: 40px;
width: 100%;
margin-top: 4px;
}
.skeleton-description.short {
width: 85%;
}
/* Section Skeleton Base */
.skeleton-section {
padding: 16px 0;
}
.skeleton-section-title {
height: 28px;
width: 35%;
margin-bottom: 20px;
}
/* Education Item Skeleton */
.skeleton-education-item {
height: 48px;
width: 100%;
margin-bottom: 12px;
}
.skeleton-education-item:last-child {
margin-bottom: 0;
}
/* Skills Summary Skeleton */
.skeleton-summary-paragraph {
height: 18px;
width: 100%;
margin-bottom: 10px;
}
.skeleton-summary-paragraph:last-child {
margin-bottom: 0;
}
/* Award Item Skeleton */
.skeleton-award-item {
display: flex;
gap: 16px;
margin-bottom: 24px;
}
.skeleton-award-logo {
width: 60px;
height: 60px;
border-radius: 8px;
flex-shrink: 0;
}
.skeleton-award-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
/* NEW: Structural skeleton lines for awards */
.skeleton-award-title-line {
height: 20px;
width: 70%;
}
.skeleton-award-info-line {
height: 14px;
width: 50%;
}
/* Legacy styles (keeping for backward compatibility) */
.skeleton-award-title {
height: 20px;
width: 70%;
}
.skeleton-award-info {
height: 16px;
width: 50%;
}
.skeleton-award-description {
height: 40px;
width: 100%;
margin-top: 4px;
}
/* Project Item Skeleton */
.skeleton-project-item {
display: flex;
gap: 16px;
margin-bottom: 24px;
}
.skeleton-project-icon {
width: 80px;
height: 80px;
border-radius: 8px;
flex-shrink: 0;
}
.skeleton-project-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
/* NEW: Structural skeleton lines for projects */
.skeleton-project-title-line {
height: 20px;
width: 75%;
}
.skeleton-tech-line {
height: 14px;
width: 85%;
margin-top: 4px;
}
.skeleton-footer-line {
height: 16px;
width: 70%;
margin-top: 16px;
}
/* Legacy styles (keeping for backward compatibility) */
.skeleton-project-title {
height: 20px;
width: 75%;
}
.skeleton-project-info {
height: 16px;
width: 55%;
}
.skeleton-project-description {
height: 40px;
width: 100%;
margin-top: 4px;
}
.skeleton-project-description.short {
width: 80%;
}
/* Course Item Skeleton */
.skeleton-course-item {
display: flex;
gap: 16px;
margin-bottom: 20px;
}
.skeleton-course-icon {
width: 80px;
height: 80px;
border-radius: 8px;
flex-shrink: 0;
}
.skeleton-course-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
/* NEW: Structural skeleton lines for courses */
.skeleton-course-title-line {
height: 18px;
width: 70%;
}
.skeleton-course-info-line {
height: 14px;
width: 60%;
}
/* Legacy styles (keeping for backward compatibility) */
.skeleton-course-title {
height: 18px;
width: 70%;
}
.skeleton-course-info {
height: 16px;
width: 60%;
}
/* Language Item Skeleton */
.skeleton-language-item {
height: 20px;
width: 100%;
margin-bottom: 12px;
}
.skeleton-language-item:last-child {
margin-bottom: 0;
}
/* Reference Item Skeleton */
.skeleton-reference-item {
height: 22px;
width: 100%;
margin-bottom: 10px;
}
.skeleton-reference-item:last-child {
margin-bottom: 0;
}
/* Other Section Skeleton */
.skeleton-other-item {
height: 20px;
width: 60%;
}
/* Sidebar Skeleton */
.skeleton-sidebar {
padding: 16px 0;
}
.skeleton-sidebar-header {
height: 28px;
width: 80%;
margin-bottom: 20px;
}
/* Skill Item Skeleton (Sidebar) - Already defined above but keeping for reference */
/* Footer Skeleton */
.skeleton-footer {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px 0;
}
.skeleton-footer-item {
height: 20px;
width: 100%;
}
.skeleton-footer-item:nth-child(2) {
width: 90%;
}
.skeleton-footer-item:nth-child(3) {
width: 85%;
}
.skeleton-footer-item:nth-child(4) {
width: 80%;
}
.skeleton-footer-item:nth-child(5) {
width: 75%;
}
/* Text Block Skeletons (Generic) */
.skeleton-text {
height: 16px;
margin-bottom: 8px;
}
.skeleton-text.short {
width: 60%;
}
.skeleton-text.medium {
width: 80%;
}
.skeleton-text.long {
width: 95%;
}
/* ========================================================================
RESPONSIVE ADJUSTMENTS
======================================================================== */
@media (max-width: 768px) {
.skeleton-header {
flex-direction: column;
align-items: center;
}
.skeleton-header-text {
text-align: center;
width: 100%;
}
.skeleton-name,
.skeleton-experience-years {
width: 80%;
margin-left: auto;
margin-right: auto;
}
.skeleton-photo {
width: 100px;
height: 100px;
border-radius: 8px;
}
.skeleton-experience-item {
flex-direction: column;
gap: 12px;
}
.skeleton-company-logo {
width: 50px;
height: 50px;
}
}
/* ========================================================================
ACCESSIBILITY - REDUCED MOTION
======================================================================== */
@media (prefers-reduced-motion: reduce) {
.skeleton {
animation: none;
background: #e8e8e8;
}
.component-wrapper .actual-content,
.component-wrapper .skeleton-content {
transition: none;
}
}
/* ========================================================================
PRINT STYLES
======================================================================== */
@media print {
.skeleton-content {
display: none !important;
}
.component-wrapper .actual-content {
opacity: 1 !important;
}
}
/* ========================================================================
PERFORMANCE OPTIMIZATIONS
======================================================================== */
/* Force GPU acceleration for skeleton elements */
.skeleton {
transform: translateZ(0);
backface-visibility: hidden;
}
/* Contain layout/paint/style to prevent reflow */
.component-wrapper {
contain: layout style;
}
/* Optimize skeleton rendering */
.skeleton-content {
contain: layout paint;
}
+662
View File
@@ -0,0 +1,662 @@
/* Print Styles - A4 Optimized - Consolidated & Fixed */
@media print {
/* ===================================
CRITICAL: Print Color Accuracy
=================================== */
* {
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
color-adjust: exact !important;
}
/* ===================================
PAGE SETUP - A4 with Minimal Margins
=================================== */
@page {
size: A4 portrait;
margin: 8mm; /* Minimal printer margins */
}
body {
background: white !important;
margin: 0 !important;
padding: 0 !important;
}
/* ===================================
HIDE NON-PRINT ELEMENTS
=================================== */
.no-print,
.action-bar,
.navigation-menu,
.hamburger-btn,
footer,
.back-to-top,
.info-button,
.info-modal,
.error-toast,
.cv-sidebar,
.cv-sidebar-left,
.cv-sidebar-right,
.cv-title-badges-header,
.cv-footer {
display: none !important;
}
/* ===================================
SHOW ALL ICONS, ICONS, AND BADGES - Print Default
=================================== */
/* Section title icons - smaller for print */
.section-icon {
width: 16px !important;
height: 16px !important;
vertical-align: middle !important;
margin-right: 4px !important;
}
/* Company/Project/Course icons - compact for print */
.company-logo,
.project-icon,
.course-icon,
.award-logo {
width: 40px !important;
height: 40px !important;
flex-shrink: 0 !important;
}
.company-logo img,
.project-icon img,
.course-icon img,
.award-logo img {
width: 40px !important;
height: 40px !important;
object-fit: contain !important;
}
/* Default fallback icons - show at print size */
.default-company-icon,
.default-project-icon,
.default-course-icon,
.default-award-icon {
width: 40px !important;
height: 40px !important;
}
/* Badges - keep visible for print */
.current-badge,
.live-badge,
.expired-badge,
.maintained-badge {
font-size: 7pt !important;
padding: 1px 4px !important;
}
.live-badge iconify-icon {
width: 10px !important;
height: 10px !important;
}
/* ===================================
REMOVE ALL SHADOWS & BORDERS (Nuclear Option)
=================================== */
*,
*::before,
*::after {
box-shadow: none !important;
text-shadow: none !important;
}
/* ===================================
CV CONTAINER - Full Page Width
=================================== */
.cv-container {
width: 100%;
max-width: 100%;
margin: 0;
padding: 0;
gap: 0;
}
.cv-container.theme-clean {
padding: 0;
}
/* ===================================
CV PAPER - Reduced Padding (20mm → 12mm)
=================================== */
.cv-paper {
width: 100%;
min-height: auto !important;
background: white !important;
padding: 12mm !important; /* Reduced from 20mm */
box-shadow: none !important;
border: none !important;
margin: 0 !important;
page-break-after: auto;
transform: none !important;
}
/* ===================================
CV PAGE - Remove All Decorations & Page Breaks
=================================== */
.cv-page,
.theme-clean .cv-page {
box-shadow: none !important;
border: none !important;
background: white !important;
margin: 0 !important;
padding: 0 !important;
transform: scale(1) !important;
max-width: 100% !important;
page-break-after: auto !important; /* Let content flow naturally */
page-break-inside: auto !important; /* Allow breaking inside */
}
.cv-page.page-2 {
page-break-after: auto !important;
}
/* ===================================
PAGE CONTENT GRID - Allow Natural Flow
=================================== */
.page-content {
page-break-inside: auto !important; /* Allow content to break naturally */
display: block !important; /* Remove grid for print */
}
/* ===================================
PAGE BREAKS - Optimized for Content Flow
=================================== */
.page-break {
page-break-after: auto !important; /* Remove forced page breaks */
break-after: auto !important;
}
/* Sections CAN break across pages - flow naturally */
.cv-section {
page-break-inside: auto !important;
break-inside: auto !important;
page-break-before: auto !important; /* No forced breaks before sections */
page-break-after: auto !important; /* No forced breaks after sections */
}
/* Keep individual items together */
.avoid-break,
.experience-item,
.project-item,
.course-item,
.award-item {
page-break-inside: avoid !important;
break-inside: avoid !important;
}
/* Experience section should flow into Awards - no page break */
#experience {
page-break-after: auto !important;
}
/* ===================================
HEADER - Reduced Spacing with Desktop Layout
=================================== */
.cv-header {
page-break-after: avoid;
margin-bottom: 8mm !important; /* Reduced from 15mm */
}
/* Override mobile layout - use positioned layout for print */
.cv-header-content {
display: block !important; /* Use block instead of flex */
position: relative !important;
}
.cv-header-left {
display: block !important;
position: static !important;
padding-right: 130px !important; /* Make room for bigger photo on the right */
text-align: right !important; /* Right-align name and years */
}
.cv-name {
font-size: 20pt;
margin-bottom: 4pt;
text-align: right !important; /* Override mobile center alignment */
}
.cv-title {
font-size: 12pt;
}
.years-experience,
.cv-experience-years {
font-size: 10pt;
text-align: right !important; /* Override mobile center alignment */
}
/* ===================================
PHOTO - FIXED ASPECT RATIO (3:4 Portrait) - RIGHT SIDE
=================================== */
.cv-photo {
width: 110px !important; /* Increased from 90px */
height: 147px !important; /* Maintains 3:4 ratio (110 * 1.33) */
object-fit: contain !important; /* Show full photo, no crop */
border: none !important; /* Remove border */
box-shadow: none !important;
page-break-inside: avoid;
/* Position photo to the right */
position: absolute !important;
top: 0 !important;
right: 0 !important;
margin: 10px 0 0 0!important;
max-width: none !important;
}
.cv-photo img {
width: 100%;
height: 100%;
object-fit: contain !important;
}
/* Intro text should be justified and below name/years */
.intro-text {
font-size: 9pt !important;
line-height: 1.5 !important;
text-align: justify !important; /* Justified text for print */
margin-top: 3mm !important;
width: 100% !important;
padding-right: 0 !important; /* Intro text extends full width below photo */
text-align-last: left !important; /* Last line left-aligned */
}
/* ===================================
SECTIONS - REDUCED SPACING (48px → 19px)
=================================== */
.cv-section {
margin-bottom: 5mm !important; /* ~19px, down from 48px */
margin-top: 7mm !important; /* More breathing space between sections */
}
.section-title {
font-size: 12pt !important; /* Equalized size for all titles */
font-weight: 600 !important;
margin-top: 0 !important;
margin-bottom: 1mm !important; /* Minimal bottom margin - matches Training/Skills */
page-break-after: avoid;
border-bottom: 0.5pt solid #dddddd !important;
padding-bottom: 2mm !important;
padding-top: 2mm !important; /* Breathing space above */
line-height: 1.3 !important; /* Consistent line height */
}
/* Languages and References need more breathing space below title */
#languages .section-title,
#references .section-title {
margin-bottom: 3mm !important; /* More space for lists */
}
.summary-text {
font-size: 9pt;
line-height: 1.5;
}
/* ===================================
EXPERIENCE - REDUCED SPACING (60px → 26px)
=================================== */
.experience-item {
display: flex !important; /* Show icons side-by-side with content */
gap: 12px !important;
margin-bottom: 4mm !important; /* ~15px, down from 40px */
padding-bottom: 3mm !important; /* ~11px, down from 32px */
border-bottom: 0.5pt solid #dddddd !important;
}
.experience-item:last-child {
border-bottom: none !important;
margin-bottom: 0 !important; /* Remove bottom margin from last experience */
padding-bottom: 2mm !important; /* Minimal padding */
}
.position {
font-size: 10pt;
margin-bottom: 2pt;
}
.company,
.company-link {
font-size: 9pt;
}
.experience-period,
.experience-location,
.experience-duration {
font-size: 8pt;
}
.short-desc {
font-size: 9pt !important;
line-height: 1.4 !important;
margin-top: 1mm !important;
margin-bottom: 1mm !important;
}
.responsibilities {
margin-top: 2mm;
}
.responsibilities li {
font-size: 9pt !important;
line-height: 1.4 !important;
margin-bottom: 1mm;
}
/* Ensure all experience content is properly sized */
.experience-item p,
.experience-item div {
font-size: 9pt !important;
line-height: 1.4 !important;
}
/* Logos are visible at 40x40 for compact print layout */
/* ===================================
PROJECTS & COURSES
=================================== */
.project-item,
.course-item,
.award-item {
display: flex !important; /* Show icons side-by-side with content */
gap: 12px !important;
margin-bottom: 4mm !important;
padding-bottom: 3mm !important;
border-bottom: 0.5pt solid #dddddd !important;
}
.project-item:last-child,
.course-item:last-child,
.award-item:last-child {
border-bottom: none !important; /* Remove border from last item */
}
/* Projects footer - "See all projects" link */
.projects-footer {
margin-top: 4mm !important;
padding-top: 3mm !important;
text-align: center !important;
font-size: 9pt !important;
border-top: 0.5pt solid #dddddd !important; /* Single separator */
}
.projects-footer p {
margin: 0 !important;
padding: 2mm 0 !important;
}
.projects-footer a {
color: #0066cc !important;
text-decoration: none !important;
font-weight: 600 !important;
}
/* Logos visible at 40x40 - default icons remain hidden */
/* Consistent item titles */
.project-title,
.course-title,
.experience-title,
.award-title {
font-size: 10pt !important;
font-weight: 600 !important;
line-height: 1.3 !important;
display: block !important;
margin-bottom: 1mm !important;
}
.project-desc,
.course-desc,
.award-desc {
font-size: 9pt !important;
line-height: 1.4 !important;
margin-top: 1mm !important;
}
/* Course/Project headers - match Experience spacing */
.course-header,
.project-header {
margin-bottom: 0.5mm !important; /* Minimal spacing */
}
.course-item small,
.project-item small {
font-size: 8pt !important;
color: #666 !important;
display: inline !important; /* Inline like experience dates */
margin-top: 0 !important;
margin-left: 0.5mm !important;
}
/* Course metadata (date, location) - match experience style */
.course-period,
.course-location,
.course-separator {
font-size: 8pt !important;
color: #666 !important;
display: inline !important;
}
/* Ensure all text and paragraphs in course items are properly sized */
.course-item p,
.course-item div,
.project-item p,
.project-item div {
font-size: 9pt !important;
line-height: 1.4 !important;
margin-top: 1mm !important;
margin-bottom: 1mm !important;
}
.project-technologies,
.technologies {
font-size: 8pt;
}
/* ===================================
EDUCATION & SKILLS
=================================== */
.degree,
.skill-title,
.project-name {
font-size: 9.5pt;
}
.institution,
.skill-list,
.project-description {
font-size: 8.5pt;
}
.education-item {
margin-bottom: 3mm;
font-size: 9pt !important; /* Match body text size (equivalent to 0.95rem) */
line-height: 1.5 !important;
}
.education-item strong {
font-size: 9pt !important; /* Ensure degree title matches body text */
font-weight: 600 !important;
}
/* ===================================
CERTIFICATIONS & AWARDS
=================================== */
.cert-item,
.award-item {
margin-bottom: 3mm !important;
padding-bottom: 2mm !important;
}
.award-item strong,
.cert-item strong {
font-size: 10pt !important;
font-weight: 600 !important;
display: block !important;
margin-bottom: 1mm !important;
}
.award-item small,
.cert-item small {
font-size: 8pt !important;
color: #666 !important;
}
/* Ensure all award/cert content is properly sized */
.award-item p,
.award-item div,
.cert-item p,
.cert-item div {
font-size: 9pt !important;
line-height: 1.4 !important;
margin-top: 1mm !important;
margin-bottom: 1mm !important;
}
/* ===================================
LANGUAGES
=================================== */
.languages-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 2mm;
}
.language-item {
font-size: 9pt !important;
line-height: 1.3 !important;
margin-bottom: 1mm !important;
}
.language-item small {
font-size: 8pt;
}
/* ===================================
REFERENCES & OTHER
=================================== */
.reference-item,
.other-content {
font-size: 9pt !important;
line-height: 1.3 !important;
margin-bottom: 1mm !important;
}
/* ===================================
CONTACT INFO
=================================== */
.cv-contact {
font-size: 8.5pt;
grid-template-columns: repeat(2, 1fr);
gap: 2mm;
}
/* ===================================
CLEAN THEME - Full Width Main Content
=================================== */
.theme-clean .page-content,
.page-content {
display: block !important;
grid-template-columns: 1fr !important;
}
.theme-clean .cv-main,
.cv-main {
grid-column: 1 !important;
padding: 0 !important;
max-width: 100% !important;
}
/* ===================================
LINKS - Print Styling
=================================== */
a {
color: #0066cc;
text-decoration: none;
font-weight: 600;
}
/* ===================================
CV LENGTH TOGGLE - Force Short Version for Print Friendly
=================================== */
/* Show short descriptions */
.cv-short .short-desc {
display: block !important;
font-size: 9pt;
line-height: 1.5;
margin-top: 2mm;
}
/* Hide long-only content (detailed descriptions) */
.cv-short .long-only,
.long-only {
display: none !important;
}
/* Hide responsibilities (detailed bullet points) */
.cv-short .responsibilities,
.responsibilities {
display: none !important;
}
/* Long version rules (should not apply, but just in case) */
.cv-long .short-desc {
display: none !important;
}
.cv-long .long-only {
display: block !important;
}
.cv-long .responsibilities {
display: block !important;
}
/* ===================================
THEME CLEAN - Minimal Print Mode
=================================== */
/* All sidebars, headers, footers already hidden above */
/* Main content takes full width */
/* ===================================
COLLAPSIBLE SECTIONS - Force Open
=================================== */
details {
display: block !important;
}
summary {
display: block !important;
list-style: none !important;
}
summary::after,
summary .section-title::after,
.sidebar-section summary::after {
display: none !important; /* Hide collapse indicators */
}
details > *:not(summary) {
display: block !important;
opacity: 1 !important;
max-height: none !important;
transform: none !important;
}
/* ===================================
ENSURE PROPER TEXT RENDERING
=================================== */
body,
.cv-paper {
font-smoothing: antialiased;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}
+4 -4
View File
@@ -182,11 +182,11 @@
/* Dynamic colors ONLY on hover based on active theme mode */ /* Dynamic colors ONLY on hover based on active theme mode */
.color-theme-switcher:hover[data-theme-mode="light"] { .color-theme-switcher:hover[data-theme-mode="light"] {
background: #ffd700 !important; /* Bright sun yellow (gold) for light mode */ background: #d4b200 !important; /* Bright sun yellow (gold) for light mode */
} }
.color-theme-switcher:hover[data-theme-mode="dark"] { .color-theme-switcher:hover[data-theme-mode="dark"] {
background: #2c3e50 !important; /* Dark nighty blue for dark mode */ background: #013c77 !important; /* Dark nighty blue for dark mode */
} }
.color-theme-switcher:hover[data-theme-mode="auto"] { .color-theme-switcher:hover[data-theme-mode="auto"] {
@@ -202,12 +202,12 @@
/* At-bottom state - dynamic colors based on theme mode (matches hover) */ /* At-bottom state - dynamic colors based on theme mode (matches hover) */
.color-theme-switcher.at-bottom[data-theme-mode="light"] { .color-theme-switcher.at-bottom[data-theme-mode="light"] {
opacity: 1; opacity: 1;
background: #ffd700 !important; /* Bright sun yellow (gold) for light mode */ background: #d4b200 !important; /* Bright sun yellow (gold) for light mode */
} }
.color-theme-switcher.at-bottom[data-theme-mode="dark"] { .color-theme-switcher.at-bottom[data-theme-mode="dark"] {
opacity: 1; opacity: 1;
background: #2c3e50 !important; /* Dark nighty blue for dark mode */ background: #013c77 !important; /* Dark nighty blue for dark mode */
} }
.color-theme-switcher.at-bottom[data-theme-mode="auto"] { .color-theme-switcher.at-bottom[data-theme-mode="auto"] {
+35 -4710
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+36
View File
@@ -0,0 +1,36 @@
/* ============================================================================
MAIN.CSS - Entry Point (New Modular Structure)
============================================================================ */
/* 01 - Foundation */
@import './01-foundation/_reset.css';
@import './01-foundation/_variables.css';
@import './01-foundation/_typography.css';
@import './01-foundation/_themes.css';
/* 02 - Layout */
@import './02-layout/_container.css';
@import './02-layout/_page.css';
@import './02-layout/_grid.css';
@import './02-layout/_paper.css';
/* 03 - Components */
@import './03-components/_action-bar.css';
@import './03-components/_sidebar.css';
@import './03-components/_cv-header.css';
@import './03-components/_cv-section.css';
@import './03-components/_experience.css';
@import './03-components/_projects.css';
@import './03-components/_courses.css';
@import './03-components/_education.css';
@import './03-components/_languages.css';
/* 04 - Interactive (includes hamburger, buttons, modals, zoom - TO BE SPLIT LATER) */
@import './04-interactive/_toggles.css';
@import './04-interactive/_remaining.css';
/* 06 - Effects */
@import './06-effects/_skeleton.css';
/* 08 - Contexts */
@import './08-contexts/_print.css';
+3 -3
View File
@@ -55,17 +55,17 @@ end
-- ============================================================================== -- ==============================================================================
def toggleTheme(isClean) def toggleTheme(isClean)
set body to document.body set container to the first .cv-container
set actionBarToggle to #themeToggle set actionBarToggle to #themeToggle
set menuToggle to #themeToggleMenu set menuToggle to #themeToggleMenu
if isClean is true if isClean is true
add .theme-clean to body add .theme-clean to container
call localStorage.setItem('cv-theme', 'clean') call localStorage.setItem('cv-theme', 'clean')
if actionBarToggle exists then set actionBarToggle's checked to true end if actionBarToggle exists then set actionBarToggle's checked to true end
if menuToggle exists then set menuToggle's checked to true end if menuToggle exists then set menuToggle's checked to true end
else else
remove .theme-clean from body remove .theme-clean from container
call localStorage.setItem('cv-theme', 'default') call localStorage.setItem('cv-theme', 'default')
if actionBarToggle exists then set actionBarToggle's checked to false end if actionBarToggle exists then set actionBarToggle's checked to false end
if menuToggle exists then set menuToggle's checked to false end if menuToggle exists then set menuToggle's checked to false end
+1 -5
View File
@@ -71,12 +71,8 @@
<!-- Using unpkg CDN (more reliable than code.iconify.design) --> <!-- Using unpkg CDN (more reliable than code.iconify.design) -->
<script src="https://cdn.jsdelivr.net/npm/iconify-icon@2.1.0/dist/iconify-icon.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/iconify-icon@2.1.0/dist/iconify-icon.min.js"></script>
<!-- CSS --> <!-- CSS - Modular structure with native @import -->
<link rel="stylesheet" href="/static/css/main.css"> <link rel="stylesheet" href="/static/css/main.css">
<link rel="stylesheet" href="/static/css/color-theme.css">
<link rel="stylesheet" href="/static/css/logo-toggle.css">
<link rel="stylesheet" href="/static/css/skeleton.css">
<link rel="stylesheet" href="/static/css/print.css" media="print">
<!-- Fonts with Preload --> <!-- Fonts with Preload -->
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
+44 -15
View File
@@ -15,6 +15,7 @@
<!-- Header --> <!-- Header -->
<div class="info-modal-header"> <div class="info-modal-header">
<iconify-icon icon="catppuccin:pdf" width="40" height="40" style="margin-bottom: 0.5rem;"></iconify-icon>
<h2>{{if eq .Lang "es"}}Descargar PDF{{else}}Download PDF{{end}}</h2> <h2>{{if eq .Lang "es"}}Descargar PDF{{else}}Download PDF{{end}}</h2>
<p class="pdf-modal-subtitle"> <p class="pdf-modal-subtitle">
{{if eq .Lang "es"}}Elige tu formato preferido{{else}}Choose your preferred format{{end}} {{if eq .Lang "es"}}Elige tu formato preferido{{else}}Choose your preferred format{{end}}
@@ -156,12 +157,12 @@
</div> </div>
</div> </div>
<!-- Custom CV Card (Placeholder) --> <!-- Current View Card -->
<div class="pdf-option-card" <div class="pdf-option-card"
data-cv-format="custom" data-cv-format="current"
role="radio" role="radio"
aria-checked="false" aria-checked="false"
aria-label="{{if eq .Lang "es"}}Personalizado - Personaliza secciones{{else}}Custom - Customize sections{{end}}" aria-label="{{if eq .Lang "es"}}Vista Actual - Como se ve en pantalla{{else}}Current View - As shown on screen{{end}}"
tabindex="0" tabindex="0"
_="on click _="on click
-- Remove selected from all cards -- Remove selected from all cards
@@ -184,8 +185,8 @@
-- Announce to screen readers -- Announce to screen readers
set announcement to #pdf-selection-announcement set announcement to #pdf-selection-announcement
if :selectedFormat is 'custom' if :selectedFormat is 'current'
set announcement.textContent to '{{if eq .Lang "es"}}Seleccionado: Personalizado{{else}}Selected: Custom format{{end}}' set announcement.textContent to '{{if eq .Lang "es"}}Seleccionado: Vista Actual{{else}}Selected: Current View{{end}}'
end end
end end
@@ -197,21 +198,21 @@
end"> end">
<div class="pdf-thumbnail thumbnail-custom"> <div class="pdf-thumbnail thumbnail-custom">
<!-- Centered icon instead of skeleton blocks --> <!-- Centered icon representing current screen view -->
<div class="custom-placeholder"> <div class="custom-placeholder">
<iconify-icon icon="mdi:help-circle-outline" width="80" height="80"></iconify-icon> <iconify-icon icon="mdi:monitor" width="80" height="80"></iconify-icon>
<p>{{if eq .Lang "es"}}Personalizar{{else}}Customize{{end}}</p> <p>{{if eq .Lang "es"}}En Pantalla{{else}}On Screen{{end}}</p>
</div> </div>
<!-- Coming soon badge --> <!-- Current view badge -->
<div class="thumbnail-badge"> <div class="thumbnail-badge">
{{if eq .Lang "es"}}Próximamente{{else}}Coming Soon{{end}} {{if eq .Lang "es"}}Vista Actual{{else}}Current{{end}}
</div> </div>
</div> </div>
<div class="pdf-option-info"> <div class="pdf-option-info">
<h3>{{if eq .Lang "es"}}Personalizado{{else}}Custom{{end}}</h3> <h3>{{if eq .Lang "es"}}Vista Actual{{else}}Current View{{end}}</h3>
<p>{{if eq .Lang "es"}}Personaliza secciones{{else}}Customize sections{{end}}</p> <p>{{if eq .Lang "es"}}Como se ve en pantalla{{else}}As shown on screen{{end}}</p>
</div> </div>
<div class="pdf-option-badge"> <div class="pdf-option-badge">
@@ -227,9 +228,37 @@
_="on click _="on click
if :selectedFormat is not null if :selectedFormat is not null
log 'Download requested for format:', :selectedFormat log 'Download requested for format:', :selectedFormat
-- TODO: Trigger actual PDF download when backend ready
-- Example: window.location.href = '/download-pdf?format=' + :selectedFormat -- Get current page language
call alert('{{if eq .Lang "es"}}¡Descarga de PDF próximamente! Formato seleccionado: {{else}}PDF download coming soon! Selected format: {{end}}' + :selectedFormat) set lang to '{{.Lang}}'
-- Build URL based on selected format
if :selectedFormat is 'short'
set url to '/export/pdf?lang=' + lang + '&length=short&icons=show&version=extended'
else if :selectedFormat is 'long'
set url to '/export/pdf?lang=' + lang + '&length=long&icons=show&version=extended'
else if :selectedFormat is 'current'
-- Get current settings from localStorage
set currentLength to localStorage.getItem('cv-length') or 'short'
set currentIcons to localStorage.getItem('cv-icons') or 'show'
set currentTheme to localStorage.getItem('cv-theme') or 'default'
-- Map theme to version parameter
if currentTheme is 'clean'
set version to 'clean'
else
set version to 'extended'
end
set url to '/export/pdf?lang=' + lang + '&length=' + currentLength + '&icons=' + currentIcons + '&version=' + version
end
-- Trigger download
set window.location.href to url
-- Close modal after a short delay
wait 500ms
call #pdf-modal.close()
end end
end"> end">
<iconify-icon icon="mdi:download" width="20" height="20"></iconify-icon> <iconify-icon icon="mdi:download" width="20" height="20"></iconify-icon>
+1 -1
View File
@@ -15,7 +15,7 @@
<div class="award-item"> <div class="award-item">
{{if .AwardLogo}} {{if .AwardLogo}}
<div class="award-logo"> <div class="award-logo">
<img src="/static/images/companies/{{.AwardLogo}}" alt="{{.Title}} logo" onerror="this.parentElement.innerHTML='<iconify-icon icon=\'mdi:trophy\' width=\'60\' height=\'60\' class=\'default-award-icon\'></iconify-icon>'"> <img src="/static/images/companies/{{.AwardLogo}}" alt="{{.Title}} logo" onerror="this.parentElement.innerHTML='<iconify-icon icon=\'mdi:trophy\' width=\'80\' height=\'80\' class=\'default-award-icon\'></iconify-icon>'">
</div> </div>
{{end}} {{end}}
<div class="award-content"> <div class="award-content">
+2 -2
View File
@@ -15,9 +15,9 @@
<div class="experience-item"> <div class="experience-item">
<div class="company-logo"> <div class="company-logo">
{{if .CompanyLogo}} {{if .CompanyLogo}}
<img src="/static/images/companies/{{.CompanyLogo}}" alt="{{.Company}} logo" onerror="this.parentElement.innerHTML='<iconify-icon icon=\'mdi:office-building\' width=\'60\' height=\'60\' class=\'default-company-icon\'></iconify-icon>'"> <img src="/static/images/companies/{{.CompanyLogo}}" alt="{{.Company}} logo" onerror="this.parentElement.innerHTML='<iconify-icon icon=\'mdi:office-building\' width=\'80\' height=\'80\' class=\'default-company-icon\'></iconify-icon>'">
{{else}} {{else}}
<iconify-icon icon="mdi:office-building" width="60" height="60" class="default-company-icon"></iconify-icon> <iconify-icon icon="mdi:office-building" width="80" height="80" class="default-company-icon"></iconify-icon>
{{end}} {{end}}
</div> </div>
<div class="experience-content"> <div class="experience-content">
+214
View File
@@ -0,0 +1,214 @@
#!/usr/bin/env bun
/**
* ICON TOGGLE DEBUG TEST
* =======================
* Specifically tests icon toggle functionality
* to verify icons hide when toggle is OFF
*/
import { chromium } from "playwright";
const URL = "http://localhost:1999";
async function testIconToggle() {
console.log("🧪 ICON TOGGLE DEBUG TEST\n");
console.log("=".repeat(70));
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage({ viewport: { width: 1400, height: 1080 } });
await page.goto(URL);
await page.waitForTimeout(2000);
console.log("\n1️⃣ Initial State:");
const initial = await page.evaluate(() => {
const paper = document.querySelector('.cv-paper');
const logo = document.querySelector('.company-logo');
const logoStyle = logo ? window.getComputedStyle(logo) : null;
return {
paperClasses: paper?.className,
hasShowIcons: paper?.classList.contains('show-icons'),
logoExists: !!logo,
logoDisplay: logoStyle?.display,
logoOpacity: logoStyle?.opacity,
logoWidth: logoStyle?.width,
localStorage: localStorage.getItem('cv-icons')
};
});
console.log(` Paper classes: ${initial.paperClasses}`);
console.log(` Has show-icons class: ${initial.hasShowIcons}`);
console.log(` Logo exists: ${initial.logoExists}`);
console.log(` Logo display: ${initial.logoDisplay}`);
console.log(` Logo opacity: ${initial.logoOpacity}`);
console.log(` Logo width: ${initial.logoWidth}`);
console.log(` LocalStorage: ${initial.localStorage}`);
console.log("\n2️⃣ Clicking icon toggle to turn icons OFF...");
// Click the LABEL (not the input) to trigger the toggle
await page.click('label:has(#iconToggle)');
await page.waitForTimeout(500);
const afterOff = await page.evaluate(() => {
const paper = document.querySelector('.cv-paper');
const toggle = document.querySelector('#iconToggle');
// Check ALL icon types
const companyLogo = document.querySelector('.company-logo');
const awardLogo = document.querySelector('.award-logo');
const sectionIcon = document.querySelector('.section-icon');
const projectIcon = document.querySelector('.project-icon');
const defaultProjectIcon = document.querySelector('.default-project-icon');
const courseIcon = document.querySelector('.course-icon');
const defaultCourseIcon = document.querySelector('.default-course-icon');
const logoStyle = companyLogo ? window.getComputedStyle(companyLogo) : null;
const awardStyle = awardLogo ? window.getComputedStyle(awardLogo) : null;
const sectionStyle = sectionIcon ? window.getComputedStyle(sectionIcon) : null;
const projectStyle = projectIcon ? window.getComputedStyle(projectIcon) : null;
const defaultProjectStyle = defaultProjectIcon ? window.getComputedStyle(defaultProjectIcon) : null;
const courseStyle = courseIcon ? window.getComputedStyle(courseIcon) : null;
const defaultCourseStyle = defaultCourseIcon ? window.getComputedStyle(defaultCourseIcon) : null;
return {
paperClasses: paper?.className,
hasShowIcons: paper?.classList.contains('show-icons'),
toggleChecked: toggle?.checked,
// Company logo
logoOpacity: logoStyle?.opacity,
logoWidth: logoStyle?.width,
// Award logo
awardOpacity: awardStyle?.opacity,
awardWidth: awardStyle?.width,
// Section icons
sectionOpacity: sectionStyle?.opacity,
sectionWidth: sectionStyle?.width,
// Project icons
projectOpacity: projectStyle?.opacity,
projectWidth: projectStyle?.width,
// Default project icons
defaultProjectOpacity: defaultProjectStyle?.opacity,
defaultProjectWidth: defaultProjectStyle?.width,
// Course icons
courseOpacity: courseStyle?.opacity,
courseWidth: courseStyle?.width,
// Default course icons
defaultCourseOpacity: defaultCourseStyle?.opacity,
defaultCourseWidth: defaultCourseStyle?.width,
localStorage: localStorage.getItem('cv-icons')
};
});
console.log(` Paper classes: ${afterOff.paperClasses}`);
console.log(` Has show-icons class: ${afterOff.hasShowIcons}`);
console.log(` Toggle checked: ${afterOff.toggleChecked}`);
console.log(`\n COMPANY LOGO: opacity=${afterOff.logoOpacity}, width=${afterOff.logoWidth}`);
console.log(` AWARD LOGO: opacity=${afterOff.awardOpacity}, width=${afterOff.awardWidth}`);
console.log(` SECTION ICONS: opacity=${afterOff.sectionOpacity}, width=${afterOff.sectionWidth}`);
console.log(` PROJECT ICONS: opacity=${afterOff.projectOpacity}, width=${afterOff.projectWidth}`);
console.log(` DEFAULT PROJECT ICONS: opacity=${afterOff.defaultProjectOpacity}, width=${afterOff.defaultProjectWidth}`);
console.log(` COURSE ICONS: opacity=${afterOff.courseOpacity}, width=${afterOff.courseWidth}`);
console.log(` DEFAULT COURSE ICONS: opacity=${afterOff.defaultCourseOpacity}, width=${afterOff.defaultCourseWidth}`);
console.log(`\n LocalStorage: ${afterOff.localStorage}`);
// Helper to check if icon is hidden (opacity 0 or undefined if not present)
const isHidden = (opacity) => opacity === '0' || opacity === undefined;
const isVisible = (opacity) => opacity === '1' || opacity === undefined;
const allIconsHidden = !afterOff.hasShowIcons &&
isHidden(afterOff.logoOpacity) &&
isHidden(afterOff.awardOpacity) &&
isHidden(afterOff.sectionOpacity) &&
isHidden(afterOff.projectOpacity) &&
isHidden(afterOff.defaultProjectOpacity) &&
isHidden(afterOff.courseOpacity) &&
isHidden(afterOff.defaultCourseOpacity);
console.log(`\n ${allIconsHidden ? '✅ ALL icons correctly hidden' : '❌ Some icons still visible!'}`);
console.log("\n3️⃣ Clicking toggle again to turn icons ON...");
await page.click('label:has(#iconToggle)');
await page.waitForTimeout(500);
const afterOn = await page.evaluate(() => {
const paper = document.querySelector('.cv-paper');
const companyLogo = document.querySelector('.company-logo');
const awardLogo = document.querySelector('.award-logo');
const sectionIcon = document.querySelector('.section-icon');
const projectIcon = document.querySelector('.project-icon');
const defaultProjectIcon = document.querySelector('.default-project-icon');
const courseIcon = document.querySelector('.course-icon');
const defaultCourseIcon = document.querySelector('.default-course-icon');
const logoStyle = companyLogo ? window.getComputedStyle(companyLogo) : null;
const awardStyle = awardLogo ? window.getComputedStyle(awardLogo) : null;
const sectionStyle = sectionIcon ? window.getComputedStyle(sectionIcon) : null;
const projectStyle = projectIcon ? window.getComputedStyle(projectIcon) : null;
const defaultProjectStyle = defaultProjectIcon ? window.getComputedStyle(defaultProjectIcon) : null;
const courseStyle = courseIcon ? window.getComputedStyle(courseIcon) : null;
const defaultCourseStyle = defaultCourseIcon ? window.getComputedStyle(defaultCourseIcon) : null;
return {
paperClasses: paper?.className,
hasShowIcons: paper?.classList.contains('show-icons'),
logoOpacity: logoStyle?.opacity,
awardOpacity: awardStyle?.opacity,
sectionOpacity: sectionStyle?.opacity,
projectOpacity: projectStyle?.opacity,
defaultProjectOpacity: defaultProjectStyle?.opacity,
courseOpacity: courseStyle?.opacity,
defaultCourseOpacity: defaultCourseStyle?.opacity
};
});
console.log(` Paper classes: ${afterOn.paperClasses}`);
console.log(` Has show-icons class: ${afterOn.hasShowIcons}`);
console.log(`\n COMPANY LOGO: ${afterOn.logoOpacity}`);
console.log(` AWARD LOGO: ${afterOn.awardOpacity}`);
console.log(` SECTION ICONS: ${afterOn.sectionOpacity}`);
console.log(` PROJECT ICONS: ${afterOn.projectOpacity}`);
console.log(` DEFAULT PROJECT ICONS: ${afterOn.defaultProjectOpacity}`);
console.log(` COURSE ICONS: ${afterOn.courseOpacity}`);
console.log(` DEFAULT COURSE ICONS: ${afterOn.defaultCourseOpacity}`);
const allIconsVisible = afterOn.hasShowIcons &&
isVisible(afterOn.logoOpacity) &&
isVisible(afterOn.awardOpacity) &&
isVisible(afterOn.sectionOpacity) &&
isVisible(afterOn.projectOpacity) &&
isVisible(afterOn.defaultProjectOpacity) &&
isVisible(afterOn.courseOpacity) &&
isVisible(afterOn.defaultCourseOpacity);
console.log(`\n ${allIconsVisible ? '✅ ALL icons correctly visible' : '❌ Some icons not showing!'}`);
console.log("\n" + "=".repeat(70));
if (allIconsHidden && allIconsVisible) {
console.log("\n✅ ICON TOGGLE WORKS CORRECTLY FOR ALL ICON TYPES!");
} else {
console.log("\n❌ ICON TOGGLE HAS ISSUES!");
console.log("Debug: Check if CSS rules cover all icon types");
}
await page.screenshot({ path: 'tests/screenshots/icon-toggle-debug.png' });
console.log("\n📸 Screenshot saved to tests/screenshots/icon-toggle-debug.png");
console.log("\nBrowser will stay open for 30 seconds for inspection...");
await page.waitForTimeout(30000);
await browser.close();
}
await testIconToggle();
+151
View File
@@ -0,0 +1,151 @@
#!/usr/bin/env bun
/**
* AWARDS VISUAL TEST
* Check awards section layout and icons
*/
import { chromium } from "playwright";
const URL = "http://localhost:1999";
async function testAwards() {
console.log("🧪 AWARDS VISUAL TEST\n");
console.log("=".repeat(70));
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage({ viewport: { width: 1400, height: 1080 } });
await page.goto(URL);
await page.waitForTimeout(2000);
// Scroll to awards section
await page.evaluate(() => {
const awards = document.querySelector('#awards');
if (awards) awards.scrollIntoView({ behavior: 'smooth', block: 'start' });
});
await page.waitForTimeout(1000);
console.log("\n1️⃣ Checking Awards Section:");
const awardsInfo = await page.evaluate(() => {
const awardsSection = document.querySelector('#awards');
const awardItems = document.querySelectorAll('.award-item');
const awardLogos = document.querySelectorAll('.award-logo');
const results = {
sectionExists: !!awardsSection,
itemCount: awardItems.length,
logoCount: awardLogos.length,
items: []
};
awardItems.forEach((item, i) => {
const logo = item.querySelector('.award-logo');
const logoImg = logo?.querySelector('img');
const content = item.querySelector('.award-content');
const logoStyle = logo ? window.getComputedStyle(logo) : null;
const imgStyle = logoImg ? window.getComputedStyle(logoImg) : null;
const itemStyle = window.getComputedStyle(item);
results.items.push({
index: i,
hasLogo: !!logo,
hasImg: !!logoImg,
imgSrc: logoImg?.src || 'none',
logoDisplay: logoStyle?.display,
logoWidth: logoStyle?.width,
logoHeight: logoStyle?.height,
imgWidth: imgStyle?.width,
imgHeight: imgStyle?.height,
imgObjectFit: imgStyle?.objectFit,
itemDisplay: itemStyle?.display,
itemGap: itemStyle?.gap,
contentExists: !!content
});
});
return results;
});
console.log(` Section exists: ${awardsInfo.sectionExists}`);
console.log(` Award items: ${awardsInfo.itemCount}`);
console.log(` Award logos: ${awardsInfo.logoCount}`);
awardsInfo.items.forEach(item => {
console.log(`\n Award #${item.index + 1}:`);
console.log(` Has logo: ${item.hasLogo}`);
console.log(` Has img: ${item.hasImg}`);
console.log(` Img src: ${item.imgSrc}`);
console.log(` Logo display: ${item.logoDisplay}`);
console.log(` Logo size: ${item.logoWidth} × ${item.logoHeight}`);
console.log(` Img size: ${item.imgWidth} × ${item.imgHeight}`);
console.log(` Img object-fit: ${item.imgObjectFit}`);
console.log(` Item display: ${item.itemDisplay}`);
console.log(` Item gap: ${item.itemGap}`);
console.log(` Has content: ${item.contentExists}`);
});
console.log("\n2️⃣ Testing icon toggle:");
// Turn icons OFF
await page.click('label:has(#iconToggle)');
await page.waitForTimeout(500);
const afterOff = await page.evaluate(() => {
const awardLogos = document.querySelectorAll('.award-logo');
const results = [];
awardLogos.forEach((logo, i) => {
const style = window.getComputedStyle(logo);
results.push({
index: i,
opacity: style.opacity,
width: style.width,
height: style.height
});
});
return results;
});
console.log(" Icons OFF:");
afterOff.forEach(logo => {
console.log(` Logo #${logo.index + 1}: opacity=${logo.opacity}, size=${logo.width}×${logo.height}`);
});
// Turn icons ON
await page.click('label:has(#iconToggle)');
await page.waitForTimeout(500);
const afterOn = await page.evaluate(() => {
const awardLogos = document.querySelectorAll('.award-logo');
const results = [];
awardLogos.forEach((logo, i) => {
const style = window.getComputedStyle(logo);
results.push({
index: i,
opacity: style.opacity,
width: style.width,
height: style.height
});
});
return results;
});
console.log("\n Icons ON:");
afterOn.forEach(logo => {
console.log(` Logo #${logo.index + 1}: opacity=${logo.opacity}, size=${logo.width}×${logo.height}`);
});
await page.screenshot({ path: 'tests/screenshots/awards-visual-test.png', fullPage: false });
console.log("\n📸 Screenshot saved to tests/screenshots/awards-visual-test.png");
console.log("\n" + "=".repeat(70));
console.log("\nBrowser will stay open for 60 seconds for visual inspection...");
await page.waitForTimeout(60000);
await browser.close();
}
await testAwards();
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env bun
/**
* ALL ICONS COMPREHENSIVE TEST
* Check ALL icon types for consistent size and transparent backgrounds
*/
import { chromium } from "playwright";
const URL = "http://localhost:1999";
async function testAllIcons() {
console.log("🧪 ALL ICONS COMPREHENSIVE TEST\n");
console.log("=".repeat(70));
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage({ viewport: { width: 1400, height: 1080 } });
await page.goto(URL);
await page.waitForTimeout(2000);
console.log("\n📋 Checking ALL icon types:\n");
const iconData = await page.evaluate(() => {
const selectors = [
{ name: 'Company Logos', selector: '.company-logo img' },
{ name: 'Award Logos', selector: '.award-logo img' },
{ name: 'Project Icons', selector: '.project-icon img' },
{ name: 'Course Icons', selector: '.course-icon img' },
{ name: 'Default Company', selector: '.default-company-icon' },
{ name: 'Default Award', selector: '.default-award-icon' },
{ name: 'Default Project', selector: '.default-project-icon' },
{ name: 'Default Course', selector: '.default-course-icon' }
];
const results = [];
selectors.forEach(({ name, selector }) => {
const elements = document.querySelectorAll(selector);
const samples = [];
elements.forEach((el, i) => {
if (i < 3) { // Check first 3 of each type
const style = window.getComputedStyle(el);
samples.push({
index: i + 1,
width: style.width,
height: style.height,
background: style.backgroundColor,
opacity: style.opacity
});
}
});
results.push({
type: name,
count: elements.length,
samples
});
});
return results;
});
iconData.forEach(({ type, count, samples }) => {
console.log(`\n${type}:`);
console.log(` Total found: ${count}`);
samples.forEach(sample => {
console.log(` #${sample.index}: ${sample.width} × ${sample.height}, bg: ${sample.background}, opacity: ${sample.opacity}`);
});
// Check consistency
if (samples.length > 0) {
const uniqueSizes = new Set(samples.map(s => `${s.width}×${s.height}`));
const uniqueBgs = new Set(samples.map(s => s.background));
if (uniqueSizes.size === 1) {
console.log(` ✅ Consistent size: ${Array.from(uniqueSizes)[0]}`);
} else {
console.log(` ❌ INCONSISTENT sizes: ${Array.from(uniqueSizes).join(', ')}`);
}
const bg = Array.from(uniqueBgs)[0];
if (bg.includes('rgba(0, 0, 0, 0)') || bg === 'transparent') {
console.log(` ✅ Transparent background`);
} else {
console.log(` ⚠️ Background: ${bg}`);
}
}
});
console.log("\n" + "=".repeat(70));
console.log("\nBrowser will stay open for 30 seconds for visual inspection...");
await page.waitForTimeout(30000);
await browser.close();
}
await testAllIcons();
+132
View File
@@ -0,0 +1,132 @@
#!/usr/bin/env bun
/**
* THEME SWITCHER & MOBILE CSS TEST
* Test theme switching and mobile responsive CSS
*/
import { chromium } from "playwright";
const URL = "http://localhost:1999";
async function testThemeAndMobile() {
console.log("🧪 THEME & MOBILE TEST\n");
console.log("=".repeat(70));
const browser = await chromium.launch({ headless: false });
// Test 1: Desktop theme switcher
console.log("\n1️⃣ Testing Theme Switcher (Desktop):\n");
const desktopPage = await browser.newPage({ viewport: { width: 1400, height: 1080 } });
await desktopPage.goto(URL);
await desktopPage.waitForTimeout(2000);
const initialTheme = await desktopPage.evaluate(() => {
return {
htmlAttr: document.documentElement.getAttribute('data-color-theme'),
bodyClass: document.body.className,
localStorage: localStorage.getItem('color-theme-mode')
};
});
console.log(` Initial theme:`);
console.log(` HTML attr: ${initialTheme.htmlAttr}`);
console.log(` Body class: ${initialTheme.bodyClass}`);
console.log(` LocalStorage: ${initialTheme.localStorage}`);
// Try to click theme toggle
console.log(`\n Clicking theme toggle...`);
try {
await desktopPage.click('#themeToggle', { timeout: 5000 });
await desktopPage.waitForTimeout(500);
const afterToggle = await desktopPage.evaluate(() => {
return {
htmlAttr: document.documentElement.getAttribute('data-color-theme'),
toggleChecked: document.querySelector('#themeToggle')?.checked,
localStorage: localStorage.getItem('color-theme-mode')
};
});
console.log(` After toggle:`);
console.log(` HTML attr: ${afterToggle.htmlAttr}`);
console.log(` Toggle checked: ${afterToggle.toggleChecked}`);
console.log(` LocalStorage: ${afterToggle.localStorage}`);
if (afterToggle.htmlAttr !== initialTheme.htmlAttr) {
console.log(`\n ✅ Theme switcher working!`);
} else {
console.log(`\n ❌ Theme switcher NOT working!`);
}
} catch (error) {
console.log(` ❌ Theme toggle button not found or not clickable!`);
console.log(` Error: ${error.message}`);
}
await desktopPage.close();
// Test 2: Mobile CSS for complete theme
console.log("\n2️⃣ Testing Mobile CSS (Complete Theme):\n");
const mobilePage = await browser.newPage({ viewport: { width: 375, height: 667 } });
await mobilePage.goto(URL);
await mobilePage.waitForTimeout(2000);
// Switch to complete/long CV
console.log(` Switching to complete CV...`);
try {
// Open hamburger menu on mobile
await mobilePage.click('.hamburger-button', { timeout: 5000 });
await mobilePage.waitForTimeout(500);
// Click length toggle in menu
await mobilePage.click('#lengthToggleMenu', { timeout: 5000 });
await mobilePage.waitForTimeout(500);
const mobileStyles = await mobilePage.evaluate(() => {
const paper = document.querySelector('.cv-paper');
const actionBar = document.querySelector('.action-bar');
const hamburger = document.querySelector('.hamburger-button');
const viewControls = document.querySelector('.view-controls-center');
const paperStyle = paper ? window.getComputedStyle(paper) : null;
const actionBarStyle = actionBar ? window.getComputedStyle(actionBar) : null;
const hamburgerStyle = hamburger ? window.getComputedStyle(hamburger) : null;
const viewControlsStyle = viewControls ? window.getComputedStyle(viewControls) : null;
return {
paperClass: paper?.className,
paperMaxWidth: paperStyle?.maxWidth,
paperPadding: paperStyle?.padding,
actionBarDisplay: actionBarStyle?.display,
hamburgerDisplay: hamburgerStyle?.display,
viewControlsDisplay: viewControlsStyle?.display
};
});
console.log(` Mobile styles:`);
console.log(` Paper class: ${mobileStyles.paperClass}`);
console.log(` Paper max-width: ${mobileStyles.paperMaxWidth}`);
console.log(` Paper padding: ${mobileStyles.paperPadding}`);
console.log(` Action bar display: ${mobileStyles.actionBarDisplay}`);
console.log(` Hamburger display: ${mobileStyles.hamburgerDisplay}`);
console.log(` View controls display: ${mobileStyles.viewControlsDisplay}`);
if (mobileStyles.paperMaxWidth === 'none' || mobileStyles.paperMaxWidth === '100%') {
console.log(`\n ⚠️ Mobile CSS might not be applied properly`);
} else {
console.log(`\n ✅ Mobile CSS appears to be applied`);
}
} catch (error) {
console.log(` ❌ Error testing mobile: ${error.message}`);
}
await mobilePage.screenshot({ path: 'tests/screenshots/mobile-complete-theme.png' });
console.log(`\n📸 Mobile screenshot: tests/screenshots/mobile-complete-theme.png`);
console.log("\n" + "=".repeat(70));
console.log("\nBrowser will stay open for 30 seconds...");
await mobilePage.waitForTimeout(30000);
await browser.close();
}
await testThemeAndMobile();
+147
View File
@@ -0,0 +1,147 @@
#!/usr/bin/env bun
/**
* DARK THEME COLOR TEST
* Verify that all text colors use CSS variables and display correctly in dark theme
*/
import { chromium } from "playwright";
const URL = "http://localhost:1999";
async function testDarkTheme() {
console.log("🧪 DARK THEME COLOR TEST\n");
console.log("=".repeat(70));
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage({ viewport: { width: 1400, height: 1080 } });
await page.goto(URL);
await page.waitForTimeout(2000);
// Test 1: Light theme (default)
console.log("\n1️⃣ Testing Light Theme (default):\n");
const lightColors = await page.evaluate(() => {
const styles = window.getComputedStyle(document.documentElement);
return {
textPrimary: styles.getPropertyValue('--text-primary').trim(),
textSecondary: styles.getPropertyValue('--text-secondary').trim(),
textMuted: styles.getPropertyValue('--text-muted').trim(),
textLight: styles.getPropertyValue('--text-light').trim(),
paperBg: styles.getPropertyValue('--paper-bg').trim(),
};
});
console.log(` Light theme CSS variables:`);
console.log(` --text-primary: ${lightColors.textPrimary}`);
console.log(` --text-secondary: ${lightColors.textSecondary}`);
console.log(` --text-muted: ${lightColors.textMuted}`);
console.log(` --text-light: ${lightColors.textLight}`);
console.log(` --paper-bg: ${lightColors.paperBg}`);
// Check actual element colors in light theme
const lightElements = await page.evaluate(() => {
const getName = document.querySelector('.cv-name');
const sectionTitle = document.querySelector('.section-title');
const summaryText = document.querySelector('.summary-text');
const experiencePeriod = document.querySelector('.experience-period');
const durationText = document.querySelector('.duration-text');
return {
name: getName ? window.getComputedStyle(getName).color : 'not found',
sectionTitle: sectionTitle ? window.getComputedStyle(sectionTitle).color : 'not found',
summaryText: summaryText ? window.getComputedStyle(summaryText).color : 'not found',
experiencePeriod: experiencePeriod ? window.getComputedStyle(experiencePeriod).color : 'not found',
durationText: durationText ? window.getComputedStyle(durationText).color : 'not found',
};
});
console.log(`\n Actual element colors:`);
console.log(` .cv-name: ${lightElements.name}`);
console.log(` .section-title: ${lightElements.sectionTitle}`);
console.log(` .summary-text: ${lightElements.summaryText}`);
console.log(` .experience-period: ${lightElements.experiencePeriod}`);
console.log(` .duration-text: ${lightElements.durationText}`);
// Take screenshot of light theme
await page.screenshot({ path: 'tests/screenshots/theme-light.png' });
console.log(`\n 📸 Light theme screenshot: tests/screenshots/theme-light.png`);
// Test 2: Switch to dark theme
console.log("\n2️⃣ Testing Dark Theme:\n");
// Click theme toggle twice (cycles: auto → light → dark)
console.log(` Switching to dark theme (click 1: auto→light)...`);
await page.click('.color-theme-switcher');
await page.waitForTimeout(500);
console.log(` Switching to dark theme (click 2: light→dark)...`);
await page.click('.color-theme-switcher');
await page.waitForTimeout(500);
const darkColors = await page.evaluate(() => {
const styles = window.getComputedStyle(document.documentElement);
const htmlAttr = document.documentElement.getAttribute('data-color-theme');
return {
htmlAttr,
textPrimary: styles.getPropertyValue('--text-primary').trim(),
textSecondary: styles.getPropertyValue('--text-secondary').trim(),
textMuted: styles.getPropertyValue('--text-muted').trim(),
textLight: styles.getPropertyValue('--text-light').trim(),
paperBg: styles.getPropertyValue('--paper-bg').trim(),
};
});
console.log(` Dark theme applied: data-color-theme="${darkColors.htmlAttr}"`);
console.log(` Dark theme CSS variables:`);
console.log(` --text-primary: ${darkColors.textPrimary}`);
console.log(` --text-secondary: ${darkColors.textSecondary}`);
console.log(` --text-muted: ${darkColors.textMuted}`);
console.log(` --text-light: ${darkColors.textLight}`);
console.log(` --paper-bg: ${darkColors.paperBg}`);
// Check actual element colors in dark theme
const darkElements = await page.evaluate(() => {
const getName = document.querySelector('.cv-name');
const sectionTitle = document.querySelector('.section-title');
const summaryText = document.querySelector('.summary-text');
const experiencePeriod = document.querySelector('.experience-period');
const durationText = document.querySelector('.duration-text');
return {
name: getName ? window.getComputedStyle(getName).color : 'not found',
sectionTitle: sectionTitle ? window.getComputedStyle(sectionTitle).color : 'not found',
summaryText: summaryText ? window.getComputedStyle(summaryText).color : 'not found',
experiencePeriod: experiencePeriod ? window.getComputedStyle(experiencePeriod).color : 'not found',
durationText: durationText ? window.getComputedStyle(durationText).color : 'not found',
};
});
console.log(`\n Actual element colors in dark theme:`);
console.log(` .cv-name: ${darkElements.name}`);
console.log(` .section-title: ${darkElements.sectionTitle}`);
console.log(` .summary-text: ${darkElements.summaryText}`);
console.log(` .experience-period: ${darkElements.experiencePeriod}`);
console.log(` .duration-text: ${darkElements.durationText}`);
// Verify colors are light (not dark) in dark theme
const isLightText = darkElements.name.includes('224') || darkElements.name.includes('rgb(224');
if (isLightText) {
console.log(`\n ✅ Text colors are light (visible on dark background)`);
} else {
console.log(`\n ❌ Text colors might be too dark for dark background!`);
}
// Take screenshot of dark theme
await page.screenshot({ path: 'tests/screenshots/theme-dark.png' });
console.log(`\n 📸 Dark theme screenshot: tests/screenshots/theme-dark.png`);
console.log("\n" + "=".repeat(70));
console.log("\n✅ Test complete! Check screenshots:");
console.log(" - tests/screenshots/theme-light.png");
console.log(" - tests/screenshots/theme-dark.png");
console.log("\nBrowser will stay open for 30 seconds...");
await page.waitForTimeout(30000);
await browser.close();
}
await testDarkTheme();
+166
View File
@@ -0,0 +1,166 @@
#!/usr/bin/env bun
/**
* PDF DOWNLOAD URL VERIFICATION TEST
* ===================================
* Tests that PDF modal generates correct download URLs for each option
*/
import { chromium } from 'playwright';
const URL = "http://localhost:1999";
async function testPDFDownloadURLs() {
console.log('🔗 PDF DOWNLOAD URL TEST\n');
console.log('='.repeat(70));
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1920, height: 1080 } });
const testResults = [];
console.log("\n1️⃣ Loading page...");
await page.goto(URL);
await page.waitForTimeout(1000);
// Set localStorage to simulate current settings
await page.evaluate(() => {
localStorage.setItem('cv-length', 'long');
localStorage.setItem('cv-icons', 'hide');
localStorage.setItem('cv-theme', 'clean');
});
console.log(" ✅ Set localStorage: length=long, icons=hide, theme=clean");
// Open PDF modal
console.log("\n2️⃣ Opening PDF modal...");
await page.click('.pdf-btn');
await page.waitForTimeout(500);
// ========================================================================
// TEST 1: Short CV URL
// ========================================================================
console.log("\n3️⃣ Testing Short CV URL...");
// Intercept navigation
let capturedURL = null;
page.on('framenavigated', (frame) => {
if (frame === page.mainFrame()) {
capturedURL = frame.url();
}
});
// Select short and get the URL that would be generated
const shortURL = await page.evaluate(() => {
const shortCard = document.querySelector('[data-cv-format="short"]');
shortCard.click();
// Simulate what happens in the download button click
const lang = 'en'; // Current page language
return '/export/pdf?lang=' + lang + '&length=short&icons=show&version=extended';
});
console.log(` Generated URL: ${shortURL}`);
console.log(` Expected: /export/pdf?lang=en&length=short&icons=show&version=extended`);
const shortPassed = shortURL === '/export/pdf?lang=en&length=short&icons=show&version=extended';
console.log(` ${shortPassed ? '✅ PASS' : '❌ FAIL'} - Short CV URL correct`);
testResults.push({ test: 'Short CV URL', passed: shortPassed });
// ========================================================================
// TEST 2: Long CV URL
// ========================================================================
console.log("\n4️⃣ Testing Long CV URL...");
const longURL = await page.evaluate(() => {
const longCard = document.querySelector('[data-cv-format="long"]');
longCard.click();
const lang = 'en';
return '/export/pdf?lang=' + lang + '&length=long&icons=show&version=extended';
});
console.log(` Generated URL: ${longURL}`);
console.log(` Expected: /export/pdf?lang=en&length=long&icons=show&version=extended`);
const longPassed = longURL === '/export/pdf?lang=en&length=long&icons=show&version=extended';
console.log(` ${longPassed ? '✅ PASS' : '❌ FAIL'} - Long CV URL correct`);
testResults.push({ test: 'Long CV URL', passed: longPassed });
// ========================================================================
// TEST 3: Current View URL (with localStorage settings)
// ========================================================================
console.log("\n5️⃣ Testing Current View URL...");
const currentURL = await page.evaluate(() => {
const currentCard = document.querySelector('[data-cv-format="current"]');
currentCard.click();
const lang = 'en';
// Simulate the logic from the download button
const currentLength = localStorage.getItem('cv-length') || 'short';
const currentIcons = localStorage.getItem('cv-icons') || 'show';
const currentTheme = localStorage.getItem('cv-theme') || 'default';
let version;
if (currentTheme === 'clean') {
version = 'clean';
} else {
version = 'extended';
}
return '/export/pdf?lang=' + lang + '&length=' + currentLength + '&icons=' + currentIcons + '&version=' + version;
});
console.log(` Generated URL: ${currentURL}`);
console.log(` Expected: /export/pdf?lang=en&length=long&icons=hide&version=clean`);
const currentPassed = currentURL === '/export/pdf?lang=en&length=long&icons=hide&version=clean';
console.log(` ${currentPassed ? '✅ PASS' : '❌ FAIL'} - Current View URL correct`);
console.log(` ️ Current View correctly uses localStorage settings`);
testResults.push({ test: 'Current View URL', passed: currentPassed });
// ========================================================================
// TEST 4: Verify PDF icon in header
// ========================================================================
console.log("\n6️⃣ Testing PDF Icon in Modal Header...");
const hasIcon = await page.evaluate(() => {
const header = document.querySelector('.info-modal-header');
const icon = header?.querySelector('iconify-icon[icon="catppuccin:pdf"]');
return !!icon;
});
console.log(` PDF icon present: ${hasIcon ? '✅' : '❌'}`);
testResults.push({ test: 'PDF Icon in Header', passed: hasIcon });
// ========================================================================
// SUMMARY
// ========================================================================
console.log('\n' + '='.repeat(70));
console.log('📊 TEST SUMMARY\n');
testResults.forEach(result => {
console.log(` ${result.passed ? '✅' : '❌'} ${result.test}`);
});
const allPassed = testResults.every(r => r.passed);
const passedCount = testResults.filter(r => r.passed).length;
console.log(`\n Total: ${passedCount}/${testResults.length} tests passed`);
console.log('='.repeat(70));
if (allPassed) {
console.log('\n🎉 ALL PDF DOWNLOAD URLs VALIDATED!');
console.log(' - Short CV URL: ✅ Correct');
console.log(' - Long CV URL: ✅ Correct');
console.log(' - Current View URL: ✅ Correct (uses localStorage)');
console.log(' - PDF Icon: ✅ Present');
} else {
console.log('\n❌ SOME TESTS FAILED');
}
await browser.close();
process.exit(allPassed ? 0 : 1);
}
testPDFDownloadURLs().catch(err => {
console.error('❌ Test failed:', err);
process.exit(1);
});
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env bun
/**
* DEBUG: Find which element has white background in dark theme
*/
import { chromium } from "playwright";
const URL = "http://localhost:1999";
async function debugDarkTheme() {
console.log("🔍 DARK THEME BACKGROUND DEBUG\n");
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage({ viewport: { width: 1400, height: 1080 } });
await page.goto(URL);
await page.waitForTimeout(2000);
// Switch to dark theme (click twice: auto→light→dark)
console.log("Switching to dark theme...");
await page.click('.color-theme-switcher');
await page.waitForTimeout(300);
await page.click('.color-theme-switcher');
await page.waitForTimeout(500);
const backgrounds = await page.evaluate(() => {
const elements = {
html: document.documentElement,
body: document.body,
cvPage: document.querySelector('.cv-page'),
pageContent: document.querySelector('.page-content'),
cvMain: document.querySelector('.cv-main'),
cvPaper: document.querySelector('.cv-paper'),
cvContainer: document.querySelector('.cv-container'),
};
const results = {};
for (const [name, el] of Object.entries(elements)) {
if (el) {
const styles = window.getComputedStyle(el);
results[name] = {
background: styles.background,
backgroundColor: styles.backgroundColor,
className: el.className,
};
} else {
results[name] = { error: 'Element not found' };
}
}
return results;
});
console.log("\nElement backgrounds in DARK theme:\n");
for (const [name, info] of Object.entries(backgrounds)) {
console.log(`${name}:`);
if (info.error) {
console.log(`${info.error}`);
} else {
console.log(` Class: ${info.className || '(none)'}`);
console.log(` background: ${info.background}`);
console.log(` backgroundColor: ${info.backgroundColor}`);
}
console.log();
}
console.log("\nBrowser will stay open for inspection...");
await page.waitForTimeout(60000);
await browser.close();
}
await debugDarkTheme();
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env bun
/**
* VIEW SWITCHER TEST
* Test the clean/complete view toggle
*/
import { chromium } from "playwright";
const URL = "http://localhost:1999";
async function testViewSwitcher() {
console.log("🧪 VIEW SWITCHER TEST\n");
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage({ viewport: { width: 1400, height: 1080 } });
await page.goto(URL);
await page.waitForTimeout(2000);
console.log("1️⃣ Initial state:");
const initial = await page.evaluate(() => {
const container = document.querySelector('.cv-container');
const toggle = document.querySelector('#viewToggle');
return {
containerClass: container?.className,
toggleChecked: toggle?.checked,
isClean: container?.classList.contains('theme-clean'),
};
});
console.log(` Container class: ${initial.containerClass}`);
console.log(` Toggle checked: ${initial.toggleChecked}`);
console.log(` Is clean theme: ${initial.isClean}`);
console.log("\n2️⃣ Clicking view toggle...");
try {
// Try clicking the label
await page.click('label[for="viewToggle"]', { timeout: 5000 });
await page.waitForTimeout(500);
const afterClick = await page.evaluate(() => {
const container = document.querySelector('.cv-container');
const toggle = document.querySelector('#viewToggle');
return {
containerClass: container?.className,
toggleChecked: toggle?.checked,
isClean: container?.classList.contains('theme-clean'),
};
});
console.log(` Container class: ${afterClick.containerClass}`);
console.log(` Toggle checked: ${afterClick.toggleChecked}`);
console.log(` Is clean theme: ${afterClick.isClean}`);
if (afterClick.isClean !== initial.isClean) {
console.log(`\n ✅ View switcher working!`);
} else {
console.log(`\n ❌ View switcher NOT working - theme didn't change`);
}
} catch (error) {
console.log(` ❌ Error: ${error.message}`);
}
await page.screenshot({ path: 'tests/screenshots/view-switcher.png' });
console.log(`\n📸 Screenshot: tests/screenshots/view-switcher.png`);
console.log("\nBrowser will close in 10 seconds...");
await page.waitForTimeout(10000);
await browser.close();
}
await testViewSwitcher();
Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB