Initial commit: Rapport Website (Hugo + Hextra)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-26 11:52:03 +02:00
commit e007bdd4e7
480 changed files with 41697 additions and 0 deletions
+89
View File
@@ -0,0 +1,89 @@
.dark .highlight {
/* Background .bg { color: #c9d1d9; background-color: #0d1117; }
/* PreWrapper .chroma { color: #c9d1d9; background-color: #0d1117; } */
/* Other */ .chroma .x { }
/* Error */ .chroma .err { color: #f85149 }
/* CodeLine */ .chroma .cl { }
/* LineLink */ .chroma .lnlinks { outline: none; text-decoration: none; color: inherit }
/* LineTableTD .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; } */
/* LineTable .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; } */
/* LineHighlight .chroma .hl { background-color: #ffffcc } */
/* LineNumbersTable .chroma .lnt { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #64686c } */
/* LineNumbers .chroma .ln { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #6e7681 } */
/* Line */ .chroma .line { display: flex; }
/* Keyword */ .chroma .k { color: #ff7b72 }
/* KeywordConstant */ .chroma .kc { color: #79c0ff }
/* KeywordDeclaration */ .chroma .kd { color: #ff7b72 }
/* KeywordNamespace */ .chroma .kn { color: #ff7b72 }
/* KeywordPseudo */ .chroma .kp { color: #79c0ff }
/* KeywordReserved */ .chroma .kr { color: #ff7b72 }
/* KeywordType */ .chroma .kt { color: #ff7b72 }
/* Name */ .chroma .n { }
/* NameAttribute */ .chroma .na { }
/* NameBuiltin */ .chroma .nb { }
/* NameBuiltinPseudo */ .chroma .bp { }
/* NameClass */ .chroma .nc { color: #f0883e; font-weight: bold }
/* NameConstant */ .chroma .no { color: #79c0ff; font-weight: bold }
/* NameDecorator */ .chroma .nd { color: #d2a8ff; font-weight: bold }
/* NameEntity */ .chroma .ni { color: #ffa657 }
/* NameException */ .chroma .ne { color: #f0883e; font-weight: bold }
/* NameFunction */ .chroma .nf { color: #d2a8ff; font-weight: bold }
/* NameFunctionMagic */ .chroma .fm { }
/* NameLabel */ .chroma .nl { color: #79c0ff; font-weight: bold }
/* NameNamespace */ .chroma .nn { color: #ff7b72 }
/* NameOther */ .chroma .nx { }
/* NameProperty */ .chroma .py { color: #79c0ff }
/* NameTag */ .chroma .nt { color: #7ee787 }
/* NameVariable */ .chroma .nv { color: #79c0ff }
/* NameVariableClass */ .chroma .vc { }
/* NameVariableGlobal */ .chroma .vg { }
/* NameVariableInstance */ .chroma .vi { }
/* NameVariableMagic */ .chroma .vm { }
/* Literal */ .chroma .l { color: #a5d6ff }
/* LiteralDate */ .chroma .ld { color: #79c0ff }
/* LiteralString */ .chroma .s { color: #a5d6ff }
/* LiteralStringAffix */ .chroma .sa { color: #79c0ff }
/* LiteralStringBacktick */ .chroma .sb { color: #a5d6ff }
/* LiteralStringChar */ .chroma .sc { color: #a5d6ff }
/* LiteralStringDelimiter */ .chroma .dl { color: #79c0ff }
/* LiteralStringDoc */ .chroma .sd { color: #a5d6ff }
/* LiteralStringDouble */ .chroma .s2 { color: #a5d6ff }
/* LiteralStringEscape */ .chroma .se { color: #79c0ff }
/* LiteralStringHeredoc */ .chroma .sh { color: #79c0ff }
/* LiteralStringInterpol */ .chroma .si { color: #a5d6ff }
/* LiteralStringOther */ .chroma .sx { color: #a5d6ff }
/* LiteralStringRegex */ .chroma .sr { color: #79c0ff }
/* LiteralStringSingle */ .chroma .s1 { color: #a5d6ff }
/* LiteralStringSymbol */ .chroma .ss { color: #a5d6ff }
/* LiteralNumber */ .chroma .m { color: #a5d6ff }
/* LiteralNumberBin */ .chroma .mb { color: #a5d6ff }
/* LiteralNumberFloat */ .chroma .mf { color: #a5d6ff }
/* LiteralNumberHex */ .chroma .mh { color: #a5d6ff }
/* LiteralNumberInteger */ .chroma .mi { color: #a5d6ff }
/* LiteralNumberIntegerLong */ .chroma .il { color: #a5d6ff }
/* LiteralNumberOct */ .chroma .mo { color: #a5d6ff }
/* Operator */ .chroma .o { color: #ff7b72; font-weight: bold }
/* OperatorWord */ .chroma .ow { color: #ff7b72; font-weight: bold }
/* Punctuation */ .chroma .p { }
/* Comment */ .chroma .c { color: #8b949e; font-style: italic }
/* CommentHashbang */ .chroma .ch { color: #8b949e; font-style: italic }
/* CommentMultiline */ .chroma .cm { color: #8b949e; font-style: italic }
/* CommentSingle */ .chroma .c1 { color: #8b949e; font-style: italic }
/* CommentSpecial */ .chroma .cs { color: #8b949e; font-weight: bold; font-style: italic }
/* CommentPreproc */ .chroma .cp { color: #8b949e; font-weight: bold; font-style: italic }
/* CommentPreprocFile */ .chroma .cpf { color: #8b949e; font-weight: bold; font-style: italic }
/* Generic */ .chroma .g { }
/* GenericDeleted */ .chroma .gd { color: #ffa198; background-color: #490202 }
/* GenericEmph */ .chroma .ge { color: inherit; font-style: italic }
/* GenericError */ .chroma .gr { color: #ffa198 }
/* GenericHeading */ .chroma .gh { color: #79c0ff; font-weight: bold }
/* GenericInserted */ .chroma .gi { color: #56d364; background-color: #0f5323 }
/* GenericOutput */ .chroma .go { color: #8b949e }
/* GenericPrompt */ .chroma .gp { color: #8b949e }
/* GenericStrong */ .chroma .gs { font-weight: bold }
/* GenericSubheading */ .chroma .gu { color: #79c0ff }
/* GenericTraceback */ .chroma .gt { color: #ff7b72 }
/* GenericUnderline */ .chroma .gl { text-decoration: underline }
/* TextWhitespace */ .chroma .w { color: #6e7681 }
}
+90
View File
@@ -0,0 +1,90 @@
/* Light theme for syntax highlight */
/* Generated using `hugo gen chromastyles --style=github` */
.highlight {
/* Background .bg { background-color: #ffffff; } */
/* PreWrapper .chroma { background-color: #ffffff; } */
/* Other .chroma .x { } */
/* Error */ .chroma .err { color: #a61717; background-color: #e3d2d2 }
/* CodeLine .chroma .cl { } */
/* LineLink */ .chroma .lnlinks { outline: none; text-decoration: none; color: inherit }
/* LineTableTD .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; } */
/* LineTable .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; } */
/* LineHighlight .chroma .hl { background-color: #ffffcc } */
/* LineNumbersTable .chroma .lnt { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f } */
/* LineNumbers .chroma .ln { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f } */
/* Line */ .chroma .line { display: flex; }
/* Keyword */ .chroma .k { color: #000000; font-weight: bold }
/* KeywordConstant */ .chroma .kc { color: #000000; font-weight: bold }
/* KeywordDeclaration */ .chroma .kd { color: #000000; font-weight: bold }
/* KeywordNamespace */ .chroma .kn { color: #000000; font-weight: bold }
/* KeywordPseudo */ .chroma .kp { color: #000000; font-weight: bold }
/* KeywordReserved */ .chroma .kr { color: #000000; font-weight: bold }
/* KeywordType */ .chroma .kt { color: #445588; font-weight: bold }
/* Name .chroma .n { } */
/* NameAttribute */ .chroma .na { color: #008080 }
/* NameBuiltin */ .chroma .nb { color: #0086b3 }
/* NameBuiltinPseudo */ .chroma .bp { color: #999999 }
/* NameClass */ .chroma .nc { color: #445588; font-weight: bold }
/* NameConstant */ .chroma .no { color: #008080 }
/* NameDecorator */ .chroma .nd { color: #3c5d5d; font-weight: bold }
/* NameEntity */ .chroma .ni { color: #800080 }
/* NameException */ .chroma .ne { color: #990000; font-weight: bold }
/* NameFunction */ .chroma .nf { color: #990000; font-weight: bold }
/* NameFunctionMagic .chroma .fm { } */
/* NameLabel */ .chroma .nl { color: #990000; font-weight: bold }
/* NameNamespace */ .chroma .nn { color: #555555 }
/* NameOther .chroma .nx { } */
/* NameProperty .chroma .py { } */
/* NameTag */ .chroma .nt { color: #000080 }
/* NameVariable */ .chroma .nv { color: #008080 }
/* NameVariableClass */ .chroma .vc { color: #008080 }
/* NameVariableGlobal */ .chroma .vg { color: #008080 }
/* NameVariableInstance */ .chroma .vi { color: #008080 }
/* NameVariableMagic .chroma .vm { } */
/* Literal .chroma .l { } */
/* LiteralDate .chroma .ld { } */
/* LiteralString */ .chroma .s { color: #dd1144 }
/* LiteralStringAffix */ .chroma .sa { color: #dd1144 }
/* LiteralStringBacktick */ .chroma .sb { color: #dd1144 }
/* LiteralStringChar */ .chroma .sc { color: #dd1144 }
/* LiteralStringDelimiter */ .chroma .dl { color: #dd1144 }
/* LiteralStringDoc */ .chroma .sd { color: #dd1144 }
/* LiteralStringDouble */ .chroma .s2 { color: #dd1144 }
/* LiteralStringEscape */ .chroma .se { color: #dd1144 }
/* LiteralStringHeredoc */ .chroma .sh { color: #dd1144 }
/* LiteralStringInterpol */ .chroma .si { color: #dd1144 }
/* LiteralStringOther */ .chroma .sx { color: #dd1144 }
/* LiteralStringRegex */ .chroma .sr { color: #009926 }
/* LiteralStringSingle */ .chroma .s1 { color: #dd1144 }
/* LiteralStringSymbol */ .chroma .ss { color: #990073 }
/* LiteralNumber */ .chroma .m { color: #009999 }
/* LiteralNumberBin */ .chroma .mb { color: #009999 }
/* LiteralNumberFloat */ .chroma .mf { color: #009999 }
/* LiteralNumberHex */ .chroma .mh { color: #009999 }
/* LiteralNumberInteger */ .chroma .mi { color: #009999 }
/* LiteralNumberIntegerLong */ .chroma .il { color: #009999 }
/* LiteralNumberOct */ .chroma .mo { color: #009999 }
/* Operator */ .chroma .o { color: #000000; font-weight: bold }
/* OperatorWord */ .chroma .ow { color: #000000; font-weight: bold }
/* Punctuation .chroma .p { } */
/* Comment */ .chroma .c { color: #999988; font-style: italic }
/* CommentHashbang */ .chroma .ch { color: #999988; font-style: italic }
/* CommentMultiline */ .chroma .cm { color: #999988; font-style: italic }
/* CommentSingle */ .chroma .c1 { color: #999988; font-style: italic }
/* CommentSpecial */ .chroma .cs { color: #999999; font-weight: bold; font-style: italic }
/* CommentPreproc */ .chroma .cp { color: #999999; font-weight: bold; font-style: italic }
/* CommentPreprocFile */ .chroma .cpf { color: #999999; font-weight: bold; font-style: italic }
/* Generic .chroma .g { } */
/* GenericDeleted */ .chroma .gd { color: #000000; background-color: #ffdddd }
/* GenericEmph */ .chroma .ge { color: #000000; font-style: italic }
/* GenericError */ .chroma .gr { color: #aa0000 }
/* GenericHeading */ .chroma .gh { color: #999999 }
/* GenericInserted */ .chroma .gi { color: #000000; background-color: #ddffdd }
/* GenericOutput */ .chroma .go { color: #888888 }
/* GenericPrompt */ .chroma .gp { color: #555555 }
/* GenericStrong */ .chroma .gs { font-weight: bold }
/* GenericSubheading */ .chroma .gu { color: #aaaaaa }
/* GenericTraceback */ .chroma .gt { color: #aa0000 }
/* GenericUnderline */ .chroma .gl { text-decoration: underline }
/* TextWhitespace */ .chroma .w { color: #bbbbbb }
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
.hextra-archive-timeline {
@apply hx:border-l-2 hx:border-black/15 hx:dark:border-white/15;
}
@@ -0,0 +1,3 @@
.hextra-badge {
@apply hx:inline-flex hx:items-center;
}
@@ -0,0 +1,12 @@
.hextra-banner-hidden .hextra-banner {
display: none;
}
.hextra-banner {
:where(a):not(:where([class~=not-prose],[class~=not-prose] *)) {
@apply hx:underline hx:decoration-from-font;
}
:where(p):not(:where([class~=not-prose],[class~=not-prose] *)) {
@apply hx:leading-7 hx:first:mt-0;
}
}
@@ -0,0 +1,46 @@
.hextra-cards {
grid-template-columns: repeat(auto-fill, minmax(max(250px, calc((100% - 1rem * 2) / var(--hextra-cards-grid-cols))), 1fr));
}
.hextra-card {
position: relative;
}
.hextra-card img {
user-select: none;
}
.hextra-card:hover .hextra-card-icon svg {
color: currentColor;
}
.hextra-card .hextra-card-icon svg {
width: 1.5rem;
color: #00000033;
transition: color 0.3s ease;
}
.hextra-card p {
margin-top: 0.5rem;
position: relative;
}
.dark .hextra-card .hextra-card-icon svg {
color: #ffffff66;
}
.dark .hextra-card:hover .hextra-card-icon svg {
color: currentColor;
}
.hextra-card-tag {
position: absolute;
z-index: 10;
top: 5px;
&:where(:dir(ltr)) {
right: 5px;
}
&:where(:dir(rtl)) {
left: 5px;
}
}
@@ -0,0 +1,7 @@
@supports (
(-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))
) {
.hextra-code-copy-btn {
@apply hx:backdrop-blur-md hx:opacity-85 hx:dark:opacity-80;
}
}
@@ -0,0 +1,5 @@
.hextra-feature-grid {
@media (min-width: 1024px) {
grid-template-columns: repeat(var(--hextra-feature-grid-cols), minmax(0, 1fr));
}
}
@@ -0,0 +1,17 @@
.hextra-jupyter-code-cell {
scrollbar-gutter: auto;
@apply hx:mt-6;
.hextra-jupyter-code-cell-outputs-container {
@apply hx:text-xs hx:overflow-hidden;
.hextra-jupyter-code-cell-outputs {
@apply hx:overflow-auto hx:max-h-[50vh];
pre {
@apply hx:text-xs hx:overflow-auto hx:max-w-full;
}
}
}
}
@@ -0,0 +1,50 @@
nav {
.hextra-search-wrapper {
@apply hx:hidden hx:md:inline-block;
}
}
@supports (
(-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))
) {
.hextra-nav-container-blur {
@apply hx:backdrop-blur-md hx:bg-white/[.85] hx:dark:bg-dark/80!;
}
}
/* Hamburger Menu - Flattened Structure */
.hextra-hamburger-menu svg g {
@apply hx:origin-center hx:transition-all hx:duration-100 hx:ease-out;
}
.hextra-hamburger-menu svg path {
@apply hx:opacity-100 hx:transition-all hx:duration-100 hx:ease-out hx:delay-100;
}
.hextra-hamburger-menu svg.open path {
@apply hx:transition-transform hx:duration-100 hx:ease-out hx:delay-0;
}
.hextra-hamburger-menu svg.open g {
@apply hx:transition-transform hx:duration-100 hx:ease-out hx:delay-100;
}
.hextra-hamburger-menu svg.open > path {
@apply hx:opacity-0;
}
.hextra-hamburger-menu svg.open > g:nth-of-type(1) {
@apply hx:rotate-45;
}
.hextra-hamburger-menu svg.open > g:nth-of-type(1) path {
@apply hx:translate-y-1;
}
.hextra-hamburger-menu svg.open > g:nth-of-type(2) {
@apply hx:-rotate-45;
}
.hextra-hamburger-menu svg.open > g:nth-of-type(2) path {
@apply hx:-translate-y-1;
}
@@ -0,0 +1,21 @@
.hextra-scrollbar, .hextra-scrollbar * {
scrollbar-width: thin; /* Firefox */
scrollbar-color: oklch(55.55% 0 0 / 40%) transparent; /* Firefox */
scrollbar-gutter: stable;
&::-webkit-scrollbar {
@apply hx:w-3 hx:h-3;
}
&::-webkit-scrollbar-track {
@apply hx:bg-transparent;
}
&::-webkit-scrollbar-thumb {
@apply hx:rounded-[10px];
}
&:hover::-webkit-scrollbar-thumb {
border: 3px solid transparent;
background-color: var(--tw-shadow-color);
background-clip: content-box;
@apply hx:shadow-neutral-500/20 hx:hover:shadow-neutral-500/40;
}
}
@@ -0,0 +1,38 @@
.hextra-search-wrapper {
li {
@apply hx:mx-2.5 hx:wrap-break-word hx:rounded-md hx:contrast-more:border hx:text-gray-800 hx:contrast-more:border-transparent hx:dark:text-gray-300;
a {
@apply hx:focus-visible:outline-none hx:block hx:scroll-m-12 hx:px-2.5 hx:py-2;
}
.hextra-search-title {
@apply hx:text-base hx:font-semibold hx:leading-5;
}
.hextra-search-active {
@apply hx:rounded-md hx:bg-primary-500/10 hx:contrast-more:border-primary-500;
}
}
.hextra-search-no-result {
@apply hx:block hx:select-none hx:p-8 hx:text-center hx:text-sm hx:text-gray-400;
}
.hextra-search-prefix {
@apply hx:mx-2.5 hx:mb-2 hx:mt-6 hx:select-none hx:border-b hx:border-black/10 hx:px-2.5 hx:pb-1.5 hx:text-xs hx:font-semibold
hx:uppercase hx:text-gray-500 hx:first:mt-0 hx:dark:border-white/20 hx:dark:text-gray-300 hx:contrast-more:border-gray-600
hx:contrast-more:text-gray-900 hx:contrast-more:dark:border-gray-50 hx:contrast-more:dark:text-gray-50;
}
.hextra-search-excerpt {
@apply hx:overflow-hidden hx:text-ellipsis hx:mt-1 hx:text-sm hx:leading-[1.35rem] hx:text-gray-600 hx:dark:text-gray-400 hx:contrast-more:dark:text-gray-50;
display: -webkit-box;
line-clamp: 1;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
}
.hextra-search-match {
@apply hx:text-primary-600;
}
}
@@ -0,0 +1,21 @@
@media (max-width: 48rem) {
.hextra-sidebar-container {
@apply hx:fixed hx:pt-[calc(var(--navbar-height)+var(--hextra-banner-height))] hx:top-0 hx:w-full hx:bottom-0 hx:z-15 hx:overscroll-contain hx:bg-white hx:dark:bg-dark;
transition: transform 0.4s cubic-bezier(0.52, 0.16, 0.04, 1);
will-change: transform, opacity;
contain: layout style;
backface-visibility: hidden;
}
}
.hextra-sidebar-container {
li > .hextra-sidebar-children {
@apply hx:h-0;
}
li.open > .hextra-sidebar-children {
@apply hx:h-auto hx:pt-1;
}
li.open > .hextra-sidebar-item > .hextra-sidebar-collapsible-button > svg > path {
@apply hx:rotate-90;
}
}
@@ -0,0 +1,22 @@
.hextra-steps {
:where(h2, h3, h4, h5, h6):not(.no-step-marker) {
counter-increment: step;
@apply hx:ltr:before:ml-[-41px] hx:rtl:before:mr-[-44px];
/* https://github.com/tailwindlabs/tailwindcss/issues/15597#issuecomment-2582673546 */
@apply hx:before:bg-gray-100 hx:dark:before:bg-neutral-800;
@apply hx:before:border-4 hx:before:border-white hx:dark:before:border-dark;
&:before {
content: counter(step);
@apply hx:absolute hx:size-[33px];
@apply hx:rounded-full hx:text-neutral-400 hx:text-base hx:font-normal hx:text-center hx:-indent-px;
}
}
}
:lang(fa) .hextra-steps {
:where(h2, h3, h4, h5, h6):not(.no-step-marker) {
&:before {
content: counter(step, persian);
}
}
}
@@ -0,0 +1,4 @@
/* Table of Contents Scroll Spy Styles */
.hextra-toc a.hextra-toc-active {
@apply hx:text-gray-900! hx:dark:text-gray-50! hx:transition-all hx:duration-200;
}
View File
+53
View File
@@ -0,0 +1,53 @@
/* Code syntax highlight */
@import "./chroma/light.css";
@import "./chroma/dark.css";
.hextra-code-block {
@apply hx:text-[.9em] hx:leading-5;
pre {
@apply hx:text-[.9em] hx:bg-primary-700/5 hx:overflow-x-auto hx:font-medium hx:subpixel-antialiased hx:dark:bg-primary-300/10 hx:contrast-more:border hx:contrast-more:border-primary-900/20 hx:contrast-more:contrast-150 hx:contrast-more:dark:border-primary-100/40;
}
.hextra-code-filename {
@apply hx:absolute hx:top-0 hx:z-1 hx:w-full hx:truncate hx:rounded-t-xl hx:bg-primary-700/5 hx:py-2 hx:px-4 hx:text-xs hx:text-gray-700 hx:dark:bg-primary-300/10 hx:dark:text-gray-200;
}
.hextra-code-filename + pre:not(.lntable pre) {
/* Override padding for code blocks with filename but no highlight */
@apply hx:pt-12;
}
}
.hextra-code-block pre:not(.lntable pre) {
@apply hx:px-4 hx:mb-4 hx:py-4 hx:rounded-xl;
}
.hextra-code-block div:nth-of-type(2) pre {
@apply hx:pt-12 hx:pb-4;
}
.chroma {
.lntable {
@apply hx:m-0 hx:block hx:w-auto hx:overflow-auto hx:rounded-xl;
pre {
@apply hx:pt-4 hx:pb-4;
}
}
.ln,
.lnt:not(.hl > .lnt),
.hl:not(.line) {
@apply hx:pl-4 hx:pr-4 hx:min-w-[2.6rem] hx:text-neutral-600 hx:dark:text-neutral-300;
}
.lntd {
@apply hx:p-0 hx:align-top;
}
.lntd:last-of-type {
@apply hx:w-full;
}
/* LineHighlight */
.hl {
@apply hx:block hx:w-full hx:bg-primary-800/10;
}
}
+1
View File
@@ -0,0 +1 @@
hx:max-w-full
+91
View File
@@ -0,0 +1,91 @@
@import "tailwindcss" prefix(hx);
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--color-primary-50: hsl(var(--primary-hue) var(--primary-saturation) calc(var(--primary-lightness) + calc(calc(100% - var(--primary-lightness)) / 50) * 47));
--color-primary-100: hsl(var(--primary-hue) var(--primary-saturation) calc(var(--primary-lightness) + calc(calc(100% - var(--primary-lightness)) / 50) * 44));
--color-primary-200: hsl(var(--primary-hue) var(--primary-saturation) calc(var(--primary-lightness) + calc(calc(100% - var(--primary-lightness)) / 50) * 36));
--color-primary-300: hsl(var(--primary-hue) var(--primary-saturation) calc(var(--primary-lightness) + calc(calc(100% - var(--primary-lightness)) / 50) * 27));
--color-primary-400: hsl(var(--primary-hue) var(--primary-saturation) calc(var(--primary-lightness) + calc(calc(100% - var(--primary-lightness)) / 50) * 16));
--color-primary-500: hsl(var(--primary-hue) var(--primary-saturation) var(--primary-lightness));
--color-primary-600: hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness) / 50) * 45));
--color-primary-700: hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness) / 50) * 39));
--color-primary-800: hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness) / 50) * 32));
--color-primary-900: hsl(var(--primary-hue) var(--primary-saturation) calc(calc(var(--primary-lightness) / 50) * 24));
--color-dark: #111;
}
html {
@apply hx:text-base hx:antialiased;
}
body {
@apply hx:w-full hx:bg-white hx:dark:bg-dark hx:dark:text-gray-100;
}
:root {
--primary-hue: 212deg;
--primary-saturation: 100%;
--primary-lightness: 50%;
--navbar-height: 4rem;
--hextra-banner-height: 2rem;
--menu-height: 3.75rem; /* 60px */
}
.dark {
--primary-hue: 204deg;
--primary-saturation: 100%;
--primary-lightness: 50%;
}
@utility hextra-focus {
@apply hx:outline-none hx:ring-2 hx:ring-primary-200 hx:ring-offset-1 hx:ring-offset-primary-300 hx:dark:ring-primary-800 hx:dark:ring-offset-primary-700;
}
@utility hextra-focus-visible {
@apply hx:focus-visible:outline-none hx:focus-visible:ring-2 hx:focus-visible:ring-primary-200 hx:focus-visible:ring-offset-1 hx:focus-visible:ring-offset-primary-300 hx:dark:focus-visible:ring-primary-800 hx:dark:focus-visible:ring-offset-primary-700;
}
@utility hextra-focus-visible-inset {
@apply hx:focus-visible:outline-none hx:focus-visible:ring-inset hx:focus-visible:ring-2 hx:focus-visible:ring-primary-200 hx:dark:focus-visible:ring-primary-800 hx:focus-visible:ring-offset-0;
}
@layer base {
abbr:where([title]) {
cursor: help;
}
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
@layer base {
:where(a, button, [role="tab"], [role="menuitem"], [role="menuitemradio"], input, select, textarea, [tabindex="0"]):not(
[class*="hextra-focus-visible"]
):focus-visible {
@apply hx:hextra-focus;
}
}
@import "./typography.css";
@import "./highlight.css";
@import "./components/cards.css";
@import "./components/steps.css";
@import "./components/search.css";
@import "./components/sidebar.css";
@import "./components/banner.css";
@import "./components/navbar.css";
@import "./components/scrollbar.css";
@import "./components/code-copy.css";
@import "./components/hextra/feature-grid.css";
@import "./components/jupyter.css";
@import "./components/badge.css";
@import "./components/toc.css";
@import "./components/archives.css";
+147
View File
@@ -0,0 +1,147 @@
.content {
:where(h1):not(:where([class~=not-prose],[class~=not-prose] *)) {
@apply hx:mt-2 hx:text-4xl hx:font-bold hx:tracking-tight hx:text-slate-900 hx:dark:text-slate-100;
}
:where(h2):not(:where([class~=not-prose],[class~=not-prose] *)) {
@apply hx:font-semibold hx:tracking-tight hx:text-slate-900 hx:dark:text-slate-100 hx:mt-10 hx:border-b hx:pb-1 hx:text-3xl hx:border-neutral-200/70 hx:contrast-more:border-neutral-400 hx:dark:border-primary-100/10 hx:contrast-more:dark:border-neutral-400;
}
:where(h3):not(:where([class~=not-prose],[class~=not-prose] *)) {
@apply hx:font-semibold hx:tracking-tight hx:text-slate-900 hx:dark:text-slate-100 hx:mt-8 hx:text-2xl;
}
:where(h4):not(:where([class~=not-prose],[class~=not-prose] *)) {
@apply hx:font-semibold hx:tracking-tight hx:text-slate-900 hx:dark:text-slate-100 hx:mt-8 hx:text-xl;
}
:where(h5):not(:where([class~=not-prose],[class~=not-prose] *)) {
@apply hx:font-semibold hx:tracking-tight hx:text-slate-900 hx:dark:text-slate-100 hx:mt-8 hx:text-lg;
}
:where(h6):not(:where([class~=not-prose],[class~=not-prose] *)) {
@apply hx:font-semibold hx:tracking-tight hx:text-slate-900 hx:dark:text-slate-100 hx:mt-8 hx:text-base;
}
:where(p):not(:where([class~=not-prose],[class~=not-prose] *)) {
@apply hx:mt-6 hx:leading-7 hx:first:mt-0;
}
:where(a):not(:where([class~=not-prose],[class~=not-prose] *)) {
@apply hx:text-primary-600 hx:underline hx:decoration-from-font;
}
:where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)) {
@apply hx:mt-6 hx:border-gray-300 hx:italic hx:text-gray-700 hx:dark:border-gray-700 hx:dark:text-gray-400 hx:first:mt-0 hx:ltr:border-l-2 hx:ltr:pl-6 hx:rtl:border-r-2 hx:rtl:pr-6;
}
:where(pre):not(:where(.hextra-code-block pre, [class~=not-prose],[class~=not-prose] *)) {
@apply hx:bg-primary-700/5 hx:mb-4 hx:overflow-x-auto hx:rounded-xl hx:font-medium hx:subpixel-antialiased hx:dark:bg-primary-300/10 hx:text-[.9em] hx:contrast-more:border hx:contrast-more:border-primary-900/20 hx:contrast-more:contrast-150 hx:contrast-more:dark:border-primary-100/40 hx:py-4;
}
:where(code):not(:where(.hextra-code-block code, [class~=not-prose],[class~=not-prose] *)) {
@apply hx:border-black/4 hx:bg-black/3 hx:wrap-break-word hx:rounded-md hx:border hx:py-0.5 hx:px-[.25em] hx:text-[.9em] hx:dark:border-white/10 hx:dark:bg-white/10;
}
:where(table):not(:where(.hextra-code-block table, [class~=not-prose],[class~=not-prose] *)) {
@apply hx:block hx:overflow-x-auto hx:my-6 hx:p-0 hx:first:mt-0 hx:w-full hx:text-sm hx:leading-5 hx:border-collapse;
thead {
@apply hx:bg-gray-50 hx:dark:bg-gray-600/20;
}
tr {
@apply hx:m-0 hx:border-t hx:border-gray-300 hx:p-0 hx:dark:border-gray-600;
}
th {
@apply hx:m-0 hx:border hx:border-gray-300 hx:p-2 hx:font-semibold hx:dark:border-gray-600;
}
td {
@apply hx:m-0 hx:border hx:border-gray-300 hx:p-2 hx:dark:border-gray-600;
}
}
:where(ol):not(:where([class~=not-prose],[class~=not-prose] *)) {
@apply hx:mt-6 hx:list-decimal hx:first:mt-0 hx:ltr:ml-6 hx:rtl:mr-6;
li {
@apply hx:my-2;
}
}
:where(ul):not(:where([class~=not-prose],[class~=not-prose] *)) {
@apply hx:mt-6 hx:list-disc hx:first:mt-0 hx:ltr:ml-6 hx:rtl:mr-6;
li {
@apply hx:my-2;
}
}
/* Task lists - hide list markers for lists containing checkboxes */
:where(ul):not(:where([class~=not-prose],[class~=not-prose] *)):has(li input[type="checkbox"]) {
@apply hx:list-none;
}
/* This CSS rule targets the first nested unordered (ul) or ordered (ol) list
inside the list item (li) of any parent ul or ol.
The rule sets the top margin of the selected list to zero. */
:where(ul, ol) > li > :where(ul, ol):not(:where([class~=not-prose],[class~=not-prose] *)) {
@apply hx:mt-0;
}
:where(kbd):not(:where([class~=not-prose],[class~=not-prose] *)) {
@apply hx:border-black/4 hx:bg-black/3 hx:wrap-break-word hx:rounded-md hx:border hx:py-0.5 hx:px-[.25em] hx:text-[.9em] hx:dark:border-white/10 hx:dark:bg-white/10;
}
:where(pre.mermaid):not(:where(.hextra-code-block pre, [class~=not-prose],[class~=not-prose] *)) {
@apply hx:bg-transparent hx:rounded-none hx:dark:bg-transparent;
}
:where(img):not(:where([class~=not-prose],[class~=not-prose] *)) {
@apply hx:mx-auto hx:my-4 hx:rounded-md;
}
:where(figure):not(:where([class~=not-prose],[class~=not-prose] *)) {
figcaption {
@apply hx:text-sm hx:text-gray-500 hx:dark:text-gray-400 hx:mt-2 hx:block hx:text-center;
}
}
/* Definition list */
:where(dl):not(:where([class~=not-prose],[class~=not-prose] *)) {
dt {
@apply hx:mt-6 hx:font-semibold;
}
dd {
@apply hx:my-2 hx:ps-6;
}
}
/* Horizontal line */
:where(hr):not(:where([class~=not-prose],[class~=not-prose] *)) {
@apply hx:my-10 hx:first:mt-0 hx:last:mb-0 hx:border-gray-200 hx:dark:border-neutral-800;
}
.footnotes {
@apply hx:mt-12 hx:text-sm;
hr {
@apply hx:border-gray-200 hx:dark:border-neutral-800;
}
}
.subheading-anchor {
@apply hx:opacity-0 hx:transition-opacity hx:ltr:ml-1 hx:rtl:mr-1;
span:target + &,
:hover > &,
&:focus-visible {
@apply hx:opacity-100;
}
span + &,
:hover > & {
@apply hx:no-underline!;
}
@apply hx:after:text-gray-300 hx:dark:after:text-neutral-700;
&:after {
@apply hx:content-['#'] hx:px-1;
span:target + & {
@apply hx:text-gray-400;
@apply hx:dark:text-neutral-500;
}
}
}
}
article details > summary {
&::-webkit-details-marker {
@apply hx:hidden;
}
&::before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='hx:h-5 hx:w-5' viewBox='0 0 20 20' fill='currentColor'%3E%3Cpath fill-rule='evenodd' d='M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z' clip-rule='evenodd' /%3E%3C/svg%3E");
height: 1.2em;
width: 1.2em;
vertical-align: -4px;
padding: 0 0.6em;
}
}
:lang(fa) ol {
list-style-type: persian;
}
+35
View File
@@ -0,0 +1,35 @@
/* Hugo template to derive CSS variables from site and page parameters */
/* Do not remove the following comment. It is used by Hugo to render CSS variables.
{{- $layoutWidthValues := dict "normal" "80rem" "wide" "90rem" "full" "100%" -}}
{{- $layoutWidthDefault := index $layoutWidthValues "normal" -}}
{{- $maxPageWidth := (index $layoutWidthValues (site.Params.page.width | default "normal")) | default $layoutWidthDefault -}}
{{- $maxNavbarWidth := (index $layoutWidthValues (site.Params.navbar.width | default "normal")) | default $layoutWidthDefault -}}
{{- $maxFooterWidth := (index $layoutWidthValues (site.Params.footer.width | default "normal")) | default $layoutWidthDefault -}}
{{- $maxContentWidth := "72rem" -}}
*/
:root {
--hextra-max-page-width: {{ $maxPageWidth }};
--hextra-max-content-width: {{ $maxContentWidth }};
--hextra-max-navbar-width: {{ $maxNavbarWidth }};
--hextra-max-footer-width: {{ $maxFooterWidth }};
}
.hextra-max-page-width {
max-width: var(--hextra-max-page-width);
}
.hextra-max-content-width {
max-width: var(--hextra-max-content-width);
}
.hextra-max-navbar-width {
max-width: var(--hextra-max-navbar-width);
}
.hextra-max-footer-width {
max-width: var(--hextra-max-footer-width);
}
@@ -0,0 +1,26 @@
// Back to top button
document.addEventListener("DOMContentLoaded", function () {
const backToTop = document.querySelector("#backToTop");
if (backToTop) {
backToTop.addEventListener("click", scrollUp);
document.addEventListener("scroll", (e) => {
if (window.scrollY > 300) {
backToTop.classList.remove("hx:opacity-0");
backToTop.removeAttribute("tabindex");
} else {
backToTop.classList.add("hx:opacity-0");
backToTop.setAttribute("tabindex", "-1");
}
});
}
});
function scrollUp() {
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
window.scroll({
top: 0,
left: 0,
behavior: prefersReducedMotion ? 'auto' : 'smooth',
});
}
+15
View File
@@ -0,0 +1,15 @@
// {{- if site.Params.banner }}
(function () {
const banner = document.querySelector(".hextra-banner")
document.documentElement.style.setProperty("--hextra-banner-height", banner.clientHeight+"px");
const closeBtn = banner.querySelector(".hextra-banner-close-button");
closeBtn.addEventListener("click", () => {
document.documentElement.classList.add("hextra-banner-hidden");
document.documentElement.style.setProperty("--hextra-banner-height", "0px");
localStorage.setItem('{{ site.Params.banner.key | default `banner-closed` }}', "0");
});
})();
// {{- end -}}
+91
View File
@@ -0,0 +1,91 @@
// Copy button for code blocks
document.addEventListener('DOMContentLoaded', function () {
const getCopyIcon = () => {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.innerHTML = `
<path stroke-linecap="round" stroke-linejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
`;
svg.setAttribute('fill', 'none');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('stroke', 'currentColor');
svg.setAttribute('stroke-width', '2');
return svg;
}
const getSuccessIcon = () => {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.innerHTML = `
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
`;
svg.setAttribute('fill', 'none');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('stroke', 'currentColor');
svg.setAttribute('stroke-width', '2');
return svg;
}
// Make scrollable code blocks focusable for keyboard users.
const updateScrollableCodeBlocks = () => {
document.querySelectorAll('.hextra-code-block pre, .highlight pre').forEach(function (pre) {
if (pre.scrollWidth > pre.clientWidth) {
pre.setAttribute('tabindex', '0');
} else {
pre.removeAttribute('tabindex');
}
});
};
updateScrollableCodeBlocks();
let resizeRaf;
window.addEventListener('resize', () => {
if (resizeRaf) {
cancelAnimationFrame(resizeRaf);
}
resizeRaf = requestAnimationFrame(updateScrollableCodeBlocks);
});
document.querySelectorAll('.hextra-code-copy-btn').forEach(function (button) {
// Add copy and success icons
button.querySelector('.hextra-copy-icon')?.appendChild(getCopyIcon());
button.querySelector('.hextra-success-icon')?.appendChild(getSuccessIcon());
// Add click event listener for copy button
button.addEventListener('click', function (e) {
e.preventDefault();
// Get the code target
const target = button.parentElement.previousElementSibling;
let codeElement;
if (target.tagName === 'CODE') {
codeElement = target;
} else {
// Select the last code element in case line numbers are present
const codeElements = target.querySelectorAll('code');
codeElement = codeElements[codeElements.length - 1];
}
if (codeElement) {
let code = codeElement.innerText;
// Replace double newlines with single newlines in the innerText
// as each line inside <span> has trailing newline '\n'
if ("lang" in codeElement.dataset) {
code = code.replace(/\n\n/g, '\n');
}
navigator.clipboard.writeText(code).then(function () {
button.classList.add('copied');
var originalLabel = button.getAttribute('aria-label');
var copiedLabel = button.dataset.copiedLabel || 'Copied!';
button.setAttribute('aria-label', copiedLabel);
setTimeout(function () {
button.classList.remove('copied');
button.setAttribute('aria-label', originalLabel);
}, 1000);
}).catch(function (err) {
console.error('Failed to copy text: ', err);
});
} else {
console.error('Target element not found');
}
});
});
});
+22
View File
@@ -0,0 +1,22 @@
// {{ $faviconDarkExists := fileExists (path.Join "static" "favicon-dark.svg") }}
(function () {
const faviconEl = document.getElementById("favicon-svg");
const faviconDarkExists = "{{ $faviconDarkExists }}" === "true";
if (faviconEl && faviconDarkExists) {
const lightFavicon = '{{ "favicon.svg" | relURL }}';
const darkFavicon = '{{ "favicon-dark.svg" | relURL }}';
const darkModeQuery = window.matchMedia("(prefers-color-scheme: dark)");
function updateFavicon(e) {
faviconEl.href = e.matches ? darkFavicon : lightFavicon;
}
// Set favicon on load
updateFavicon(darkModeQuery);
// Listen for system preference changes
darkModeQuery.addEventListener("change", updateFavicon);
}
})();
+15
View File
@@ -0,0 +1,15 @@
// Script for filetree shortcode collapsing/expanding folders used in the theme
// ======================================================================
document.addEventListener("DOMContentLoaded", function () {
const folders = document.querySelectorAll(".hextra-filetree-folder");
folders.forEach(function (folder) {
folder.addEventListener("click", function () {
Array.from(folder.children).forEach(function (el) {
el.dataset.state = el.dataset.state === "open" ? "closed" : "open";
});
var newState = folder.nextElementSibling.dataset.state === "open" ? "closed" : "open";
folder.nextElementSibling.dataset.state = newState;
folder.setAttribute('aria-expanded', newState === 'open' ? 'true' : 'false');
});
});
});
+103
View File
@@ -0,0 +1,103 @@
(function () {
const languageSwitchers = document.querySelectorAll('.hextra-language-switcher');
const closeSwitcher = (switcher, focusSwitcher = false) => {
switcher.dataset.state = 'closed';
switcher.setAttribute('aria-expanded', 'false');
const optionsElement = switcher.nextElementSibling;
optionsElement.classList.add('hx:hidden');
if (focusSwitcher) {
switcher.focus();
}
};
const openSwitcher = (switcher, focusTarget = "none") => {
switcher.dataset.state = 'open';
switcher.setAttribute('aria-expanded', 'true');
const optionsElement = switcher.nextElementSibling;
if (optionsElement.classList.contains('hx:hidden')) {
toggleMenu(switcher);
} else {
resizeMenu(switcher);
}
if (focusTarget !== "none") {
const items = Array.from(optionsElement.querySelectorAll('[role="menuitem"]'));
if (items.length > 0) {
const target = focusTarget === "last" ? items[items.length - 1] : items[0];
target.focus();
}
}
};
languageSwitchers.forEach((switcher) => {
switcher.addEventListener('click', (e) => {
e.preventDefault();
if (switcher.dataset.state === 'open') {
closeSwitcher(switcher);
} else {
openSwitcher(switcher);
}
});
switcher.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown') {
e.preventDefault();
openSwitcher(switcher, 'first');
} else if (e.key === 'ArrowUp') {
e.preventDefault();
openSwitcher(switcher, 'last');
}
});
});
document.querySelectorAll('.hextra-language-options[role=menu]').forEach((menu) => {
menu.addEventListener('keydown', (e) => {
const items = Array.from(menu.querySelectorAll('[role="menuitem"]'));
if (items.length === 0) return;
const currentIndex = items.indexOf(document.activeElement);
let newIndex;
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
newIndex = (currentIndex + 1) % items.length;
items[newIndex].focus();
break;
case 'ArrowUp':
e.preventDefault();
newIndex = (currentIndex - 1 + items.length) % items.length;
items[newIndex].focus();
break;
case 'Home':
e.preventDefault();
items[0].focus();
break;
case 'End':
e.preventDefault();
items[items.length - 1].focus();
break;
case 'Escape': {
e.preventDefault();
const switcher = menu.previousElementSibling;
if (switcher) {
closeSwitcher(switcher, true);
}
break;
}
}
});
});
window.addEventListener("resize", () => languageSwitchers.forEach(resizeMenu));
// Dismiss language switcher when clicking outside.
document.addEventListener('click', (e) => {
if (!e.target.closest('.hextra-language-switcher') && !e.target.closest('.hextra-language-options')) {
languageSwitchers.forEach((switcher) => {
closeSwitcher(switcher);
});
}
});
})();
+84
View File
@@ -0,0 +1,84 @@
// Hamburger menu for mobile navigation
document.addEventListener('DOMContentLoaded', function () {
const menu = document.querySelector('.hextra-hamburger-menu');
const sidebarContainer = document.querySelector('.hextra-sidebar-container');
const mobileQuery = window.matchMedia('(max-width: 767px)');
function isMenuOpen() {
return menu.querySelector('svg').classList.contains('open');
}
// On mobile, the sidebar is off-screen so hide it from assistive tech
function syncAriaHidden() {
if (mobileQuery.matches) {
sidebarContainer.setAttribute('aria-hidden', isMenuOpen() ? 'false' : 'true');
} else {
sidebarContainer.removeAttribute('aria-hidden');
}
}
// Set initial state
syncAriaHidden();
mobileQuery.addEventListener('change', syncAriaHidden);
function toggleMenu(options = {}) {
const { focusOnOpen = true } = options;
// Toggle the hamburger menu
menu.querySelector('svg').classList.toggle('open');
// When the menu is open, we want to show the navigation sidebar
sidebarContainer.classList.toggle('hx:max-md:[transform:translate3d(0,-100%,0)]');
sidebarContainer.classList.toggle('hx:max-md:[transform:translate3d(0,0,0)]');
// When the menu is open, we want to prevent the body from scrolling
document.body.classList.toggle('hx:overflow-hidden');
document.body.classList.toggle('hx:md:overflow-auto');
// Sync aria-expanded and aria-hidden
const isOpen = isMenuOpen();
menu.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
syncAriaHidden();
// Move focus into sidebar when opening, restore when closing
if (isOpen) {
if (focusOnOpen) {
const firstFocusable = sidebarContainer.querySelector('a, button, input, [tabindex="0"]');
if (firstFocusable) firstFocusable.focus();
}
} else {
menu.focus();
}
}
menu.addEventListener('click', (e) => {
e.preventDefault();
// Pointer-initiated clicks on mobile should not force focus into the search input,
// which opens the software keyboard immediately.
toggleMenu({ focusOnOpen: e.detail === 0 });
});
// Close menu on Escape key (mobile only)
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && mobileQuery.matches && isMenuOpen()) {
toggleMenu();
}
});
// Select all anchor tags in the sidebar container
const sidebarLinks = sidebarContainer.querySelectorAll('a');
// Add click event listener to each anchor tag
sidebarLinks.forEach(link => {
link.addEventListener('click', (e) => {
// Check if the href attribute contains a hash symbol (links to a heading)
if (link.getAttribute('href') && link.getAttribute('href').startsWith('#')) {
// Only dismiss overlay on mobile view
if (window.innerWidth < 768) {
toggleMenu();
}
}
});
});
});
+126
View File
@@ -0,0 +1,126 @@
(function () {
const hiddenClass = "hx:hidden";
const dropdownToggles = document.querySelectorAll(".hextra-nav-menu-toggle");
const closeDropdown = (toggle, focusToggle = false) => {
toggle.dataset.state = "closed";
toggle.setAttribute("aria-expanded", "false");
const menuItemsElement = toggle.nextElementSibling;
menuItemsElement.classList.add(hiddenClass);
if (focusToggle) {
toggle.focus();
}
};
const openDropdown = (toggle, focusTarget = "none") => {
// Close all other dropdowns first.
dropdownToggles.forEach((otherToggle) => {
if (otherToggle !== toggle) {
closeDropdown(otherToggle);
}
});
toggle.dataset.state = "open";
toggle.setAttribute("aria-expanded", "true");
const menuItemsElement = toggle.nextElementSibling;
// Position dropdown centered with toggle.
menuItemsElement.style.position = "absolute";
menuItemsElement.style.top = "100%";
menuItemsElement.style.left = "50%";
menuItemsElement.style.transform = "translateX(-50%)";
menuItemsElement.style.zIndex = "1000";
menuItemsElement.classList.remove(hiddenClass);
if (focusTarget !== "none") {
const items = Array.from(menuItemsElement.querySelectorAll('[role="menuitem"]'));
if (items.length > 0) {
const target = focusTarget === "last" ? items[items.length - 1] : items[0];
target.focus();
}
}
};
dropdownToggles.forEach((toggle) => {
toggle.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
// Toggle current dropdown.
const isOpen = toggle.dataset.state === "open";
if (isOpen) {
closeDropdown(toggle);
} else {
openDropdown(toggle);
}
});
toggle.addEventListener("keydown", (e) => {
if (e.key === "ArrowDown") {
e.preventDefault();
openDropdown(toggle, "first");
} else if (e.key === "ArrowUp") {
e.preventDefault();
openDropdown(toggle, "last");
}
});
});
document.querySelectorAll(".hextra-nav-menu-items[role=menu]").forEach((menu) => {
menu.addEventListener("keydown", (e) => {
const items = Array.from(menu.querySelectorAll('[role="menuitem"]'));
if (items.length === 0) return;
const currentIndex = items.indexOf(document.activeElement);
let newIndex;
switch (e.key) {
case "ArrowDown":
e.preventDefault();
newIndex = (currentIndex + 1) % items.length;
items[newIndex].focus();
break;
case "ArrowUp":
e.preventDefault();
newIndex = (currentIndex - 1 + items.length) % items.length;
items[newIndex].focus();
break;
case "Home":
e.preventDefault();
items[0].focus();
break;
case "End":
e.preventDefault();
items[items.length - 1].focus();
break;
case "Escape": {
e.preventDefault();
const toggle = menu.previousElementSibling;
if (toggle) {
closeDropdown(toggle, true);
}
break;
}
}
});
});
// Dismiss dropdown when clicking outside.
document.addEventListener("click", (e) => {
if (!e.target.closest(".hextra-nav-menu-toggle") && !e.target.closest(".hextra-nav-menu-items")) {
dropdownToggles.forEach((toggle) => {
closeDropdown(toggle);
});
}
});
// Close dropdowns on escape key.
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
dropdownToggles.forEach((toggle) => {
if (toggle.dataset.state === "open") {
closeDropdown(toggle, true);
}
});
}
});
})();
@@ -0,0 +1,172 @@
document.addEventListener('DOMContentLoaded', () => {
// Pre-fetch markdown content for all copy buttons to avoid Safari NotAllowedError
// Safari requires clipboard writes to happen synchronously within user gesture
const copyButtons = document.querySelectorAll('.hextra-page-context-menu-copy');
const contentCache = new Map();
// Pre-fetch content for each button on page load
copyButtons.forEach(button => {
const url = button.dataset.url;
if (url) {
fetch(url)
.then(response => {
if (response.ok) return response.text();
throw new Error('Failed to fetch');
})
.then(markdown => contentCache.set(url, markdown))
.catch(error => console.error('Failed to pre-fetch markdown:', error));
}
});
// Initialize copy buttons with synchronous clipboard access
copyButtons.forEach(button => {
button.addEventListener('click', () => {
const url = button.dataset.url;
const markdown = contentCache.get(url);
if (markdown) {
// Synchronous clipboard write initiation - works in Safari
navigator.clipboard.writeText(markdown)
.then(() => {
button.classList.add('copied');
setTimeout(() => button.classList.remove('copied'), 1000);
})
.catch(error => console.error('Failed to copy markdown:', error));
} else {
// Fallback: fetch and copy (may fail in Safari if content not pre-fetched)
fetch(url)
.then(response => {
if (!response.ok) throw new Error('Failed to fetch');
return response.text();
})
.then(text => {
contentCache.set(url, text);
return navigator.clipboard.writeText(text);
})
.then(() => {
button.classList.add('copied');
setTimeout(() => button.classList.remove('copied'), 1000);
})
.catch(error => console.error('Failed to copy markdown:', error));
}
});
});
// Initialize dropdown toggles
const dropdownToggles = document.querySelectorAll('.hextra-page-context-menu-toggle');
dropdownToggles.forEach(toggle => {
const container = toggle.closest('.hextra-page-context-menu');
const menu = container.querySelector('.hextra-page-context-menu-dropdown');
const chevron = toggle.querySelector('[data-chevron]');
toggle.addEventListener('click', (e) => {
e.stopPropagation();
const isOpen = toggle.dataset.state === 'open';
// Close all other dropdowns first
dropdownToggles.forEach(t => {
if (t !== toggle) {
t.dataset.state = 'closed';
t.setAttribute('aria-expanded', 'false');
const otherContainer = t.closest('.hextra-page-context-menu');
const otherMenu = otherContainer.querySelector('.hextra-page-context-menu-dropdown');
const otherChevron = t.querySelector('[data-chevron]');
otherMenu.classList.add('hx:hidden');
if (otherChevron) {
otherChevron.style.transform = '';
}
}
});
// Toggle current
toggle.dataset.state = isOpen ? 'closed' : 'open';
toggle.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
menu.classList.toggle('hx:hidden', isOpen);
// Rotate chevron icon
if (chevron) {
chevron.style.transform = isOpen ? '' : 'rotate(180deg)';
}
});
});
// Close dropdown when clicking outside
document.addEventListener('click', (e) => {
// Check if click is outside any dropdown container
const isOutside = !e.target.closest('.hextra-page-context-menu');
if (isOutside) {
dropdownToggles.forEach(toggle => {
toggle.dataset.state = 'closed';
toggle.setAttribute('aria-expanded', 'false');
const container = toggle.closest('.hextra-page-context-menu');
const menu = container.querySelector('.hextra-page-context-menu-dropdown');
const chevron = toggle.querySelector('[data-chevron]');
menu.classList.add('hx:hidden');
if (chevron) {
chevron.style.transform = '';
}
});
}
});
// Close dropdown on Escape key and return focus to toggle
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
dropdownToggles.forEach(toggle => {
if (toggle.dataset.state === 'open') {
const container = toggle.closest('.hextra-page-context-menu');
closeDropdown(container);
toggle.focus();
}
});
}
});
// Helper to close dropdown
const closeDropdown = (container) => {
if (!container) return;
const toggle = container.querySelector('.hextra-page-context-menu-toggle');
const menu = container.querySelector('.hextra-page-context-menu-dropdown');
if (!toggle || !menu) return;
const chevron = toggle.querySelector('[data-chevron]');
toggle.dataset.state = 'closed';
toggle.setAttribute('aria-expanded', 'false');
menu.classList.add('hx:hidden');
if (chevron) {
chevron.style.transform = '';
}
};
// Handle dropdown menu copy action
document.querySelectorAll('.hextra-page-context-menu-dropdown button[data-action="copy"]').forEach(btn => {
btn.addEventListener('click', async (e) => {
e.stopPropagation();
const container = btn.closest('.hextra-page-context-menu');
if (!container) return;
const copyBtn = container.querySelector('.hextra-page-context-menu-copy');
if (!copyBtn) return;
closeDropdown(container);
copyBtn.click();
});
});
// Handle dropdown menu view action
document.querySelectorAll('.hextra-page-context-menu-dropdown button[data-action="view"]').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const container = btn.closest('.hextra-page-context-menu');
if (!container) return;
const url = btn.dataset.url;
if (!url) return;
closeDropdown(container);
window.open(url, '_blank', 'noopener,noreferrer');
});
});
});
+37
View File
@@ -0,0 +1,37 @@
document.addEventListener("DOMContentLoaded", function () {
scrollToActiveItem();
enableCollapsibles();
});
function enableCollapsibles() {
const buttons = document.querySelectorAll(".hextra-sidebar-collapsible-button");
buttons.forEach(function (button) {
button.addEventListener("click", function (e) {
e.preventDefault();
const list = button.closest('li');
if (list) {
list.classList.toggle("open");
button.setAttribute('aria-expanded', list.classList.contains('open') ? 'true' : 'false');
}
});
});
}
function scrollToActiveItem() {
const sidebarScrollbar = document.querySelector("aside.hextra-sidebar-container > .hextra-scrollbar");
const activeItems = document.querySelectorAll(".hextra-sidebar-active-item");
const visibleActiveItem = Array.from(activeItems).find(function (activeItem) {
return activeItem.getBoundingClientRect().height > 0;
});
if (!visibleActiveItem) {
return;
}
const yOffset = visibleActiveItem.clientHeight;
const yDistance = visibleActiveItem.getBoundingClientRect().top - sidebarScrollbar.getBoundingClientRect().top;
sidebarScrollbar.scrollTo({
behavior: "instant",
top: yDistance - yOffset
});
}
@@ -0,0 +1,52 @@
function computeMenuTranslation(switcher, optionsElement) {
// Calculate the position of a language options element.
const switcherRect = switcher.getBoundingClientRect();
// Must be called before optionsElement.clientWidth.
optionsElement.style.minWidth = `${Math.max(switcherRect.width, 50)}px`;
const isOnTop = switcher.dataset.location === 'top';
const isOnBottom = switcher.dataset.location === 'bottom';
const isOnBottomRight = switcher.dataset.location === 'bottom-right';
const isRTL = document.documentElement.dir === 'rtl'
// Stuck on the left side of the switcher.
let x = switcherRect.left;
if (isOnTop && !isRTL || isOnBottom && isRTL || isOnBottomRight && !isRTL) {
// Stuck on the right side of the switcher.
x = switcherRect.right - optionsElement.clientWidth;
}
// Stuck on the top of the switcher.
let y = switcherRect.top - window.innerHeight - 10;
if (isOnTop) {
// Stuck on the bottom of the switcher.
y = switcherRect.top - window.innerHeight + optionsElement.clientHeight + switcher.clientHeight + 4;
}
return { x: x, y: y };
}
function toggleMenu(switcher) {
const optionsElement = switcher.nextElementSibling;
optionsElement.classList.toggle('hx:hidden');
// Calculate the position of a language options element.
const translate = computeMenuTranslation(switcher, optionsElement);
optionsElement.style.transform = `translate3d(${translate.x}px, ${translate.y}px, 0)`;
}
function resizeMenu(switcher) {
const optionsElement = switcher.nextElementSibling;
if (optionsElement.classList.contains('hx:hidden')) return;
// Calculate the position of a language options element.
const translate = computeMenuTranslation(switcher, optionsElement);
optionsElement.style.transform = `translate3d(${translate.x}px, ${translate.y}px, 0)`;
}
+102
View File
@@ -0,0 +1,102 @@
(function () {
function updateGroup(container, index) {
const tabs = Array.from(container.querySelectorAll('.hextra-tabs-toggle'));
tabs.forEach((tab, i) => {
tab.dataset.state = i === index ? 'selected' : '';
if (i === index) {
tab.setAttribute('aria-selected', 'true');
tab.tabIndex = 0;
} else {
tab.setAttribute('aria-selected', 'false');
tab.tabIndex = -1;
}
});
const panelsContainer = container.parentElement.nextElementSibling;
if (!panelsContainer) return;
Array.from(panelsContainer.children).forEach((panel, i) => {
panel.dataset.state = i === index ? 'selected' : '';
panel.setAttribute('aria-hidden', i === index ? 'false' : 'true');
if (i === index) {
panel.tabIndex = 0;
} else {
panel.removeAttribute('tabindex');
}
});
}
const syncGroups = document.querySelectorAll('[data-tab-group]');
syncGroups.forEach((group) => {
const key = encodeURIComponent(group.dataset.tabGroup);
const saved = localStorage.getItem('hextra-tab-' + key);
if (saved !== null) {
updateGroup(group, parseInt(saved, 10));
}
});
document.querySelectorAll('.hextra-tabs-toggle').forEach((button) => {
button.addEventListener('click', function (e) {
const targetButton = e.currentTarget;
const container = targetButton.parentElement;
const index = Array.from(container.querySelectorAll('.hextra-tabs-toggle')).indexOf(
targetButton
);
if (container.dataset.tabGroup) {
// Sync behavior: update all tab groups with the same name
const tabGroupValue = container.dataset.tabGroup;
const key = encodeURIComponent(tabGroupValue);
document
.querySelectorAll('[data-tab-group="' + tabGroupValue + '"]')
.forEach((grp) => updateGroup(grp, index));
localStorage.setItem('hextra-tab-' + key, index.toString());
} else {
// Non-sync behavior: update only this specific tab group
updateGroup(container, index);
}
});
// Keyboard navigation for tabs
button.addEventListener('keydown', function (e) {
const container = button.parentElement;
const tabs = Array.from(container.querySelectorAll('.hextra-tabs-toggle'));
const currentIndex = tabs.indexOf(button);
let newIndex;
switch (e.key) {
case 'ArrowRight':
case 'ArrowDown':
e.preventDefault();
newIndex = (currentIndex + 1) % tabs.length;
break;
case 'ArrowLeft':
case 'ArrowUp':
e.preventDefault();
newIndex = (currentIndex - 1 + tabs.length) % tabs.length;
break;
case 'Home':
e.preventDefault();
newIndex = 0;
break;
case 'End':
e.preventDefault();
newIndex = tabs.length - 1;
break;
default:
return;
}
if (container.dataset.tabGroup) {
const tabGroupValue = container.dataset.tabGroup;
const key = encodeURIComponent(tabGroupValue);
document
.querySelectorAll('[data-tab-group="' + tabGroupValue + '"]')
.forEach((grp) => updateGroup(grp, newIndex));
localStorage.setItem('hextra-tab-' + key, newIndex.toString());
} else {
updateGroup(container, newIndex);
}
tabs[newIndex].focus();
});
});
})();
+15
View File
@@ -0,0 +1,15 @@
document.addEventListener("DOMContentLoaded", function () {
// Hugo task lists render bare checkboxes; provide an accessible name.
document.querySelectorAll("main#content li > input[type='checkbox']").forEach(function (checkbox) {
if (checkbox.hasAttribute("aria-label") || checkbox.hasAttribute("aria-labelledby")) {
return;
}
var listItem = checkbox.closest("li");
if (!listItem) return;
var labelText = listItem.textContent.replace(/\s+/g, " ").trim();
if (labelText) {
checkbox.setAttribute("aria-label", labelText);
}
});
});
+112
View File
@@ -0,0 +1,112 @@
// Light / Dark theme toggle
(function () {
const defaultTheme = '{{ site.Params.theme.default | default `system`}}'
const themes = ["light", "dark"];
const themeToggleButtons = document.querySelectorAll(".hextra-theme-toggle");
const themeToggleOptions = document.querySelectorAll(".hextra-theme-toggle-options button[role=menuitemradio]");
function applyTheme(theme) {
theme = themes.includes(theme) ? theme : "system";
themeToggleButtons.forEach((btn) => btn.parentElement.dataset.theme = theme );
themeToggleOptions.forEach((option) => {
option.setAttribute('aria-checked', option.dataset.item === theme ? 'true' : 'false');
});
localStorage.setItem("color-theme", theme);
}
function switchTheme(theme) {
setTheme(theme);
applyTheme(theme);
}
const colorTheme = "color-theme" in localStorage ? localStorage.getItem("color-theme") : defaultTheme;
switchTheme(colorTheme);
// Add click event handler to the menu items.
themeToggleOptions.forEach((option) => {
option.addEventListener("click", function (e) {
e.preventDefault();
switchTheme(option.dataset.item);
})
})
// Add click event handler to the buttons
themeToggleButtons.forEach((toggler) => {
toggler.addEventListener("click", function (e) {
e.preventDefault();
toggler.dataset.state = toggler.dataset.state === 'open' ? 'closed' : 'open';
toggleMenu(toggler);
const isOpen = toggler.dataset.state === 'open';
toggler.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
// Focus first menuitem when opening
if (isOpen) {
const firstItem = toggler.nextElementSibling.querySelector('button[role=menuitemradio]');
if (firstItem) firstItem.focus();
}
});
});
window.addEventListener("resize", () => themeToggleButtons.forEach(resizeMenu))
// Dismiss the menu when clicking outside
document.addEventListener('click', (e) => {
if (e.target.closest('.hextra-theme-toggle') === null) {
themeToggleButtons.forEach((toggler) => {
toggler.dataset.state = 'closed';
toggler.setAttribute('aria-expanded', 'false');
toggler.nextElementSibling.classList.add('hx:hidden');
});
}
});
// Keyboard navigation for the theme menu
document.querySelectorAll('.hextra-theme-toggle-options[role=menu]').forEach(function (menu) {
menu.addEventListener('keydown', function (e) {
const items = Array.from(menu.querySelectorAll('button[role=menuitemradio]'));
const currentIndex = items.indexOf(document.activeElement);
let newIndex;
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
newIndex = (currentIndex + 1) % items.length;
items[newIndex].focus();
break;
case 'ArrowUp':
e.preventDefault();
newIndex = (currentIndex - 1 + items.length) % items.length;
items[newIndex].focus();
break;
case 'Home':
e.preventDefault();
items[0].focus();
break;
case 'End':
e.preventDefault();
items[items.length - 1].focus();
break;
case 'Escape':
e.preventDefault();
var toggler = menu.previousElementSibling;
toggler.dataset.state = 'closed';
toggler.setAttribute('aria-expanded', 'false');
menu.classList.add('hx:hidden');
toggler.focus();
break;
}
});
});
// Listen for system theme changes
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => {
if (localStorage.getItem("color-theme") === "system") {
setTheme("system");
}
});
})();
@@ -0,0 +1,97 @@
/**
* TOC Scroll - Highlights active TOC links based on visible headings
*
* Uses Intersection Observer to track heading visibility and applies
* 'hextra-toc-active' class to corresponding TOC links. Selects the
* topmost heading when multiple are visible.
*
* Requires: .hextra-toc element, matching heading IDs, toc.css styles
*/
document.addEventListener("DOMContentLoaded", function () {
const toc = document.querySelector(".hextra-toc");
if (!toc) return;
const tocLinks = toc.querySelectorAll('a[href^="#"]');
if (tocLinks.length === 0) return;
const headingIds = Array.from(tocLinks).map((link) => link.getAttribute("href").substring(1));
const headings = headingIds.map((id) => document.getElementById(decodeURIComponent(id))).filter(Boolean);
if (headings.length === 0) return;
let currentActiveLink = null;
let isHashNavigation = false;
// Create intersection observer
const observer = new IntersectionObserver(
(entries) => {
// Skip observer updates during hash navigation
if (isHashNavigation) return;
const visibleHeadings = entries.filter((entry) => entry.isIntersecting).map((entry) => entry.target);
if (visibleHeadings.length === 0) return;
// Find the heading closest to the top of the viewport
const topMostHeading = visibleHeadings.reduce((closest, heading) => {
const headingTop = heading.getBoundingClientRect().top;
const closestTop = closest.getBoundingClientRect().top;
return Math.abs(headingTop) < Math.abs(closestTop) ? heading : closest;
});
// Encode the id and make it lowercase to match the TOC link
const targetId = encodeURIComponent(topMostHeading.id).toLowerCase();
const targetLink = toc.querySelector(`a[href="#${targetId}"]`);
if (targetLink && targetLink !== currentActiveLink) {
// Remove active class from previous link
if (currentActiveLink) {
currentActiveLink.classList.remove("hextra-toc-active");
currentActiveLink.removeAttribute("aria-current");
}
// Add active class to current link
targetLink.classList.add("hextra-toc-active");
targetLink.setAttribute("aria-current", "location");
currentActiveLink = targetLink;
}
},
{
rootMargin: "-20px 0px -60% 0px", // Adjust sensitivity
threshold: [0, 0.1, 0.5, 1],
}
);
// Observe all headings
headings.forEach((heading) => observer.observe(heading));
// Handle direct navigation to page with hash
function handleHashNavigation() {
const hash = window.location.hash; // already url encoded
if (hash) {
const targetLink = toc.querySelector(`a[href="${hash}"]`);
if (targetLink) {
// Disable observer temporarily during hash navigation
isHashNavigation = true;
if (currentActiveLink) {
currentActiveLink.classList.remove("hextra-toc-active");
currentActiveLink.removeAttribute("aria-current");
}
targetLink.classList.add("hextra-toc-active");
targetLink.setAttribute("aria-current", "location");
currentActiveLink = targetLink;
// Re-enable observer after scroll settles
setTimeout(() => { isHashNavigation = false; }, 500);
return;
}
}
}
// Handle hash changes navigation
window.addEventListener("hashchange", handleHashNavigation);
// Handle initial load
setTimeout(handleHashNavigation, 100);
});
+492
View File
@@ -0,0 +1,492 @@
// Search functionality using FlexSearch.
// Change shortcut key to cmd+k on Mac, iPad or iPhone.
document.addEventListener("DOMContentLoaded", function () {
if (/iPad|iPhone|Macintosh/.test(navigator.userAgent)) {
// select the kbd element under the .hextra-search-wrapper class
const keys = document.querySelectorAll(".hextra-search-wrapper kbd");
keys.forEach(key => {
key.innerHTML = '<span class="hx:text-xs">⌘</span>K';
});
}
});
// Render the search data as JSON.
// {{ $searchDataFile := printf "%s.search-data.json" .Language.Lang }}
// {{ $searchData := resources.Get "json/search-data.json" | resources.ExecuteAsTemplate $searchDataFile . }}
// {{ if hugo.IsProduction }}
// {{ $searchData := $searchData | minify | fingerprint }}
// {{ end }}
// {{ $noResultsFound := (T "noResultsFound") | default "No results found." }}
(function () {
const searchDataURL = '{{ $searchData.RelPermalink }}';
const resultsFoundTemplate = '{{ (T "resultsFound") | default "%d results found" }}';
const inputElements = document.querySelectorAll('.hextra-search-input');
for (const el of inputElements) {
el.addEventListener('focus', init);
el.addEventListener('keyup', search);
el.addEventListener('keydown', handleKeyDown);
el.addEventListener('input', handleInputChange);
}
const shortcutElements = document.querySelectorAll('.hextra-search-wrapper kbd');
function setShortcutElementsOpacity(opacity) {
shortcutElements.forEach(el => {
el.style.opacity = opacity;
});
}
function handleInputChange(e) {
const opacity = e.target.value.length > 0 ? 0 : 100;
setShortcutElementsOpacity(opacity);
}
// Get the search wrapper, input, and results elements.
function getActiveSearchElement() {
const inputs = Array.from(document.querySelectorAll('.hextra-search-wrapper')).filter(el => el.clientHeight > 0);
if (inputs.length === 1) {
return {
wrapper: inputs[0],
inputElement: inputs[0].querySelector('.hextra-search-input'),
resultsElement: inputs[0].querySelector('.hextra-search-results')
};
}
return undefined;
}
const INPUTS = ['input', 'select', 'button', 'textarea']
// Focus the search input when pressing ctrl+k/cmd+k or /.
document.addEventListener('keydown', function (e) {
const { inputElement } = getActiveSearchElement();
if (!inputElement) return;
const activeElement = document.activeElement;
const tagName = activeElement && activeElement.tagName;
if (
inputElement === activeElement ||
!tagName ||
INPUTS.includes(tagName) ||
(activeElement && activeElement.isContentEditable))
return;
if (
e.key === '/' ||
(e.key === 'k' &&
(e.metaKey /* for Mac */ || /* for non-Mac */ e.ctrlKey))
) {
e.preventDefault();
inputElement.focus();
} else if (e.key === 'Escape' && inputElement.value) {
inputElement.blur();
}
});
// Dismiss the search results when clicking outside the search box.
document.addEventListener('mousedown', function (e) {
const { inputElement, resultsElement } = getActiveSearchElement();
if (!inputElement || !resultsElement) return;
if (
e.target !== inputElement &&
e.target !== resultsElement &&
!resultsElement.contains(e.target)
) {
setShortcutElementsOpacity(100);
hideSearchResults();
}
});
// Get the currently active result and its index.
function getActiveResult() {
const { resultsElement } = getActiveSearchElement();
if (!resultsElement) return { result: undefined, index: -1 };
const result = resultsElement.querySelector('.hextra-search-active');
if (!result) return { result: undefined, index: -1 };
const index = parseInt(result.dataset.index, 10);
return { result, index };
}
// Set the active result by index.
function setActiveResult(index) {
const { resultsElement } = getActiveSearchElement();
if (!resultsElement) return;
const { result: activeResult } = getActiveResult();
activeResult && activeResult.classList.remove('hextra-search-active');
const result = resultsElement.querySelector(`[data-index="${index}"]`);
if (result) {
result.classList.add('hextra-search-active');
result.focus();
}
}
// Get the number of search results from the DOM.
function getResultsLength() {
const { resultsElement } = getActiveSearchElement();
if (!resultsElement) return 0;
return resultsElement.dataset.count;
}
// Finish the search by hiding the results and clearing the input.
function finishSearch() {
const { inputElement } = getActiveSearchElement();
if (!inputElement) return;
hideSearchResults();
inputElement.value = '';
inputElement.blur();
}
function hideSearchResults() {
const { resultsElement } = getActiveSearchElement();
if (!resultsElement) return;
resultsElement.classList.add('hx:hidden');
}
// Handle keyboard events.
function handleKeyDown(e) {
const { inputElement } = getActiveSearchElement();
if (!inputElement) return;
const resultsLength = getResultsLength();
const { result: activeResult, index: activeIndex } = getActiveResult();
switch (e.key) {
case 'ArrowUp':
e.preventDefault();
if (activeIndex > 0) setActiveResult(activeIndex - 1);
break;
case 'ArrowDown':
e.preventDefault();
if (activeIndex + 1 < resultsLength) setActiveResult(activeIndex + 1);
break;
case 'Enter':
e.preventDefault();
if (activeResult) {
activeResult.click();
}
finishSearch();
case 'Escape':
e.preventDefault();
hideSearchResults();
// Clear the input when pressing escape
inputElement.value = '';
inputElement.dispatchEvent(new Event('input'));
// Remove focus from the input
inputElement.blur();
break;
}
}
// Initializes the search.
function init(e) {
e.target.removeEventListener('focus', init);
if (!(window.pageIndex && window.sectionIndex)) {
preloadIndex();
}
}
/**
* Preloads the search index by fetching data and adding it to the FlexSearch index.
* @returns {Promise<void>} A promise that resolves when the index is preloaded.
*/
async function preloadIndex() {
const tokenize = '{{- site.Params.search.flexsearch.tokenize | default "forward" -}}';
// https://github.com/TryGhost/Ghost/pull/21148
const regex = new RegExp(
`[\u{4E00}-\u{9FFF}\u{3040}-\u{309F}\u{30A0}-\u{30FF}\u{AC00}-\u{D7A3}\u{3400}-\u{4DBF}\u{20000}-\u{2A6DF}\u{2A700}-\u{2B73F}\u{2B740}-\u{2B81F}\u{2B820}-\u{2CEAF}\u{2CEB0}-\u{2EBEF}\u{30000}-\u{3134F}\u{31350}-\u{323AF}\u{2EBF0}-\u{2EE5F}\u{F900}-\u{FAFF}\u{2F800}-\u{2FA1F}]|[0-9A-Za-zа\u00C0-\u017F\u0400-\u04FF\u0600-\u06FF\u0980-\u09FF\u1E00-\u1EFF\u0590-\u05FF]+`,
'mug'
);
const encode = (str) => { return ('' + str).toLowerCase().match(regex) ?? []; }
window.pageIndex = new FlexSearch.Document({
tokenize,
encode,
cache: 100,
document: {
id: 'id',
store: ['title', 'crumb'],
index: "content"
}
});
window.sectionIndex = new FlexSearch.Document({
tokenize,
encode,
cache: 100,
document: {
id: 'id',
store: ['title', 'content', 'url', 'display', 'crumb'],
index: "content",
tag: [{
field: "pageId"
}]
}
});
const resp = await fetch(searchDataURL);
const data = await resp.json();
let pageId = 0;
for (const route in data) {
let pageContent = '';
++pageId;
const urlParts = route.split('/').filter(x => x != "" && !x.startsWith('#'));
let crumb = '';
let searchUrl = '/';
for (let i = 0; i < urlParts.length; i++) {
const urlPart = urlParts[i];
searchUrl += urlPart + '/'
const crumbData = data[searchUrl];
if (!crumbData) {
console.debug('Excluded page', searchUrl, '- will not be included for search result breadcrumb for', route);
continue;
}
let title = data[searchUrl].title;
if (title == "_index") {
title = urlPart.split("-").map(x => x).join(" ");
}
crumb += title;
if (i < urlParts.length - 1) {
crumb += ' > ';
}
}
for (const heading in data[route].data) {
const [hash, text] = heading.split('#');
const url = route.trimEnd('/') + (hash ? '#' + hash : '');
const title = text || data[route].title;
const content = data[route].data[heading] || '';
const paragraphs = content.split('\n').filter(Boolean);
sectionIndex.add({
id: url,
url,
title,
crumb,
pageId: `page_${pageId}`,
content: title,
...(paragraphs[0] && { display: paragraphs[0] })
});
for (let i = 0; i < paragraphs.length; i++) {
sectionIndex.add({
id: `${url}_${i}`,
url,
title,
crumb,
pageId: `page_${pageId}`,
content: paragraphs[i]
});
}
pageContent += ` ${title} ${content}`;
}
window.pageIndex.add({
id: pageId,
title: data[route].title,
crumb,
content: pageContent
});
}
}
/**
* Performs a search based on the provided query and displays the results.
* @param {Event} e - The event object.
*/
function search(e) {
const query = e.target.value;
if (!e.target.value) {
hideSearchResults();
return;
}
const { resultsElement } = getActiveSearchElement();
while (resultsElement.firstChild) {
resultsElement.removeChild(resultsElement.firstChild);
}
resultsElement.classList.remove('hx:hidden');
// Configurable search limits with sensible defaults
const maxPageResults = parseInt('{{- site.Params.search.flexsearch.maxPageResults | default 20 -}}', 10);
const maxSectionResults = parseInt('{{- site.Params.search.flexsearch.maxSectionResults | default 10 -}}', 10);
const pageResults = window.pageIndex.search(query, maxPageResults, { enrich: true, suggest: true })[0]?.result || [];
const results = [];
const pageTitleMatches = {};
for (let i = 0; i < pageResults.length; i++) {
const result = pageResults[i];
pageTitleMatches[i] = 0;
const sectionResults = window.sectionIndex.search(query,
{ enrich: true, suggest: true, tag: { 'pageId': `page_${result.id}` } })[0]?.result || [];
let isFirstItemOfPage = true
const occurred = {}
const nResults = Math.min(sectionResults.length, maxSectionResults);
for (let j = 0; j < nResults; j++) {
const { doc } = sectionResults[j]
const isMatchingTitle = doc.display !== undefined
if (isMatchingTitle) {
pageTitleMatches[i]++
}
const { url, title } = doc
const content = doc.display || doc.content
if (occurred[url + '@' + content]) continue
occurred[url + '@' + content] = true
results.push({
_page_rk: i,
_section_rk: j,
route: url,
prefix: isFirstItemOfPage ? result.doc.crumb : undefined,
children: { title, content }
})
isFirstItemOfPage = false
}
}
const sortedResults = results
.sort((a, b) => {
// Sort by number of matches in the title.
if (a._page_rk === b._page_rk) {
return a._section_rk - b._section_rk
}
if (pageTitleMatches[a._page_rk] !== pageTitleMatches[b._page_rk]) {
return pageTitleMatches[b._page_rk] - pageTitleMatches[a._page_rk]
}
return a._page_rk - b._page_rk
})
.map(res => ({
id: `${res._page_rk}_${res._section_rk}`,
route: res.route,
prefix: res.prefix,
children: res.children
}));
displayResults(sortedResults, query);
}
/**
* Displays the search results on the page.
*
* @param {Array} results - The array of search results.
* @param {string} query - The search query.
*/
function displayResults(results, query) {
const { resultsElement } = getActiveSearchElement();
if (!resultsElement) return;
if (!results.length) {
resultsElement.innerHTML = `<span class="hextra-search-no-result">{{ $noResultsFound | safeHTML }}</span>`;
// Announce no results to screen readers
const wrapper = resultsElement.closest('.hextra-search-wrapper');
const statusEl = wrapper ? wrapper.querySelector('.hextra-search-status') : null;
if (statusEl) {
statusEl.textContent = '{{ $noResultsFound | safeHTML }}';
}
return;
}
// Append text with highlighted matches using safe text nodes.
function appendHighlightedText(container, text, query) {
if (!text) return;
if (!query) {
container.textContent = text;
return;
}
const escapedQuery = query.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&');
if (!escapedQuery) {
container.textContent = text;
return;
}
const regex = new RegExp(escapedQuery, 'gi');
let lastIndex = 0;
let match;
while ((match = regex.exec(text)) !== null) {
if (match.index > lastIndex) {
container.appendChild(document.createTextNode(text.slice(lastIndex, match.index)));
}
const span = document.createElement('span');
span.className = 'hextra-search-match';
span.textContent = match[0];
container.appendChild(span);
lastIndex = match.index + match[0].length;
}
if (lastIndex < text.length) {
container.appendChild(document.createTextNode(text.slice(lastIndex)));
}
}
function handleMouseMove(e) {
const target = e.target.closest('a');
if (target) {
const active = resultsElement.querySelector('a.hextra-search-active');
if (active) {
active.classList.remove('hextra-search-active');
}
target.classList.add('hextra-search-active');
}
}
const fragment = document.createDocumentFragment();
for (let i = 0; i < results.length; i++) {
const result = results[i];
if (result.prefix) {
const prefix = document.createElement('div');
prefix.className = 'hextra-search-prefix';
prefix.textContent = result.prefix;
fragment.appendChild(prefix);
}
const li = document.createElement('li');
const link = document.createElement('a');
link.dataset.index = i;
link.href = result.route;
if (i === 0) {
link.classList.add('hextra-search-active');
}
const title = document.createElement('div');
title.className = 'hextra-search-title';
appendHighlightedText(title, result.children.title, query);
link.appendChild(title);
if (result.children.content) {
const excerpt = document.createElement('div');
excerpt.className = 'hextra-search-excerpt';
appendHighlightedText(excerpt, result.children.content, query);
link.appendChild(excerpt);
}
li.appendChild(link);
li.addEventListener('mousemove', handleMouseMove);
li.addEventListener('keydown', handleKeyDown);
link.addEventListener('click', finishSearch);
fragment.appendChild(li);
}
resultsElement.appendChild(fragment);
resultsElement.dataset.count = results.length;
// Announce results count to screen readers
const wrapper = resultsElement.closest('.hextra-search-wrapper');
const statusEl = wrapper ? wrapper.querySelector('.hextra-search-status') : null;
if (statusEl) {
statusEl.textContent = results.length > 0
? resultsFoundTemplate.replace('%d', results.length.toString())
: '{{ $noResultsFound | safeHTML }}';
}
}
})();
+6
View File
@@ -0,0 +1,6 @@
// The section must not be in the banner.js (body) file because it can create a quick flash.
if (localStorage.getItem('{{ site.Params.banner.key | default `banner-closed` }}')) {
document.documentElement.style.setProperty("--hextra-banner-height", "0px");
document.documentElement.classList.add("hextra-banner-hidden");
}
+14
View File
@@ -0,0 +1,14 @@
// The section must not be in the theme.js (body) file because it can create a quick flash (switch between light and dark).
function setTheme(theme) {
document.documentElement.classList.remove("light", "dark");
if (theme !== "light" && theme !== "dark") {
theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
document.documentElement.classList.add(theme);
document.documentElement.style.colorScheme = theme;
}
setTheme("color-theme" in localStorage ? localStorage.getItem("color-theme") : '{{ site.Params.theme.default | default `system`}}')
@@ -0,0 +1,38 @@
{{/* FlexSearch Index Data */}}
{{- $indexType := site.Params.search.flexsearch.index | default "content" -}}
{{- if not (in (slice "content" "summary" "heading" "title" ) $indexType) -}}
{{- errorf "unknown flexsearch index type: %s" $indexType -}}
{{- end -}}
{{- $pages := where .Site.Pages "Kind" "in" (slice "page" "section") -}}
{{- $pages = where $pages "Params.excludeSearch" "!=" true -}}
{{- $pages = where $pages "Content" "!=" "" -}}
{{- $output := dict -}}
{{- range $index, $page := $pages -}}
{{- $pageTitle := $page.LinkTitle | default $page.File.BaseFileName -}}
{{- $pageLink := $page.RelPermalink -}}
{{- $data := partial "utils/fragments" (dict "context" $page "type" $indexType) -}}
{{- $output = $output | merge (dict $pageLink (dict "title" $pageTitle "data" $data)) -}}
{{- end -}}
{{/* Extract glossary data entries */}}
{{- $glossaryEntries := dict -}}
{{- $siteData := partial "utils/hugo-compat/site-data.html" . -}}
{{- with (index $siteData .Site.Language.Lang "termbase") -}}
{{- range . -}}
{{- $entry := cond (.abbr) (printf "%s %s %s" .abbr .term .definition) (printf "%s %s" .term .definition) -}}
{{- $glossaryEntries = $glossaryEntries | merge (dict .term $entry) -}}
{{- end -}}
{{- end -}}
{{- $glossary := dict
"title" "Glossary"
"data" $glossaryEntries
-}}
{{- $output = $output | merge (dict (relLangURL "glossary") $glossary )}}
{{- $output | jsonify -}}