Personal Site
1---
2import { urls, type nav } from "/site-config";
3import boxBlr from "/assets/box-blr.png";
4import dot from "/assets/dot.png";
5import boxTlbr from "/assets/box-tlbr.png";
6
7const betweenArr = <A, I>(arr: A[], item: I): (A | I)[] =>
8 arr.flatMap((x, i) => (i !== 0 ? [item, x] : [x]));
9
10const strToX = <I,>(
11 arr: string,
12 map: [string, I][],
13): (string | I)[] | string => {
14 const [key, value] = map.pop() ?? [undefined, undefined];
15 if (!key) return arr;
16 return betweenArr(arr.split(key), value).flatMap((x: string | I) =>
17 typeof x === "string" ? strToX(x, map) : x,
18 );
19};
20---
21
22<header style={`--box-blr-png: url("${boxBlr.src}")`}>
23 <h1>vielle.dev</h1>
24
25 <nav>
26 <ul
27 style={`--dot-png: url("${dot.src}");
28 --box-tlbr-png: url("${boxTlbr.src}")`}
29 >
30 {
31 (() => {
32 const name = (url: nav) =>
33 url.slug ? (
34 <a href={url.slug}>
35 {strToX(url.name, [
36 ["­", <>­</>],
37 ["<wbr>", <wbr />],
38 ])}
39 </a>
40 ) : (
41 url.name
42 );
43 const flatten = (urls: nav[]) =>
44 urls.map((url) => (
45 <li>
46 {name(url)}
47 {url.children && <ul>{flatten(url.children)}</ul>}
48 </li>
49 ));
50 return urls.map((url) => (
51 <li>
52 {url.children ? (
53 <details name="nav">
54 <summary>{name(url)}</summary>
55 <ul>{flatten(url.children)}</ul>
56 </details>
57 ) : (
58 name(url)
59 )}
60 </li>
61 ));
62 })()
63 }
64 </ul>
65 </nav>
66</header>
67
68<style>
69 /* UPDATE RSS STYLE XML WHEN YOU CHANGE THESE */
70 header {
71 border-image: var(--box-blr-png) 10 fill / 20px / 20px round;
72 margin: 0 20px 20px;
73 padding: 10px 20px;
74 height: 3rem; /* 2rem * 1.5 */
75 /* render over feed bg */
76 z-index: 2;
77
78 display: flex;
79 flex-direction: row;
80 justify-content: space-between;
81 align-items: center;
82
83 @media (max-width: 650px) {
84 flex-direction: column;
85 align-items: start;
86 height: 4.5rem; /* (2rem + 1rem) * 1.5 */
87
88 nav {
89 margin-inline: auto;
90 contain: inline-size;
91 width: 100%;
92 overflow: auto;
93 scrollbar-width: thin;
94 }
95
96 h1 {
97 width: 100%;
98 text-align: center;
99 }
100 }
101
102 & > nav > ul {
103 display: flex;
104 flex-direction: row;
105 align-items: center;
106 justify-content: start;
107 gap: 10px;
108 z-index: 999;
109 width: fit-content;
110 margin-inline: auto;
111
112 & > li {
113 display: flex;
114 flex-direction: row;
115 align-items: center;
116 gap: 10px;
117
118 &::marker {
119 content: none;
120 }
121
122 & + &::before {
123 content: "";
124 background-image: var(--dot-png);
125 background-size: contain;
126 width: 9px;
127 height: 9px;
128 display: block;
129 }
130 }
131 }
132 }
133
134 nav > ul > li:last-child > details > ul {
135 right: 10px;
136 }
137
138 details {
139 summary {
140 cursor: pointer;
141 text-wrap: nowrap;
142 }
143
144 & > ul {
145 position: absolute;
146 z-index: 99999;
147
148 @media (max-width: 650px) {
149 inset: auto 15px;
150 }
151
152 margin-top: 10px;
153 padding: 20px;
154 padding-left: 40px;
155 & ul {
156 margin-left: 10px;
157 }
158
159 border-image: var(--box-tlbr-png) 10 fill / 20px round;
160 }
161 }
162
163 h1 {
164 margin-block: 0;
165 position: sticky;
166 inset: 0;
167 }
168</style>