silly personal website
2

Configure Feed

Select the types of activity you want to include in your feed.

initial refactor 💤

july (May 17, 2026, 5:39 AM -0300) 79c788a4 8b1ff4ff

+721 -452
+2 -1
deno.json
··· 5 5 "permissions": { 6 6 "default": { 7 7 "net": true, 8 - "read": true 8 + "read": true, 9 + "env": true 9 10 } 10 11 }, 11 12 "imports": {
+30 -23
src/components/Fm.tsx
··· 3 3 * SPDX-License-Identifier: AGPL-3.0-or-later 4 4 */ 5 5 6 + import { boundaries, elevation, fontSize, radius, spacing, theme } from "~/layout.tsx"; 6 7 import { css, Import } from "../mech/css.ts"; 7 8 import { FM_USER, tracks } from "../mech/fm.ts"; 8 9 9 10 const fm = css(` 11 + :scope { 12 + margin-top: 0.5rem; 13 + } 14 + 10 15 .fm-more:hover { 11 16 opacity: 1; 12 17 } ··· 16 21 position: absolute; 17 22 color: transparent; 18 23 font-size: 15rem; 19 - -webkit-text-stroke: 2px var(--theme-surface-border); 20 - z-index: var(--z-background); 24 + -webkit-text-stroke: 2px ${theme.surfaceBorder}; 25 + z-index: ${elevation.below}; 21 26 } 22 27 23 28 .fm-more { ··· 26 31 align-items: center; 27 32 justify-content: center; 28 33 flex-direction: column; 29 - background: var(--theme-inner); 30 - border: 1px solid var(--theme-surface-border); 31 - border-radius: var(--radius-music); 32 34 overflow: hidden; 33 - font-size: var(--font-size-sm); 34 - color: var(--theme-foreground); 35 - padding: var(--space-3); 36 - z-index: var(--z-base); 37 35 width: 100%; 38 36 height: 100%; 39 - opacity: 0.75; 37 + opacity: 75%; 38 + 39 + background: ${theme.base}; 40 + border: 1px solid ${theme.baseBorder}; 41 + border-radius: ${radius.art}; 42 + font-size: ${fontSize.sm}; 43 + color: ${theme.text}; 44 + padding: ${spacing[3]}; 45 + z-index: ${elevation.base}; 46 + 40 47 @media (max-width: 930px) { 41 48 display: none; 42 49 } ··· 45 52 :scope { 46 53 display: grid; 47 54 grid-template-columns: repeat(auto-fill, minmax(120px, 160px)); 48 - gap: var(--spacing-lg); 55 + gap: ${spacing[6]}; 56 + margin-top: ${spacing[2]}; 49 57 padding: 0; 50 58 margin: 0; 51 - margin-top: var(--space-2); 52 59 list-style: none; 53 60 } 54 61 ··· 56 63 display: flex; 57 64 flex-direction: column; 58 65 align-items: center; 59 - gap: var(--space-3); 60 66 text-align: center; 61 - max-width: var(--avatar-size); 67 + gap: ${spacing[3]}; 68 + max-width: ${boundaries.avatarSize}; 62 69 } 63 70 64 71 li > a > .cover { 65 - width: var(--avatar-size); 66 - height: var(--avatar-size); 67 - border-radius: var(--radius-music); 72 + width: ${boundaries.avatarSize}; 73 + height: ${boundaries.avatarSize}; 74 + border-radius: ${radius.art}; 75 + background: ${theme.lift}; 68 76 object-fit: cover; 69 - background: var(--theme-background-alt); 70 77 } 71 78 72 79 li > a > .meta { 73 80 display: flex; 74 81 flex-direction: column; 75 - font-size: var(--font-size-md); 76 - color: var(--theme-foreground-alt); 82 + font-size: ${fontSize.md}; 83 + color: ${theme.subtext}; 77 84 } 78 85 79 86 li > a > .meta > .loved { 80 - color: var(--theme-primary); 87 + color: ${theme.accent}; 81 88 } 82 89 83 90 li > a > .meta strong { 84 91 font-weight: 600; 85 - font-size: var(--font-size-headsub); 86 - color: var(--theme-foreground); 92 + font-size: ${fontSize.base}; 93 + color: ${theme.text}; 87 94 } 88 95 89 96 li > a > .meta strong,
+23 -17
src/components/Footer.tsx
··· 5 5 6 6 import { css, Import } from "~/mech/css.ts"; 7 7 import { Fluxer, GitHub, Mail, Tangled } from "./Icon.tsx"; 8 + import { fontSize, spacing, theme } from "~/layout.tsx"; 8 9 9 10 const footer = css(` 10 11 :scope { 11 - color: var(--theme-foreground-bruh); 12 - font-size: var(--font-size-md); 12 + color: ${theme.textMuted}; 13 + font-size: ${fontSize.md}; 13 14 } 14 15 15 16 ul { 16 17 display: flex; 17 18 flex-wrap: wrap; 18 19 padding-left: 0; 19 - margin-top: var(--space-4); 20 + margin-top: ${spacing[4]}; 20 21 list-style: none; 21 - gap: var(--space-2); 22 + gap: ${spacing[2]}; 22 23 } 23 24 24 25 ul > li > :is(a,button) { 25 - font-size: var(--font-size-sm); 26 + font-size: ${fontSize.sm}; 26 27 display: flex; 27 28 align-items: center; 28 29 justify-content: center; 29 - padding: var(--space-1) var(--space-4); 30 + padding: ${spacing[1]} ${spacing[4]}; 30 31 gap: 1ch; 31 - color: var(--theme-foreground-alt); 32 - background-color: var(--theme-surface); 32 + color: ${theme.subtext}; 33 + background-color: ${theme.surface}; 33 34 border: none; 34 35 outline: none; 35 36 border-radius: 1000px; 36 37 cursor: pointer; 38 + 37 39 &:hover { 38 40 filter: brightness(0.75); 39 41 } ··· 41 43 42 44 hr { 43 45 border: none; 44 - border-top: 1px solid var(--theme-surface-border); 45 - margin: var(--section-spacing) 0; 46 + border-top: 1px solid ${theme.surfaceBorder}; 47 + margin: ${spacing.section} 0; 48 + } 49 + 50 + .highlight-rose { 51 + color: ${theme.rose}; 46 52 } 47 53 `); 48 54 ··· 54 60 <p> 55 61 © {new Date().getFullYear()} <span class="highlight">kyu.re</span> 56 62 {" · "} 57 - Made with <span class="highlight">❤</span> · Source code available at{" "} 63 + Made with <span class="highlight-rose">❤</span> · Source code available at{" "} 58 64 <a href="https://kyu.re/~web">https://kyu.re/~web</a> under the{" "} 59 65 <a href="https://spdx.org/licenses/AGPL-3.0-or-later.html"> 60 66 GNU Affero General Public License v3.0 ··· 72 78 adoravel 73 79 </a> 74 80 </li> 75 - {/* <li> */} 76 - {/* <a href="https://tangled.org/kyu.re"> */} 77 - {/* <Tangled /> */} 78 - {/* kyu.re */} 79 - {/* </a> */} 80 - {/* </li> */} 81 + <li> 82 + <a href="https://tangled.org/kyu.re"> 83 + <Tangled /> 84 + kyu.re 85 + </a> 86 + </li> 81 87 <li> 82 88 <button onclick="let t=this.lastChild,n=t.nodeValue;navigator.clipboard.writeText(n.trim());if(!this.dataset.t){this.dataset.t=n;t.nodeValue=' copied to clipboard';setTimeout(()=>{t.nodeValue=this.dataset.t;delete this.dataset.t},3000)}"> 83 89 <Fluxer />
+171
src/components/Heading.tsx
··· 1 + import { css, Import } from "~/mech/css.ts"; 2 + import { ease, fontFamily, fontSize, spacing, theme } from "~/layout.tsx"; 3 + 4 + const repeat = (input: string, count: number, delim: string = ",") => Array(count).fill(input).join(delim); 5 + 6 + type FlagPalette = Record<string, readonly string[]>; 7 + 8 + const flags = { 9 + brazil: ["#4c664d", "#d9bf77", "#566b99"], 10 + transgender: ["#98b9db", "#e0aab7", "#e7e0e7", "#e0aab7", "#98b9db"], 11 + lesbian: ["#c46a4f", "#e09f7a", "#e7e0e7", "#cca1be", "#965275"], 12 + } as const satisfies FlagPalette; 13 + 14 + const palette: string = Object.values(flags) 15 + .flatMap((colors) => colors.flatMap((color) => [color, color])) 16 + .join(", "); 17 + 18 + const style = css(` 19 + :scope { 20 + margin-bottom: ${spacing.section}; 21 + margin-top: ${spacing[6]}; 22 + display: flex; 23 + flex-direction: column; 24 + width: 100%; 25 + } 26 + 27 + .typography-stack { 28 + position: relative; 29 + display: flex; 30 + flex-direction: column; 31 + line-height: 0.85; 32 + font-family: ${fontFamily.heading}; 33 + } 34 + 35 + .first-name { 36 + font-size: calc(${fontSize["2xl"]} * 1.75); 37 + font-weight: 800; 38 + letter-spacing: ${spacing.letter.tight}; 39 + color: ${theme.text}; 40 + width: fit-content; 41 + } 42 + 43 + .connecting-arrow { 44 + position: absolute; 45 + left: -1rem; 46 + top: 3rem; 47 + width: 6.48rem; 48 + height: 4.05rem; 49 + color: ${theme.text}; 50 + pointer-events: none; 51 + transition: color ${ease.fast}, opacity ${ease.fast}; 52 + filter: url(#ds-chalk); 53 + animation: boil 2s steps(1) infinite; 54 + } 55 + 56 + .connecting-arrow g { opacity: 0; } 57 + .connecting-arrow[data-frame="1"] .f1 { opacity: 1; } 58 + .connecting-arrow[data-frame="2"] .f2 { opacity: 1; } 59 + .connecting-arrow[data-frame="3"] .f3 { opacity: 1; } 60 + .connecting-arrow[data-frame="4"] .f4 { opacity: 1; } 61 + .connecting-arrow[data-frame="5"] .f5 { opacity: 1; } 62 + 63 + .second-name { 64 + font-size: calc(${fontSize["2xl"]} * 1.5); 65 + font-weight: 800; 66 + 67 + margin-left: ${spacing[18]}; 68 + letter-spacing: ${spacing.letter.tight}; 69 + width: fit-content; 70 + 71 + background: repeating-linear-gradient(-90deg, ${repeat(theme.text, 48)}, ${palette}, ${repeat(theme.text, 48)}); 72 + background-size: 1000% 100%; 73 + -webkit-background-clip: text; 74 + background-clip: text; 75 + -webkit-text-fill-color: transparent; 76 + animation: identity-animation 90s linear infinite; 77 + } 78 + 79 + @keyframes boil { 80 + 0% { transform: translate(0px, 0px) scale(1) rotate(0deg); } 81 + 20% { transform: translate(1.5px, -1px) scale(0.98) rotate(1.5deg); } 82 + 40% { transform: translate(-1px, 1.5px) scale(1.02) rotate(-1deg); } 83 + 60% { transform: translate(1px, 1px) scale(0.97) rotate(2deg); } 84 + 80% { transform: translate(-1.5px, -0.5px) scale(1.03) rotate(-2deg); } 85 + 100% { transform: translate(0px, 0px) scale(1) rotate(0deg); } 86 + } 87 + 88 + @keyframes identity-animation { 89 + 0% { background-position: 0% 50%; } 90 + 100% { background-position: -900% 50%; } 91 + } 92 + `); 93 + 94 + function Pointy() { 95 + return ( 96 + <svg 97 + class="connecting-arrow" 98 + data-frame="1" 99 + viewBox="0 0 100 60" 100 + fill="none" 101 + stroke="currentColor" 102 + stroke-width="3" 103 + stroke-linecap="round" 104 + stroke-linejoin="round" 105 + aria-hidden="true" 106 + xmlns="http://www.w3.org/2000/svg" 107 + version="1.1" 108 + xmlns:xlink="http://www.w3.org/1999/xlink" 109 + > 110 + <defs> 111 + <filter id="ds-chalk" x="-20%" y="-20%" width="140%" height="140%"> 112 + <feTurbulence type="fractalNoise" baseFrequency="1.4" numOctaves="4" result="noise" /> 113 + <feDisplacementMap 114 + in="SourceGraphic" 115 + in2="noise" 116 + scale="1.5" 117 + xChannelSelector="R" 118 + yChannelSelector="G" 119 + result="displaced" 120 + /> 121 + <feComponentTransfer in="displaced"> 122 + <feFuncA type="discrete" tableValues="0 0 0 0 1 1 1 1" /> 123 + </feComponentTransfer> 124 + </filter> 125 + </defs> 126 + <g class="f1"> 127 + <path d="M 25,8 C 30,18 36,30 52,32 C 62,33 58,16 48,20 C 40,24 45,48 80,48" /> 128 + <path d="M 72,40 L 82,48 L 71,55" /> 129 + </g> 130 + <g class="f2"> 131 + <path d="M 26,9 C 31,19 35,29 51,31 C 61,32 59,17 49,21 C 41,25 46,47 79,47" /> 132 + <path d="M 71,39 L 81,47 L 70,54" /> 133 + </g> 134 + <g class="f3"> 135 + <path d="M 24,7 C 29,17 37,31 53,33 C 63,34 57,15 47,19 C 39,23 44,49 81,49" /> 136 + <path d="M 73,41 L 83,49 L 72,56" /> 137 + </g> 138 + <g class="f4"> 139 + <path d="M 25,8 C 30,18 36,30 52,32 C 62,33 58,16 48,20 C 40,24 45,48 80,48" /> 140 + <path d="M 72,40 L 82,48 L 71,55" /> 141 + </g> 142 + <g class="f5"> 143 + <path d="M 27,8 C 32,18 35,30 51,33 C 61,34 58,16 49,20 C 41,24 46,48 79,48" /> 144 + <path d="M 71,40 L 81,48 L 70,55" /> 145 + </g> 146 + </svg> 147 + ); 148 + } 149 + 150 + const useBoilDriver = ` 151 + const el = document.currentScript.previousElementSibling; 152 + el.addEventListener("animationiteration", () => { 153 + let frame = parseInt(el.getAttribute("data-frame") || "1"); 154 + el.setAttribute("data-frame", (frame % 5) + 1); 155 + }); 156 + `; 157 + 158 + export default function Heading() { 159 + return ( 160 + <header class={style.scope}> 161 + <Import styles={[style]} /> 162 + 163 + <h1 class="typography-stack"> 164 + <span class="first-name">Júlia</span> 165 + <Pointy /> 166 + <script dangerouslySetInnerHTML={{ __html: useBoilDriver }} /> 167 + <span class="second-name">Lívia</span> 168 + </h1> 169 + </header> 170 + ); 171 + }
+56 -25
src/components/Icon.tsx
··· 2 2 * Copyright (c) 2025 adoravel 3 3 * SPDX-License-Identifier: AGPL-3.0-or-later 4 4 */ 5 - // @ts-nocheck bruh 6 5 7 6 interface IconProps { 8 7 size?: number | string; ··· 10 9 title?: string; 11 10 } 12 11 13 - export function Tangled({ size = 24, class: className, title }: IconProps) { 12 + export function Tangled({ size = 24, class: className, title = "Tangled" }: IconProps) { 14 13 return ( 15 14 <svg 16 15 width={size} ··· 18 17 viewBox="0 0 24.122343 23.274094" 19 18 xmlns="http://www.w3.org/2000/svg" 20 19 class={className} 21 - aria-label={title ?? "Tangled Dolly"} 20 + aria-label={title ?? "Tangled"} 22 21 role="img" 23 22 > 24 23 {title && <title>{title}</title>} ··· 30 29 ); 31 30 } 32 31 33 - export function GitHub({ size = 24, class: className, title }: IconProps) { 32 + export function GitHub({ size = 24, class: className, title = "GitHub" }: IconProps) { 34 33 return ( 35 34 <svg 36 35 role="img" ··· 41 40 fill="currentColor" 42 41 class={className} 43 42 > 44 - <title>{title ?? "GitHub"}</title> 43 + {title && <title>{title}</title>} 45 44 <path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" /> 46 45 </svg> 47 46 ); 48 47 } 49 48 50 - export function Discord({ size = 24, class: className, title }: IconProps) { 49 + export function Discord({ size = 24, class: className, title = "Discord" }: IconProps) { 51 50 return ( 52 51 <svg 53 52 role="img" ··· 58 57 fill="currentColor" 59 58 class={className} 60 59 > 61 - <title>{title ?? "Discord"}</title> 60 + {title && <title>{title}</title>} 62 61 <path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z" /> 63 62 </svg> 64 63 ); 65 64 } 66 65 67 - export function Fluxer({ size = 24, class: className, title }: IconProps) { 66 + export function Fluxer({ size = 24, class: className, title = "Fluxer" }: IconProps) { 68 67 return ( 69 68 <svg 70 69 role="img" ··· 84 83 ); 85 84 } 86 85 87 - export function Signal({ size = 24, class: className, title }: IconProps) { 86 + export function Signal({ size = 24, class: className, title = "Signal" }: IconProps) { 88 87 return ( 89 88 <svg 90 89 role="img" ··· 95 94 fill="currentColor" 96 95 class={className} 97 96 > 98 - <title>{title ?? "Signal"}</title> 97 + {title && <title>{title}</title>} 99 98 <path d="M12 0q-.934 0-1.83.139l.17 1.111a11 11 0 0 1 3.32 0l.172-1.111A12 12 0 0 0 12 0M9.152.34A12 12 0 0 0 5.77 1.742l.584.961a10.8 10.8 0 0 1 3.066-1.27zm5.696 0-.268 1.094a10.8 10.8 0 0 1 3.066 1.27l.584-.962A12 12 0 0 0 14.848.34M12 2.25a9.75 9.75 0 0 0-8.539 14.459c.074.134.1.292.064.441l-1.013 4.338 4.338-1.013a.62.62 0 0 1 .441.064A9.7 9.7 0 0 0 12 21.75c5.385 0 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25m-7.092.068a12 12 0 0 0-2.59 2.59l.909.664a11 11 0 0 1 2.345-2.345zm14.184 0-.664.909a11 11 0 0 1 2.345 2.345l.909-.664a12 12 0 0 0-2.59-2.59M1.742 5.77A12 12 0 0 0 .34 9.152l1.094.268a10.8 10.8 0 0 1 1.269-3.066zm20.516 0-.961.584a10.8 10.8 0 0 1 1.27 3.066l1.093-.268a12 12 0 0 0-1.402-3.383M.138 10.168A12 12 0 0 0 0 12q0 .934.139 1.83l1.111-.17A11 11 0 0 1 1.125 12q0-.848.125-1.66zm23.723.002-1.111.17q.125.812.125 1.66c0 .848-.042 1.12-.125 1.66l1.111.172a12.1 12.1 0 0 0 0-3.662M1.434 14.58l-1.094.268a12 12 0 0 0 .96 2.591l-.265 1.14 1.096.255.36-1.539-.188-.365a10.8 10.8 0 0 1-.87-2.35m21.133 0a10.8 10.8 0 0 1-1.27 3.067l.962.584a12 12 0 0 0 1.402-3.383zm-1.793 3.848a11 11 0 0 1-2.345 2.345l.664.909a12 12 0 0 0 2.59-2.59zm-19.959 1.1L.357 21.48a1.8 1.8 0 0 0 2.162 2.161l1.954-.455-.256-1.095-1.953.455a.675.675 0 0 1-.81-.81l.454-1.954zm16.832 1.769a10.8 10.8 0 0 1-3.066 1.27l.268 1.093a12 12 0 0 0 3.382-1.402zm-10.94.213-1.54.36.256 1.095 1.139-.266c.814.415 1.683.74 2.591.961l.268-1.094a10.8 10.8 0 0 1-2.35-.869zm3.634 1.24-.172 1.111a12.1 12.1 0 0 0 3.662 0l-.17-1.111q-.812.125-1.66.125a11 11 0 0 1-1.66-.125" /> 100 99 </svg> 101 100 ); 102 101 } 103 102 104 - export function Telegram({ size = 24, class: className, title }: IconProps) { 103 + export function Bluesky({ size = 24, class: className, title = "Bluesky" }: IconProps) { 105 104 return ( 106 105 <svg 107 106 role="img" ··· 112 111 fill="currentColor" 113 112 class={className} 114 113 > 115 - <title>{title ?? "Telegram"}</title> 116 - <path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z" /> 114 + {title && <title>{title}</title>} 115 + <path d="M5.202 2.857C7.954 4.922 10.913 9.11 12 11.358c1.087-2.247 4.046-6.436 6.798-8.501C20.783 1.366 24 .213 24 3.883c0 .732-.42 6.156-.667 7.037-.856 3.061-3.978 3.842-6.755 3.37 4.854.826 6.089 3.562 3.422 6.299-5.065 5.196-7.28-1.304-7.847-2.97-.104-.305-.152-.448-.153-.327 0-.121-.05.022-.153.327-.568 1.666-2.782 8.166-7.847 2.97-2.667-2.737-1.432-5.473 3.422-6.3-2.777.473-5.899-.308-6.755-3.369C.42 10.04 0 4.615 0 3.883c0-3.67 3.217-2.517 5.202-1.026" /> 117 116 </svg> 118 117 ); 119 118 } 120 119 121 - export function Bluesky({ size = 24, class: className, title }: IconProps) { 120 + export function Twitter({ size = 24, class: className, title = "Twitter" }: IconProps) { 122 121 return ( 123 122 <svg 124 123 role="img" ··· 129 128 fill="currentColor" 130 129 class={className} 131 130 > 132 - <title>{title ?? "Bluesky"}</title> 133 - <path d="M5.202 2.857C7.954 4.922 10.913 9.11 12 11.358c1.087-2.247 4.046-6.436 6.798-8.501C20.783 1.366 24 .213 24 3.883c0 .732-.42 6.156-.667 7.037-.856 3.061-3.978 3.842-6.755 3.37 4.854.826 6.089 3.562 3.422 6.299-5.065 5.196-7.28-1.304-7.847-2.97-.104-.305-.152-.448-.153-.327 0-.121-.05.022-.153.327-.568 1.666-2.782 8.166-7.847 2.97-2.667-2.737-1.432-5.473 3.422-6.3-2.777.473-5.899-.308-6.755-3.369C.42 10.04 0 4.615 0 3.883c0-3.67 3.217-2.517 5.202-1.026" /> 131 + {title && <title>{title}</title>} 132 + <path d="M21.543 7.104c.015.211.015.423.015.636 0 6.507-4.954 14.01-14.01 14.01v-.003A13.94 13.94 0 0 1 0 19.539a9.88 9.88 0 0 0 7.287-2.041 4.93 4.93 0 0 1-4.6-3.42 4.916 4.916 0 0 0 2.223-.084A4.926 4.926 0 0 1 .96 9.167v-.062a4.887 4.887 0 0 0 2.235.616A4.928 4.928 0 0 1 1.67 3.148 13.98 13.98 0 0 0 11.82 8.292a4.929 4.929 0 0 1 8.39-4.49 9.868 9.868 0 0 0 3.128-1.196 4.941 4.941 0 0 1-2.165 2.724A9.828 9.828 0 0 0 24 4.555a10.019 10.019 0 0 1-2.457 2.549z" /> 134 133 </svg> 135 134 ); 136 135 } 137 136 138 - export function Twitter({ size = 24, class: className, title }: IconProps) { 137 + export function Mail({ size = 24, class: className, title = "Mail" }: IconProps) { 139 138 return ( 140 139 <svg 140 + xmlns="http://www.w3.org/2000/svg" 141 + viewBox="0 0 24 24" 142 + width={size} 143 + height={size} 144 + fill="currentColor" 145 + class={className} 141 146 role="img" 147 + > 148 + {title && <title>{title}</title>} 149 + <path d="M1.5 8.67v8.58a3 3 0 0 0 3 3h15a3 3 0 0 0 3-3V8.67l-8.928 5.493a3 3 0 0 1-3.144 0L1.5 8.67Z" /> 150 + <path d="M22.5 6.908V6.75a3 3 0 0 0-3-3h-15a3 3 0 0 0-3 3v.158l9.714 5.978a1.5 1.5 0 0 0 1.572 0L22.5 6.908Z" /> 151 + </svg> 152 + ); 153 + } 154 + 155 + export function License({ size = 24, class: className, title = "License" }: IconProps) { 156 + return ( 157 + <svg 158 + xmlns="http://www.w3.org/2000/svg" 142 159 viewBox="0 0 24 24" 143 160 width={size} 144 161 height={size} 145 - xmlns="http://www.w3.org/2000/svg" 146 - fill="currentColor" 162 + fill="none" 147 163 class={className} 164 + role="img" 165 + stroke="currentColor" 166 + stroke-width="2" 167 + stroke-linecap="round" 168 + stroke-linejoin="round" 148 169 > 149 - <title>{title ?? "Twitter"}</title> 150 - <path d="M21.543 7.104c.015.211.015.423.015.636 0 6.507-4.954 14.01-14.01 14.01v-.003A13.94 13.94 0 0 1 0 19.539a9.88 9.88 0 0 0 7.287-2.041 4.93 4.93 0 0 1-4.6-3.42 4.916 4.916 0 0 0 2.223-.084A4.926 4.926 0 0 1 .96 9.167v-.062a4.887 4.887 0 0 0 2.235.616A4.928 4.928 0 0 1 1.67 3.148 13.98 13.98 0 0 0 11.82 8.292a4.929 4.929 0 0 1 8.39-4.49 9.868 9.868 0 0 0 3.128-1.196 4.941 4.941 0 0 1-2.165 2.724A9.828 9.828 0 0 0 24 4.555a10.019 10.019 0 0 1-2.457 2.549z" /> 170 + {title && <title>{title}</title>} 171 + <path d="m16 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"></path> 172 + <path d="m2 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"></path> 173 + <path d="M7 21h10"></path> 174 + <path d="M12 3v18"></path> 175 + <path d="M3 7h2c2 0 5-1 7-2 2 1 5 2 7 2h2"></path> 151 176 </svg> 152 177 ); 153 178 } 154 179 155 - export function Mail({ size = 24, class: className, title }: IconProps) { 180 + export function ExternalLink({ size = 24, class: className, title = "Open Externally" }: IconProps) { 156 181 return ( 157 182 <svg 158 183 xmlns="http://www.w3.org/2000/svg" 159 184 viewBox="0 0 24 24" 160 185 width={size} 161 186 height={size} 162 - fill="currentColor" 187 + fill="none" 163 188 class={className} 164 189 role="img" 190 + stroke="currentColor" 191 + stroke-width="2" 192 + stroke-linecap="round" 193 + stroke-linejoin="round" 165 194 > 166 195 {title && <title>{title}</title>} 167 - <path d="M1.5 8.67v8.58a3 3 0 0 0 3 3h15a3 3 0 0 0 3-3V8.67l-8.928 5.493a3 3 0 0 1-3.144 0L1.5 8.67Z" /> 168 - <path d="M22.5 6.908V6.75a3 3 0 0 0-3-3h-15a3 3 0 0 0-3 3v.158l9.714 5.978a1.5 1.5 0 0 0 1.572 0L22.5 6.908Z" /> 196 + <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> 197 + <path d="M12 6h-6a2 2 0 0 0 -2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-6"></path> 198 + <path d="M11 13l9 -9"></path> 199 + <path d="M15 4h5v5"></path> 169 200 </svg> 170 201 ); 171 202 }
+14 -12
src/components/KVTable.tsx
··· 3 3 * SPDX-License-Identifier: AGPL-3.0-or-later 4 4 */ 5 5 6 - import { css, Import } from "../mech/css.ts"; 6 + import { css, Import } from "~/mech/css.ts"; 7 + import { ease, fontSize, spacing, theme } from "~/layout.tsx"; 7 8 8 9 const table = css(` 9 10 :scope { 10 - border: 1px solid var(--theme-surface-border); 11 + border: 1px solid ${theme.surfaceBorder}; 11 12 } 12 13 13 14 .kv-row { 14 15 display: grid; 15 16 grid-template-columns: minmax(80px, 140px) 1fr; 16 - border-bottom: 1px solid var(--theme-surface-border); 17 - transition: background 0.1s var(--transition-fast); 17 + border-bottom: 1px solid ${theme.surfaceBorder}; 18 + transition: background ${ease.fast}; 18 19 } 19 20 20 21 .kv-row:last-child { border-bottom: none; } 21 22 22 - .kv-row:hover { background: var(--theme-surface); } 23 + .kv-row:hover { background: ${theme.surface}; } 24 + 23 25 .kv-key, .kv-val { 24 - padding: 11px 16px; 26 + padding: ${spacing[3]} ${spacing[4]}; 25 27 line-height: 1; 26 28 font-family: inherit; 27 29 } 28 30 29 31 .kv-key { 30 - font-size: var(--font-size-xs); 32 + font-size: ${fontSize.xs}; 33 + letter-spacing: ${spacing.letter.plus}; 34 + color: ${theme.textMuted}; 35 + border-right: 1px solid ${theme.surfaceBorder}; 31 36 font-weight: bold; 32 - letter-spacing: 0.08em; 33 37 text-transform: uppercase; 34 - color: var(--theme-foreground-bruh); 35 - border-right: 1px solid var(--theme-surface-border); 36 38 } 37 39 38 40 .kv-val { 39 - font-size: var(--font-size-sm); 40 - color: var(--theme-primary); 41 + font-size: ${fontSize.sm}; 42 + color: ${theme.accent}; 41 43 } 42 44 `); 43 45
+42 -40
src/components/MediaItem.tsx
··· 4 4 */ 5 5 6 6 import { css, Import } from "~/mech/css.ts"; 7 + import { boundaries, fontSize, radius, spacing, theme } from "~/layout.tsx"; 7 8 8 9 export interface MediaItemProps { 9 10 header: string; ··· 19 20 :scope { 20 21 display: flex; 21 22 flex-direction: column; 22 - background: var(--theme-inner); 23 - border: 1px solid var(--theme-surface-border); 24 - border-radius: var(--radius-lg); 23 + background: ${theme.base}; 24 + border: 1px solid ${theme.baseBorder}; 25 + border-radius: ${radius.lg}; 26 + font-size: ${fontSize.sm}; 27 + color: ${theme.subtext}; 25 28 width: 100%; 26 - font-size: var(--font-size-sm); 27 - color: var(--theme-foreground-alt); 28 29 } 29 30 30 - .link { 31 + .listen-link { 31 32 margin-left: auto; 32 - gap: var(--space-1); 33 - font-size: var(--font-size-xs); 34 - color: var(--theme-foreground-bruh); 33 + gap: ${spacing[1]}; 34 + font-size: ${fontSize.xs}; 35 + color: ${theme.textMuted}; 35 36 } 36 37 37 - .link::after { 38 - background-color: var(--theme-foreground-bruh) !important; 38 + .listen-link::after { 39 + background-color: ${theme.textMuted} !important; 39 40 } 40 41 41 - .link:hover { 42 + .listen-link:hover { 42 43 &::after { 43 - background-color: var(--theme-primary) !important; 44 + background-color: ${theme.accent} !important; 44 45 } 45 - color: var(--theme-primary); 46 + color: ${theme.accent}; 46 47 } 47 48 48 49 .header { 49 - border-bottom: 1px solid var(--theme-inner-border); 50 - padding: var(--space-2) var(--space-4); 51 50 display: flex; 52 51 align-items: center; 53 - gap: var(--space-2); 52 + border-bottom: 1px solid ${theme.baseBorder}; 53 + padding: ${spacing[2]} ${spacing[4]}; 54 + gap: ${spacing[2]}; 54 55 } 55 56 56 57 .release-date { 57 58 display: flex; 58 59 align-items: center; 59 - font-size: var(--font-size-xs); 60 - color: var(--theme-foreground-bruh); 61 - letter-spacing: 0.06em; 60 + font-size: ${fontSize.xs}; 61 + color: ${theme.textMuted}; 62 + letter-spacing: ${spacing.letter.plus}; 62 63 } 63 64 64 65 .body { 65 - padding: var(--space-4); 66 66 display: flex; 67 - gap: var(--space-4); 68 67 align-items: flex-start; 69 - @media (max-width: 600px) { 68 + padding: ${spacing[4]}; 69 + gap: ${spacing[4]}; 70 + 71 + @media (max-width: ${boundaries.mobileMaxWidth}) { 70 72 align-items: center; 71 73 text-align: center; 72 74 flex-direction: column; ··· 76 78 .art { 77 79 width: 64px; 78 80 height: 64px; 79 - background: var(--theme-inner); 80 - border: 1px solid var(--theme-inner-border); 81 - border-radius: var(--radius-music); 82 81 overflow: hidden; 83 82 flex-shrink: 0; 83 + background: ${theme.base}; 84 + border: 1px solid ${theme.baseBorder}; 85 + border-radius: ${radius.art}; 84 86 } 85 87 86 88 .info-tag { 87 89 margin-left: 1ch; 88 - color: var(--theme-foreground-bruh); 90 + color: ${theme.textMuted}; 89 91 } 90 92 91 93 .default-pattern { ··· 96 98 grid-template-rows: repeat(4, 1fr); 97 99 } 98 100 99 - .art-px { display: block; } 100 - .art-px-0 { background: var(--theme-inner); } 101 - .art-px-1 { background: var(--theme-primary); opacity: 0.85; } 102 - .art-px-2 { background: var(--theme-on-primary); opacity: 0.9; } 103 - .art-px-3 { background: var(--theme-foreground-bruh); opacity: 0.25; } 104 - .art-px-4 { background: var(--theme-surface-border); } 101 + .art-px { display: block } 102 + .art-px-0 { background: ${theme.base} } 103 + .art-px-1 { background: ${theme.accent} opacity: 0.85; } 104 + .art-px-2 { background: ${theme.onAccent} opacity: 0.9; } 105 + .art-px-3 { background: ${theme.textMuted} opacity: 0.25; } 106 + .art-px-4 { background: ${theme.surfaceBorder} } 105 107 106 108 .art[data-loaded] > .default-pattern { 107 109 display: none; ··· 114 116 } 115 117 116 118 .info-title { 117 - font-size: var(--font-size-md); 118 - color: var(--theme-foreground); 119 + font-size: ${fontSize.md}; 120 + color: ${theme.text}; 119 121 font-weight: 500; 120 122 } 121 123 122 124 .info-artist { 123 - font-size: var(--font-size-sm); 124 - color: var(--theme-foreground-alt); 125 + font-size: ${fontSize.sm}; 126 + color: ${theme.textMuted}; 125 127 } 126 128 127 129 .info-artist > span { 128 - color: var(--theme-foreground-bruh); 130 + colo: ${theme.textMuted}; 129 131 } 130 132 131 133 .art-cover { 132 134 width: 64px; 133 135 height: 64px; 134 - object-fit: cover; 136 + object-fit: cover; 135 137 } 136 138 `); 137 139 ··· 170 172 <div class="release-date"> 171 173 {releaseDate} 172 174 </div> 173 - <a class="link" href={url} target="_blank" rel="noopener noreferrer"> 175 + <a class="listen-link link" href={url} target="_blank" rel="noopener noreferrer"> 174 176 listen 175 177 </a> 176 178 </div>
+14 -13
src/components/NavigationBar.tsx
··· 4 4 */ 5 5 6 6 import { css, Import } from "~/mech/css.ts"; 7 + import { fontSize, spacing, theme } from "~/layout.tsx"; 7 8 8 9 const style = css(` 9 10 :scope { ··· 11 12 flex-wrap: wrap; 12 13 align-items: center; 13 14 justify-content: center; 14 - margin-bottom: var(--spacing-2xl); 15 + margin-bottom: ${spacing.section}; 15 16 } 16 17 17 18 .itemList { 18 19 display: flex; 19 20 flex-wrap: wrap; 20 21 list-style: none; 21 - padding: var(--space-1); 22 - gap: var(--space-1); 23 - font-size: var(--font-size-md); 24 22 font-weight: 500; 25 - background-color: var(--theme-surface); 26 - border: 1px solid var(--theme-surface-border); 23 + width: fit-content; 24 + padding: ${spacing[1]}; 25 + gap: ${spacing[1]}; 26 + font-size: ${fontSize.md}; 27 + background-color: ${theme.surface}; 28 + border: 1px solid ${theme.surfaceBorder}; 27 29 border-radius: 1000px; 28 - width: fit-content; 29 30 } 30 31 31 32 .selected > .item { 32 - background-color: var(--theme-primary); 33 - color: var(--theme-background); 33 + background-color: ${theme.accent}; 34 + color: ${theme.background}; 34 35 font-weight: 600; 35 - border-radius: 1000px; 36 + border-radius: 1000px; 36 37 } 37 38 38 39 .item { 39 - color: var(--theme-foreground-alt); 40 - padding: var(--space-pad-surface) var(--space-4); 41 - line-height: 1rem; 40 + color: ${theme.subtext}; 41 + padding: ${spacing[2]} ${spacing[4]}; 42 + line-height: 1; 42 43 display: block; 43 44 } 44 45 `);
+42 -74
src/components/Projects.tsx
··· 4 4 */ 5 5 6 6 import { css, Import } from "~/mech/css.ts"; 7 + import { boundaries, ease, elevation, fontSize, radius, spacing, theme } from "~/layout.tsx"; 8 + import { ExternalLink, License } from "~/components/Icon.tsx"; 7 9 8 10 export interface Project { 9 11 author: string; ··· 14 16 url: string; 15 17 } 16 18 17 - export const ProjectLanguage = { 18 - TypeScript: { label: "TypeScript", colour: "#3178c6" }, 19 - C: { label: "C", colour: "#555" }, 20 - Scala: { label: "Scala", colour: "#c22d40" }, 21 - } as const; 22 - 23 - export type ProjectLanguage = typeof ProjectLanguage[keyof typeof ProjectLanguage]; 19 + export enum ProjectLanguage { 20 + TypeScript = 0x3178c6, 21 + C = 0x555, 22 + Scala = 0xc22d40, 23 + } 24 24 25 25 export interface ProjectsProps { 26 26 projects: Project[]; ··· 31 31 list-style: none; 32 32 padding-left: 0; 33 33 display: grid; 34 - margin-top: var(--space-2); 35 34 grid-template-columns: repeat(2, minmax(0, 1fr)); 36 - gap: var(--space-2); 37 35 38 - @media (max-width: 600px) { 36 + @media (max-width: ${boundaries.mobileMaxWidth}) { 39 37 grid-template-columns: 1fr; 40 38 } 39 + 40 + margin-top: ${spacing[2]}; 41 + gap: ${spacing[2]}; 41 42 } 42 43 43 44 .project-card { 44 45 display: flex; 45 46 flex-direction: column; 46 47 text-decoration: none; 47 - line-height: 1.75; 48 - height: 100%; 49 - color: var(--theme-foreground); 50 - background-color: var(--theme-inner); 51 - border: 1px solid var(--theme-inner-border); 52 - padding: var(--space-3) var(--space-4); 53 - border-radius: var(--radius-lg); 54 48 position: relative; 55 49 overflow: hidden; 50 + height: 100%; 51 + color: ${theme.text}; 52 + background-color: ${theme.base}; 53 + border: 1px solid ${theme.baseBorder}; 54 + padding: ${spacing[3]} ${spacing[4]}; 55 + border-radius: ${radius.lg}; 56 + line-height: ${boundaries.lineHeight + .25}; 56 57 } 57 58 58 59 .project-card:hover { 59 - background-color: rgba(255, 255, 255, 0.025); 60 - border-color: rgba(255, 255, 255, 0.125); 60 + background-color: ${theme.surfaceHover}; 61 + border-color: ${theme.surfaceBorderHover}; 61 62 62 63 &::before { 63 64 transform: scale(1.125); ··· 78 79 text-align: center; 79 80 align-items: center; 80 81 justify-content: center; 81 - transition: transform 0.1s var(--transition-fast); 82 - -webkit-text-stroke: 2px var(--theme-inner-border); 82 + transition: transform ${ease.fast}; 83 + -webkit-text-stroke: 2px ${theme.baseBorder}; 83 84 opacity: 0.32; 84 85 } 85 86 86 87 .external-icon { 87 88 position: absolute; 88 - color: var(--theme-foreground-alt); 89 - top: var(--space-3); 90 - right: var(--space-3); 89 + color: ${theme.subtext}; 90 + top: ${spacing[3]}; 91 + right: ${spacing[3]}; 91 92 opacity: 33%; 92 93 } 93 94 94 95 .license { 95 - font-size: var(--font-size-sm); 96 - color: var(--theme-foreground-alt); 96 + font-size: ${fontSize.sm}; 97 + color: ${theme.subtext}; 97 98 line-height: 1; 98 99 opacity: 0.5; 99 100 } ··· 104 105 } 105 106 106 107 .author { 107 - font-size: var(--font-size-md); 108 - margin-bottom: var(--space-1) 108 + font-size: ${fontSize.md}; 109 + margin-bottom: ${spacing[1]}; 109 110 } 110 111 111 112 .description { 112 - font-size: var(--font-size-sm); 113 - color: var(--theme-foreground-alt); 114 - margin-bottom: var(--space-3); 113 + font-size: ${fontSize.sm}; 114 + color: ${theme.subtext}; 115 + margin-bottom: ${spacing[3]}; 115 116 overflow: hidden; 116 117 display: -webkit-box; 117 118 -webkit-box-orient: vertical; ··· 120 121 121 122 .description, .info, .author { 122 123 position: relative; 123 - z-index: 1; 124 + z-index: ${elevation.base}; 124 125 } 125 126 126 127 .info { 127 128 display: flex; 128 129 align-items: center; 129 - gap: var(--space-2); 130 - font-size: var(--font-size-xs); 131 - color: var(--theme-foreground-alt); 130 + gap: ${spacing[2]}; 131 + font-size: ${fontSize.xs}; 132 + color: ${theme.subtext}; 132 133 margin-top: auto; 133 134 } 134 135 135 136 .language { 136 137 width: 10px; 137 138 height: 10px; 138 - border-radius: var(--radius-circle); 139 - background-color: var(--lang-color, #ccc); 139 + border-radius: ${radius.circle}; 140 + background-color: var(--lang-colour, #ccc); 140 141 display: inline-block; 141 142 } 142 143 `); ··· 151 152 <li> 152 153 <a class="project-card" href={project.url} target="_blank" rel="noopener noreferrer"> 153 154 <span class="external-icon" aria-label="Open externally"> 154 - <svg 155 - xmlns="http://www.w3.org/2000/svg" 156 - width="18" 157 - height="18" 158 - viewBox="0 0 24 24" 159 - fill="none" 160 - stroke="currentColor" 161 - stroke-width="2" 162 - stroke-linecap="round" 163 - stroke-linejoin="round" 164 - > 165 - <title>Open Externally</title> 166 - <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> 167 - <path d="M12 6h-6a2 2 0 0 0 -2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-6"></path> 168 - <path d="M11 13l9 -9"></path> 169 - <path d="M15 4h5v5"></path> 170 - </svg> 155 + <ExternalLink size={18} /> 171 156 </span> 172 157 <div class="author"> 173 158 <strong>{project.author}</strong>/{project.name} 174 159 </div> 175 160 <p class="description">{project.description}</p> 176 161 <div class="info"> 177 - <span class="language" style={`--lang-color:${project.lang.colour};`}></span> 178 - {project.lang.label} 162 + <span class="language" style={"--lang-colour: #" + project.lang.toString(16)}></span> 163 + {ProjectLanguage[project.lang]} 179 164 <div class="license" aria-label="License"> 180 - <svg 181 - xmlns="http://www.w3.org/2000/svg" 182 - width="12" 183 - height="12" 184 - viewBox="0 0 24 24" 185 - fill="none" 186 - stroke="currentColor" 187 - stroke-width="2" 188 - stroke-linecap="round" 189 - stroke-linejoin="round" 190 - > 191 - <title>License</title> 192 - <path d="m16 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"></path> 193 - <path d="m2 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"></path> 194 - <path d="M7 21h10"></path> 195 - <path d="M12 3v18"></path> 196 - <path d="M3 7h2c2 0 5-1 7-2 2 1 5 2 7 2h2"></path> 197 - </svg> 165 + <License size={12} /> 198 166 <span>{project.license}</span> 199 167 </div> 200 168 </div>
-67
src/components/Typecycle.tsx
··· 1 - /** 2 - * Copyright (c) 2025 adoravel 3 - * SPDX-License-Identifier: AGPL-3.0-or-later 4 - */ 5 - 6 - import { css, Import } from "~/mech/css.ts"; 7 - 8 - const typecycle = css(` 9 - :scope { 10 - --type-duration: 5s; 11 - --type-pause: 30%; 12 - display: inline-block; 13 - } 14 - 15 - :scope::after { 16 - content: ""; 17 - border-right: 0.15em solid var(--theme-primary); 18 - animation: caret 1s ease-in-out infinite; 19 - padding-left: 0.25em; 20 - } 21 - 22 - :scope > .text { 23 - overflow: hidden; 24 - display: inline-block; 25 - vertical-align: bottom; 26 - margin-left: 0.875ch; 27 - animation: type-delete var(--type-duration) ease-in-out infinite; 28 - } 29 - 30 - @keyframes type-delete { 31 - 0%, 32 - 100% { 33 - max-width: 0; 34 - } 35 - 30%, 36 - 60% { 37 - max-width: 100%; 38 - } 39 - 90% { 40 - max-width: 0; 41 - } 42 - } 43 - 44 - @keyframes caret { 45 - 0%, 46 - 100% { 47 - opacity: 1; 48 - } 49 - 30%, 50 - 60% { 51 - opacity: 0; 52 - } 53 - 90% { 54 - opacity: 1; 55 - } 56 - } 57 - `); 58 - 59 - export function Typecycle() { 60 - return ( 61 - <span class={typecycle.scope}> 62 - <Import styles={[typecycle]} /> 63 - <span style="color: var(--theme-foreground-alt)">λ</span> 64 - <span class="text">kyu.re</span> 65 - </span> 66 - ); 67 - }
+4 -3
src/constants.ts
··· 1 - import { ProjectLanguage } from "~/components/Projects.tsx"; 1 + import { Project, ProjectLanguage } from "~/components/Projects.tsx"; 2 2 import { MediaItemProps } from "./components/MediaItem.tsx"; 3 3 4 4 function art(path: string, size = "512x512bb"): string { ··· 35 35 }, 36 36 ]; 37 37 38 - export const projects = [ 38 + export const projects: Project[] = [ 39 39 { 40 40 author: "w", 41 41 name: "snarl", ··· 73 73 author: "w", 74 74 name: "terracotta", 75 75 description: "mill-based toolchain for crossplatform and multi-version minecraft mod development", 76 + url: "/~terracotta", 76 77 license: "LGPL-3.0", 77 78 lang: ProjectLanguage.Scala, 78 79 }, ··· 89 90 export const friends = [ 90 91 { href: "https://katelyn.moe/", src: "https://katelyn.moe/8831.png", alt: "katelyn" }, 91 92 { href: "https://worf.win", src: "https://worf.win/images/worfwin.gif", alt: "worf.win" }, 92 - { href: "https://urwq.moe", src: "https://urwq.moe/88x31.png", alt: "urwq" }, 93 + // { href: "https://urwq.moe", src: "https://urwq.moe/88x31.png", alt: "urwq" }, 93 94 { href: "https://mugman.tech", src: "https://mugman.tech/88x31/me.gif", alt: "mugman" }, 94 95 { href: "https://www.juwuba.xyz", src: "https://www.juwuba.xyz/88x31.gif", alt: "Júlia" }, 95 96 { href: "https://codeberg.org/paige", src: "/88x31/paige.gif", alt: "paige" },
+168 -104
src/layout.tsx
··· 5 5 6 6 import { jsx, JsxElement, JsxNode } from "@july/snarl/jsx-runtime"; 7 7 8 - const styles = /* css */ ` 9 - :root { 10 - --theme-background: #151217; 11 - --theme-background-alt: #3b383d; 12 - --theme-surface: #211e24; 13 - --theme-surface-border: #2c292e; 14 - --theme-foreground: #e7e0e7; 15 - --theme-primary: #cba6f7; 16 - --theme-on-primary: #57377f; 17 - --theme-foreground-alt: #cdc3d1; 18 - --theme-foreground-bruh: #968e9a; 19 - --theme-inner: #100d12; 20 - --theme-inner-border: #211e24; 21 - 22 - --space-0: 0.16rem; 23 - --space-1: 0.25rem; 24 - --space-2: 0.5rem; 25 - --space-pad-surface: 0.625rem; 26 - --space-pad-surface-2: 0.325rem; 27 - --space-3: 0.75rem; 28 - --space-4: 1rem; 29 - --space-5: 1.25rem; 30 - --space-6: 1.5rem; 31 - --space-7: 2rem; 32 - --space-8: 2.5rem; 33 - --space-9: 3rem; 34 - --space-10: 4rem; 35 - 36 - --spacing-xs: var(--space-1); 37 - --spacing-sm: var(--space-2); 38 - --spacing-md: var(--space-4); 39 - --spacing-lg: var(--space-6); 40 - --spacing-xl: var(--space-7); 41 - --spacing-2xl: var(--space-9); 42 - --spacing-3xl: var(--space-10); 43 - 44 - --section-margin: var(--space-7); 45 - --card-padding: var(--space-4); 46 - --button-padding-x: var(--space-2); 47 - --button-padding-y: var(--space-1); 48 - --input-padding: var(--space-3); 49 - 50 - --radius-sm: 2px; 51 - --radius-md: 3px; 52 - --radius-lg: 10px; 53 - --radius-circle: 1000px; 54 - --radius-music: 15%; 8 + export const rem = (val: number | string) => `${val}rem`; 9 + export const em = (val: number | string) => `${val}em`; 10 + export const px = (val: number | string) => `${val}px`; 11 + export const url = (val: string) => `url("${val}")`; 55 12 56 - --transition-fast: 0.16s ease-in-out; 57 - --transition-smooth: 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); 13 + type DeepResolveTokens<T> = { 14 + [K in keyof T]: T[K] extends object ? { 15 + [P in keyof T[K] as P | (P extends `${infer N extends number}` ? N : never)]: T[K][P] extends object 16 + ? DeepResolveTokens<T[K][P]> 17 + : T[K][P] extends number ? string 18 + : T[K][P]; 19 + } 20 + : T[K] extends number ? string 21 + : T[K]; 22 + }; 58 23 59 - --font-size-xs: 0.6875rem; 60 - --font-size-sm: 0.75rem; 61 - --font-size-md: 0.875rem; 62 - --font-size-text: 0.9rem; 63 - --font-size-headsub: 1rem; 64 - --font-size-base: 1.0625rem; 65 - --font-size-lg: 1.1rem; 66 - --font-size-xl: 1.5rem; 67 - --font-size-2xl: 2.4rem; 24 + export function createTheme<T extends object>(tokens: T): DeepResolveTokens<T> { 25 + const resolve = (input: any): any => { 26 + if (typeof input !== "object" || input === null) { 27 + return typeof input === "number" ? `${input}` : input; 28 + } 68 29 69 - --avatar-size: 160px; 30 + return Object.fromEntries( 31 + Object.entries(input).map(([key, value]) => [key, resolve(value)]), 32 + ); 33 + }; 70 34 71 - --max-width: 55em; 72 - --section-spacing: 2em; 35 + return resolve(tokens); 36 + } 73 37 74 - --z-background: -1; 75 - --z-base: 0; 76 - --z-elevated: 10; 77 - } 38 + export const tokens = createTheme({ 39 + theme: { 40 + base: "#0d0e12", 41 + baseBorder: "#1f1e24", 42 + background: "#121317", 43 + surface: "#1e1f24", 44 + surfaceBorder: "#29292e", 45 + lift: "#38393d", 46 + text: "#e7e0e7", 47 + subtext: "#c3c7d1", 48 + textMuted: "#968e9a", 49 + accent: "#a6b5f7", 50 + accentDim: "#7c8fdb", 51 + accentBackground: "#2d3d7f", 52 + onAccent: "#d8e0ff", 53 + rose: "#f0b3c8", 54 + roseBackground: "#5c1d36", 55 + onRose: "#ffd9e4", 56 + surfaceHover: "rgba(255, 255, 255, 0.025)", 57 + surfaceBorderHover: "rgba(255, 255, 255, 0.125)", 58 + }, 59 + spacing: { 60 + letter: { 61 + tight: em(-.05), 62 + plus: em(.03), 63 + }, 64 + "1": rem(.25), 65 + "2": rem(.5), 66 + "3": rem(.75), 67 + "4": rem(1), 68 + "5": rem(1.25), 69 + "6": rem(1.5), 70 + "8": rem(2), 71 + "10": rem(2.5), 72 + "12": rem(3), 73 + "16": rem(4), 74 + "18": rem(5), 75 + "section": rem(2), 76 + }, 77 + fontSize: { 78 + xs: rem(.6875), 79 + sm: rem(.75), 80 + md: rem(.875), 81 + body: rem(.9), 82 + base: rem(1), 83 + root: px(17), 84 + lg: rem(1.1), 85 + xl: rem(1.5), 86 + "2xl": rem(2.4), 87 + }, 88 + radius: { 89 + sm: px(2), 90 + md: px(3), 91 + lg: px(10), 92 + circle: px(1000), 93 + art: "15%", 94 + }, 95 + ease: { 96 + fast: "0.16s ease-in-out", 97 + spring: "0.3s cubic-bezier(0.34, 1.56, 0.64, 1)", 98 + }, 99 + elevation: { 100 + below: "-1", 101 + base: "0", 102 + raised: "10", 103 + overlay: "20", 104 + modal: "30", 105 + }, 106 + boundaries: { 107 + mobileMaxWidth: px(600), 108 + desktopMinWidth: px(1550), 109 + maxWidth: em(55), 110 + avatarSize: px(160), 111 + lineHeight: rem(1.6), 112 + }, 113 + misc: { 114 + arrow: url( 115 + "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA4IDgiIHNoYXBlLXJlbmRlcmluZz0iY3Jpc3BFZGdlcyI+CiAgPHJlY3QgeD0iMiIgeT0iMCIgd2lkdGg9IjYiIGhlaWdodD0iMiIgZmlsbD0id2hpdGUiLz4KICA8cmVjdCB4PSI0IiB5PSIyIiB3aWR0aD0iNCIgaGVpZ2h0PSIyIiBmaWxsPSJ3aGl0ZSIvPgogIDxyZWN0IHg9IjIiIHk9IjQiIHdpZHRoPSIyIiBoZWlnaHQ9IjIiIGZpbGw9IndoaXRlIi8+CiAgPHJlY3QgeD0iNiIgeT0iNCIgd2lkdGg9IjIiIGhlaWdodD0iMiIgZmlsbD0id2hpdGUiLz4KICA8cmVjdCB4PSIwIiB5PSI2IiB3aWR0aD0iMiIgaGVpZ2h0PSIyIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K", 116 + ), 117 + }, 118 + fontFamily: { 119 + default: '"Iosevka Custom Web", "Iosevka Custom", Iosevka, monospace, sans-serif', 120 + heading: "'Space Grotesk', sans-serif", 121 + }, 122 + }); 78 123 124 + const styles = /* css */ ` 79 125 *, *::before, *::after { 80 126 box-sizing: border-box; 81 127 margin: 0; 82 128 } 83 129 84 - body { 85 - margin: 0 auto; 86 - padding: clamp(1rem, 5vw, var(--spacing-xl)) clamp(1rem, 4vw, 1.5rem); 87 - max-width: var(--max-width); 88 - } 89 - 90 130 html, body { 91 - background-color: var(--theme-background); 92 - color: var(--theme-foreground); 131 + background-color: ${tokens.theme.background}; 132 + color: ${tokens.theme.text}; 93 133 } 94 134 95 135 html { 96 - overflow-x:clip; 136 + overflow-x: clip; 137 + font-family: ${tokens.fontFamily.default}; 138 + line-height: ${tokens.boundaries.lineHeight}; 139 + font-size: ${tokens.fontSize.root}; 140 + text-rendering: optimizeLegibility; 141 + } 142 + 143 + body { 144 + margin: 0 auto; 145 + padding: ${tokens.spacing[8]} ${tokens.spacing[3]}; 146 + max-width: ${tokens.boundaries.maxWidth}; 97 147 } 98 148 99 - html, 100 - body, 101 149 pre, 102 150 code, 103 - button { 104 - font-family: 105 - "Iosevka Custom Web", "Iosevka Custom", Iosevka, monospace, sans-serif; 106 - font-size: var(--font-size-base); 151 + button, 152 + input, 153 + textarea { 154 + font-family: inherit; 155 + font-size: ${tokens.fontSize.root}; 107 156 text-rendering: optimizeLegibility; 108 - line-height: 1.6rem; 109 157 } 110 158 111 159 a { 112 - color: var(--theme-primary); 160 + color: ${tokens.theme.accent}; 113 161 text-decoration: none; 114 162 } 115 163 116 164 section p { 117 - margin-bottom: var(--spacing-md); 118 - font-size: var(--font-size-text); 119 - color: var(--theme-foreground-alt); 165 + margin-bottom: ${tokens.spacing[4]}; 166 + font-size: ${tokens.fontSize.body}; 167 + color: ${tokens.theme.subtext}; 120 168 } 121 169 122 170 @view-transition { ··· 125 173 126 174 body *, body *::before, body *::after { 127 175 transition: 128 - color var(--transition-fast), 129 - background-color var(--transition-fast), 130 - border-color var(--transition-fast), 131 - opacity var(--transition-fast), 132 - filter var(--transition-fast); 176 + color ${tokens.ease.fast}, 177 + background-color ${tokens.ease.fast}, 178 + border-color ${tokens.ease.fast}, 179 + opacity ${tokens.ease.fast}, 180 + filter ${tokens.ease.fast}; 133 181 } 134 182 135 - section > a, .link, p a { 136 - &::after { 137 - content: ''; 138 - display: inline-block; 139 - width: 8px; 140 - height: 8px; 141 - margin-left: 8px; 142 - background-color: var(--theme-primary); 143 - mask: no-repeat center / contain url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA4IDgiIHNoYXBlLXJlbmRlcmluZz0iY3Jpc3BFZGdlcyI+CiAgPHJlY3QgeD0iMiIgeT0iMCIgd2lkdGg9IjYiIGhlaWdodD0iMiIgZmlsbD0id2hpdGUiLz4KICA8cmVjdCB4PSI0IiB5PSIyIiB3aWR0aD0iNCIgaGVpZ2h0PSIyIiBmaWxsPSJ3aGl0ZSIvPgogIDxyZWN0IHg9IjIiIHk9IjQiIHdpZHRoPSIyIiBoZWlnaHQ9IjIiIGZpbGw9IndoaXRlIi8+CiAgPHJlY3QgeD0iNiIgeT0iNCIgd2lkdGg9IjIiIGhlaWdodD0iMiIgZmlsbD0id2hpdGUiLz4KICA8cmVjdCB4PSIwIiB5PSI2IiB3aWR0aD0iMiIgaGVpZ2h0PSIyIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K"); 144 - } 183 + section > a::after, 184 + .link::after, 185 + p a::after { 186 + content: ''; 187 + display: inline-block; 188 + width: 8px; 189 + height: 8px; 190 + margin-left: 8px; 191 + background-color: ${tokens.theme.accent}; 192 + mask: no-repeat center / contain ${tokens.misc.arrow}; 193 + } 194 + 195 + ::-webkit-scrollbar { 196 + width: 3px; 197 + } 198 + 199 + ::-webkit-scrollbar-thumb { 200 + background: ${tokens.theme.lift}; 201 + } 202 + 203 + ::selection { 204 + background: ${tokens.theme.accent}; 205 + color: ${tokens.theme.background}; 145 206 } 146 207 `; 147 208 ··· 203 264 <meta property="og:site_name" content="kyu.re" /> 204 265 <meta property="og:type" content="profile" /> 205 266 <meta property="og:description" content="one of the girls of all time" /> 206 - <meta property="og:image" content="/bnuy.webp" /> 207 - <meta property="og:title" content="random corner" /> 208 - <meta name="theme-color" content="#151217" /> 267 + <meta property="og:image" content="https://kyu.re/~.png" /> 268 + <meta property="og:image:type" content="image/png" /> 269 + <meta property="og:title" content="júlia lívia" /> 270 + <meta name="theme-color" content={tokens.theme.accentDim} /> 271 + <link rel="stylesheet" href="/fonts/iosevka-custom/import.css" /> 209 272 <style>{styles}</style> 210 - <link rel="stylesheet" href="/fonts/iosevka-custom/import.css" /> 211 273 {result.head} 212 274 </head> 213 275 <body class={className}> ··· 216 278 </html> 217 279 ); 218 280 } 281 + 282 + export const { theme, spacing, fontSize, radius, ease, boundaries, misc, elevation, fontFamily } = tokens;
+88 -7
src/mech/css.ts
··· 10 10 const CLASS_RE = /\.([a-zA-Z_][\w-]*)/g; 11 11 12 12 const styleRegistry = new Map<string, string>(); 13 - 14 13 const contextualisedStyles = new WeakMap<Context<any>, Set<string>>(); 15 14 16 15 type ExtractFlatClassKeys<T extends string> = T extends `${infer _}.${infer Rest}` ··· 27 26 & { 28 27 readonly scope: string; 29 28 href: string; 30 - 31 - /** mark this stylesheet as used for the current request (for contextual injection) */ 32 29 use(ctx: Context): void; 33 30 }; 34 31 32 + // brace characters inside string values should be replaced with unicode escapes 33 + const CONDITIONAL_AT = /^@(media|supports|layer|container|document)\b/i; 34 + 35 + const RAW_AT = /^@(keyframes|font-face|font-palette-values|counter-style|page|viewport|color-profile)\b/i; 36 + 37 + function prefixSelectors(selector: string, scope: string): string { 38 + return selector 39 + .split(",") 40 + .map((s) => s.trim()) 41 + .filter(Boolean) 42 + .map((s) => { 43 + if (s.includes(":scope")) return s.replace(/:scope\b/g, scope); 44 + if (s.startsWith("&")) return s; 45 + return `${scope} ${s}`; 46 + }) 47 + .join(","); 48 + } 49 + 50 + function transformRules(src: string, scope: string): string { 51 + let out = ""; 52 + let i = 0; 53 + 54 + while (i < src.length) { 55 + while (i < src.length && src[i] <= " ") i++; 56 + if (i >= src.length) break; 57 + 58 + if (src[i] === "/" && src[i + 1] === "*") { 59 + const end = src.indexOf("*/", i + 2); 60 + i = end === -1 ? src.length : end + 2; 61 + continue; 62 + } 63 + 64 + if (src[i] === "}") { 65 + i++; 66 + break; 67 + } 68 + 69 + let sel = ""; 70 + let hasBlock = false; 71 + 72 + scan: while (i < src.length) { 73 + if (src[i] === "/" && src[i + 1] === "*") { 74 + const end = src.indexOf("*/", i + 2); 75 + i = end === -1 ? src.length : end + 2; 76 + continue; 77 + } 78 + switch (src[i]) { 79 + case "{": 80 + hasBlock = true; 81 + i++; 82 + break scan; 83 + case "}": 84 + break scan; 85 + default: 86 + sel += src[i++]; 87 + } 88 + } 89 + 90 + sel = sel.trim(); 91 + if (!hasBlock || !sel) break; 92 + 93 + const blockStart = i; 94 + let depth = 1; 95 + while (i < src.length && depth > 0) { 96 + if (src[i] === "{") depth++; 97 + else if (src[i] === "}") depth--; 98 + i++; 99 + } 100 + 101 + const block = src.slice(blockStart, i - 1); 102 + 103 + if (RAW_AT.test(sel)) { 104 + out += `${sel}{${block}}`; 105 + } else if (CONDITIONAL_AT.test(sel)) { 106 + out += `${sel}{${transformRules(block, scope)}}`; 107 + } else { 108 + out += `${prefixSelectors(sel, scope)}{${block}}`; 109 + } 110 + } 111 + 112 + return out; 113 + } 114 + 115 + function generateScopedStyle(content: string, scopeId: string): string { 116 + return transformRules(content.trim(), `.${scopeId}`); 117 + } 118 + 35 119 function extractClassKeys(input: string): Set<string> { 36 120 const classes = new Set<string>(); 37 121 CLASS_RE.lastIndex = 0; ··· 42 126 } 43 127 44 128 return classes; 45 - } 46 - 47 - function generateScopedStyle(content: string, scopeId: string): string { 48 - return `@scope(.${scopeId}){${content}}`; 49 129 } 50 130 51 131 export function css<const S extends string>(src: S): ScopedStyles<S> { ··· 77 157 for (const key of keys) { 78 158 result[key] = key; 79 159 } 160 + 80 161 return result as ScopedStyles<S>; 81 162 } 82 163
+1 -1
src/mech/fm.ts
··· 1 1 import { withInterval } from "../utils/temp.ts"; 2 2 3 - const FM_API_KEY = "790c37d90400163a5a5fe00d6ca32ef0"; 3 + const FM_API_KEY = Deno.env.get("LASTFM_API_KEY") ?? "790c37d90400163a5a5fe00d6ca32ef0"; 4 4 5 5 export interface Parameters { 6 6 api_key: string;
+57 -61
src/routes/mod.tsx
··· 4 4 */ 5 5 6 6 import { css, Import } from "~/mech/css.ts"; 7 - import { Layout } from "~/layout.tsx"; 7 + import { boundaries, fontSize, Layout, spacing, theme } from "~/layout.tsx"; 8 8 import { NavigationBar } from "~/components/NavigationBar.tsx"; 9 - import { Typecycle } from "~/components/Typecycle.tsx"; 9 + import Heading from "~/components/Heading.tsx"; 10 10 import SectionTitle from "~/components/SectionTitle.tsx"; 11 11 import Projects from "~/components/Projects.tsx"; 12 12 import { MediaItem } from "~/components/MediaItem.tsx"; 13 13 import { Fm } from "~/components/Fm.tsx"; 14 14 import KVTable from "~/components/KVTable.tsx"; 15 - import { Footer } from "../components/Footer.tsx"; 16 - import { friends, projects, songs } from "../constants.ts"; 15 + import { Footer } from "~/components/Footer.tsx"; 16 + import { friends, projects, songs } from "~/constants.ts"; 17 17 18 18 const home = css(` 19 19 #heading { 20 - margin-bottom: var(--section-spacing); 20 + margin-bottom: ${spacing.section}; 21 21 position: relative; 22 22 } 23 23 24 + /* 24 25 #heading h1 { 25 - font-size: clamp(1.5rem, 5vw + 0.5rem, var(--font-size-2xl)); 26 + font-size: clamp(1.5rem, 5vw + 0.5rem, ${fontSize["2xl"]}); 27 + margin-bottom: ${spacing[1]}; 26 28 line-height: 1.25; 27 - margin-bottom: var(--space-1); 28 29 display: block; 29 30 } 30 31 31 32 #heading h2 { 32 33 display: inline-block; 33 - font-size: var(--font-size-md); 34 - color: var(--theme-foreground-alt); 35 34 margin-top: 1ch; 36 35 width: fit-content; 36 + color: ${theme.subtext}; 37 + font-size: ${fontSize.md}; 37 38 } 38 39 39 40 #intro-text-container > p { 40 41 display: inline; 41 42 } 43 + */ 42 44 43 - 44 - @media (min-width: 1550px) { 45 + @media (min-width: ${boundaries.desktopMinWidth}) { 45 46 section, #heading { 46 47 position: relative; 47 48 ··· 51 52 font-weight: 700; 52 53 user-select: none; 53 54 letter-spacing: -0.075em; 54 - -webkit-text-stroke: 2px var(--theme-foreground); 55 + -webkit-text-stroke: 2px ${theme.text}; 55 56 color: transparent; 56 57 opacity: 0.025; 57 58 pointer-events: none; ··· 61 62 #heading::before { 62 63 content: ":3"; 63 64 font-size: 7rem !important; 64 - top: -0.25em; 65 - left: -5.25rem; 65 + top: 0.1em; 66 + left: -7rem; 66 67 } 67 68 68 69 #intro::before { ··· 74 75 #projects::before { 75 76 content: "↩"; 76 77 transform: rotate(15deg); 77 - font-size: calc(var(--font-size-2xl) * 5); 78 + font-size: calc(${fontSize["2xl"]} * 5); 78 79 right: -16rem; 79 80 top: 2rem; 80 81 } ··· 88 89 #friends::before { 89 90 content: "<3"; 90 91 transform: rotate(15deg); 91 - font-size: calc(var(--font-size-2xl) * 5); 92 + font-size: calc(${fontSize["2xl"]} * 5); 92 93 right: -5rem; 93 94 top: 3rem; 94 95 } 95 96 } 96 97 97 98 section { 98 - margin-bottom: var(--section-spacing); 99 + margin-bottom: ${spacing.section}; 99 100 } 100 101 101 102 .media { 102 103 list-style: none; 103 104 padding-left: 0; 104 105 display: grid; 105 - margin-top: var(--space-2); 106 106 grid-template-columns: repeat(2, minmax(0, 1fr)); 107 - gap: var(--space-2); 108 - @media (max-width: 600px) { 107 + 108 + margin-top: ${spacing[2]}; 109 + gap: ${spacing[2]}; 110 + 111 + @media (max-width: ${boundaries.mobileMaxWidth}) { 109 112 grid-template-columns: 1fr; 110 113 } 111 114 } 112 115 113 116 .highlight { 114 - color: var(--theme-primary); 117 + color: ${theme.accent}; 115 118 } 116 119 117 - .dats-me { 118 - background-clip: text !important; 119 - -webkit-background-clip: text !important; 120 - -webkit-text-fill-color: transparent; 121 - animation: dats-lowkey-me 60s linear infinite; 122 - background-size: 1000% 100% !important; 123 - background: repeating-linear-gradient( -90deg, #a6e3a1, #a6e3a1, #e5c76b, #e5c76b, #1e66f5, #1e66f5, #89b4fa, #89b4fa, #f5c2e7, #f5c2e7, #cdd6f4, #cdd6f4, #f5c2e7, #f5c2e7, #89b4fa, #89b4fa, #d97742, #d97742, #fab387, #fab387, #f2e9e1, #f2e9e1, #f5bde6, #f5bde6, #b4637a, #b4637a ); 124 - background-size: auto; 125 - background-clip: border-box; 126 - } 127 - 128 - @keyframes dats-lowkey-me { 129 - 0% { 130 - background-position: 0% 50%; 131 - } 132 - 100% { 133 - background-position: -900% 50%; 134 - } 135 - } 136 - 137 120 .read-more-checkbox { 138 121 display: none; 139 122 ··· 149 132 150 133 .read-more-btn { 151 134 margin-left: 1ch; 152 - color: var(--theme-primary); 153 135 cursor: pointer; 154 136 text-decoration: none; 155 - font-size: var(--font-size-text); 156 137 user-select: none; 138 + font-size: ${fontSize.body}; 139 + color: ${theme.accent}; 157 140 } 158 141 159 142 .break { ··· 163 146 .buttons { 164 147 display: flex; 165 148 flex-wrap: wrap; 166 - gap: var(--spacing-sm); 149 + gap: ${spacing[1]}; 167 150 } 168 151 169 - .buttons a { 152 + .buttons a, 153 + iframe { 170 154 display: inline-block; 171 155 text-align: center; 172 156 width: 88px; 173 157 height: 31px; 174 158 image-rendering: pixelated; 175 - background-color: var(--theme-background-alt); 176 - color: var(--theme-foreground); 159 + background-color: ${theme.lift}; 160 + color: ${theme.text}; 161 + clip-path: polygon( 162 + 0px calc(100% - 2px), 163 + 2px calc(100% - 2px), 164 + 2px 100%, 165 + calc(100% - 2px) 100%, 166 + calc(100% - 2px) calc(100% - 2px), 167 + 100% calc(100% - 2px), 168 + 100% 2px, 169 + calc(100% - 2px) 2px, 170 + calc(100% - 2px) 0px, 171 + 2px 0px, 172 + 2px 2px, 173 + 0px 2px 174 + ); 177 175 } 178 176 `); 179 177 ··· 183 181 <head> 184 182 <title>hewo!!</title> 185 183 <Import styles={[home]} /> 184 + <link rel="stylesheet" href="/fonts/space-grotesk/import.css" /> 186 185 </head> 187 - <NavigationBar items={[["home", "/"], ["reports", "/reports"]]} selected="home" /> 186 + {/* <NavigationBar items={[["home", "/"], ["reports", "/reports"]]} selected="home" /> */} 188 187 <header id="heading"> 189 - <h1> 190 - <Typecycle /> 191 - </h1> 192 - <h2>she/her ∘ ૮ ˶ᵔ ᵕ ᵔ˶ ა</h2> 188 + <Heading /> 193 189 </header> 194 190 <section id="intro"> 195 191 <SectionTitle>Introduction</SectionTitle> ··· 204 200 <div id="read-more-content"> 205 201 <div class="break" /> 206 202 <p> 207 - <i>In a more personal tone~.</i> Haiiii! I'm <span class="dats-me">Júlia Lívia</span>, nice to meet ya! 203 + <i>In a more personal tone~</i> 208 204 </p> 209 205 <p> 210 - I'm also especially interested in atypical low-level systems and the intersection of hardware and software 206 + I'm especially interested in atypical low-level systems and the intersection of hardware and software 211 207 interface, with a particular fascination for the x86 and RISC-V ISAs, and I love finding elegant solutions 212 - in places most people don't bother to look at,, 208 + in places most people don't bother to look at. 213 209 </p> 214 210 <p> 215 - I'm passionate about linguistics and philosophy, and I love meeting new people and learning from different 216 - perspectives!! I'm also autistic n' ADHD, so I might struggle with humour once in a while. I really like 211 + I'm passionate about linguistics and philosophy, and I love meeting new people to learn from their 212 + different perspectives!! I'm autistic, so I might struggle with tone time to time. Still, I really enjoy 217 213 befriending new people and growing as a person every single day!!! If you've read this far, thanks for 218 214 stopping by and getting to know me a bit ^-^ 219 215 </p> 220 216 <p> 221 - <i>Ummm…</i>{" "} 222 - this site is powered by snarl, my own web framework, built on the principle that understanding comes best 223 - from building in a fun way. Go check it out!!!111! 217 + <i>Ummm…</i> this site is powered by{" "} 218 + <a href="/~snarl">snarl</a>, my own web framework, built on the principle that the best way to understand 219 + something is to build it yourself and have fun doing it. Go check it out!!!111! 224 220 </p> 225 221 </div> 226 222 </div> ··· 262 258 /> 263 259 {friends.slice(1).map(({ href, src, alt }) => ( 264 260 <a href={href} rel="noopener nofollow"> 265 - <img src={src} alt={alt} width="88" height="31" /> 261 + <img src={src} alt={alt} width="88" height="31" onerror="this.parentElement.remove()" /> 266 262 </a> 267 263 ))} 268 264 </div>
+2 -4
src/utils/temp.ts
··· 8 8 seconds: number, 9 9 ): Promise<() => T> { 10 10 let value: T = await callback(); 11 - let timer: number | undefined; 12 11 13 12 async function tick() { 14 - clearInterval(timer); 15 13 try { 16 14 value = await callback(); 17 15 } catch (e) { 18 16 console.warn("withInterval: op failed, keeping stale value", e); 19 17 } 20 - timer = setInterval(tick, seconds * 1000); 18 + setInterval(tick, seconds * 1000); 21 19 } 22 - timer = setInterval(tick, seconds * 1000); 20 + setInterval(tick, seconds * 1000); 23 21 24 22 return () => value; 25 23 }
static/fonts/space-grotesk/WOFF2/space-grotesk-v22-latin-700.woff2

This is a binary file and will not be displayed.

+7
static/fonts/space-grotesk/import.css
··· 1 + @font-face { 2 + font-family: 'Space Grotesk'; 3 + font-style: normal; 4 + font-weight: 700; 5 + src: local('Space Grotesk Bold'), local('SpaceGrotesk-Bold'), 6 + url('WOFF2/space-grotesk-v22-latin-700.woff2') format('woff2'); 7 + }