feat: lazy load ninja-keys + HTML Invoker Commands API
- Lazy load ninja-keys only on CMD+K press (0 requests on initial load) - Use esm.sh bundled module (3 requests vs ~81 previously) - Add esm.sh to CSP whitelist - Implement HTML Invoker Commands API for modals: - commandfor="modal-id" + command="show-modal" for opening - commandfor="modal-id" + command="close" for closing - Removes need for onclick handlers on modal buttons - Refactor index.html into layout partials (head, body-scripts) - Add comprehensive tests for both features
This commit is contained in:
+23
-369
@@ -1,342 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{if eq .Lang "es"}}es{{else}}en{{end}}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<!-- Primary Meta Tags -->
|
||||
<title>{{.CV.Personal.Name}} - {{.CV.SEO.PageTitle}}</title>
|
||||
<meta name="title" content="{{.CV.Personal.Name}} - {{.CV.SEO.MetaTitle}}">
|
||||
<meta name="description" content="{{.CV.Personal.Title}} | {{.CV.SEO.MetaDescription}}">
|
||||
<meta name="keywords" content="{{.CV.Personal.Name}}, {{.CV.SEO.Keywords}}">
|
||||
<meta name="author" content="{{.CV.Personal.Name}}">
|
||||
<meta name="robots" content="index, follow">
|
||||
<link rel="canonical" href="{{.CanonicalURL}}">
|
||||
|
||||
<!-- Hreflang tags for international SEO -->
|
||||
<link rel="alternate" hreflang="en" href="{{.AlternateEN}}">
|
||||
<link rel="alternate" hreflang="es" href="{{.AlternateES}}">
|
||||
<link rel="alternate" hreflang="x-default" href="https://juan.andres.morenorub.io/?lang=en">
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="profile">
|
||||
<meta property="og:url" content="{{.CV.Personal.Website}}">
|
||||
<meta property="og:title" content="{{.CV.Personal.Name}} - {{.CV.SEO.MetaTitle}}">
|
||||
<meta property="og:description" content="{{.CV.Personal.Title}} | {{.CV.SEO.OgDescription}}">
|
||||
<meta property="og:image" content="{{.CV.Personal.Website}}/static/images/profile.jpg">
|
||||
<meta property="og:locale" content="{{if eq .Lang "es"}}es_ES{{else}}en_US{{end}}">
|
||||
<meta property="og:site_name" content="{{.CV.Personal.Name}}">
|
||||
<meta property="profile:first_name" content="{{.CV.Personal.FirstName}}">
|
||||
<meta property="profile:last_name" content="{{.CV.Personal.LastName}}">
|
||||
<meta property="profile:username" content="{{.CV.Personal.Username}}">
|
||||
|
||||
<!-- Social Media Card (Generic) -->
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:title" content="{{.CV.Personal.Name}} - {{.CV.SEO.MetaTitle}}">
|
||||
<meta name="twitter:description" content="{{.CV.Personal.Title}}">
|
||||
<meta name="twitter:image" content="{{.CV.Personal.Website}}/static/images/profile.jpg">
|
||||
|
||||
<!-- HTMX Configuration -->
|
||||
<meta name="htmx-config" content='{"timeout":5000,"defaultSwapStyle":"innerHTML","defaultSwapDelay":0,"defaultSettleDelay":20}'>
|
||||
|
||||
<!-- FOUC Prevention: Inline critical CSS + Apply color theme before page render -->
|
||||
<!-- Critical theme variables inlined to prevent flash of unstyled content -->
|
||||
<style>
|
||||
/* Light theme (default) - critical variables only */
|
||||
:root {
|
||||
--page-bg: #d6d6d6;
|
||||
--paper-bg: #ffffff;
|
||||
--text-primary: #1a1a1a;
|
||||
--sidebar-bg: #d1d4d2;
|
||||
}
|
||||
/* Dark theme - critical variables only */
|
||||
[data-color-theme="dark"] {
|
||||
--page-bg: #3a3a3a;
|
||||
--paper-bg: #1a1a1a;
|
||||
--text-primary: #e0e0e0;
|
||||
--sidebar-bg: #3a3d3e;
|
||||
}
|
||||
/* Auto theme follows system preference */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
[data-color-theme="auto"] {
|
||||
--page-bg: #3a3a3a;
|
||||
--paper-bg: #1a1a1a;
|
||||
--text-primary: #e0e0e0;
|
||||
--sidebar-bg: #3a3d3e;
|
||||
}
|
||||
}
|
||||
/* Apply critical styles immediately */
|
||||
html { background-color: var(--page-bg); }
|
||||
body { color: var(--text-primary); }
|
||||
</style>
|
||||
<script>
|
||||
(function() {
|
||||
// First-time visitors ALWAYS get light theme (paper aesthetic)
|
||||
// Users can switch to dark/auto and their preference is saved
|
||||
let theme = localStorage.getItem('color-theme-mode') || 'light';
|
||||
document.documentElement.setAttribute('data-color-theme', theme);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Device Detection - Detect real mobile devices vs desktop browser -->
|
||||
<script src="/static/js/device-detection.js"></script>
|
||||
|
||||
<!-- HTMX with SRI (Subresource Integrity) -->
|
||||
<script src="https://unpkg.com/htmx.org@1.9.10"
|
||||
integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC"
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
<!-- Hyperscript Functions - Must load BEFORE hyperscript library -->
|
||||
<!-- NOTE: cv-functions.js removed - hyperscript def statements are globally available -->
|
||||
<!-- ✅ NO def limit with latest hyperscript - organized by category -->
|
||||
<script type="text/hyperscript" src="/static/hyperscript/utils._hs"></script>
|
||||
<script type="text/hyperscript" src="/static/hyperscript/toggles._hs"></script>
|
||||
<script type="text/hyperscript" src="/static/hyperscript/hover-sync._hs"></script>
|
||||
<script type="text/hyperscript" src="/static/hyperscript/keyboard._hs"></script>
|
||||
<script type="text/hyperscript" src="/static/hyperscript/zoom._hs"></script>
|
||||
<script type="text/hyperscript" src="/static/hyperscript/pdf-modal._hs"></script>
|
||||
|
||||
<!-- Color Theme System (JavaScript - hyperscript had parsing issues with colons in strings) -->
|
||||
<script src="/static/js/color-theme.js"></script>
|
||||
|
||||
<!-- NOTE: footer-buttons-interaction.js removed - moved to hyperscript on footer element -->
|
||||
<!-- NOTE: scroll-at-bottom-handler.js removed - duplicate of handleScroll() in utils._hs -->
|
||||
|
||||
<!-- Hyperscript - Declarative event handling for enhanced interactivity -->
|
||||
<script src="https://unpkg.com/hyperscript.org@0.9.14"></script>
|
||||
|
||||
<!-- Ninja Keys - CMD+K Command Bar -->
|
||||
<script type="module" src="https://unpkg.com/ninja-keys?module"></script>
|
||||
|
||||
<!-- Iconify - Load synchronously for immediate rendering -->
|
||||
<!-- 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>
|
||||
|
||||
<!-- CSS - Conditional loading: bundled in production, modular in development -->
|
||||
{{if .IsProduction}}
|
||||
<link rel="stylesheet" href="/static/dist/bundle.min.css">
|
||||
{{else}}
|
||||
<link rel="stylesheet" href="/static/css/main.css">
|
||||
{{end}}
|
||||
<!-- Print styles - loaded separately, only applied when printing -->
|
||||
<link rel="stylesheet" href="/static/css/print.css" media="print">
|
||||
|
||||
<!-- Fonts with Preload -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="dns-prefetch" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@300;400;500;600;700&family=Source+Sans+Pro:wght@300;400;600&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Structured Data (JSON-LD) - Enhanced for AI-era SEO -->
|
||||
<!-- Person Schema -->
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Person",
|
||||
"@id": "{{.CV.Personal.Website}}/#person",
|
||||
"name": "{{.CV.Personal.Name}}",
|
||||
"givenName": "{{.CV.Personal.FirstName}}",
|
||||
"familyName": "{{.CV.Personal.LastName}}",
|
||||
"jobTitle": "{{.CV.Personal.Title}}",
|
||||
"description": "{{.CV.Summary}}",
|
||||
"url": "{{.CV.Personal.Website}}",
|
||||
"image": "{{.CV.Personal.Website}}/static/images/profile.jpg",
|
||||
"email": "{{.CV.Personal.Email}}",
|
||||
"telephone": "{{.CV.Personal.Phone}}",
|
||||
"birthDate": "{{.CV.Personal.DateOfBirth}}",
|
||||
"birthPlace": {
|
||||
"@type": "Place",
|
||||
"name": "{{.CV.Personal.PlaceOfBirth}}"
|
||||
},
|
||||
"nationality": {
|
||||
"@type": "Country",
|
||||
"name": "{{.CV.Personal.Citizenship}}"
|
||||
},
|
||||
"address": {
|
||||
"@type": "PostalAddress",
|
||||
"addressLocality": "{{.CV.Personal.Location}}",
|
||||
"addressCountry": "ES"
|
||||
},
|
||||
"sameAs": [
|
||||
"{{.CV.Personal.LinkedIn}}",
|
||||
"{{.CV.Personal.GitHub}}",
|
||||
"{{.CV.Personal.Domestika}}"
|
||||
],
|
||||
"alumniOf": {
|
||||
"@type": "CollegeOrUniversity",
|
||||
"name": "Universidad de Extremadura",
|
||||
"address": {
|
||||
"@type": "PostalAddress",
|
||||
"addressLocality": "Cáceres",
|
||||
"addressCountry": "ES"
|
||||
}
|
||||
},
|
||||
"knowsAbout": [
|
||||
"Web Development",
|
||||
"SAP Customer Data Cloud",
|
||||
"React",
|
||||
"Node.js",
|
||||
"Go",
|
||||
"HTMX",
|
||||
"AI-Assisted Development",
|
||||
"Full Stack Development",
|
||||
"Authentication Systems",
|
||||
"GDPR Compliance",
|
||||
"Identity Management"
|
||||
],
|
||||
"knowsLanguage": [
|
||||
{
|
||||
"@type": "Language",
|
||||
"name": "Spanish",
|
||||
"alternateName": "es"
|
||||
},
|
||||
{
|
||||
"@type": "Language",
|
||||
"name": "English",
|
||||
"alternateName": "en"
|
||||
},
|
||||
{
|
||||
"@type": "Language",
|
||||
"name": "Portuguese",
|
||||
"alternateName": "pt"
|
||||
}
|
||||
],
|
||||
"worksFor": [
|
||||
{
|
||||
"@type": "Organization",
|
||||
"name": "Olympic Broadcasting Services",
|
||||
"url": "https://www.obs.tv/"
|
||||
},
|
||||
{
|
||||
"@type": "Organization",
|
||||
"name": "LIV Golf",
|
||||
"url": "https://www.livgolf.com/"
|
||||
}
|
||||
],
|
||||
"hasOccupation": [
|
||||
{{- range $i, $exp := .CV.Experience}}{{if $i}},{{end}}
|
||||
{
|
||||
"@type": "Occupation",
|
||||
"name": "{{$exp.Position}}",
|
||||
"occupationLocation": {
|
||||
"@type": "Place",
|
||||
"name": "{{$exp.Location}}"
|
||||
},
|
||||
"description": "{{$exp.ShortDescription}}",
|
||||
"skills": "{{range $j, $tech := $exp.Technologies}}{{if $j}}, {{end}}{{$tech}}{{end}}"
|
||||
}
|
||||
{{- end}}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- WebSite Schema -->
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
"@id": "{{.CV.Personal.Website}}/#website",
|
||||
"name": "{{.CV.Personal.Name}} - Professional CV",
|
||||
"url": "{{.CV.Personal.Website}}",
|
||||
"description": "Interactive curriculum vitae of {{.CV.Personal.Name}}, {{.CV.Personal.Title}}",
|
||||
"inLanguage": ["en", "es"],
|
||||
"author": {
|
||||
"@id": "{{.CV.Personal.Website}}/#person"
|
||||
},
|
||||
"potentialAction": {
|
||||
"@type": "SearchAction",
|
||||
"target": "{{.CV.Personal.Website}}/?lang={search_term_string}",
|
||||
"query-input": "required name=search_term_string"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- BreadcrumbList Schema -->
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BreadcrumbList",
|
||||
"itemListElement": [
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 1,
|
||||
"name": "Home",
|
||||
"item": "{{.CV.Personal.Website}}"
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 2,
|
||||
"name": "CV {{if eq .Lang "es"}}(Español){{else}}(English){{end}}",
|
||||
"item": "{{.CV.Personal.Website}}/?lang={{.Lang}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- ProfilePage Schema (for CV/Resume) -->
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "ProfilePage",
|
||||
"mainEntity": {
|
||||
"@id": "{{.CV.Personal.Website}}/#person"
|
||||
},
|
||||
"dateCreated": "2024-01-01",
|
||||
"dateModified": "{{.CV.Meta.LastUpdated}}",
|
||||
"name": "{{.CV.Personal.Name}} - Curriculum Vitae",
|
||||
"description": "{{.CV.SEO.MetaDescription}}",
|
||||
"inLanguage": "{{.Lang}}"
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- EducationalOccupationalCredential Schema -->
|
||||
{{range .CV.Education}}
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "EducationalOccupationalCredential",
|
||||
"name": "{{.Degree}}",
|
||||
"description": "{{.Field}}",
|
||||
"educationalLevel": "Bachelor's Degree",
|
||||
"credentialCategory": "degree",
|
||||
"recognizedBy": {
|
||||
"@type": "CollegeOrUniversity",
|
||||
"name": "{{.Institution}}",
|
||||
"address": {
|
||||
"@type": "PostalAddress",
|
||||
"addressLocality": "{{.Location}}"
|
||||
}
|
||||
},
|
||||
"dateCreated": "{{.EndDate}}"
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
<!-- Course Schemas -->
|
||||
{{range .CV.Courses}}
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Course",
|
||||
"name": "{{.Title}}",
|
||||
"description": "{{if .ShortDescription}}{{.ShortDescription}}{{else}}{{.Description}}{{end}}",
|
||||
"provider": {
|
||||
"@type": "Organization",
|
||||
"name": "{{.Institution}}"
|
||||
},
|
||||
"hasCourseInstance": {
|
||||
"@type": "CourseInstance",
|
||||
"courseMode": "onsite",
|
||||
"location": {
|
||||
"@type": "Place",
|
||||
"name": "{{.Location}}"
|
||||
}
|
||||
},
|
||||
"timeRequired": "{{.Duration}}"
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
</head>
|
||||
{{template "head" .}}
|
||||
<body {{if .ThemeClean}}class="theme-clean"{{end}}
|
||||
_="on load call initScrollBehavior()
|
||||
on scroll from window call handleScroll()
|
||||
@@ -351,29 +15,35 @@
|
||||
if (event.key is 'i' or event.key is 'I') and noMod and not skip then halt the event then call handleToggleShortcut('iconToggle', 'iconToggleMenu') end
|
||||
if (event.key is 'v' or event.key is 'V') and noMod and not skip then halt the event then call handleToggleShortcut('themeToggle', 'themeToggleMenu') end
|
||||
end">
|
||||
<!-- Top anchor for back-to-top link -->
|
||||
<div id="top"></div>
|
||||
|
||||
<!-- ============================================ -->
|
||||
<!-- TOP NAVIGATION & CONTROLS -->
|
||||
<!-- ============================================ -->
|
||||
<div id="top"></div>
|
||||
{{template "action-bar" .}}
|
||||
{{template "hamburger-menu" .}}
|
||||
{{template "color-theme-switcher" .}}
|
||||
|
||||
<!-- Zoom Wrapper (for zoom functionality) -->
|
||||
<!-- ============================================ -->
|
||||
<!-- MAIN CV CONTENT -->
|
||||
<!-- ============================================ -->
|
||||
<div id="zoom-wrapper" class="zoom-wrapper">
|
||||
<!-- CV Content Container -->
|
||||
<div class="cv-container">
|
||||
{{template "cv-content.html" .}}
|
||||
</div>
|
||||
</div> <!-- End zoom-wrapper -->
|
||||
</div>
|
||||
|
||||
<!-- ============================================ -->
|
||||
<!-- PAGE FOOTER & NOTIFICATIONS -->
|
||||
<!-- ============================================ -->
|
||||
{{template "page-footer" .}}
|
||||
|
||||
{{template "error-toast" .}}
|
||||
{{template "pdf-toast" .}}
|
||||
|
||||
<!-- iOS-style blur backdrop for mobile buttons -->
|
||||
<!-- ============================================ -->
|
||||
<!-- FLOATING BUTTONS -->
|
||||
<!-- ============================================ -->
|
||||
<div class="fixed-buttons-backdrop no-print"></div>
|
||||
|
||||
{{template "back-to-top" .}}
|
||||
{{template "info-button" .}}
|
||||
{{template "download-button" .}}
|
||||
@@ -382,35 +52,19 @@
|
||||
{{template "zoom-toggle-button" .}}
|
||||
{{template "shortcuts-button" .}}
|
||||
{{template "cmd-k-button" .}}
|
||||
|
||||
<!-- ============================================ -->
|
||||
<!-- MODALS -->
|
||||
<!-- ============================================ -->
|
||||
{{template "info-modal" .}}
|
||||
{{template "shortcuts-modal" .}}
|
||||
{{template "pdf-modal" .}}
|
||||
{{template "contact-modal" .}}
|
||||
{{template "zoom-control" .}}
|
||||
|
||||
<!-- CMD+K Command Bar -->
|
||||
<ninja-keys id="cmd-k-bar" placeholder="Type a command or search..." hideBreadcrumbs openHotkey="cmd+k,ctrl+k"></ninja-keys>
|
||||
|
||||
<!-- External JavaScript - CSP Compliant -->
|
||||
<script src="/static/js/main.js"></script>
|
||||
<script src="/static/js/ninja-keys-init.js"></script>
|
||||
|
||||
<!-- Matomo Analytics - First-party subdomain to bypass ad blockers -->
|
||||
<script>
|
||||
var _paq = window._paq = window._paq || [];
|
||||
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
|
||||
_paq.push(['trackPageView']);
|
||||
_paq.push(['enableLinkTracking']);
|
||||
(function() {
|
||||
var u="https://matomo.morenorub.io/";
|
||||
_paq.push(['setTrackerUrl', u+'matomo.php']);
|
||||
_paq.push(['setSiteId', '4']);
|
||||
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
|
||||
g.async=true;
|
||||
g.src=u+'matomo.js';
|
||||
s.parentNode.insertBefore(g,s);
|
||||
})();
|
||||
</script>
|
||||
<!-- End Matomo Code -->
|
||||
<!-- ============================================ -->
|
||||
<!-- SCRIPTS & ANALYTICS -->
|
||||
<!-- ============================================ -->
|
||||
{{template "body-scripts" .}}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
{{define "body-scripts"}}
|
||||
<!-- CMD+K Command Bar - Lazy loaded placeholder -->
|
||||
<div id="cmd-k-container"></div>
|
||||
|
||||
<!-- External JavaScript - CSP Compliant -->
|
||||
<script src="/static/js/main.js"></script>
|
||||
|
||||
<!-- Ninja Keys Lazy Loader - Only loads when CMD+K is pressed -->
|
||||
<script>
|
||||
(function() {
|
||||
let ninjaLoaded = false;
|
||||
let ninjaLoading = false;
|
||||
|
||||
async function loadNinjaKeys() {
|
||||
if (ninjaLoaded || ninjaLoading) return;
|
||||
ninjaLoading = true;
|
||||
|
||||
// Use esm.sh with bundle option to reduce module requests
|
||||
await import('https://esm.sh/ninja-keys@1.2.2?bundle');
|
||||
|
||||
// Create the ninja-keys element
|
||||
const container = document.getElementById('cmd-k-container');
|
||||
const ninjaKeys = document.createElement('ninja-keys');
|
||||
ninjaKeys.id = 'cmd-k-bar';
|
||||
ninjaKeys.placeholder = 'Type a command or search...';
|
||||
ninjaKeys.hideBreadcrumbs = true;
|
||||
container.appendChild(ninjaKeys);
|
||||
|
||||
// Load the initialization script
|
||||
const script = document.createElement('script');
|
||||
script.src = '/static/js/ninja-keys-init.js';
|
||||
document.body.appendChild(script);
|
||||
|
||||
ninjaLoaded = true;
|
||||
ninjaLoading = false;
|
||||
|
||||
// Open the palette after a brief delay for initialization
|
||||
setTimeout(() => {
|
||||
if (ninjaKeys.open) ninjaKeys.open();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function openNinjaKeys() {
|
||||
const nk = document.getElementById('cmd-k-bar');
|
||||
if (nk && typeof nk.open === 'function') {
|
||||
nk.open();
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for CMD+K / Ctrl+K
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
||||
e.preventDefault();
|
||||
if (ninjaLoaded) {
|
||||
openNinjaKeys();
|
||||
} else {
|
||||
loadNinjaKeys();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Also handle click on cmd-k button
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.closest('#cmd-k-button, .cmd-k-trigger')) {
|
||||
e.preventDefault();
|
||||
if (ninjaLoaded) {
|
||||
openNinjaKeys();
|
||||
} else {
|
||||
loadNinjaKeys();
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Matomo Analytics - First-party subdomain to bypass ad blockers -->
|
||||
<script>
|
||||
var _paq = window._paq = window._paq || [];
|
||||
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
|
||||
_paq.push(['trackPageView']);
|
||||
_paq.push(['enableLinkTracking']);
|
||||
(function() {
|
||||
var u="https://matomo.morenorub.io/";
|
||||
_paq.push(['setTrackerUrl', u+'matomo.php']);
|
||||
_paq.push(['setSiteId', '4']);
|
||||
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
|
||||
g.async=true;
|
||||
g.src=u+'matomo.js';
|
||||
s.parentNode.insertBefore(g,s);
|
||||
})();
|
||||
</script>
|
||||
<!-- End Matomo Code -->
|
||||
{{end}}
|
||||
@@ -0,0 +1,7 @@
|
||||
{{define "head-fonts"}}
|
||||
<!-- Fonts with Preload -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="dns-prefetch" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@300;400;500;600;700&family=Source+Sans+Pro:wght@300;400;600&display=swap" rel="stylesheet">
|
||||
{{end}}
|
||||
@@ -0,0 +1,40 @@
|
||||
{{define "head-fouc-prevention"}}
|
||||
<!-- FOUC Prevention: Inline critical CSS + Apply color theme before page render -->
|
||||
<!-- Critical theme variables inlined to prevent flash of unstyled content -->
|
||||
<style>
|
||||
/* Light theme (default) - critical variables only */
|
||||
:root {
|
||||
--page-bg: #d6d6d6;
|
||||
--paper-bg: #ffffff;
|
||||
--text-primary: #1a1a1a;
|
||||
--sidebar-bg: #d1d4d2;
|
||||
}
|
||||
/* Dark theme - critical variables only */
|
||||
[data-color-theme="dark"] {
|
||||
--page-bg: #3a3a3a;
|
||||
--paper-bg: #1a1a1a;
|
||||
--text-primary: #e0e0e0;
|
||||
--sidebar-bg: #3a3d3e;
|
||||
}
|
||||
/* Auto theme follows system preference */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
[data-color-theme="auto"] {
|
||||
--page-bg: #3a3a3a;
|
||||
--paper-bg: #1a1a1a;
|
||||
--text-primary: #e0e0e0;
|
||||
--sidebar-bg: #3a3d3e;
|
||||
}
|
||||
}
|
||||
/* Apply critical styles immediately */
|
||||
html { background-color: var(--page-bg); }
|
||||
body { color: var(--text-primary); }
|
||||
</style>
|
||||
<script>
|
||||
(function() {
|
||||
// First-time visitors ALWAYS get light theme (paper aesthetic)
|
||||
// Users can switch to dark/auto and their preference is saved
|
||||
let theme = localStorage.getItem('color-theme-mode') || 'light';
|
||||
document.documentElement.setAttribute('data-color-theme', theme);
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
@@ -0,0 +1,34 @@
|
||||
{{define "head-scripts"}}
|
||||
<!-- Device Detection - Detect real mobile devices vs desktop browser -->
|
||||
<script src="/static/js/device-detection.js"></script>
|
||||
|
||||
<!-- HTMX with SRI (Subresource Integrity) -->
|
||||
<script src="https://unpkg.com/htmx.org@1.9.10"
|
||||
integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC"
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
<!-- Hyperscript Functions - Must load BEFORE hyperscript library -->
|
||||
<!-- NOTE: cv-functions.js removed - hyperscript def statements are globally available -->
|
||||
<!-- ✅ NO def limit with latest hyperscript - organized by category -->
|
||||
<script type="text/hyperscript" src="/static/hyperscript/utils._hs"></script>
|
||||
<script type="text/hyperscript" src="/static/hyperscript/toggles._hs"></script>
|
||||
<script type="text/hyperscript" src="/static/hyperscript/hover-sync._hs"></script>
|
||||
<script type="text/hyperscript" src="/static/hyperscript/keyboard._hs"></script>
|
||||
<script type="text/hyperscript" src="/static/hyperscript/zoom._hs"></script>
|
||||
<script type="text/hyperscript" src="/static/hyperscript/pdf-modal._hs"></script>
|
||||
|
||||
<!-- Color Theme System (JavaScript - hyperscript had parsing issues with colons in strings) -->
|
||||
<script src="/static/js/color-theme.js"></script>
|
||||
|
||||
<!-- NOTE: footer-buttons-interaction.js removed - moved to hyperscript on footer element -->
|
||||
<!-- NOTE: scroll-at-bottom-handler.js removed - duplicate of handleScroll() in utils._hs -->
|
||||
|
||||
<!-- Hyperscript - Declarative event handling for enhanced interactivity -->
|
||||
<script src="https://unpkg.com/hyperscript.org@0.9.14"></script>
|
||||
|
||||
<!-- Ninja Keys - Lazy loaded on CMD+K (see body-scripts for loader) -->
|
||||
|
||||
<!-- Iconify - Load synchronously for immediate rendering -->
|
||||
<!-- 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>
|
||||
{{end}}
|
||||
@@ -0,0 +1,211 @@
|
||||
{{define "head-structured-data"}}
|
||||
<!-- Structured Data (JSON-LD) - Enhanced for AI-era SEO -->
|
||||
<!-- Person Schema -->
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Person",
|
||||
"@id": "{{.CV.Personal.Website}}/#person",
|
||||
"name": "{{.CV.Personal.Name}}",
|
||||
"givenName": "{{.CV.Personal.FirstName}}",
|
||||
"familyName": "{{.CV.Personal.LastName}}",
|
||||
"jobTitle": "{{.CV.Personal.Title}}",
|
||||
"description": "{{.CV.Summary}}",
|
||||
"url": "{{.CV.Personal.Website}}",
|
||||
"image": "{{.CV.Personal.Website}}/static/images/profile.jpg",
|
||||
"email": "{{.CV.Personal.Email}}",
|
||||
"telephone": "{{.CV.Personal.Phone}}",
|
||||
"birthDate": "{{.CV.Personal.DateOfBirth}}",
|
||||
"birthPlace": {
|
||||
"@type": "Place",
|
||||
"name": "{{.CV.Personal.PlaceOfBirth}}"
|
||||
},
|
||||
"nationality": {
|
||||
"@type": "Country",
|
||||
"name": "{{.CV.Personal.Citizenship}}"
|
||||
},
|
||||
"address": {
|
||||
"@type": "PostalAddress",
|
||||
"addressLocality": "{{.CV.Personal.Location}}",
|
||||
"addressCountry": "ES"
|
||||
},
|
||||
"sameAs": [
|
||||
"{{.CV.Personal.LinkedIn}}",
|
||||
"{{.CV.Personal.GitHub}}",
|
||||
"{{.CV.Personal.Domestika}}"
|
||||
],
|
||||
"alumniOf": {
|
||||
"@type": "CollegeOrUniversity",
|
||||
"name": "Universidad de Extremadura",
|
||||
"address": {
|
||||
"@type": "PostalAddress",
|
||||
"addressLocality": "Cáceres",
|
||||
"addressCountry": "ES"
|
||||
}
|
||||
},
|
||||
"knowsAbout": [
|
||||
"Web Development",
|
||||
"SAP Customer Data Cloud",
|
||||
"React",
|
||||
"Node.js",
|
||||
"Go",
|
||||
"HTMX",
|
||||
"AI-Assisted Development",
|
||||
"Full Stack Development",
|
||||
"Authentication Systems",
|
||||
"GDPR Compliance",
|
||||
"Identity Management"
|
||||
],
|
||||
"knowsLanguage": [
|
||||
{
|
||||
"@type": "Language",
|
||||
"name": "Spanish",
|
||||
"alternateName": "es"
|
||||
},
|
||||
{
|
||||
"@type": "Language",
|
||||
"name": "English",
|
||||
"alternateName": "en"
|
||||
},
|
||||
{
|
||||
"@type": "Language",
|
||||
"name": "Portuguese",
|
||||
"alternateName": "pt"
|
||||
}
|
||||
],
|
||||
"worksFor": [
|
||||
{
|
||||
"@type": "Organization",
|
||||
"name": "Olympic Broadcasting Services",
|
||||
"url": "https://www.obs.tv/"
|
||||
},
|
||||
{
|
||||
"@type": "Organization",
|
||||
"name": "LIV Golf",
|
||||
"url": "https://www.livgolf.com/"
|
||||
}
|
||||
],
|
||||
"hasOccupation": [
|
||||
{{- range $i, $exp := .CV.Experience}}{{if $i}},{{end}}
|
||||
{
|
||||
"@type": "Occupation",
|
||||
"name": "{{$exp.Position}}",
|
||||
"occupationLocation": {
|
||||
"@type": "Place",
|
||||
"name": "{{$exp.Location}}"
|
||||
},
|
||||
"description": "{{$exp.ShortDescription}}",
|
||||
"skills": "{{range $j, $tech := $exp.Technologies}}{{if $j}}, {{end}}{{$tech}}{{end}}"
|
||||
}
|
||||
{{- end}}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- WebSite Schema -->
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
"@id": "{{.CV.Personal.Website}}/#website",
|
||||
"name": "{{.CV.Personal.Name}} - Professional CV",
|
||||
"url": "{{.CV.Personal.Website}}",
|
||||
"description": "Interactive curriculum vitae of {{.CV.Personal.Name}}, {{.CV.Personal.Title}}",
|
||||
"inLanguage": ["en", "es"],
|
||||
"author": {
|
||||
"@id": "{{.CV.Personal.Website}}/#person"
|
||||
},
|
||||
"potentialAction": {
|
||||
"@type": "SearchAction",
|
||||
"target": "{{.CV.Personal.Website}}/?lang={search_term_string}",
|
||||
"query-input": "required name=search_term_string"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- BreadcrumbList Schema -->
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BreadcrumbList",
|
||||
"itemListElement": [
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 1,
|
||||
"name": "Home",
|
||||
"item": "{{.CV.Personal.Website}}"
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 2,
|
||||
"name": "CV {{if eq .Lang "es"}}(Español){{else}}(English){{end}}",
|
||||
"item": "{{.CV.Personal.Website}}/?lang={{.Lang}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- ProfilePage Schema (for CV/Resume) -->
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "ProfilePage",
|
||||
"mainEntity": {
|
||||
"@id": "{{.CV.Personal.Website}}/#person"
|
||||
},
|
||||
"dateCreated": "2024-01-01",
|
||||
"dateModified": "{{.CV.Meta.LastUpdated}}",
|
||||
"name": "{{.CV.Personal.Name}} - Curriculum Vitae",
|
||||
"description": "{{.CV.SEO.MetaDescription}}",
|
||||
"inLanguage": "{{.Lang}}"
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- EducationalOccupationalCredential Schema -->
|
||||
{{range .CV.Education}}
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "EducationalOccupationalCredential",
|
||||
"name": "{{.Degree}}",
|
||||
"description": "{{.Field}}",
|
||||
"educationalLevel": "Bachelor's Degree",
|
||||
"credentialCategory": "degree",
|
||||
"recognizedBy": {
|
||||
"@type": "CollegeOrUniversity",
|
||||
"name": "{{.Institution}}",
|
||||
"address": {
|
||||
"@type": "PostalAddress",
|
||||
"addressLocality": "{{.Location}}"
|
||||
}
|
||||
},
|
||||
"dateCreated": "{{.EndDate}}"
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
<!-- Course Schemas -->
|
||||
{{range .CV.Courses}}
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Course",
|
||||
"name": "{{.Title}}",
|
||||
"description": "{{if .ShortDescription}}{{.ShortDescription}}{{else}}{{.Description}}{{end}}",
|
||||
"provider": {
|
||||
"@type": "Organization",
|
||||
"name": "{{.Institution}}"
|
||||
},
|
||||
"hasCourseInstance": {
|
||||
"@type": "CourseInstance",
|
||||
"courseMode": "onsite",
|
||||
"location": {
|
||||
"@type": "Place",
|
||||
"name": "{{.Location}}"
|
||||
}
|
||||
},
|
||||
"timeRequired": "{{.Duration}}"
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
{{end}}
|
||||
@@ -0,0 +1,10 @@
|
||||
{{define "head-styles"}}
|
||||
<!-- CSS - Conditional loading: bundled in production, modular in development -->
|
||||
{{if .IsProduction}}
|
||||
<link rel="stylesheet" href="/static/dist/bundle.min.css">
|
||||
{{else}}
|
||||
<link rel="stylesheet" href="/static/css/main.css">
|
||||
{{end}}
|
||||
<!-- Print styles - loaded separately, only applied when printing -->
|
||||
<link rel="stylesheet" href="/static/css/print.css" media="print">
|
||||
{{end}}
|
||||
@@ -0,0 +1,47 @@
|
||||
{{define "head"}}
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<!-- Primary Meta Tags -->
|
||||
<title>{{.CV.Personal.Name}} - {{.CV.SEO.PageTitle}}</title>
|
||||
<meta name="title" content="{{.CV.Personal.Name}} - {{.CV.SEO.MetaTitle}}">
|
||||
<meta name="description" content="{{.CV.Personal.Title}} | {{.CV.SEO.MetaDescription}}">
|
||||
<meta name="keywords" content="{{.CV.Personal.Name}}, {{.CV.SEO.Keywords}}">
|
||||
<meta name="author" content="{{.CV.Personal.Name}}">
|
||||
<meta name="robots" content="index, follow">
|
||||
<link rel="canonical" href="{{.CanonicalURL}}">
|
||||
|
||||
<!-- Hreflang tags for international SEO -->
|
||||
<link rel="alternate" hreflang="en" href="{{.AlternateEN}}">
|
||||
<link rel="alternate" hreflang="es" href="{{.AlternateES}}">
|
||||
<link rel="alternate" hreflang="x-default" href="https://juan.andres.morenorub.io/?lang=en">
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="profile">
|
||||
<meta property="og:url" content="{{.CV.Personal.Website}}">
|
||||
<meta property="og:title" content="{{.CV.Personal.Name}} - {{.CV.SEO.MetaTitle}}">
|
||||
<meta property="og:description" content="{{.CV.Personal.Title}} | {{.CV.SEO.OgDescription}}">
|
||||
<meta property="og:image" content="{{.CV.Personal.Website}}/static/images/profile.jpg">
|
||||
<meta property="og:locale" content="{{if eq .Lang "es"}}es_ES{{else}}en_US{{end}}">
|
||||
<meta property="og:site_name" content="{{.CV.Personal.Name}}">
|
||||
<meta property="profile:first_name" content="{{.CV.Personal.FirstName}}">
|
||||
<meta property="profile:last_name" content="{{.CV.Personal.LastName}}">
|
||||
<meta property="profile:username" content="{{.CV.Personal.Username}}">
|
||||
|
||||
<!-- Social Media Card (Generic) -->
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:title" content="{{.CV.Personal.Name}} - {{.CV.SEO.MetaTitle}}">
|
||||
<meta name="twitter:description" content="{{.CV.Personal.Title}}">
|
||||
<meta name="twitter:image" content="{{.CV.Personal.Website}}/static/images/profile.jpg">
|
||||
|
||||
<!-- HTMX Configuration -->
|
||||
<meta name="htmx-config" content='{"timeout":5000,"defaultSwapStyle":"innerHTML","defaultSwapDelay":0,"defaultSettleDelay":20}'>
|
||||
|
||||
{{template "head-fouc-prevention" .}}
|
||||
{{template "head-scripts" .}}
|
||||
{{template "head-styles" .}}
|
||||
{{template "head-fonts" .}}
|
||||
{{template "head-structured-data" .}}
|
||||
</head>
|
||||
{{end}}
|
||||
@@ -17,7 +17,7 @@
|
||||
if responseDiv set responseDiv.innerHTML to ''
|
||||
end">
|
||||
<div class="info-modal-content">
|
||||
<button class="info-modal-close" onclick="document.getElementById('contact-modal').close()" aria-label="{{.UI.ContactModal.Close}}">
|
||||
<button class="info-modal-close" commandfor="contact-modal" command="close" aria-label="{{.UI.ContactModal.Close}}">
|
||||
<iconify-icon icon="mdi:close" width="24" height="24"></iconify-icon>
|
||||
</button>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<dialog id="info-modal" class="info-modal no-print"
|
||||
_="on click call closeOnBackdrop(me, event)">
|
||||
<div class="info-modal-content">
|
||||
<button class="info-modal-close" onclick="document.getElementById('info-modal').close()" aria-label="{{.UI.PdfModal.Close}}">
|
||||
<button class="info-modal-close" commandfor="info-modal" command="close" aria-label="{{.UI.PdfModal.Close}}">
|
||||
<iconify-icon icon="mdi:close" width="24" height="24"></iconify-icon>
|
||||
</button>
|
||||
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
|
||||
<!-- Close Button -->
|
||||
<button class="info-modal-close"
|
||||
onclick="document.getElementById('pdf-modal').close()"
|
||||
commandfor="pdf-modal"
|
||||
command="close"
|
||||
aria-label="{{.UI.PdfModal.Close}}">
|
||||
<iconify-icon icon="mdi:close" width="24" height="24"></iconify-icon>
|
||||
</button>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<dialog id="shortcuts-modal" class="info-modal no-print"
|
||||
_="on click call closeOnBackdrop(me, event)">
|
||||
<div class="info-modal-content">
|
||||
<button class="info-modal-close" onclick="document.getElementById('shortcuts-modal').close()" aria-label="{{.UI.ShortcutsModal.Close}}">
|
||||
<button class="info-modal-close" commandfor="shortcuts-modal" command="close" aria-label="{{.UI.ShortcutsModal.Close}}">
|
||||
<iconify-icon icon="mdi:close" width="24" height="24"></iconify-icon>
|
||||
</button>
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
<button
|
||||
id="action-bar-pdf-btn"
|
||||
class="action-btn pdf-btn has-tooltip"
|
||||
onclick="document.getElementById('pdf-modal').showModal()"
|
||||
commandfor="pdf-modal"
|
||||
command="show-modal"
|
||||
aria-label="{{.UI.Widgets.ActionButtons.DownloadPdf}}"
|
||||
data-tooltip="{{.UI.Widgets.ActionButtons.DownloadPdf}}"
|
||||
_="on mouseenter call syncPdfHover(true)
|
||||
@@ -23,10 +24,9 @@
|
||||
{{.UI.Widgets.ActionButtons.PrintFriendly}}
|
||||
</button>
|
||||
<button
|
||||
class="action-btn search-btn has-tooltip"
|
||||
class="action-btn search-btn has-tooltip cmd-k-trigger"
|
||||
aria-label="{{.UI.Widgets.ActionButtons.SearchAriaLabel}}"
|
||||
data-tooltip="{{.UI.Widgets.ActionButtons.Search}}"
|
||||
_="on click set #cmd-k-bar's @open to true">
|
||||
data-tooltip="{{.UI.Widgets.ActionButtons.Search}}">
|
||||
<iconify-icon icon="mdi:magnify" width="24" height="24"></iconify-icon>
|
||||
{{.UI.Widgets.ActionButtons.Search}}
|
||||
</button>
|
||||
|
||||
@@ -177,7 +177,8 @@
|
||||
</div>
|
||||
|
||||
<button class="menu-action-btn menu-pdf-btn"
|
||||
onclick="document.getElementById('pdf-modal').showModal()"
|
||||
commandfor="pdf-modal"
|
||||
command="show-modal"
|
||||
_="on mouseenter call syncPdfHover(true)
|
||||
on mouseleave call syncPdfHover(false)">
|
||||
<iconify-icon icon="catppuccin:pdf" width="20" height="20"></iconify-icon>
|
||||
@@ -193,7 +194,8 @@
|
||||
</button>
|
||||
|
||||
<button class="menu-action-btn menu-contact-btn"
|
||||
onclick="document.getElementById('contact-modal').showModal()">
|
||||
commandfor="contact-modal"
|
||||
command="show-modal">
|
||||
<iconify-icon icon="mdi:email-outline" width="20" height="20"></iconify-icon>
|
||||
<span>{{.UI.Widgets.ActionButtons.Contact}}</span>
|
||||
</button>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{{define "cmd-k-button"}}
|
||||
<!-- CMD+K Command Bar Button (Fixed Left - Last) -->
|
||||
<!-- Uses lazy loading - ninja-keys loads on first click -->
|
||||
<button
|
||||
id="cmd-k-button"
|
||||
class="fixed-btn cmd-k-btn no-print has-tooltip"
|
||||
class="fixed-btn cmd-k-btn no-print has-tooltip cmd-k-trigger"
|
||||
aria-label="{{.UI.CmdK.Button.AriaLabel}}"
|
||||
data-tooltip="{{.UI.CmdK.Button.Tooltip}}"
|
||||
_="on click set #cmd-k-bar's @open to true">
|
||||
data-tooltip="{{.UI.CmdK.Button.Tooltip}}">
|
||||
<iconify-icon icon="mdi:text-box-search-outline"></iconify-icon>
|
||||
</button>
|
||||
{{end}}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{{define "contact-button"}}
|
||||
<!-- Contact Button (Fixed Left) -->
|
||||
<!-- Uses HTML Invoker Commands API: commandfor + command="show-modal" -->
|
||||
<button
|
||||
id="contact-button"
|
||||
class="fixed-btn contact-btn no-print has-tooltip"
|
||||
onclick="document.getElementById('contact-modal').showModal()"
|
||||
commandfor="contact-modal"
|
||||
command="show-modal"
|
||||
aria-label="{{.UI.Widgets.Contact.AriaLabel}}"
|
||||
data-tooltip="{{.UI.Widgets.Contact.Tooltip}}">
|
||||
<iconify-icon icon="mdi:email-outline"></iconify-icon>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{{define "info-button"}}
|
||||
<!-- Info Button (Bottom Left) -->
|
||||
<!-- Uses HTML Invoker Commands API: commandfor + command="show-modal" -->
|
||||
<button id="info-button" class="info-button no-print has-tooltip"
|
||||
aria-label="{{.UI.Widgets.Info.AriaLabel}}"
|
||||
data-tooltip="{{.UI.Widgets.Info.Tooltip}}"
|
||||
onclick="document.getElementById('info-modal').showModal()">
|
||||
commandfor="info-modal"
|
||||
command="show-modal">
|
||||
<iconify-icon icon="mdi:information-outline"></iconify-icon>
|
||||
</button>
|
||||
{{end}}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{{define "shortcuts-button"}}
|
||||
<!-- Keyboard Shortcuts Button (Fixed Left) -->
|
||||
<!-- Uses HTML Invoker Commands API: commandfor + command="show-modal" -->
|
||||
<button
|
||||
id="shortcuts-button"
|
||||
class="fixed-btn shortcuts-btn no-print has-tooltip"
|
||||
onclick="document.getElementById('shortcuts-modal').showModal()"
|
||||
commandfor="shortcuts-modal"
|
||||
command="show-modal"
|
||||
aria-label="{{.UI.Widgets.Shortcuts.AriaLabel}}"
|
||||
data-tooltip="{{.UI.Widgets.Shortcuts.Tooltip}}">
|
||||
<iconify-icon icon="mdi:keyboard-outline"></iconify-icon>
|
||||
|
||||
Reference in New Issue
Block a user