summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKyren223 <contact@kyren.codes>2026-10-07 03:14:47 +0300
committerKyren223 <contact@kyren.codes>2026-10-07 03:14:47 +0300
commit6e70fe447ec9504757fbdc152b5b3697c9af8def (patch)
tree4da46c4f479e87e17ac5f786baafff4e285b37c1
parentba1be9573ef20d6424ae987257a1c727c47d4b53 (diff)
Initial iteration
-rw-r--r--public/main.css6
-rw-r--r--public/style.css162
-rw-r--r--public/styles/fonts/JetBrainsMono.css108
-rw-r--r--public/styles/fonts/Monocraft.css108
-rw-r--r--public/styles/markdown/asides.css49
-rw-r--r--public/styles/markdown/markdown.css251
-rw-r--r--public/styles/markdown/props.css189
-rw-r--r--public/styles/markdown/reset.css48
-rw-r--r--public/styles/markdown/shiki.css13
-rw-r--r--public/styles/markdown/util.css61
-rw-r--r--public/styles/terminal.css47
-rw-r--r--public/theme.js12
-rw-r--r--website.go285
-rw-r--r--website.templ147
-rw-r--r--website_templ.go356
15 files changed, 1658 insertions, 184 deletions
diff --git a/public/main.css b/public/main.css
index 8457bc5..f93965c 100644
--- a/public/main.css
+++ b/public/main.css
@@ -1,5 +1,7 @@
-@import "tailwindcss";
+@layer theme, base, components, utilities;
+@import "tailwindcss/theme.css" layer(theme);
+@import "tailwindcss/preflight.css" layer(base);
+@import "tailwindcss/utilities.css" layer(utilities);
@source "../**/*.go";
@source "../**/*.templ";
-@source "..";
diff --git a/public/style.css b/public/style.css
index 5369786..d1e775f 100644
--- a/public/style.css
+++ b/public/style.css
@@ -7,11 +7,11 @@
sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
monospace;
- --color-fuchsia-600: oklch(59.1% 0.293 322.896);
--color-gray-700: oklch(37.3% 0.034 259.733);
--spacing: 0.25rem;
--text-3xl: 1.875rem;
--text-3xl--line-height: calc(2.25 / 1.875);
+ --font-weight-normal: 400;
--font-weight-bold: 700;
--default-font-family: var(--font-sans);
--default-mono-font-family: var(--font-mono);
@@ -238,6 +238,15 @@
.mb-0 {
margin-bottom: 0px;
}
+ .mb-6 {
+ margin-bottom: calc(var(--spacing) * 6);
+ }
+ .mb-6\.25 {
+ margin-bottom: calc(var(--spacing) * 6.25);
+ }
+ .mb-\[1\.5625rem\] {
+ margin-bottom: 1.5625rem;
+ }
.mb-\[1\.5625vw\] {
margin-bottom: 1.5625vw;
}
@@ -274,6 +283,9 @@
.h-\[9\.5vw\] {
height: 9.5vw;
}
+ .h-\[40rem\] {
+ height: 40rem;
+ }
.h-\[62\%\] {
height: 62%;
}
@@ -298,15 +310,42 @@
.w-0 {
width: 0px;
}
+ .w-300 {
+ width: calc(var(--spacing) * 300);
+ }
.w-\[30\%\] {
width: 30%;
}
+ .w-\[40rem\] {
+ width: 40rem;
+ }
.w-\[67\%\] {
width: 67%;
}
.w-\[70\%\] {
width: 70%;
}
+ .w-\[75\.0rem\] {
+ width: 75.0rem;
+ }
+ .w-\[75\.001rem\] {
+ width: 75.001rem;
+ }
+ .w-\[75\.1rem\] {
+ width: 75.1rem;
+ }
+ .w-\[75\.12rem\] {
+ width: 75.12rem;
+ }
+ .w-\[75\.13rem\] {
+ width: 75.13rem;
+ }
+ .w-\[75\.123rem\] {
+ width: 75.123rem;
+ }
+ .w-\[75rem\] {
+ width: 75rem;
+ }
.w-\[95vw\] {
width: 95vw;
}
@@ -325,6 +364,9 @@
.flex-1 {
flex: 1;
}
+ .border-collapse {
+ border-collapse: collapse;
+ }
.transform {
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
}
@@ -352,12 +394,21 @@
.justify-between {
justify-content: space-between;
}
+ .gap-10 {
+ gap: calc(var(--spacing) * 10);
+ }
.gap-16 {
gap: calc(var(--spacing) * 16);
}
+ .gap-20 {
+ gap: calc(var(--spacing) * 20);
+ }
.gap-\[1\.25vw\] {
gap: 1.25vw;
}
+ .gap-\[1ch\] {
+ gap: 1ch;
+ }
.gap-x-\[5vw\] {
column-gap: 5vw;
}
@@ -374,6 +425,9 @@
.overflow-auto {
overflow: auto;
}
+ .overflow-hidden {
+ overflow: hidden;
+ }
.overflow-x-hidden {
overflow-x: hidden;
}
@@ -384,18 +438,59 @@
border-style: var(--tw-border-style);
border-width: 1px;
}
+ .border-0 {
+ border-style: var(--tw-border-style);
+ border-width: 0px;
+ }
+ .border-1 {
+ border-style: var(--tw-border-style);
+ border-width: 1px;
+ }
+ .border-2 {
+ border-style: var(--tw-border-style);
+ border-width: 2px;
+ }
+ .border-3 {
+ border-style: var(--tw-border-style);
+ border-width: 3px;
+ }
+ .border-10 {
+ border-style: var(--tw-border-style);
+ border-width: 10px;
+ }
.border-\[0\.1vw\] {
border-style: var(--tw-border-style);
border-width: 0.1vw;
}
+ .border-\[0\.2rem\] {
+ border-style: var(--tw-border-style);
+ border-width: 0.2rem;
+ }
+ .border-\[0\.25rem\] {
+ border-style: var(--tw-border-style);
+ border-width: 0.25rem;
+ }
.border-\[0\.25vh\] {
border-style: var(--tw-border-style);
border-width: 0.25vh;
}
+ .border-\[0\.125rem\] {
+ border-style: var(--tw-border-style);
+ border-width: 0.125rem;
+ }
.border-solid {
--tw-border-style: solid;
border-style: solid;
}
+ .border-\(--alt\) {
+ border-color: var(--alt);
+ }
+ .border-\(--text\) {
+ border-color: var(--text);
+ }
+ .border-\[color\:var\(--alt\)\] {
+ border-color: var(--alt);
+ }
.border-\[var\(--alt\)\] {
border-color: var(--alt);
}
@@ -414,12 +509,42 @@
.p-0 {
padding: 0px;
}
+ .p-2 {
+ padding: calc(var(--spacing) * 2);
+ }
+ .p-3 {
+ padding: calc(var(--spacing) * 3);
+ }
+ .p-4 {
+ padding: calc(var(--spacing) * 4);
+ }
+ .p-8 {
+ padding: calc(var(--spacing) * 8);
+ }
+ .p-10 {
+ padding: calc(var(--spacing) * 10);
+ }
.p-\[0\.8em\] {
padding: 0.8em;
}
+ .p-\[2\.0rem\] {
+ padding: 2.0rem;
+ }
+ .p-\[2\.2rem\] {
+ padding: 2.2rem;
+ }
.p-\[2\.2svh\] {
padding: 2.2svh;
}
+ .p-\[2\.5rem\] {
+ padding: 2.5rem;
+ }
+ .p-\[2\.rem\] {
+ padding: 2.rem;
+ }
+ .p-\[2rem\] {
+ padding: 2rem;
+ }
.p-\[3vw\] {
padding: 3vw;
}
@@ -456,6 +581,9 @@
.font-\[\'JetBrains_Mono\'\] {
font-family: 'JetBrains Mono';
}
+ .font-mono {
+ font-family: var(--font-mono);
+ }
.text-3xl {
font-size: var(--text-3xl);
line-height: var(--tw-leading, var(--text-3xl--line-height));
@@ -463,6 +591,12 @@
.text-\[0\.96vw\] {
font-size: 0.96vw;
}
+ .text-\[1\.125rem\] {
+ font-size: 1.125rem;
+ }
+ .text-\[1\.375rem\] {
+ font-size: 1.375rem;
+ }
.text-\[2\.5vw\] {
font-size: 2.5vw;
}
@@ -488,9 +622,19 @@
--tw-font-weight: var(--font-weight-bold);
font-weight: var(--font-weight-bold);
}
+ .font-normal {
+ --tw-font-weight: var(--font-weight-normal);
+ font-weight: var(--font-weight-normal);
+ }
+ .text-\(--primary\) {
+ color: var(--primary);
+ }
.text-\[var\(--accent\)\] {
color: var(--accent);
}
+ .text-\[var\(--primary\)\] {
+ color: var(--primary);
+ }
.text-\[var\(--secondary\)\] {
color: var(--secondary);
}
@@ -500,6 +644,10 @@
.opacity-0 {
opacity: 0%;
}
+ .outline {
+ outline-style: var(--tw-outline-style);
+ outline-width: 1px;
+ }
@media (hover: hover) {
.group-hover\:text-\[var\(--alt\)\]:is(:where(.group):hover *) {
color: var(--alt);
@@ -510,6 +658,12 @@
.group-hover\:underline:is(:where(.group):hover *) {
text-decoration-line: underline;
}
+ .hover\:text-\(--accent\):hover {
+ color: var(--accent);
+ }
+ .hover\:text-\[var\(--accent\)\]:hover {
+ color: var(--accent);
+ }
.hover\:text-\[var\(--alt\)\]:hover {
color: var(--alt);
}
@@ -716,6 +870,11 @@
syntax: "*";
inherits: false;
}
+@property --tw-outline-style {
+ syntax: "*";
+ inherits: false;
+ initial-value: solid;
+}
@layer properties {
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
*, ::before, ::after, ::backdrop {
@@ -727,6 +886,7 @@
--tw-space-x-reverse: 0;
--tw-border-style: solid;
--tw-font-weight: initial;
+ --tw-outline-style: solid;
}
}
}
diff --git a/public/styles/fonts/JetBrainsMono.css b/public/styles/fonts/JetBrainsMono.css
new file mode 100644
index 0000000..807fe1f
--- /dev/null
+++ b/public/styles/fonts/JetBrainsMono.css
@@ -0,0 +1,108 @@
+@font-face {
+ font-family: "Jetbrains Mono";
+ src: url("/fonts/JetBrainsMono-Thin.woff2") format("woff2");
+ font-weight: 100;
+ font-style: normal;
+}
+@font-face {
+ font-family: "Jetbrains Mono";
+ src: url("/fonts/JetBrainsMono-ThinItalic.woff2") format("woff2");
+ font-weight: 100;
+ font-style: italic;
+}
+@font-face {
+ font-family: "Jetbrains Mono";
+ src: url("/fonts/JetBrainsMono-ExtraLight.woff2") format("woff2");
+ font-weight: 200;
+ font-style: normal;
+}
+@font-face {
+ font-family: "Jetbrains Mono";
+ src: url("/fonts/JetBrainsMono-ExtraLightItalic.woff2") format("woff2");
+ font-weight: 200;
+ font-style: italic;
+}
+@font-face {
+ font-family: "Jetbrains Mono";
+ src: url("/fonts/JetBrainsMono-Light.woff2") format("woff2");
+ font-weight: 300;
+ font-style: normal;
+}
+@font-face {
+ font-family: "Jetbrains Mono";
+ src: url("/fonts/JetBrainsMono-LightItalic.woff2") format("woff2");
+ font-weight: 300;
+ font-style: italic;
+}
+@font-face {
+ font-family: "Jetbrains Mono";
+ src: url("/fonts/JetBrainsMono-Regular.woff2") format("woff2");
+ font-weight: 400;
+ font-style: normal;
+}
+@font-face {
+ font-family: "Jetbrains Mono";
+ src: url("/fonts/JetBrainsMono-Italic.woff2") format("woff2");
+ font-weight: 400;
+ font-style: italic;
+}
+@font-face {
+ font-family: "Jetbrains Mono";
+ src: url("/fonts/JetBrainsMono-Medium.woff2") format("woff2");
+ font-weight: 500;
+ font-style: normal;
+}
+@font-face {
+ font-family: "Jetbrains Mono";
+ src: url("/fonts/JetBrainsMono-MediumItalic.woff2") format("woff2");
+ font-weight: 500;
+ font-style: italic;
+}
+@font-face {
+ font-family: "Jetbrains Mono";
+ src: url("/fonts/JetBrainsMono-SemiBold.woff2") format("woff2");
+ font-weight: 600;
+ font-style: normal;
+}
+@font-face {
+ font-family: "Jetbrains Mono";
+ src: url("/fonts/JetBrainsMono-SemiBoldItalic.woff2") format("woff2");
+ font-weight: 600;
+ font-style: italic;
+}
+@font-face {
+ font-family: "Jetbrains Mono";
+ src: url("/fonts/JetBrainsMono-Bold.woff2") format("woff2");
+ font-weight: 700;
+ font-style: normal;
+}
+@font-face {
+ font-family: "Jetbrains Mono";
+ src: url("/fonts/JetBrainsMono-BoldItalic.woff2") format("woff2");
+ font-weight: 700;
+ font-style: italic;
+}
+@font-face {
+ font-family: "Jetbrains Mono";
+ src: url("/fonts/JetBrainsMono-ExtraBold.woff2") format("woff2");
+ font-weight: 800;
+ font-style: normal;
+}
+@font-face {
+ font-family: "Jetbrains Mono";
+ src: url("/fonts/JetBrainsMono-ExtraBoldItalic.woff2") format("woff2");
+ font-weight: 800;
+ font-style: italic;
+}
+@font-face {
+ font-family: "Jetbrains Mono";
+ src: url("/fonts/JetBrainsMono-ExtraBold.woff2") format("woff2");
+ font-weight: 900;
+ font-style: normal;
+}
+@font-face {
+ font-family: "Jetbrains Mono";
+ src: url("/fonts/JetBrainsMono-ExtraBoldItalic.woff2") format("woff2");
+ font-weight: 900;
+ font-style: italic;
+}
diff --git a/public/styles/fonts/Monocraft.css b/public/styles/fonts/Monocraft.css
new file mode 100644
index 0000000..34fd24a
--- /dev/null
+++ b/public/styles/fonts/Monocraft.css
@@ -0,0 +1,108 @@
+@font-face {
+ font-family: "Monocraft";
+ src: url("/fonts/Monocraft-ExtraLight.ttf") format("truetype");
+ font-weight: 100;
+ font-style: normal;
+}
+@font-face {
+ font-family: "Monocraft";
+ src: url("/fonts/Monocraft-ExtraLight-Italic.ttf") format("truetype");
+ font-weight: 100;
+ font-style: italic;
+}
+@font-face {
+ font-family: "Monocraft";
+ src: url("/fonts/Monocraft-ExtraLight.ttf") format("truetype");
+ font-weight: 200;
+ font-style: normal;
+}
+@font-face {
+ font-family: "Monocraft";
+ src: url("/fonts/Monocraft-ExtraLight-Italic.ttf") format("truetype");
+ font-weight: 200;
+ font-style: italic;
+}
+@font-face {
+ font-family: "Monocraft";
+ src: url("/fonts/Monocraft-Light.ttf") format("truetype");
+ font-weight: 300;
+ font-style: normal;
+}
+@font-face {
+ font-family: "Monocraft";
+ src: url("/fonts/Monocraft-Light-Italic.ttf") format("truetype");
+ font-weight: 300;
+ font-style: italic;
+}
+@font-face {
+ font-family: "Monocraft";
+ src: url("/fonts/Monocraft.ttf") format("truetype");
+ font-weight: 400;
+ font-style: normal;
+}
+@font-face {
+ font-family: "Monocraft";
+ src: url("/fonts/Monocraft-Italic.ttf") format("truetype");
+ font-weight: 400;
+ font-style: italic;
+}
+@font-face {
+ font-family: "Monocraft";
+ src: url("/fonts/Monocraft.ttf") format("truetype");
+ font-weight: 500;
+ font-style: normal;
+}
+@font-face {
+ font-family: "Monocraft";
+ src: url("/fonts/Monocraft-Italic.ttf") format("truetype");
+ font-weight: 500;
+ font-style: italic;
+}
+@font-face {
+ font-family: "Monocraft";
+ src: url("/fonts/Monocraft-SemiBold.ttf") format("truetype");
+ font-weight: 600;
+ font-style: normal;
+}
+@font-face {
+ font-family: "Monocraft";
+ src: url("/fonts/Monocraft-SemiBold-Italic.ttf") format("truetype");
+ font-weight: 600;
+ font-style: italic;
+}
+@font-face {
+ font-family: "Monocraft";
+ src: url("/fonts/Monocraft-Bold.ttf") format("truetype");
+ font-weight: 700;
+ font-style: normal;
+}
+@font-face {
+ font-family: "Monocraft";
+ src: url("/fonts/Monocraft-Bold-Italic.ttf") format("truetype");
+ font-weight: 700;
+ font-style: italic;
+}
+@font-face {
+ font-family: "Monocraft";
+ src: url("/fonts/Monocraft-Black.ttf") format("truetype");
+ font-weight: 800;
+ font-style: normal;
+}
+@font-face {
+ font-family: "Monocraft";
+ src: url("/fonts/Monocraft-Black-Italic.ttf") format("truetype");
+ font-weight: 800;
+ font-style: italic;
+}
+@font-face {
+ font-family: "Monocraft";
+ src: url("/fonts/Monocraft-Black.ttf") format("truetype");
+ font-weight: 900;
+ font-style: normal;
+}
+@font-face {
+ font-family: "Monocraft";
+ src: url("/fonts/Monocraft-Black-Italic.ttf") format("truetype");
+ font-weight: 900;
+ font-style: italic;
+}
diff --git a/public/styles/markdown/asides.css b/public/styles/markdown/asides.css
new file mode 100644
index 0000000..86aff8d
--- /dev/null
+++ b/public/styles/markdown/asides.css
@@ -0,0 +1,49 @@
+.starlight-aside {
+ padding: 1rem;
+ border-inline-start: 0.25rem solid var(--sl-color-asides-border);
+ color: var(--sl-color-white);
+}
+.starlight-aside--note {
+ --sl-color-asides-text-accent: var(--sl-color-blue-high);
+ --sl-color-asides-border: var(--sl-color-blue);
+ background-color: var(--sl-color-blue-low);
+}
+.starlight-aside--tip {
+ --sl-color-asides-text-accent: var(--sl-color-purple-high);
+ --sl-color-asides-border: var(--sl-color-purple);
+ background-color: var(--sl-color-purple-low);
+}
+.starlight-aside--caution {
+ --sl-color-asides-text-accent: var(--sl-color-orange-high);
+ --sl-color-asides-border: var(--sl-color-orange);
+ background-color: var(--sl-color-orange-low);
+}
+.starlight-aside--danger {
+ --sl-color-asides-text-accent: var(--sl-color-red-high);
+ --sl-color-asides-border: var(--sl-color-red);
+ background-color: var(--sl-color-red-low);
+}
+
+.starlight-aside__title {
+ display: flex;
+ gap: 0.5rem;
+ align-items: center;
+ font-size: var(--sl-text-h5);
+ font-weight: 600;
+ line-height: var(--sl-line-height-headings);
+ color: var(--sl-color-asides-text-accent);
+}
+
+.starlight-aside__icon {
+ font-size: 1.333em;
+ width: 1em;
+ height: 1em;
+}
+
+.starlight-aside__title + .starlight-aside__content {
+ margin-top: 0.5rem;
+}
+
+.starlight-aside__content a {
+ color: var(--sl-color-asides-text-accent);
+}
diff --git a/public/styles/markdown/markdown.css b/public/styles/markdown/markdown.css
new file mode 100644
index 0000000..eccbf7d
--- /dev/null
+++ b/public/styles/markdown/markdown.css
@@ -0,0 +1,251 @@
+.sl-markdown-content
+ :not(.expressive-code *)
+ + :not(a, strong, em, del, span, input, code, br)
+ + :not(a, strong, em, del, span, input, code, br, :where(.not-content *)) {
+ margin-top: 1rem;
+}
+
+/* Headings after non-headings have more spacing. */
+.sl-markdown-content
+ :not(h1, h2, h3, h4, h5, h6)
+ + :is(h1, h2, h3, h4, h5, h6):not(:where(.not-content *)) {
+ margin-top: 1.5em;
+}
+
+.sl-markdown-content li + li:not(:where(.not-content *)),
+.sl-markdown-content dt + dt:not(:where(.not-content *)),
+.sl-markdown-content dt + dd:not(:where(.not-content *)),
+.sl-markdown-content dd + dd:not(:where(.not-content *)) {
+ margin-top: 0.25rem;
+}
+
+.sl-markdown-content li:not(:where(.not-content *)) {
+ overflow-wrap: anywhere;
+}
+
+.sl-markdown-content
+ li
+ > :last-child:not(li, ul, ol):not(
+ a,
+ strong,
+ em,
+ del,
+ span,
+ input,
+ :where(.not-content *)
+ ) {
+ margin-bottom: 1.25rem;
+}
+
+.sl-markdown-content dt:not(:where(.not-content *)) {
+ font-weight: 700;
+}
+.sl-markdown-content dd:not(:where(.not-content *)) {
+ padding-inline-start: 1rem;
+}
+
+.sl-markdown-content :is(h1, h2, h3, h4, h5, h6):not(:where(.not-content *)) {
+ color: var(--sl-color-white);
+ line-height: var(--sl-line-height-headings);
+ font-weight: 600;
+ font-size: 10000%;
+}
+
+.sl-markdown-content
+ :is(img, picture, video, canvas, svg, iframe):not(:where(.not-content *)) {
+ display: block;
+ max-width: 100%;
+ height: auto;
+}
+
+.sl-markdown-content h1:not(:where(.not-content *)) {
+ font-size: var(--sl-text-h1);
+}
+.sl-markdown-content h2:not(:where(.not-content *)) {
+ font-size: var(--sl-text-h2);
+}
+.sl-markdown-content h3:not(:where(.not-content *)) {
+ font-size: var(--sl-text-h3);
+}
+.sl-markdown-content h4:not(:where(.not-content *)) {
+ font-size: var(--sl-text-h4);
+}
+.sl-markdown-content h5:not(:where(.not-content *)) {
+ font-size: var(--sl-text-h5);
+}
+.sl-markdown-content h6:not(:where(.not-content *)) {
+ font-size: var(--sl-text-h6);
+}
+
+.sl-markdown-content a:not(:where(.not-content *)) {
+ color: var(--sl-color-text-accent);
+}
+.sl-markdown-content a:hover:not(:where(.not-content *)) {
+ color: var(--sl-color-white);
+}
+
+.sl-markdown-content code:not(:where(.not-content *)) {
+ background-color: var(--sl-color-bg-inline-code);
+ margin-block: -0.125rem;
+ padding: 0.125rem 0.375rem;
+ font-size: var(--sl-text-code-sm);
+}
+.sl-markdown-content :is(h1, h2, h3, h4, h5, h6) code {
+ font-size: inherit;
+}
+
+.sl-markdown-content pre:not(:where(.not-content *)) {
+ border: 1px solid var(--sl-color-gray-5);
+ padding: 0.75rem 1rem;
+ font-size: var(--sl-text-code);
+ tab-size: 2;
+}
+
+.sl-markdown-content pre code:not(:where(.not-content *)) {
+ all: unset;
+ font-family: var(--__sl-font-mono);
+}
+
+.sl-markdown-content blockquote:not(:where(.not-content *)) {
+ border-inline-start: 1px solid var(--sl-color-gray-5);
+ padding-inline-start: 1rem;
+}
+
+/* Table styling */
+.sl-markdown-content table:not(:where(.not-content *)) {
+ display: block;
+ overflow: auto;
+ border-spacing: 0;
+}
+.sl-markdown-content :is(th, td):not(:where(.not-content *)) {
+ border-bottom: 1px solid var(--sl-color-gray-5);
+ padding: 0.5rem 1rem;
+ /* Align text to the top of the row in multiline tables. */
+ vertical-align: baseline;
+}
+.sl-markdown-content
+ :is(th:first-child, td:first-child):not(:where(.not-content *)) {
+ padding-inline-start: 0;
+}
+.sl-markdown-content
+ :is(th:last-child, td:last-child):not(:where(.not-content *)) {
+ padding-inline-end: 0;
+}
+.sl-markdown-content th:not(:where(.not-content *)) {
+ color: var(--sl-color-white);
+ font-weight: 600;
+}
+/* Align headings to the start of the line unless set by the `align` attribute. */
+.sl-markdown-content th:not([align]):not(:where(.not-content *)) {
+ text-align: start;
+}
+/* <table>s, <hr>s, and <blockquote>s inside asides */
+.sl-markdown-content
+ .starlight-aside
+ :is(th, td, hr, blockquote):not(:where(.not-content *)) {
+ border-color: var(--sl-color-gray-4);
+}
+@supports (
+ border-color:
+ color-mix(in srgb, var(--sl-color-asides-text-accent) 30%, transparent)
+) {
+ .sl-markdown-content
+ .starlight-aside
+ :is(th, td, hr, blockquote):not(:where(.not-content *)) {
+ border-color: color-mix(
+ in srgb,
+ var(--sl-color-asides-text-accent) 30%,
+ transparent
+ );
+ }
+}
+
+/* <code> inside asides */
+@supports (
+ border-color:
+ color-mix(in srgb, var(--sl-color-asides-text-accent) 12%, transparent)
+) {
+ .sl-markdown-content .starlight-aside code:not(:where(.not-content *)) {
+ background-color: color-mix(
+ in srgb,
+ var(--sl-color-asides-text-accent) 12%,
+ transparent
+ );
+ }
+}
+
+.sl-markdown-content hr:not(:where(.not-content *)) {
+ border: 0;
+ border-bottom: 1px solid var(--sl-color-hairline);
+}
+
+/* <details> and <summary> styles */
+.sl-markdown-content details:not(:where(.not-content *)) {
+ --sl-details-border-color: var(--sl-color-gray-5);
+ --sl-details-border-color--hover: var(--sl-color-text-accent);
+
+ border-inline-start: 2px solid var(--sl-details-border-color);
+ padding-inline-start: 1rem;
+}
+.sl-markdown-content details:not([open]):hover:not(:where(.not-content *)),
+.sl-markdown-content details:has(> summary:hover):not(:where(.not-content *)) {
+ border-color: var(--sl-details-border-color--hover);
+}
+.sl-markdown-content summary:not(:where(.not-content *)) {
+ color: var(--sl-color-white);
+ cursor: pointer;
+ display: block; /* Needed to hide the default marker in some browsers. */
+ font-weight: 600;
+ /* Expand the outline so that the marker cannot distort it. */
+ margin-inline-start: -0.5rem;
+ padding-inline-start: 0.5rem;
+}
+.sl-markdown-content details[open] > summary:not(:where(.not-content *)) {
+ margin-bottom: 1rem;
+}
+
+/* <summary> marker styles */
+.sl-markdown-content summary:not(:where(.not-content *))::marker,
+.sl-markdown-content
+ summary:not(:where(.not-content *))::-webkit-details-marker {
+ display: none;
+}
+.sl-markdown-content summary:not(:where(.not-content *))::before {
+ --sl-details-marker-size: 1.25rem;
+
+ background-color: currentColor;
+ content: "";
+ display: inline-block;
+ height: var(--sl-details-marker-size);
+ width: var(--sl-details-marker-size);
+ margin-inline: calc((var(--sl-details-marker-size) / 4) * -1) 0.25rem;
+ vertical-align: middle;
+ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M14.8 11.3 10.6 7a1 1 0 1 0-1.4 1.5l3.5 3.5-3.5 3.5a1 1 0 0 0 0 1.4 1 1 0 0 0 .7.3 1 1 0 0 0 .7-.3l4.2-4.2a1 1 0 0 0 0-1.4Z'/%3E%3C/svg%3E%0A");
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M14.8 11.3 10.6 7a1 1 0 1 0-1.4 1.5l3.5 3.5-3.5 3.5a1 1 0 0 0 0 1.4 1 1 0 0 0 .7.3 1 1 0 0 0 .7-.3l4.2-4.2a1 1 0 0 0 0-1.4Z'/%3E%3C/svg%3E%0A");
+ -webkit-mask-repeat: no-repeat;
+ mask-repeat: no-repeat;
+}
+@media (prefers-reduced-motion: no-preference) {
+ .sl-markdown-content summary:not(:where(.not-content *))::before {
+ transition: transform 0.2s ease-in-out;
+ }
+}
+.sl-markdown-content
+ details[open]
+ > summary:not(:where(.not-content *))::before {
+ transform: rotateZ(90deg);
+}
+[dir="rtl"] .sl-markdown-content summary:not(:where(.not-content *))::before,
+.sl-markdown-content [dir="rtl"] summary:not(:where(.not-content *))::before {
+ transform: rotateZ(180deg);
+}
+/* <summary> with only a paragraph automatically added when using MDX */
+.sl-markdown-content summary:not(:where(.not-content *)) p:only-child {
+ display: inline;
+}
+
+/* <details> styles inside asides */
+.sl-markdown-content .starlight-aside details:not(:where(.not-content *)) {
+ --sl-details-border-color: var(--sl-color-asides-border);
+ --sl-details-border-color--hover: var(--sl-color-asides-text-accent);
+}
diff --git a/public/styles/markdown/props.css b/public/styles/markdown/props.css
new file mode 100644
index 0000000..58a09f6
--- /dev/null
+++ b/public/styles/markdown/props.css
@@ -0,0 +1,189 @@
+:root,
+::backdrop {
+ /* Colors (dark mode) */
+ --sl-color-white: hsl(0, 0%, 100%); /* “white” */
+ --sl-color-gray-1: hsl(224, 20%, 94%);
+ --sl-color-gray-2: hsl(224, 6%, 77%);
+ --sl-color-gray-3: hsl(224, 6%, 56%);
+ --sl-color-gray-4: hsl(224, 7%, 36%);
+ --sl-color-gray-5: hsl(224, 10%, 23%);
+ --sl-color-gray-6: hsl(224, 14%, 16%);
+ --sl-color-black: hsl(224, 10%, 10%);
+
+ --sl-hue-orange: 41;
+ --sl-color-orange-low: hsl(var(--sl-hue-orange), 39%, 22%);
+ --sl-color-orange: hsl(var(--sl-hue-orange), 82%, 63%);
+ --sl-color-orange-high: hsl(var(--sl-hue-orange), 82%, 87%);
+ --sl-hue-green: 101;
+ --sl-color-green-low: hsl(var(--sl-hue-green), 39%, 22%);
+ --sl-color-green: hsl(var(--sl-hue-green), 82%, 63%);
+ --sl-color-green-high: hsl(var(--sl-hue-green), 82%, 80%);
+ --sl-hue-blue: 234;
+ --sl-color-blue-low: hsl(var(--sl-hue-blue), 54%, 20%);
+ --sl-color-blue: hsl(var(--sl-hue-blue), 100%, 60%);
+ --sl-color-blue-high: hsl(var(--sl-hue-blue), 100%, 87%);
+ --sl-hue-purple: 281;
+ --sl-color-purple-low: hsl(var(--sl-hue-purple), 39%, 22%);
+ --sl-color-purple: hsl(var(--sl-hue-purple), 82%, 63%);
+ --sl-color-purple-high: hsl(var(--sl-hue-purple), 82%, 89%);
+ --sl-hue-red: 339;
+ --sl-color-red-low: hsl(var(--sl-hue-red), 39%, 22%);
+ --sl-color-red: hsl(var(--sl-hue-red), 82%, 63%);
+ --sl-color-red-high: hsl(var(--sl-hue-red), 82%, 87%);
+
+ --sl-color-accent-low: hsl(224, 54%, 20%);
+ --sl-color-accent: hsl(224, 100%, 60%);
+ --sl-color-accent-high: hsl(224, 100%, 85%);
+
+ --sl-color-text: var(--sl-color-gray-2);
+ --sl-color-text-accent: var(--sl-color-accent-high);
+ --sl-color-text-invert: var(--sl-color-accent-low);
+ --sl-color-bg: var(--sl-color-black);
+ --sl-color-bg-nav: var(--sl-color-gray-6);
+ --sl-color-bg-sidebar: var(--sl-color-gray-6);
+ --sl-color-bg-inline-code: var(--sl-color-gray-5);
+ --sl-color-bg-accent: var(--sl-color-accent-high);
+ --sl-color-hairline-light: var(--sl-color-gray-5);
+ --sl-color-hairline: var(--sl-color-gray-6);
+ --sl-color-hairline-shade: var(--sl-color-black);
+
+ --sl-color-backdrop-overlay: hsla(223, 13%, 10%, 0.66);
+
+ /* Shadows (dark mode) */
+ --sl-shadow-sm: 0px 1px 1px hsla(0, 0%, 0%, 0.12),
+ 0px 2px 1px hsla(0, 0%, 0%, 0.24);
+ --sl-shadow-md: 0px 8px 4px hsla(0, 0%, 0%, 0.08),
+ 0px 5px 2px hsla(0, 0%, 0%, 0.08), 0px 3px 2px hsla(0, 0%, 0%, 0.12),
+ 0px 1px 1px hsla(0, 0%, 0%, 0.15);
+ --sl-shadow-lg: 0px 25px 7px hsla(0, 0%, 0%, 0.03),
+ 0px 16px 6px hsla(0, 0%, 0%, 0.1), 0px 9px 5px hsla(223, 13%, 10%, 0.33),
+ 0px 4px 4px hsla(0, 0%, 0%, 0.75), 0px 4px 2px hsla(0, 0%, 0%, 0.25);
+
+ /* Text size and line height */
+ --sl-text-2xs: 0.75rem; /* 12px */
+ --sl-text-xs: 0.8125rem; /* 13px */
+ --sl-text-sm: 0.875rem; /* 14px */
+ --sl-text-base: 1rem; /* 16px */
+ --sl-text-lg: 1.125rem; /* 18px */
+ --sl-text-xl: 1.25rem; /* 20px */
+ --sl-text-2xl: 1.5rem; /* 24px */
+ --sl-text-3xl: 1.8125rem; /* 29px */
+ --sl-text-4xl: 2.1875rem; /* 35px */
+ --sl-text-5xl: 2.625rem; /* 42px */
+ --sl-text-6xl: 4rem; /* 64px */
+
+ --sl-text-body: var(--sl-text-base);
+ --sl-text-body-sm: var(--sl-text-xs);
+ --sl-text-code: var(--sl-text-sm);
+ --sl-text-code-sm: var(--sl-text-xs);
+ --sl-text-h1: var(--sl-text-4xl);
+ --sl-text-h2: var(--sl-text-3xl);
+ --sl-text-h3: var(--sl-text-2xl);
+ --sl-text-h4: var(--sl-text-xl);
+ --sl-text-h5: var(--sl-text-lg);
+
+ --sl-line-height: 1.75;
+ --sl-line-height-headings: 1.2;
+
+ --sl-font-system: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
+ "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif,
+ "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+ --sl-font-system-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
+ "Liberation Mono", "Courier New", monospace;
+ --__sl-font: var(--sl-font, var(--sl-font-system)), var(--sl-font-system);
+ --__sl-font-mono: var(--sl-font-mono, var(--sl-font-system-mono)),
+ var(--sl-font-system-mono);
+
+ /** Key layout values */
+ --sl-nav-height: 3.5rem;
+ --sl-nav-pad-x: 1rem;
+ --sl-nav-pad-y: 0.75rem;
+ --sl-mobile-toc-height: 3rem;
+ --sl-sidebar-width: 18.75rem;
+ --sl-sidebar-pad-x: 1rem;
+ --sl-content-width: 45rem;
+ --sl-content-pad-x: 1rem;
+ --sl-menu-button-size: 2rem;
+ --sl-nav-gap: var(--sl-content-pad-x);
+ /* Offset required to show outline inside an element instead of round the outside */
+ --sl-outline-offset-inside: -0.1875rem;
+
+ /* Global z-index values */
+ --sl-z-index-toc: 4;
+ --sl-z-index-menu: 5;
+ --sl-z-index-navbar: 10;
+ --sl-z-index-skiplink: 20;
+}
+
+:root[data-theme="light"],
+[data-theme="light"] ::backdrop {
+ /* Colours (light mode) */
+ --sl-color-white: hsl(224, 10%, 10%);
+ --sl-color-gray-1: hsl(224, 14%, 16%);
+ --sl-color-gray-2: hsl(224, 10%, 23%);
+ --sl-color-gray-3: hsl(224, 7%, 36%);
+ --sl-color-gray-4: hsl(224, 6%, 56%);
+ --sl-color-gray-5: hsl(224, 6%, 77%);
+ --sl-color-gray-6: hsl(224, 20%, 94%);
+ --sl-color-gray-7: hsl(224, 19%, 97%);
+ --sl-color-black: hsl(0, 0%, 100%);
+
+ --sl-color-orange-high: hsl(var(--sl-hue-orange), 80%, 25%);
+ --sl-color-orange: hsl(var(--sl-hue-orange), 90%, 60%);
+ --sl-color-orange-low: hsl(var(--sl-hue-orange), 90%, 88%);
+ --sl-color-green-high: hsl(var(--sl-hue-green), 80%, 22%);
+ --sl-color-green: hsl(var(--sl-hue-green), 90%, 46%);
+ --sl-color-green-low: hsl(var(--sl-hue-green), 85%, 90%);
+ --sl-color-blue-high: hsl(var(--sl-hue-blue), 80%, 30%);
+ --sl-color-blue: hsl(var(--sl-hue-blue), 90%, 60%);
+ --sl-color-blue-low: hsl(var(--sl-hue-blue), 88%, 90%);
+ --sl-color-purple-high: hsl(var(--sl-hue-purple), 90%, 30%);
+ --sl-color-purple: hsl(var(--sl-hue-purple), 90%, 60%);
+ --sl-color-purple-low: hsl(var(--sl-hue-purple), 80%, 90%);
+ --sl-color-red-high: hsl(var(--sl-hue-red), 80%, 30%);
+ --sl-color-red: hsl(var(--sl-hue-red), 90%, 60%);
+ --sl-color-red-low: hsl(var(--sl-hue-red), 80%, 90%);
+
+ --sl-color-accent-high: hsl(234, 80%, 30%);
+ --sl-color-accent: hsl(234, 90%, 60%);
+ --sl-color-accent-low: hsl(234, 88%, 90%);
+
+ --sl-color-text-accent: var(--sl-color-accent);
+ --sl-color-text-invert: var(--sl-color-black);
+ --sl-color-bg-nav: var(--sl-color-gray-7);
+ --sl-color-bg-sidebar: var(--sl-color-bg);
+ --sl-color-bg-inline-code: var(--sl-color-gray-6);
+ --sl-color-bg-accent: var(--sl-color-accent);
+ --sl-color-hairline-light: var(--sl-color-gray-6);
+ --sl-color-hairline-shade: var(--sl-color-gray-6);
+
+ --sl-color-backdrop-overlay: hsla(225, 9%, 36%, 0.66);
+
+ /* Shadows (light mode) */
+ --sl-shadow-sm: 0px 1px 1px hsla(0, 0%, 0%, 0.06),
+ 0px 2px 1px hsla(0, 0%, 0%, 0.06);
+ --sl-shadow-md: 0px 8px 4px hsla(0, 0%, 0%, 0.03),
+ 0px 5px 2px hsla(0, 0%, 0%, 0.03), 0px 3px 2px hsla(0, 0%, 0%, 0.06),
+ 0px 1px 1px hsla(0, 0%, 0%, 0.06);
+ --sl-shadow-lg: 0px 25px 7px rgba(0, 0, 0, 0.01),
+ 0px 16px 6px hsla(0, 0%, 0%, 0.03), 0px 9px 5px hsla(223, 13%, 10%, 0.08),
+ 0px 4px 4px hsla(0, 0%, 0%, 0.16), 0px 4px 2px hsla(0, 0%, 0%, 0.04);
+}
+
+@media (min-width: 50em) {
+ :root {
+ --sl-nav-height: 4rem;
+ --sl-nav-pad-x: 1.5rem;
+ --sl-text-h1: var(--sl-text-5xl);
+ --sl-text-h2: var(--sl-text-4xl);
+ --sl-text-h3: var(--sl-text-3xl);
+ --sl-text-h4: var(--sl-text-2xl);
+ }
+}
+
+@media (min-width: 72rem) {
+ :root {
+ --sl-content-pad-x: 1.5rem;
+ --sl-mobile-toc-height: 0rem;
+ }
+}
diff --git a/public/styles/markdown/reset.css b/public/styles/markdown/reset.css
new file mode 100644
index 0000000..42be0c5
--- /dev/null
+++ b/public/styles/markdown/reset.css
@@ -0,0 +1,48 @@
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+* {
+ margin: 0;
+}
+
+html {
+ color-scheme: dark;
+ accent-color: var(--sl-color-accent);
+}
+
+html[data-theme="light"] {
+ color-scheme: light;
+}
+
+body {
+ font-family: var(--__sl-font);
+ line-height: var(--sl-line-height);
+ -webkit-font-smoothing: antialiased;
+ color: var(--sl-color-text);
+ background-color: var(--sl-color-bg);
+}
+
+input,
+button,
+textarea,
+select {
+ font: inherit;
+}
+
+p,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+code {
+ overflow-wrap: anywhere;
+}
+
+code {
+ font-family: var(--__sl-font-mono);
+}
diff --git a/public/styles/markdown/shiki.css b/public/styles/markdown/shiki.css
new file mode 100644
index 0000000..704eea8
--- /dev/null
+++ b/public/styles/markdown/shiki.css
@@ -0,0 +1,13 @@
+:root {
+ --astro-code-color-text: var(--sl-color-white);
+ --astro-code-color-background: var(--sl-color-gray-6);
+ --astro-code-token-constant: var(--sl-color-blue-high);
+ --astro-code-token-string: var(--sl-color-green-high);
+ --astro-code-token-comment: var(--sl-color-gray-2);
+ --astro-code-token-keyword: var(--sl-color-purple-high);
+ --astro-code-token-parameter: var(--sl-color-red-high);
+ --astro-code-token-function: var(--sl-color-red-high);
+ --astro-code-token-string-expression: var(--sl-color-green-high);
+ --astro-code-token-punctuation: var(--sl-color-gray-2);
+ --astro-code-token-link: var(--sl-color-blue-high);
+}
diff --git a/public/styles/markdown/util.css b/public/styles/markdown/util.css
new file mode 100644
index 0000000..63ede50
--- /dev/null
+++ b/public/styles/markdown/util.css
@@ -0,0 +1,61 @@
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border-width: 0;
+}
+
+.sl-hidden {
+ display: none;
+}
+.sl-flex {
+ display: flex;
+}
+.sl-block {
+ display: block;
+}
+@media (min-width: 50rem) {
+ .md\:sl-hidden {
+ display: none;
+ }
+ .md\:sl-flex {
+ display: flex;
+ }
+ .md\:sl-block {
+ display: block;
+ }
+}
+@media (min-width: 72rem) {
+ .lg\:sl-hidden {
+ display: none;
+ }
+ .lg\:sl-flex {
+ display: flex;
+ }
+ .lg\:sl-block {
+ display: block;
+ }
+}
+[data-theme="light"] .light\:sl-hidden {
+ display: none;
+}
+[data-theme="dark"] .dark\:sl-hidden {
+ display: none;
+}
+
+/*
+Flip an element around the y-axis when in an RTL context.
+Primarily useful for things where we can’t rely on writing direction like icons.
+
+<Icon name="right-arrow" class="rtl:flip" />
+
+In a LTR context: → In a RTL context: ←
+*/
+[dir="rtl"] .rtl\:flip:not(:where([dir="rtl"] [dir="ltr"] *)) {
+ transform: matrix(-1, 0, 0, 1, 0, 0);
+}
diff --git a/public/styles/terminal.css b/public/styles/terminal.css
new file mode 100644
index 0000000..f44bcd0
--- /dev/null
+++ b/public/styles/terminal.css
@@ -0,0 +1,47 @@
+@import url("@styles/fonts/JetBrainsMono.css");
+
+@font-face {
+ font-family: "Monocraft";
+ src: url("/fonts/Monocraft.ttf") format("truetype");
+ font-display: swap;
+}
+
+:root {
+ --primary: #54d7a9;
+ --secondary: #9b7eca;
+ --accent: #7ed4fb;
+ --extra: #8fcc75;
+ --text: #ffffff;
+ --alt: #999999;
+ --background: #000000;
+ --font: "Jetbrains Mono", monospace;
+}
+
+html {
+ background-color: var(--background);
+ color: var(--text);
+ font-family: var(--font);
+ user-select: none;
+
+ font-size: 1vw;
+}
+
+body {
+ min-height: 100vh;
+ display: grid;
+ place-items: center;
+}
+
+@layer base {
+ a {
+ font-weight: 300;
+ text-decoration: none;
+ color: var(--secondary);
+ transition-duration: 0.2s;
+ transition-property: color, text-decoration;
+ }
+ a:hover {
+ text-decoration: underline;
+ color: var(--accent);
+ }
+}
diff --git a/public/theme.js b/public/theme.js
new file mode 100644
index 0000000..90bd1de
--- /dev/null
+++ b/public/theme.js
@@ -0,0 +1,12 @@
+async function applyStyle(path, name) {
+ const res = await fetch(`${path}/${encodeURIComponent(name)}`);
+ if (!res.ok) return false;
+ for (const [key, value] of Object.entries(await res.json())) {
+ document.documentElement.style.setProperty(`--${key}`, value);
+ }
+ return true;
+}
+
+window.setStyle = (path, name) => applyStyle(path, name);
+window.setTheme = (name) => applyStyle("/theme", name);
+window.setFontPack = (name) => applyStyle("/font-pack", name);
diff --git a/website.go b/website.go
index d1cf44d..8802bd1 100644
--- a/website.go
+++ b/website.go
@@ -1,12 +1,271 @@
package main
import (
+ "encoding/json"
"fmt"
"net/http"
+ "strings"
+ "sync"
+
+ "github.com/a-h/templ"
)
+func Assert(ok bool) {
+ if !ok {
+ panic("assertion failure")
+ }
+}
+
+type Theme struct {
+ primary string
+ secondary string
+ accent string
+ extra string
+ text string
+ alt string
+ background string
+}
+
+type FontPack struct {
+ Monospace string
+ Normal string
+}
+
+type Style struct {
+ theme Theme
+ fontPack FontPack
+}
+
+var (
+ mu sync.RWMutex
+ themes = map[string]Theme{}
+ fontPacks = map[string]FontPack{}
+)
+
+func SetFontPack(name string, fontPack FontPack) {
+ mu.Lock()
+ defer mu.Unlock()
+ fontPacks[name] = fontPack
+}
+
+func GetFontPack(name string) (FontPack, bool) {
+ mu.RLock()
+ defer mu.RUnlock()
+ fontPack, ok := fontPacks[name]
+ return fontPack, ok
+}
+
+func SetTheme(name string, theme Theme) {
+ mu.Lock()
+ defer mu.Unlock()
+ themes[name] = theme
+}
+
+func GetTheme(name string) (Theme, bool) {
+ mu.RLock()
+ defer mu.RUnlock()
+ theme, ok := themes[name]
+ return theme, ok
+}
+
+func currentStyle(r *http.Request) Style {
+ theme, ok := GetTheme("default")
+ Assert(ok)
+ fontPack, ok := GetFontPack("default")
+ Assert(ok)
+ style := Style{
+ theme: theme,
+ fontPack: fontPack,
+ }
+
+ if cookie, err := r.Cookie("theme"); err == nil {
+ if theme, ok := GetTheme(cookie.Value); ok {
+ style.theme = theme
+ }
+ }
+
+ if cookie, err := r.Cookie("font-pack"); err == nil {
+ if fontPack, ok := GetFontPack(cookie.Value); ok {
+ style.fontPack = fontPack
+ }
+ }
+
+ return style
+}
+
+func (theme Theme) Vars() map[string]string {
+ return map[string]string{
+ "primary": theme.primary,
+ "secondary": theme.secondary,
+ "accent": theme.accent,
+ "extra": theme.extra,
+ "text": theme.text,
+ "alt": theme.alt,
+ "background": theme.background,
+ }
+}
+
+func (fontPack FontPack) Vars() map[string]string {
+ return map[string]string{
+ "font-mono": fontPack.Monospace,
+ "font-normal": fontPack.Normal,
+ }
+}
+
+func (style Style) Vars() map[string]string {
+ vars := style.theme.Vars()
+ for k, v := range style.fontPack.Vars() {
+ vars[k] = v
+ }
+ return vars
+}
+
+func styleToCss(style Style) templ.SafeCSS {
+ var b strings.Builder
+ for k, v := range style.Vars() {
+ fmt.Fprintf(&b, "--%s:%s;", k, v)
+ }
+ return templ.SafeCSS(b.String())
+}
+
+type Project struct {
+ Name string
+ Type string
+ Desc []string
+ Cmd string
+ Link string
+}
+
+var projects = []Project{
+ {
+ Name: "CPU vs AI",
+ Type: "Game (made with Zig)",
+ Desc: []string{
+ "A tower defense game where an AI chip on your motherboard",
+ "generates bugs! Place CPUs to destroy the bugs!",
+ "Don't let them corrupt your RAM!",
+ },
+ },
+ {
+ Name: "Eko",
+ Type: "Social Media Platform",
+ Desc: []string{
+ "A terminal-native alternative",
+ "to Discord for Terminal",
+ "nerds!",
+ },
+ },
+ {
+ Name: "Trident",
+ Type: "IntelliJ Plugin",
+ Desc: []string{
+ "A blazingly fast way to",
+ "Navigate to your mosta used",
+ "files. Minimize keystrokes",
+ "and maximize productivity!",
+ },
+ },
+ {
+ Name: "Dotfiles",
+ Type: "Configuration",
+ Desc: []string{
+ "My Linux configuration,",
+ "includes Neovim, tmux and",
+ "other programs.",
+ },
+ },
+}
+
+func styleHandler(cookie string, lookup func(string) (map[string]string, bool)) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ name := r.PathValue("name")
+ vars, ok := lookup(name)
+ if !ok {
+ http.NotFound(w, r)
+ return
+ }
+ http.SetCookie(w, &http.Cookie{
+ Name: cookie, Value: name, Path: "/",
+ MaxAge: 31536000, SameSite: http.SameSiteLaxMode,
+ })
+ w.Header().Set("Cache-Control", "no-store")
+ json.NewEncoder(w).Encode(vars)
+ }
+}
+
func main() {
- fmt.Println("foo")
+ carbonightCat := Theme{
+ primary: "#94e2d5",
+ secondary: "#b4befe",
+ accent: "#74c7ec",
+ extra: "#a6e3a1",
+ text: "#cdd6f4",
+ alt: "#585b70",
+ background: "#1e1e2e",
+ }
+ darkness := Theme{
+ primary: "#000000",
+ secondary: "#000000",
+ accent: "#000000",
+ extra: "#000000",
+ text: "#000000",
+ alt: "#000000",
+ background: "#000000",
+ }
+ dark := Theme{
+ primary: "#54d7a9",
+ secondary: "#9b7eca",
+ accent: "#7ed4fb",
+ extra: "#8fcc75",
+ text: "#ffffff",
+ alt: "#999999",
+ background: "#000000",
+ }
+ light := Theme{
+ primary: "#009399",
+ secondary: "#440b8e",
+ accent: "#177282",
+ extra: "#40a02b",
+ text: "#000000",
+ alt: "#4d4d4d",
+ background: "#ffffff",
+ }
+ banana := Theme{
+ primary: "#ffdd44",
+ secondary: "#9b7eca",
+ accent: "#7ed4fb",
+ extra: "#000000",
+ text: "#ffffff",
+ alt: "#999999",
+ background: "#000000",
+ }
+
+ jetbrains := FontPack{
+ Monospace: "JetBrains Mono",
+ Normal: `system-ui, sans-serif`,
+ }
+
+ minecraft := FontPack{
+ Monospace: `"Monocraft", "JetBrains Mono", monospace`,
+ Normal: `"Monocraft", system-ui, sans-serif`,
+ }
+
+ SetTheme("carbonight-cat", carbonightCat)
+ SetTheme("banana", banana)
+ SetTheme("black", dark)
+ SetTheme("dark", darkness)
+
+ SetTheme("light", light)
+ SetTheme("white", light)
+ SetTheme("sun", light)
+ SetTheme("sunlight", light)
+ SetTheme("flashbang", light)
+
+ SetTheme("default", carbonightCat)
+
+ SetFontPack("jetbrains", jetbrains)
+ SetFontPack("minecraft", minecraft)
+ SetFontPack("default", jetbrains)
mux := http.NewServeMux()
@@ -14,22 +273,26 @@ func main() {
mux.Handle("GET /public/", http.StripPrefix("/public/", fileServer))
mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) {
- HomePage().Render(r.Context(), w)
+ HomePage(currentStyle(r)).Render(r.Context(), w)
})
- mux.HandleFunc("GET /about", func(w http.ResponseWriter, r *http.Request) {
- AboutPage().Render(r.Context(), w)
+ mux.HandleFunc("GET /skills", func(w http.ResponseWriter, r *http.Request) {
+ SkillsPage(currentStyle(r)).Render(r.Context(), w)
})
- mux.HandleFunc("GET /items", func(w http.ResponseWriter, r *http.Request) {
- data := []Item{
- {Name: "Foo", Active: true},
- {Name: "Bar", Active: true},
- {Name: "Baz", Active: false},
- }
- ItemsPage(data).Render(r.Context(), w)
+ mux.HandleFunc("GET /projects", func(w http.ResponseWriter, r *http.Request) {
+ ProjectsPage(currentStyle(r), projects).Render(r.Context(), w)
})
+ mux.HandleFunc("GET /theme/{name}", styleHandler("theme", func(n string) (map[string]string, bool) {
+ theme, ok := GetTheme(n)
+ return theme.Vars(), ok
+ }))
+ mux.HandleFunc("GET /font-pack/{name}", styleHandler("font-pack", func(n string) (map[string]string, bool) {
+ fontPack, ok := GetFontPack(n)
+ return fontPack.Vars(), ok
+ }))
+
fmt.Println("Server running at http://localhost:8080")
if err := http.ListenAndServe(":8080", mux); err != nil {
panic(err)
diff --git a/website.templ b/website.templ
index a3f137a..face9f9 100644
--- a/website.templ
+++ b/website.templ
@@ -1,81 +1,88 @@
package main
-import "fmt"
+templ HeadComponent(title string, description string) {
+ <head>
+ <meta charset="UTF-8"/>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
+ <meta name="color-scheme" content="dark"/>
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg"/>
+ <meta name="darkreader-lock"/>
+ <meta name="robots" content="index, follow, noarchive"/>
+ <meta name="keywords" content="Kyren223, Krypton, Compiler, Minecraft, Developer, Language, Langdev, Gamedev, Game, Software, Software Engineer, Game developer"/>
+ <meta name="author" content="Kyren223"/>
+ <meta name="description" content={ description }/>
+ <title>{ title }</title>
+ <meta property="og:title" content={ title }/>
+ <meta property="og:description" content={ description }/>
+ <meta property="og:type" content="website"/>
+ <meta property="og:url" content="https://kyren.codes/"/>
+ <meta property="og:image" content="https://kyren.codes/image.png"/>
+ <meta property="og:image:alt" content="a cyan sphere with a dark background"/>
+ <meta property="og:site_name" content="Kyren's Website"/>
+ <script src="/public/theme.js" defer></script>
+ <link rel="stylesheet" href="/public/style.css"/>
+ { children... }
+ </head>
+}
+
+templ NavLink(href string, label string) {
+ <a href={ templ.SafeURL(href) } class="text-(--primary) hover:text-(--accent)">{ label }</a>
+}
-// 1. Shared Base Layout Component
-templ PageLayout(title string) {
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8"/>
- <title>{ title }</title>
- <style>
- body { font-family: monospace; background: #0f172a; color: #f8fafc; padding: 2rem; line-height: 1.5; }
- nav a { color: #38bdf8; margin-right: 1rem; text-decoration: none; }
- nav a:hover { text-decoration: underline; }
- .card { background: #1e293b; padding: 1rem; border-radius: 8px; margin-top: 1rem; border: 1px solid #334155; }
- .badge-active { color: #4ade80; }
- .badge-inactive { color: #f87171; }
- </style>
- <link rel="stylesheet" href="/public/style.css"/>
- </head>
- <body>
- <nav>
- <a href="/">/home</a>
- <a href="/about">/about</a>
- <a href="/items">/items</a>
- </nav>
- <hr style="border-color: #334155; margin: 1rem 0;"/>
- { children... }
- </body>
- </html>
+templ NavComponent(path string) {
+ <nav class="flex justify-between gap-10 p-4 mb-[1.5625rem] border-2 border-(--alt) text-[1.375rem]">
+ <span>{ path }</span>
+ <span class="flex gap-[1ch]">
+ @NavLink("/", "home")
+ for _, dir := range []string{"skills", "projects", "blogs"} {
+ @NavLink("/"+dir, dir)
+ }
+ @NavLink("/shell", "shell")
+ </span>
+ </nav>
}
-// 2. Page 1: Home
-templ HomePage() {
- @PageLayout("Home") {
- <h1>Home</h1>
- <p>Go server running with Templ templates and Air hot-reloading.</p>
- }
+templ TerminalLayout(style Style, path string) {
+ <!DOCTYPE html>
+ <html lang="en" style={ styleToCss(style) }>
+ @HeadComponent(path, "My personal website") {
+ <link rel="stylesheet" href="/public/styles/terminal.css"/>
+ }
+ <body>
+ <main class="w-[75rem] h-[40rem] p-8 border-[0.2rem] border-(--text) overflow-hidden text-[1.125rem]">
+ @NavComponent(path)
+ { children... }
+ </main>
+ </body>
+ </html>
}
-// 3. Page 2: About
-templ AboutPage() {
- @PageLayout("About") {
- <h1 class="text-3xl font-bold underline">About</h1>
- <p>Single-binary web app using Go's standard library net/http.</p>
- }
+templ HomePage(style Style) {
+ @TerminalLayout(style, "/home/kyren/") {
+ <div id="content" class="w-0 h-0 invisible sm:w-full sm:h-full sm:visible">
+ <h1>Home</h1>
+ <p>Go server running with Templ templates and Air hot-reloading.</p>
+ </div>
+ }
}
-// Data struct for dynamic rendering
-type Item struct {
- Name string
- Active bool
+templ SkillsPage(style Style) {
+ @TerminalLayout(style, "/home/kyren/skills.md") {
+ <h1 class="text-3xl font-bold">About</h1>
+ <p>Single-binary web app using Go's standard library net/http.</p>
+ }
}
-// 4. Page 3: Dynamic Data (Conditionals + Loops)
-templ ItemsPage(items []Item) {
- @PageLayout("Items") {
- <h1>Items</h1>
- <div class="card">
- if len(items) == 0 {
- <p>No items in database.</p>
- } else {
- <ul>
- for _, item := range items {
- <li>
- <span>{ item.Name }</span>
- if item.Active {
- <span class="badge-active"> [ACTIVE]</span>
- } else {
- <span class="badge-inactive"> [INACTIVE]</span>
- }
- </li>
- }
- </ul>
- }
- </div>
- <p style="color: #94a3b8; font-size: 0.8rem; margin-top: 1rem;">
- { fmt.Sprintf("Total items processed: %d", len(items)) }
- </p>
- }
+templ ProjectsPage(style Style, projects []Project) {
+ @TerminalLayout(style, "/home/kyren/projects") {
+ <h1>Items</h1>
+ <div class="card">
+ <ul>
+ <li>
+ <span>foo</span>
+ <span class="badge-active">[ACTIVE]</span>
+ </li>
+ </ul>
+ </div>
+ }
+}
diff --git a/website_templ.go b/website_templ.go
index 9acf03c..5782d94 100644
--- a/website_templ.go
+++ b/website_templ.go
@@ -8,10 +8,7 @@ package main
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
-import "fmt"
-
-// 1. Shared Base Layout Component
-func PageLayout(title string) templ.Component {
+func HeadComponent(title string, description string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -32,20 +29,59 @@ func PageLayout(title string) templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><title>")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><meta name=\"color-scheme\" content=\"dark\"><link rel=\"icon\" type=\"image/svg+xml\" href=\"/favicon.svg\"><meta name=\"darkreader-lock\"><meta name=\"robots\" content=\"index, follow, noarchive\"><meta name=\"keywords\" content=\"Kyren223, Krypton, Compiler, Minecraft, Developer, Language, Langdev, Gamedev, Game, Software, Software Engineer, Game developer\"><meta name=\"author\" content=\"Kyren223\"><meta name=\"description\" content=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
- templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
+ templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(description)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `website.templ`, Line: 13, Col: 48}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\"><title>")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var3 string
+ templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(title)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `website.templ`, Line: 14, Col: 16}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</title><meta property=\"og:title\" content=\"")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var4 string
+ templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(title)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `website.templ`, Line: 15, Col: 43}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `website.templ`, Line: 11, Col: 22}
+ return templ_7745c5c3_Err
}
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\"><meta property=\"og:description\" content=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title><style>\n body { font-family: monospace; background: #0f172a; color: #f8fafc; padding: 2rem; line-height: 1.5; }\n nav a { color: #38bdf8; margin-right: 1rem; text-decoration: none; }\n nav a:hover { text-decoration: underline; }\n .card { background: #1e293b; padding: 1rem; border-radius: 8px; margin-top: 1rem; border: 1px solid #334155; }\n .badge-active { color: #4ade80; }\n .badge-inactive { color: #f87171; }\n </style><link rel=\"stylesheet\" href=\"/public/style.css\"></head><body><nav><a href=\"/\">/home</a> <a href=\"/about\">/about</a> <a href=\"/items\">/items</a></nav><hr style=\"border-color: #334155; margin: 1rem 0;\">")
+ var templ_7745c5c3_Var5 string
+ templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(description)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `website.templ`, Line: 16, Col: 55}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\"><meta property=\"og:type\" content=\"website\"><meta property=\"og:url\" content=\"https://kyren.codes/\"><meta property=\"og:image\" content=\"https://kyren.codes/image.png\"><meta property=\"og:image:alt\" content=\"a cyan sphere with a dark background\"><meta property=\"og:site_name\" content=\"Kyren's Website\"><script src=\"/public/theme.js\" defer></script><link rel=\"stylesheet\" href=\"/public/style.css\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -53,7 +89,7 @@ func PageLayout(title string) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</body></html>")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</head>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -61,8 +97,7 @@ func PageLayout(title string) templ.Component {
})
}
-// 2. Page 1: Home
-func HomePage() templ.Component {
+func NavLink(href string, label string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -78,12 +113,144 @@ func HomePage() templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
- templ_7745c5c3_Var3 := templ.GetChildren(ctx)
- if templ_7745c5c3_Var3 == nil {
- templ_7745c5c3_Var3 = templ.NopComponent
+ templ_7745c5c3_Var6 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var6 == nil {
+ templ_7745c5c3_Var6 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
- templ_7745c5c3_Var4 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<a href=\"")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var7 templ.SafeURL
+ templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(href))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `website.templ`, Line: 29, Col: 30}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" class=\"text-(--primary) hover:text-(--accent)\">")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var8 string
+ templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(label)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `website.templ`, Line: 29, Col: 87}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</a>")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+func NavComponent(path string) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var9 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var9 == nil {
+ templ_7745c5c3_Var9 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<nav class=\"flex justify-between gap-10 p-4 mb-[1.5625rem] border-2 border-(--alt) text-[1.375rem]\"><span>")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var10 string
+ templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(path)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `website.templ`, Line: 34, Col: 14}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</span> <span class=\"flex gap-[1ch]\">")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = NavLink("/", "home").Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ for _, dir := range []string{"skills", "projects", "blogs"} {
+ templ_7745c5c3_Err = NavLink("/"+dir, dir).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = NavLink("/shell", "shell").Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</span></nav>")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+func TerminalLayout(style Style, path string) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
+ }
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var11 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var11 == nil {
+ templ_7745c5c3_Var11 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<!doctype html><html lang=\"en\" style=\"")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var12 string
+ templ_7745c5c3_Var12, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(styleToCss(style))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `website.templ`, Line: 47, Col: 42}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\">")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Var13 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
@@ -95,13 +262,29 @@ func HomePage() templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<h1>Home</h1><p>Go server running with Templ templates and Air hot-reloading.</p>")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<link rel=\"stylesheet\" href=\"/public/styles/terminal.css\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
- templ_7745c5c3_Err = PageLayout("Home").Render(templ.WithChildren(ctx, templ_7745c5c3_Var4), templ_7745c5c3_Buffer)
+ templ_7745c5c3_Err = HeadComponent(path, "My personal website").Render(templ.WithChildren(ctx, templ_7745c5c3_Var13), templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<body><main class=\"w-[75rem] h-[40rem] p-8 border-[0.2rem] border-(--text) overflow-hidden text-[1.125rem]\">")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = NavComponent(path).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templ_7745c5c3_Var11.Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</main></body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -109,8 +292,7 @@ func HomePage() templ.Component {
})
}
-// 3. Page 2: About
-func AboutPage() templ.Component {
+func HomePage(style Style) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -126,12 +308,12 @@ func AboutPage() templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
- templ_7745c5c3_Var5 := templ.GetChildren(ctx)
- if templ_7745c5c3_Var5 == nil {
- templ_7745c5c3_Var5 = templ.NopComponent
+ templ_7745c5c3_Var14 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var14 == nil {
+ templ_7745c5c3_Var14 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
- templ_7745c5c3_Var6 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_Var15 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
@@ -143,13 +325,13 @@ func AboutPage() templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<h1 class=\"text-3xl font-bold underline\">About</h1><p>Single-binary web app using Go's standard library net/http.</p>")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<div id=\"content\" class=\"w-0 h-0 invisible sm:w-full sm:h-full sm:visible\"><h1>Home</h1><p>Go server running with Templ templates and Air hot-reloading.</p></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
- templ_7745c5c3_Err = PageLayout("About").Render(templ.WithChildren(ctx, templ_7745c5c3_Var6), templ_7745c5c3_Buffer)
+ templ_7745c5c3_Err = TerminalLayout(style, "/home/kyren/").Render(templ.WithChildren(ctx, templ_7745c5c3_Var15), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -157,14 +339,7 @@ func AboutPage() templ.Component {
})
}
-// Data struct for dynamic rendering
-type Item struct {
- Name string
- Active bool
-}
-
-// 4. Page 3: Dynamic Data (Conditionals + Loops)
-func ItemsPage(items []Item) templ.Component {
+func SkillsPage(style Style) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -180,12 +355,12 @@ func ItemsPage(items []Item) templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
- templ_7745c5c3_Var7 := templ.GetChildren(ctx)
- if templ_7745c5c3_Var7 == nil {
- templ_7745c5c3_Var7 = templ.NopComponent
+ templ_7745c5c3_Var16 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var16 == nil {
+ templ_7745c5c3_Var16 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
- templ_7745c5c3_Var8 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_Var17 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
@@ -197,79 +372,60 @@ func ItemsPage(items []Item) templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<h1>Items</h1><div class=\"card\">")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<h1 class=\"text-3xl font-bold\">About</h1><p>Single-binary web app using Go's standard library net/http.</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- if len(items) == 0 {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<p>No items in database.</p>")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- } else {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<ul>")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
+ return nil
+ })
+ templ_7745c5c3_Err = TerminalLayout(style, "/home/kyren/skills.md").Render(templ.WithChildren(ctx, templ_7745c5c3_Var17), templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+func ProjectsPage(style Style, projects []Project) templ.Component {
+ return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+ return templ_7745c5c3_CtxErr
+ }
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
- for _, item := range items {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<li><span>")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- var templ_7745c5c3_Var9 string
- templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(item.Name)
- if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `website.templ`, Line: 67, Col: 45}
- }
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</span> ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- if item.Active {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<span class=\"badge-active\">[ACTIVE]</span>")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- } else {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<span class=\"badge-inactive\">[INACTIVE]</span>")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</li>")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
+ }()
+ }
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Var18 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var18 == nil {
+ templ_7745c5c3_Var18 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Var19 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+ templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+ templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+ if !templ_7745c5c3_IsBuffer {
+ defer func() {
+ templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err == nil {
+ templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
- }
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</ul>")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</div><p style=\"color: #94a3b8; font-size: 0.8rem; margin-top: 1rem;\">")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- var templ_7745c5c3_Var10 string
- templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("Total items processed: %d", len(items)))
- if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `website.templ`, Line: 79, Col: 66}
- }
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
+ }()
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</p>")
+ ctx = templ.InitializeContext(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<h1>Items</h1><div class=\"card\"><ul><li><span>foo</span> <span class=\"badge-active\">[ACTIVE]</span></li></ul></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
- templ_7745c5c3_Err = PageLayout("Items").Render(templ.WithChildren(ctx, templ_7745c5c3_Var8), templ_7745c5c3_Buffer)
+ templ_7745c5c3_Err = TerminalLayout(style, "/home/kyren/projects").Render(templ.WithChildren(ctx, templ_7745c5c3_Var19), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}