Source code + assets for pluie.me

treewide: format with dprint & remove old files

pluie.me 47fb477b be783b43

verified
+343 -756
+9 -2
.helix/languages.toml
··· 1 1 [[language]] 2 2 name = "json" 3 - formatter = { command = "deno", args = ["fmt", "--ext", "json", "-"] } 3 + formatter = { command = "dprint", args = ["fmt", "--stdin", "json"] } 4 4 5 5 [[language]] 6 6 name = "css" 7 - formatter = { command = "deno", args = ["fmt", "--ext", "css", "-"] } 7 + formatter = { command = "dprint", args = ["fmt", "--stdin", "css"] } 8 + 9 + [[language]] 10 + name = "vento" 11 + file-types = ["vto"] 12 + auto-format = true 13 + indent = { tab-width = 2, unit = " " } 14 + formatter = { command = "dprint", args = ["fmt", "--stdin", "vto"] } 8 15 9 16 [[language]] 10 17 name = "typescript"
+1
LICENSE.md
··· 8 8 The full texts of each license are included below: 9 9 10 10 # Mozilla Public License, version 2.0 11 + 11 12 ``` 12 13 Mozilla Public License Version 2.0 13 14 ==================================
-1
README.md
··· 1 1 Source code of [pluie.me](https://pluie.me). 2 -
+7 -7
_config.ts
··· 1 1 import lume from "lume/mod.ts"; 2 - import inline from "lume/plugins/inline.ts"; 3 2 import date from "lume/plugins/date.ts"; 4 - import vento from "lume/plugins/vento.ts"; 5 - import metas from "lume/plugins/metas.ts"; 3 + import esbuild from "lume/plugins/esbuild.ts"; 6 4 import extractDate from "lume/plugins/extract_date.ts"; 7 - import multilanguage from "lume/plugins/multilanguage.ts"; 8 - import esbuild from "lume/plugins/esbuild.ts"; 9 - import tailwindcss from "lume/plugins/tailwindcss.ts"; 10 5 import feed from "lume/plugins/feed.ts"; 6 + import inline from "lume/plugins/inline.ts"; 7 + import metas from "lume/plugins/metas.ts"; 8 + import multilanguage from "lume/plugins/multilanguage.ts"; 11 9 import sitemap from "lume/plugins/sitemap.ts"; 10 + import tailwindcss from "lume/plugins/tailwindcss.ts"; 11 + import vento from "lume/plugins/vento.ts"; 12 12 13 13 // Remark plugins 14 14 import remark from "lume/plugins/remark.ts"; 15 - import emoji from "npm:remark-emoji"; 16 15 import a11yEmoji from "npm:@fec/remark-a11y-emoji"; 17 16 import smartyPants from "npm:@ngsctt/remark-smartypants"; 17 + import emoji from "npm:remark-emoji"; 18 18 19 19 import stripIndent from "npm:strip-indent"; 20 20
+29
dprint.json
··· 1 + { 2 + "typescript": { 3 + }, 4 + "json": { 5 + }, 6 + "markdown": { 7 + }, 8 + "toml": { 9 + }, 10 + "malva": { 11 + }, 12 + "markup": { 13 + }, 14 + "yaml": { 15 + }, 16 + "excludes": [ 17 + "**/node_modules", 18 + "**/*-lock.json" 19 + ], 20 + "plugins": [ 21 + "https://plugins.dprint.dev/typescript-0.95.8.wasm", 22 + "https://plugins.dprint.dev/json-0.20.0.wasm", 23 + "https://plugins.dprint.dev/markdown-0.19.0.wasm", 24 + "https://plugins.dprint.dev/toml-0.7.0.wasm", 25 + "https://plugins.dprint.dev/g-plane/malva-v0.12.1.wasm", 26 + "https://plugins.dprint.dev/g-plane/markup_fmt-v0.22.0.wasm", 27 + "https://plugins.dprint.dev/g-plane/pretty_yaml-v0.5.1.wasm" 28 + ] 29 + }
+5 -3
flake.nix
··· 7 7 self, 8 8 nixpkgs, 9 9 }: let 10 - systems = ["x86_64-linux" "x86_64-darwin"]; 11 - forAllSystems = f: nixpkgs.lib.genAttrs systems (s: f (import nixpkgs {system = s;})); 10 + forAllSystems = f: with nixpkgs.lib; genAttrs systems.flakeExposed (s: f (import nixpkgs {system = s;})); 12 11 in { 13 12 devShells = forAllSystems (pkgs: { 14 13 default = pkgs.mkShell { 15 - buildInputs = with pkgs; [deno]; 14 + buildInputs = with pkgs; [ 15 + deno 16 + dprint 17 + ]; 16 18 }; 17 19 }); 18 20 };
+10 -8
src/404.vto
··· 8 8 --- 9 9 10 10 <div class="flex flex-col text-center place-content-center place-items-center h-main-screen"> 11 - <h1 class="text-6xl font-bold mb-4">404 🦀</h1> 12 - <h2 class="text-3xl mb-4">Oops!</h2> 13 - {{ echo |> strip_indent |> md }} 14 - The page you're looking for doesn't exist. Sorry 'bout that! :p 15 - {{ /echo }} 11 + <h1 class="text-6xl font-bold mb-4">404 🦀</h1> 12 + <h2 class="text-3xl mb-4">Oops!</h2> 13 + <p>The page you’re looking for doesn’t exist. Sorry ’bout that! :p</p> 16 14 17 - <a class="button rounded-full mt-8 px-10 py-3 bg-brand" href="/" target="_self"> 18 - Back to main page 19 - </a> 15 + <a 16 + class="button rounded-full mt-8 px-10 py-3 bg-brand" 17 + href="/" 18 + target="_self" 19 + > 20 + Back to main page 21 + </a> 20 22 </div>
+7 -3
src/_includes/comps/post_embed.vto
··· 1 1 {{ export function post_embed(post) }} 2 2 <p class="text-md"> 3 - <span class="icon-[mdi--calendar] align-icon-offset inline-block vertical-sub"></span> 3 + <span 4 + class="icon-[mdi--calendar] align-icon-offset inline-block vertical-sub" 5 + ></span> 4 6 {{ post.date |> date("HUMAN_DATE") }} 5 - 7 + 6 8 {{ for tag of post.tags }} 7 9 {{ if tag !== "post" }} 8 - <span class="border-brand-dark border-1 text-brand-dark dark:border-brand dark:text-brand text-xs rounded-full px-2 inline-flex">{{ tag }}</span> 10 + <span 11 + class="border-brand-dark border-1 text-brand-dark dark:border-brand dark:text-brand text-xs rounded-full px-2 inline-flex" 12 + >{{ tag }}</span> 9 13 {{ /if }} 10 14 {{ /for }} 11 15 </p>
+6 -7
src/_includes/layouts/base.vto
··· 1 1 <!DOCTYPE html> 2 2 3 3 <html> 4 - {{ include "parts/head.vto" }} 5 - <body class="pt-navbar bg-bg text-fg flex flex-col items-center"> 6 - {{ include "parts/navbar.vto" }} 7 - {{ content }} 8 - {{ include "parts/footer.vto" }} 9 - </body> 4 + {{ include "parts/head.vto" }} 5 + <body class="pt-navbar bg-bg text-fg flex flex-col items-center"> 6 + {{ include "parts/navbar.vto" }} 7 + {{ content }} 8 + {{ include "parts/footer.vto" }} 9 + </body> 10 10 </html> 11 -
+27 -8
src/_includes/layouts/blog.vto
··· 4 4 {{ import { post_embed } from "../comps/post_embed.vto" }} 5 5 {{> it.title = it.categories.blog.title }} 6 6 7 - 8 7 <div class="flex flex-col lg:flex-row gap-8"> 9 8 <nav class="flex justify-between lg:flex-col lg:justify-start lg:shrink-0 lg:basis-60 text-2xl"> 10 9 <div> 11 10 <h1 class="text-4xl font-bold">{{ categories.blog.title }}</h1> 12 - <p>{{ it.blog.page_a_of_b.replace("{0}", currentPage).replace("{1}", totalPages)}}</p> 11 + <p> 12 + {{ 13 + it.blog.page_a_of_b.replace("{0}", currentPage).replace( 14 + "{1}", 15 + totalPages, 16 + ) 17 + }} 18 + </p> 13 19 </div> 14 20 15 21 <div class="flex justify-between items-center lg:mt-8 basis-1/3 lg:basis-auto"> 16 22 {{ if pagination.previous }} 17 - <a class="icon-[mdi--chevron-left] text-brand" href={{ pagination.previous }}></a> 23 + <a 24 + class="icon-[mdi--chevron-left] text-brand" 25 + href="{{ pagination.previous }}" 26 + ></a> 18 27 {{ else }} 19 28 <span class="icon-[mdi--chevron-left] text-main-fg-sub"></span> 20 29 {{ /if }} ··· 24 33 {{ set page = search.page(`id=blog-${num} lang=` + lang) }} 25 34 26 35 {{ if num === pagination.page }} 27 - <span class="pagination-link bg-brand-dark px-3 py-2 text-lg text-white rounded-sm" href="{{ pagination.url }}">{{ num }}</span> 36 + <span 37 + class="pagination-link bg-brand-dark px-3 py-2 text-lg text-white rounded-sm" 38 + href="{{ pagination.url }}" 39 + >{{ num }}</span> 28 40 {{ else if num > 0 }} 29 - <a class="pagination-link border-1 border-brand-dark px-3 py-2 text-lg text-fg rounded-sm hover:bg-brand-darker hover:text-white transition" href="{{ page.url }}">{{ num }}</a> 41 + <a 42 + class="pagination-link border-1 border-brand-dark px-3 py-2 text-lg text-fg rounded-sm hover:bg-brand-darker hover:text-white transition" 43 + href="{{ page.url }}" 44 + >{{ num }}</a> 30 45 {{ else }} 31 46 <span class="text-main-fg-sub">&hellip;</span> 32 47 {{ /if }} ··· 34 49 </ul> 35 50 36 51 {{ if pagination.next }} 37 - <a class="icon-[mdi--chevron-right] text-brand" href={{ pagination.next }}></a> 52 + <a 53 + class="icon-[mdi--chevron-right] text-brand" 54 + href="{{ pagination.next }}" 55 + ></a> 38 56 {{ else }} 39 - <span class="inline-block icon-[mdi--chevron-right] text-main-fg-sub"></span> 57 + <span 58 + class="inline-block icon-[mdi--chevron-right] text-main-fg-sub" 59 + ></span> 40 60 {{ /if }} 41 61 </div> 42 62 </nav> 43 - 44 63 45 64 <section class="flex flex-col gap-8"> 46 65 {{ for post of results }}
+3 -6
src/_includes/layouts/default.vto
··· 1 1 {{ layout "layouts/base.vto" }} 2 - 3 - <main class="py-16 max-md:max-w-48ch"> 4 - {{ content }} 5 - </main> 6 - 2 + <main class="py-16 max-md:max-w-48ch"> 3 + {{ content }} 4 + </main> 7 5 {{ /layout }} 8 -
+30 -21
src/_includes/layouts/post.vto
··· 4 4 {{ import { post_embed } from "../comps/post_embed.vto" }} 5 5 6 6 <style> 7 + .post-grid { 8 + display: grid; 9 + grid-template-columns: 1fr; 10 + grid-template-areas: 11 + "- back" 12 + "info info" 13 + "desc desc"; 14 + 15 + column-gap: 3rem; 16 + } 17 + .post-grid .info { 18 + grid-area: info; 19 + } 20 + .post-grid .back { 21 + grid-area: back; 22 + } 23 + .post-grid .desc { 24 + grid-area: desc; 25 + } 26 + 27 + @media (min-width: 1024px) { 7 28 .post-grid { 8 - display: grid; 9 - grid-template-columns: 1fr; 10 29 grid-template-areas: 11 - "- back" 12 - "info info" 13 - "desc desc" 14 - ; 15 - column-gap: 3rem; 30 + "info back" 31 + "desc -"; 16 32 } 17 - .post-grid .info { grid-area: info; } 18 - .post-grid .back { grid-area: back; } 19 - .post-grid .desc { grid-area: desc; } 20 - 21 - @media (min-width: 1024px) { 22 - .post-grid { 23 - grid-template-areas: 24 - "info back" 25 - "desc -"; 26 - } 27 - } 33 + } 28 34 </style> 29 35 30 36 <div class="post-grid items-center"> ··· 32 38 <h1 class="text-3xl text-brand">{{ page.data.title }}</h1> 33 39 {{ post_embed(page.data) }} 34 40 </div> 35 - <a class="back mb-4 w-fit shrink-0 arrow-button rtl text-white bg-brand-dark flex items-center gap-1" href="/blog">Other posts</a> 41 + <a 42 + class="back mb-4 w-fit shrink-0 arrow-button rtl text-white bg-brand-dark flex items-center gap-1" 43 + href="/blog" 44 + >Other posts</a> 36 45 37 46 <div class="desc mt-4"> 38 - {{ page.data.description |> strip_indent |> md }} 47 + {{ page.data.description |> md }} 39 48 </div> 40 49 </div> 41 50 42 - <hr class="border-brand-dark mt-6 mb-12"/> 51 + <hr class="border-brand-dark mt-6 mb-12" /> 43 52 44 53 <div class="prose dark:prose-invert mx-auto"> 45 54 {{ content }}
+6 -4
src/_includes/parts/footer.vto
··· 1 1 {{ set currentYear = new Date().getFullYear() }} 2 2 {{ set firstYear = 2022 }} 3 - {{ set yearText = currentYear > firstYear ? `${firstYear}–${currentYear}` : `${firstYear}` }} 3 + {{ 4 + set yearText = currentYear > firstYear ? `${firstYear}–${currentYear}` : `${firstYear}` 5 + }} 4 6 5 7 <footer class="px-16 pt-12 pb-20 text-center prose dark:prose-invert prose-a:text-brand prose-a:no-underline max-w-none w-full border-t-2 border-t-main-border"> 6 - <p>© {{yearText}} Leah Amelia “pluie” Chen</p> 8 + <p>© {{ yearText }} Leah Amelia “pluie” Chen</p> 7 9 8 - {{ footer.license |> strip_indent |> md }} 9 - {{ footer.credits |> strip_indent |> md }} 10 + {{ footer.license |> md }} 11 + {{ footer.credits |> md }} 10 12 </footer>
+10 -10
src/_includes/parts/head.vto
··· 1 1 <head> 2 - <meta charset="utf-8" /> 2 + <meta charset="utf-8" /> 3 3 4 - <title>{{it.title}} | pluie.me</title> 4 + <title>{{ it.title }} | pluie.me</title> 5 5 6 - <meta name="viewport" content="width=device-width, initial-scale=0.75" /> 6 + <meta name="viewport" content="width=device-width, initial-scale=0.75" /> 7 7 8 - <link 9 - rel="icon" 10 - type="image/png" 11 - sizes="250x250" 12 - href="/img/avatar.png" 13 - /> 8 + <link 9 + rel="icon" 10 + type="image/png" 11 + sizes="250x250" 12 + href="/img/avatar.png" 13 + /> 14 14 15 - <link rel="stylesheet" href="/style.css" /> 15 + <link rel="stylesheet" href="/style.css" /> 16 16 <script src="/scripts/theme.js"></script> 17 17 </head>
+39 -12
src/_includes/parts/navbar.vto
··· 1 1 <nav class="top-0 fixed z-30 h-navbar flex min-w-full bg-bg border-b-2 border-b-main-border"> 2 2 <div class="flex grow"> 3 - {{> const pfx = lang === "en" ? "/" : `/${lang}/`; }} 3 + {{> const pfx = lang === "en" ? "/" : `/${lang}/` }} 4 4 <a class="navbar-tab navbar-brand" href="{{ pfx }}" title="pluie.me"></a> 5 5 6 - {{ for category of categories }} 7 - <a class="navbar-tab bg-transition text-lg" href="{{ pfx }}{{ category.url }}" title="{{ category.title }}"> 8 - {{ category.title }} 9 - </a> 10 - {{ /for }} 6 + {{ for category of categories }} 7 + <a 8 + class="navbar-tab bg-transition text-lg" 9 + href="{{ pfx }}{{ category.url }}" 10 + title="{{ category.title }}" 11 + > 12 + {{ category.title }} 13 + </a> 14 + {{ /for }} 11 15 </div> 12 16 13 17 <div class="flex items-center gap-4 me-6 text-main-fg-sub text-2xl"> 14 - <button id="theme-switcher" class="cursor-pointer" title="{{ it.navbar.theme_switcher }}"> 15 - <img class="split-icon theme scale-90" inline src="/icons/mdi-theme-light-dark-split.svg" /> 18 + <button 19 + id="theme-switcher" 20 + class="cursor-pointer" 21 + title="{{ it.navbar.theme_switcher }}" 22 + > 23 + <img 24 + class="split-icon theme scale-90" 25 + inline 26 + src="/icons/mdi-theme-light-dark-split.svg" 27 + /> 16 28 </button> 17 29 {{> const counterpart = it.alternates?.find(p => p.lang != lang) }} 18 30 {{ if counterpart?.url }} 19 - <a class="button" id="language-switcher" href="{{counterpart.url}}" title="{{ it.navbar.language_switcher }}"> 20 - <img class="split-icon lang scale-90" inline src="/icons/mdi-translate-split.svg" /> 31 + <a 32 + class="button" 33 + id="language-switcher" 34 + href="{{counterpart.url}}" 35 + title="{{ it.navbar.language_switcher }}" 36 + > 37 + <img 38 + class="split-icon lang scale-90" 39 + inline 40 + src="/icons/mdi-translate-split.svg" 41 + /> 21 42 </a> 22 43 {{ else }} 23 - <div class="i-mdi:translate-off" title="{{ it.navbar.no_translation }}"></div> 44 + <div class="i-mdi:translate-off" title="{{ it.navbar.no_translation }}"> 45 + </div> 24 46 {{ /if }} 25 - <a href="{{ it.source }}" class="button icon-[mdi--git] hover:text-brand transition-colors duration-100" target="_blank" title="{{ it.navbar.source }}"></a> 47 + <a 48 + href="{{ it.source }}" 49 + class="button icon-[mdi--git] hover:text-brand transition-colors duration-100" 50 + target="_blank" 51 + title="{{ it.navbar.source }}" 52 + ></a> 26 53 </div> 27 54 </nav>
-113
src/_styles/components/display.css
··· 1 - @use "sass:math"; 2 - @use "sass:list"; 3 - 4 - $display-frame-inner-blur: 0.5px !default; 5 - $display-frame-inner-width: 2.5px !default; 6 - $display-frame-inner-color: rgb(255, 250, 246) !default; 7 - $display-frame-inner: 0 8 - 0 9 - $display-frame-inner-blur 10 - $display-frame-inner-width 11 - $display-frame-inner-color !default; 12 - 13 - $display-frame-width: 25px !default; 14 - $display-frame-color: rgb(53, 34, 19) !default; 15 - $display-frame: 0 0 0px $display-frame-width $display-frame-color !default; 16 - 17 - $display-frame-shadow-offset: 8px 5px; 18 - $display-frame-shadow-blur: 5px; 19 - $display-frame-shadow-width: $display-frame-width; 20 - $display-frame-shadow-color: rgb(0 0 0 / 0.25) !default; 21 - $display-frame-shadow: $display-frame-shadow-offset 22 - $display-frame-shadow-blur 23 - $display-frame-shadow-width 24 - $display-frame-shadow-color !default; 25 - 26 - $display-frame-margin: calc( 27 - $display-frame-width + math.max($display-frame-shadow-offset...) 28 - ) !default; 29 - $display-frame-sizes: (large: 500px, medium: 400px, _: 300px); 30 - $display-frame-hover-scale-factor: 1.035; 31 - $display-frame-hover-transition-length: 0.5s; 32 - 33 - $display-frame-popup-spinner-size: 8rem; 34 - $display-frame-popup-spinner-margin: 3rem; 35 - 36 - figure.display { 37 - @apply flex items-center; 38 - 39 - figcaption { 40 - @apply text-justify m-6 font-normal; 41 - 42 - :nth-child(-n+2) { 43 - @apply text-center lg:text-left; 44 - } 45 - } 46 - &.rtl { 47 - @apply lg:flex-row-reverse; 48 - 49 - figcaption :nth-child(-n+2) { 50 - @apply lg:text-right; 51 - } 52 - } 53 - 54 - &.has-frame { 55 - @each $size in $display-frame-sizes { 56 - $name: list.nth($size, 1); 57 - $px: list.nth($size, 2); 58 - 59 - @if $name == "_" { 60 - img { 61 - max-width: $px; 62 - } 63 - } @else { 64 - &[data-size="#{$name}"] img { 65 - max-width: $px; 66 - } 67 - } 68 - } 69 - img { 70 - width: 80vw; 71 - margin: $display-frame-margin; 72 - box-shadow: $display-frame-inner, $display-frame, $display-frame-shadow; 73 - } 74 - } 75 - 76 - img { 77 - transition: transform $display-frame-hover-transition-length; 78 - } 79 - 80 - label:hover, label:focus, label:focus-visible { 81 - img { 82 - transform: scale($display-frame-hover-scale-factor); 83 - } 84 - } 85 - 86 - & + * .modal .modal-content { 87 - @apply flex justify-center flex-col lg:flex-row; 88 - width: unset; 89 - min-width: 70vw; 90 - max-height: calc(100vh - 80px); 91 - 92 - &::before { 93 - //@include loader; 94 - position: absolute; 95 - top: calc( 96 - 50% - $display-frame-popup-spinner-size 97 - + 0.5 * $display-frame-popup-spinner-margin 98 - ); 99 - left: calc( 100 - 50% - $display-frame-popup-spinner-size 101 - + 0.5 * $display-frame-popup-spinner-margin 102 - ); 103 - z-index: 35; 104 - height: $display-frame-popup-spinner-size; 105 - width: $display-frame-popup-spinner-size; 106 - margin: $display-frame-popup-spinner-margin; 107 - } 108 - 109 - .image { 110 - z-index: 40; 111 - } 112 - } 113 - }
-44
src/_styles/components/message.css
··· 1 - @keyframes sweep { 2 - 0% { 3 - opacity: 0; 4 - transform: translateX(-10px); 5 - } 6 - 100% { 7 - opacity: 1; 8 - transform: translateX(0); 9 - } 10 - } 11 - 12 - .message { 13 - padding-bottom: 10px; 14 - 15 - summary { 16 - cursor: pointer; 17 - transition: margin 150ms ease-out; 18 - } 19 - 20 - &[open] summary { 21 - margin-bottom: 10px; 22 - } 23 - 24 - &:not(.is-arrowless) { 25 - .message-header::after { 26 - @include arrow($black-ter); 27 - top: 45%; 28 - right: 1rem; 29 - transition: transform 0.1s; 30 - } 31 - 32 - &:not([open]) .message-header::after { 33 - transform: translate(-2px, 2.5px) rotateZ(225deg); 34 - } 35 - } 36 - } 37 - 38 - @each $name, $color in $colors { 39 - $invert: nth($color, 2); 40 - 41 - .message.is-#{$name}:not(.is-arrowless) .message-header::after { 42 - border-color: $invert; 43 - } 44 - }
+41 -43
src/_styles/components/split-icon.css
··· 1 - @layer components { 2 - .split-icon { 3 - path { 4 - transition-duration: 0.1s; 5 - transition-property: fill, filter; 6 - } 7 - --base-side: 0; 8 - --side: var(--base-side); 9 - --highlight: 0; 10 - --highlight-color: var(--color-brand); 1 + .split-icon { 2 + path { 3 + transition-duration: 0.1s; 4 + transition-property: fill, filter; 5 + } 6 + --base-side: 0; 7 + --side: var(--base-side); 8 + --highlight: 0; 9 + --highlight-color: var(--color-brand); 11 10 12 - #left { 13 - filter: opacity(calc(0.5 * (2 - var(--side)))); 14 - fill: color-mix( 15 - in oklab, 16 - currentColor calc(100% * (1 - var(--highlight) * (1 - var(--side)))), 17 - var(--highlight-color) 18 - ); 19 - } 20 - #right { 21 - filter: opacity(calc(0.5 * (1 + var(--side)))); 22 - fill: color-mix( 23 - in oklab, 24 - currentColor calc(100% * (1 - var(--highlight) * var(--side))), 25 - var(--highlight-color) 26 - ); 27 - } 11 + #left { 12 + filter: opacity(calc(0.5 * (2 - var(--side)))); 13 + fill: color-mix( 14 + in oklab, 15 + currentColor calc(100% * (1 - var(--highlight) * (1 - var(--side)))), 16 + var(--highlight-color) 17 + ); 18 + } 19 + #right { 20 + filter: opacity(calc(0.5 * (1 + var(--side)))); 21 + fill: color-mix( 22 + in oklab, 23 + currentColor calc(100% * (1 - var(--highlight) * var(--side))), 24 + var(--highlight-color) 25 + ); 26 + } 28 27 29 - &:hover { 30 - --side: calc(1 - var(--base-side)) !important; 31 - --highlight: 1; 32 - } 28 + &:hover { 29 + --side: calc(1 - var(--base-side)) !important; 30 + --highlight: 1; 31 + } 33 32 34 - /* Theme-based split icon */ 33 + /* Theme-based split icon */ 35 34 36 - &.theme { 37 - --base-side: 1; 35 + &.theme { 36 + --base-side: 1; 38 37 39 - @variant dark { 40 - --base-side: 0; 41 - } 38 + @variant dark { 39 + --base-side: 0; 42 40 } 43 41 } 42 + } 44 43 45 - /* Language based split icon */ 44 + /* Language based split icon */ 46 45 47 - [lang="zh"] .split-icon.lang { 48 - --base-side: 0; 49 - } 46 + [lang="zh"] .split-icon.lang { 47 + --base-side: 0; 48 + } 50 49 51 - [lang="en"] .split-icon.lang { 52 - --base-side: 1; 53 - } 50 + [lang="en"] .split-icon.lang { 51 + --base-side: 1; 54 52 }
-7
src/_styles/components/toggleable-modal.css
··· 1 - .toggleable-modal input.modal-toggle { 2 - display: none; 3 - 4 - &:checked + .modal { 5 - display: flex !important; 6 - } 7 - }
-16
src/_styles/components/tooltip.css
··· 1 - [data-tooltip] { 2 - @apply relative; 3 - 4 - &::after { 5 - content: attr(data-tooltip); 6 - @apply hidden absolute text-sm text-center inline-block align-middle 7 - bg-bg/90; 8 - @apply py-1 px-2 rounded-md border-[1px] border-brand min-w-fit -inset-x-2 9 - bottom-[90%] z-20; 10 - } 11 - &:hover { 12 - &::before, &::after { 13 - @apply block; 14 - } 15 - } 16 - }
-76
src/_styles/fonts.scss
··· 1 - /* adapted from fonts.bunny.net's generated CSS file, with `font-display: swap` enabled. */ 2 - 3 - $variants: ( 4 - cyrillic: U+0400-045F U+0490-0491 U+04B0-04B1 U+2116, 5 - cyrillic-ext: U+0460-052F 6 - U+1C80-1C88 7 - U+20B4 8 - U+2DE0-2DFF 9 - U+A640-A69F 10 - U+FE2E-FE2F, 11 - hebrew: U+0590-05FF U+200C-2010 U+20AA U+25CC U+FB1D-FB4F, 12 - latin: U+00?? 13 - U+0131 14 - U+0152-0153 15 - U+02BB-02BC 16 - U+02C6 17 - U+02DA 18 - U+02DC 19 - U+2000-206F 20 - U+2074 21 - U+20AC 22 - U+2122 23 - U+2191 24 - U+2193 25 - U+2212 26 - U+2215 27 - U+FEFF 28 - U+FFFD, 29 - latin-ext: U+0100-024F 30 - U+0259 31 - U+1E?? 32 - U+2020 33 - U+20A0-20AB 34 - U+20AD-20CF 35 - U+2113 36 - U+2C60-2C7F 37 - U+A720-A7FF, 38 - ); 39 - 40 - @mixin font-family($name, $id, $weights, $variants) { 41 - @each $weight, $styles in $weights { 42 - @each $style in $styles { 43 - @each $variant-name, $ranges in $variants { 44 - @font-face { 45 - font-family: $name; 46 - font-style: $style; 47 - font-weight: $weight; 48 - font-display: swap; 49 - 50 - $src: (); 51 - @each $format in "woff2", "woff" { 52 - $src: append( 53 - $src, 54 - url(https://fonts.bunny.net/#{$id}/files/#{$id}-#{$variant-name}-#{$weight}-#{$style}.#{$format}) format( 55 - $format 56 - ), 57 - $separator: comma 58 - ); 59 - } 60 - src: $src; 61 - unicode-range: $ranges; 62 - } 63 - } 64 - } 65 - } 66 - } 67 - 68 - @include font-family( 69 - "Rubik", 70 - "rubik", 71 - ( 72 - 400: normal italic, 73 - 600: normal italic, 74 - ), 75 - $variants, 76 - );
+1 -1
src/blog.page.ts
··· 4 4 5 5 // TODO: figure out all the types here 6 6 7 - export default async function* ({ search, paginate, lang }: Lume.Helpers) { 7 + export default async function*({ search, paginate, lang }: Lume.Helpers) { 8 8 const pages = search.pages(`post lang=${lang}`, "date=desc"); 9 9 10 10 yield* paginate(pages, {
+107 -83
src/index/page.vto
··· 1 1 <div class="top-detector"></div> 2 + 2 3 <main class="splash flex flex-col items-center justify-center gap-y-12 gap-x-12 py-16 lg:grid lg:grid-cols-main-screen lg:gap-y-20 lg:pt-0"> 3 - <figure class="intro place-self-center"> 4 - <img class="rounded-full border-2 border-brand" src="/img/avatar.png" alt="Avatar" width="320" height="320" /> 5 - </figure> 4 + <figure class="intro place-self-center"> 5 + <img 6 + class="rounded-full border-2 border-brand" 7 + src="/img/avatar.png" 8 + alt="Avatar" 9 + width="320" 10 + height="320" 11 + /> 12 + </figure> 6 13 7 - <div class="intro col-span-2 place-self-center flex flex-col em-brand"> 8 - <h1 class="text-4xl lg:text-5xl font-bold font-sans"> 9 - {{ intro.hi }} 10 - <a href="https://pronouns.cc/@pluieuwu" class="text-lg lg:text-xl text-brand animated"> 11 - · {{ intro.pronouns }} <span class="icon-[twemoji--transgender-flag] align-icon-offset"></span> 12 - </a> 13 - </h1> 14 + <div class="intro col-span-2 place-self-center flex flex-col em-brand"> 15 + <h1 class="text-4xl lg:text-5xl font-bold font-sans"> 16 + {{ intro.hi }} 17 + <a 18 + href="https://pronouns.cc/@pluieuwu" 19 + class="text-lg lg:text-xl text-brand animated" 20 + > 21 + · {{ intro.pronouns }} <span 22 + class="icon-[twemoji--transgender-flag] align-icon-offset" 23 + ></span> 24 + </a> 25 + </h1> 14 26 15 - <p class="text-2xl mt-2"> 16 - {{ intro.alias }} 17 - </p> 18 - <p class="text-xl mt-8"> 19 - {{ intro.desc }} 20 - </p> 21 - </div> 27 + <p class="text-2xl mt-2"> 28 + {{ intro.alias }} 29 + </p> 30 + <p class="text-xl mt-8"> 31 + {{ intro.desc }} 32 + </p> 33 + </div> 22 34 23 - <div class="scroll-down hidden lg:block"> 24 - {{ intro.scroll_down }} 25 - <div class="bottom-detector"></div> 26 - </div> 35 + <div class="scroll-down hidden lg:block"> 36 + {{ intro.scroll_down }} 37 + <div class="bottom-detector"></div> 38 + </div> 27 39 28 - <div id="scroll-indicator-vanish-point" class="prose text-justify col-span-2 dark:prose-invert"> 29 - {{ it.desc |> strip_indent |> md }} 30 - </div> 40 + <div 41 + id="scroll-indicator-vanish-point" 42 + class="prose text-justify col-span-2 dark:prose-invert" 43 + > 44 + {{ it.desc |> md }} 45 + </div> 31 46 32 - <div class="w-full"> 47 + <div class="w-full"> 33 48 <h2 class="text-xl mb-2">{{ sections.links }} ↘</h2> 34 - <div class="sm:columns-2 lg:columns-1 w-full"> 35 - {{ for _, link of links }} 36 - {{ set fg = link.fg ?? "white" }} 37 - <a 38 - href="{{ link.link }}" 39 - target="_blank" 40 - class="arrow-button bg-{{ link.color }} text-{{ fg }} m-be-2 flex items-center gap-1" 41 - > 42 - <span class="body inline-flex items-center gap-1"> 43 - <div class="inline-block icon-[{{ link.icon }}]"></div> 44 - {{ link.text }} 45 - </span> 46 - </a> 47 - {{ /for }} 49 + <div class="sm:columns-2 lg:columns-1 w-full"> 50 + {{ for _, link of links }} 51 + {{ set fg = link.fg ?? "white" }} 52 + <a 53 + href="{{ link.link }}" 54 + target="_blank" 55 + class="arrow-button bg-{{ link.color }} text-{{ fg }} m-be-2 flex items-center gap-1" 56 + > 57 + <span class="body inline-flex items-center gap-1"> 58 + <div class="inline-block icon-[{{ link.icon }}]"></div> 59 + {{ link.text }} 60 + </span> 61 + </a> 62 + {{ /for }} 48 63 </div> 49 - </div> 64 + </div> 50 65 51 - <div class="text-centered w-full sm:text-left col-span-3"> 66 + <div class="text-centered w-full sm:text-left col-span-3"> 52 67 <h2 class="text-2xl font-bold">{{ sections.technologies }}</h2> 53 68 <div class="grid gap-y-8 sm:grid-cols-2 lg:grid-cols-3 lg:gap-16"> 54 - {{ for _, techs of technologies }} 55 - <div> 56 - <h3 class="text-xl my-3">{{ techs.title }}</h3> 69 + {{ for _, techs of technologies }} 70 + <div> 71 + <h3 class="text-xl my-3">{{ techs.title }}</h3> 57 72 58 - <div class="icon-grid w-3/4 lg:w-auto mx-auto"> 59 - {{ for id, icon of techs.icons }} 73 + <div class="icon-grid w-3/4 lg:w-auto mx-auto"> 74 + {{ for id, icon of techs.icons }} 60 75 <label for="lang-{{ id }}" class="icon"> 61 - <input id="lang-{{ id }}" name="icon-select" type="radio" class="hidden"> 76 + <input 77 + id="lang-{{ id }}" 78 + name="icon-select" 79 + type="radio" 80 + class="hidden" 81 + > 62 82 <div class="name" inert>{{ icon.name }}</div> 63 - <div class="icon-[{{ icon.icon }}] rounded-none" data-name="{{ icon.name }}"></div> 83 + <div 84 + class="icon-[{{ icon.icon }}] rounded-none" 85 + data-name="{{ icon.name }}" 86 + > 87 + </div> 64 88 </label> 65 - {{ /for }} 66 - </div> 67 - </div> 68 - {{ /for }} 89 + {{ /for }} 90 + </div> 91 + </div> 92 + {{ /for }} 69 93 </div> 70 - </div> 94 + </div> 71 95 </main> 72 96 73 97 <script> 74 - const scroll = document.querySelector('.scroll-down'); 75 - const intro = document.querySelector('main'); 98 + const scroll = document.querySelector(".scroll-down"); 99 + const intro = document.querySelector("main"); 76 100 77 - const t = new IntersectionObserver(entries => { 78 - entries.forEach(entry => { 79 - if (entry.isIntersecting) { 80 - scroll.classList.remove('opacity-0'); 81 - intro.classList.remove('compact'); 82 - } 83 - }); 84 - }); 85 - const b = new IntersectionObserver(entries => { 86 - entries.forEach(entry => { 87 - if (entry.isIntersecting) { 88 - scroll.classList.add('opacity-0'); 89 - intro.classList.add('compact'); 90 - } 91 - }); 92 - }); 101 + const t = new IntersectionObserver(entries => { 102 + entries.forEach(entry => { 103 + if (entry.isIntersecting) { 104 + scroll.classList.remove("opacity-0"); 105 + intro.classList.remove("compact"); 106 + } 107 + }); 108 + }); 109 + const b = new IntersectionObserver(entries => { 110 + entries.forEach(entry => { 111 + if (entry.isIntersecting) { 112 + scroll.classList.add("opacity-0"); 113 + intro.classList.add("compact"); 114 + } 115 + }); 116 + }); 93 117 94 - function init(ev) { 95 - if (window.innerHeight < 600 || window.innerWidth < 1024) { 96 - scroll.classList.add('opacity-0'); 97 - t.disconnect(); 98 - b.disconnect(); 99 - } else { 100 - t.observe(document.querySelector('.top-detector')); 101 - b.observe(document.querySelector('.bottom-detector')); 102 - } 103 - } 118 + function init(ev) { 119 + if (window.innerHeight < 600 || window.innerWidth < 1024) { 120 + scroll.classList.add("opacity-0"); 121 + t.disconnect(); 122 + b.disconnect(); 123 + } else { 124 + t.observe(document.querySelector(".top-detector")); 125 + b.observe(document.querySelector(".bottom-detector")); 126 + } 127 + } 104 128 105 - window.addEventListener('resize', init) 106 - window.addEventListener('load', init) 129 + window.addEventListener("resize", init); 130 + window.addEventListener("load", init); 107 131 </script>
+2 -2
src/scripts/theme.ts
··· 3 3 4 4 const theme = new Proxy( 5 5 { 6 - value: localStorage.getItem("theme") ?? 7 - (isDark.matches ? "neon" : "strawberry-milkshake"), 6 + value: localStorage.getItem("theme") 7 + ?? (isDark.matches ? "neon" : "strawberry-milkshake"), 8 8 }, 9 9 { 10 10 set(target, p, newValue) {
+3 -3
src/style.css
··· 7 7 @import "components/langs.css" layer(components); 8 8 @import "components/navbar.css" layer(components); 9 9 @import "components/splash.css" layer(components); 10 - @import "components/split-icon.css"; 10 + @import "components/split-icon.css" layer(components); 11 11 12 12 @custom-variant dark (&:where([data-theme=neon], [data-theme=neon] *)); 13 13 ··· 18 18 --color-brand-darker: oklch(0.5067 0.1955 2.08); 19 19 --color-brand-darkest: oklch(0.4667 0.1955 2.08); 20 20 21 - --color-bg: #121214; 22 - --color-fg: white; 21 + --color-bg: oklch(0.1831 0.004 285.99); 22 + --color-fg: oklch(0.9911 0.004 285.99); 23 23 24 24 --color-main-bg: var(--color-zinc-900); 25 25 --color-main-fg: var(--color-zinc-200);
-196
src/works.vto
··· 1 - --- 2 - title: Works 3 - description: Projects, artworks, and other things I've done... 4 - draft: true 5 - 6 - artworks: 7 - - id: tiger 8 - name: Ferality 9 - imgid: 558e95a4-f685-4dee-32a6-8c33e49d9600 10 - size: medium 11 - aspect_ratio: square 12 - width: 400 13 - height: 408 14 - 15 - - id: blossoms 16 - name: Altitudinous blossoms 17 - imgid: 170696fd-d9b4-4652-e168-2cc80250d400 18 - size: medium 19 - aspect_ratio: 3by5 20 - width: 400 21 - height: 660 22 - rtl: true 23 - 24 - - id: fish 25 - name: A piscine aura in the depths 26 - imgid: 278a69c6-ec0a-4461-c3f3-79aa615ad800 27 - size: large 28 - aspect_ratio: 4by3 29 - width: 500 30 - height: 380 31 - 32 - - id: red_cotton 33 - name: A flower — what's her name again? 34 - imgid: 35db3ba1-542a-46aa-2d10-850d34463e00 35 - size: large 36 - aspect_ratio: 4by3 37 - width: 500 38 - height: 372 39 - rtl: true 40 - 41 - - id: laotie 42 - name: 老铁 (Laotie) 43 - imgid: d35a98e6-9215-4ca7-ac79-133c93d17700 44 - size: medium 45 - aspect_ratio: 3by4 46 - width: 400 47 - height: 533 48 - 49 - - id: mom_and_i 50 - name: By the lake, beneath the canopies, in the beforetimes 51 - imgid: 34e6b9ba-08a0-4a1a-437c-2e8839591500 52 - size: medium 53 - aspect_ratio: 3by4 54 - width: 400 55 - height: 515 56 - --- 57 - 58 - 59 - 60 - {{# 61 - include _includes/comps/icon 62 - include _includes/comps/modal 63 - #}} 64 - 65 - <h2 class="text-4xl font-bold mb-6 mt-24">Projects</h2> 66 - 67 - {{ echo |> strip_indent |> md }} 68 - Under construction! :purple_heart: 69 - (Yeah, I know, it's been ages, gimme some more time) 70 - {{ /echo }} 71 - 72 - <h2 class="text-4xl font-bold mb-6 mt-24">Artworks</h2> 73 - 74 - {{ function display(attrs) }} 75 - {{> const { id, name, imgid, height, width, aspect_ratio, rtl, size } = attrs }} 76 - 77 - <figure class="display has-frame {{ rtl ? "rtl" : "" }}" data-size={{size}}> 78 - <label for={{ id }} role="button" tabindex="0"> 79 - <img 80 - src="https://imagedelivery.net/TLP_u-wyyvTEPKkgbA6Osg/{{ imgid }}/public" 81 - alt={{ title }} 82 - width={{ width }} 83 - height={{ height }} 84 - /> 85 - </label> 86 - 87 - <figcaption class="prose dark:prose-invert"> 88 - <h3 class="text-3xl font-bold mb-0">{{ name }}</h3> 89 - {{ it[id] }} 90 - </figcaption> 91 - {{# 92 - +modal(id) 93 - .image.is-fullwidth(class=aspectRatio) 94 - img( 95 - src=`https://imagedelivery.net/TLP_u-wyyvTEPKkgbA6Osg/${imgid}/full` 96 - alt=title 97 - fetchpriority="low" 98 - loading="lazy" 99 - ) 100 - #}} 101 - </figure> 102 - {{ /function }} 103 - 104 - {{ function header(date, medium) }} 105 - <p class="text-lg mb-6"> 106 - <i data-lucide="calendar"></i> 107 - {{ date |> date("PP") }} 108 - <i data-lucide="brush"></i> 109 - {{ medium }} 110 - </p> 111 - {{ /function }} 112 - 113 - {{ set tiger }} 114 - {{ header("2018-12-02", "Oil on canvas") }} 115 - {{ echo |> strip_indent |> md }} 116 - One of the older works of mine — I think I'd only spent 2 years dabbling with oil paintings back then, 117 - and the technical inexpertise definitely shows — but a brazen and unpolished technique does not 118 - necessarily hinder expressiveness. ;) 119 - 120 - I really wanted to capture the anger in the tiger, and as I exaggerated its features and started furiously 121 - laying down sweeping strokes of striking orange, I too experienced the feral fury in me. 122 - Wish I could experience that again... 123 - {{ /echo }} 124 - {{ /set }} 125 - 126 - {{ set blossoms }} 127 - {{ header("2021-02-11", "Oil on canvas") }} 128 - {{ echo |> strip_indent |> md }} 129 - A friend of my mother's is a hobbyist photographer, and I think he took the original photo when he went to Tibet to... 130 - well, take pictures of the wild flora and fauna and the majestic landscape. 131 - (Y'know, like a photographer? Why else would he be there anyway?) 132 - 133 - My mother and I were both enthralled by the contrast between the razor-sharp edges of the mountains in the background, 134 - and the wavy, flailing flora trying to survive on the hostile, altitudinous plateau. In face of all the odds, the 135 - flowers still bloom, facing the intensely blue sky of the Tibetan Plateau. 136 - 137 - To recreate the chiseled, thick look of the mountain, I had to use a spatula, which was *very* fun to use — 138 - I did take a while to figure it out though. I also used a toothbrush to ~~paint~~ *spray* the white dots on, 139 - which are actually specks of mint-scented toothpaste and not white paint! 140 - Although, they look like they hold up just fine on the canvas. 141 - {{ /echo }} 142 - {{ /set }} 143 - 144 - {{ set fish }} 145 - {{ header("2021-06-05", "Oil on canvas") }} 146 - {{ echo |> strip_indent |> md }} 147 - Some kind of tropical fish whose name I have completely forgotten. It didn't even occur to past-Leah to write these things down! *sigh* 148 - 149 - It's really pretty, especially for its lake-blue scales and blood-red fins! Sure did take a long time to paint though... It was around this time 150 - when my style really departed from the raw, emotional expression like *Ferality*, towards a finer sort of aesthetic. 151 - {{ /echo }} 152 - {{ /set }} 153 - 154 - {{ set red_cotton }} 155 - {{ header("2021-12-11", "Oil on canvas") }} 156 - {{ echo |> strip_indent |> md }} 157 - Turns out it's a flower of the tree [*Bombax ceiba*](https://en.wikipedia.org/wiki/Bombax_ceiba), better known as 158 - the red cotton tree, or <ruby>木棉 <rp>(</rp><rt>mù mián</rt><rp>)</rp></ruby> in Chinese. 159 - I was going to leave it unidentified, but my mother's a bonafide botanist and identified it for me. 160 - 161 - Its striking red color is honestly captivating, and I still have it displayed on the wall of my room, 162 - facing the entrance for I hope any visitors would appreciate its vividity and life force seeping out of the canvas, 163 - as much as I would. 164 - {{ /echo }} 165 - {{ /set }} 166 - 167 - {{ set laotie }} 168 - {{ header("2022-03-20", "Oil on canvas") }} 169 - {{ echo |> strip_indent |> md }} 170 - He's a corgi in the studio we used to paint in, and his presence made most of our weekly painting hours turn into communal dog-petting sessions. 171 - It was lovely to paint with such an adorable cutie around, and time and stress all went away so swiftly with him. 172 - I still have like, *so* many photos of him playing around with us... Too bad that that studio was shut down about 2 months ago, on February 2023. 173 - 174 - My mother insists I painted Santa Claus in disguise of a dog! But really, he's just wearing some festive clothes... 175 - {{ /echo }} 176 - {{ /set }} 177 - 178 - {{ set mom_and_i }} 179 - {{ header("2022-07-23", "Oil on canvas") }} 180 - {{ echo |> strip_indent |> md }} 181 - One summer afternoon, on the eastern shore of Jinji Lake, Suzhou. It was an awfully nice day, so my mother and I decided 182 - to take a selfie on our daily stroll outside. 183 - Originally, I decided to paint this as a work of memorabilia, to make me cherish the good times I spent with my mother at home, 184 - before I start my studies abroad, and our chance of spending quality time together becomes unfeasibly slim. 185 - 186 - But now, it has taken an additional level of meaning — this was the last painting I finished before I realized I was a trans woman. 187 - That tall silhouette with the masculine build was supposed to be me, but now it's representative of the person I used to be. 188 - 189 - However, I have to remain truthful to myself — regardless of who I am now, I *was* like that back then, and this fossilized image 190 - of me is one that I can't, and don't want to change. The past is not to be changed — the future, however, is. 191 - {{ /echo }} 192 - {{ /set }} 193 - 194 - {{ for artwork of artworks }} 195 - {{ display(artwork) }} 196 - {{ /for }}
-80
uno.config.ts
··· 1 - import { 2 - presetIcons, 3 - presetTypography, 4 - presetWebFonts, 5 - transformerVariantGroup, 6 - } from "npm:unocss"; 7 - 8 - import presetWind4 from "npm:@unocss/preset-wind4"; 9 - 10 - import logos from "npm:@iconify-json/logos/icons.json" with { type: "json" }; 11 - import simpleIcons from "npm:@iconify-json/simple-icons/icons.json" with { 12 - type: "json", 13 - }; 14 - import devicon from "npm:@iconify-json/devicon/icons.json" with { 15 - type: "json", 16 - }; 17 - import vscodeIcons from "npm:@iconify-json/vscode-icons/icons.json" with { 18 - type: "json", 19 - }; 20 - import twemoji from "npm:@iconify-json/twemoji/icons.json" with { 21 - type: "json", 22 - }; 23 - import mdi from "npm:@iconify-json/mdi/icons.json" with { type: "json" }; 24 - 25 - const themeColors = [ 26 - "fg", 27 - "bg", 28 - "main-bg", 29 - "main-fg", 30 - "main-fg-sub", 31 - "main-border", 32 - ]; 33 - 34 - const colors = Object.fromEntries( 35 - themeColors.map((c) => [c, `rgb(var(--${c}) / <alpha-value>)`]), 36 - ); 37 - 38 - export default { 39 - options: { 40 - theme: { 41 - colors, 42 - }, 43 - rules: [ 44 - [ 45 - "grid-cols-main-screen", 46 - { "grid-template-columns": "20rem auto 20rem" }, 47 - ], 48 - ["h-main-screen", { height: "calc(100vh - 4 * 3.5rem)" }], 49 - ["h-navbar", { height: "3.5rem" }], 50 - ], 51 - presets: [ 52 - presetWind4({ 53 - preflights: { 54 - reset: true, 55 - }, 56 - }), 57 - presetIcons({ 58 - collections: { 59 - logos: () => logos, 60 - "simple-icons": () => simpleIcons, 61 - devicon: () => devicon, 62 - "vscode-icons": () => vscodeIcons, 63 - twemoji: () => twemoji, 64 - mdi: () => mdi, 65 - }, 66 - }), 67 - presetTypography(), 68 - presetWebFonts({ 69 - themeKey: "font", // Required by Wind4 70 - provider: "google", 71 - fonts: { 72 - sans: ["DM Sans:400,700", "Noto Sans SC:400,700"], 73 - mono: ["Iosevka Nerd Font", "Iosevka", "JetBrains Mono"], 74 - }, 75 - }), 76 - ], 77 - }, 78 - cssFile: "uno.css", 79 - transformers: [transformerVariantGroup()], 80 - };