website

#astro#js#html#css

git clone https://git.pyrossh.dev/website

木 Personal website of pyrossh. Built with astrojs, shiki, vite.


44991ddpyrossh 2026-08-04T20:19:48+05:30
remove stuff
assets/css/markdown.css CHANGED
@@ -1,62 +1,79 @@
1
1
  /* Shared typography for rendered markdown content (blog posts + repo
2
- READMEs) — anything wrapped in <markdown-view>'s <article
2
+ READMEs) — anything wrapped in a plain <article class="markdown-body">
3
- class="markdown-body">. Extracted from the old posts-detail.css so repo
3
+ (see repo.njk/post.njk; no wrapping custom element anymore). Extracted
4
- pages get the same look instead of no styling at all. Deliberately
4
+ from the old posts-detail.css so repo pages get the same look instead
5
+ of no styling at all. Deliberately scoped to .markdown-body p/li code
5
- scoped to .markdown-body p/li code (inline code), not .markdown-body
6
+ (inline code), not .markdown-body code generally — a bare `code`
6
- code generally — a bare `code` selector would also catch the <code>
7
+ selector would also catch the <code> inside fenced <pre> blocks and
8
+ stomp the syntax-highlighting styles from shared.css with the small
7
- inside fenced <pre> blocks and stomp the syntax-highlighting styles
9
+ inline-code padding/background (that shared styling is cross-cutting
8
- from shared.css with the small inline-code padding/background. */
10
+ between markdown content and the file-viewer, so it stays global
9
-
11
+ rather than duplicated here). */
10
- .markdown-body h1 {
12
+ .markdown-body {
11
- font-size: 1.5rem;
13
+ font-size: 1.1rem;
12
- font-weight: 700;
14
+ line-height: 1.5;
13
- margin-top: 0.5rem;
14
- margin-bottom: 0.5rem;
15
- }
16
15
 
17
- .markdown-body h2 {
16
+ h1 {
18
- font-size: 1.5rem;
17
+ font-size: 1.5rem;
19
- font-weight: 700;
18
+ font-weight: 700;
20
- margin-top: 0.5rem;
19
+ margin-top: 0;
21
- margin-bottom: 0.1rem;
20
+ margin-bottom: 0.5rem;
22
- }
21
+ }
23
22
 
24
- .markdown-body h3 {
23
+ h2 {
25
- font-size: 1.25rem;
24
+ font-size: 1.5rem;
26
- font-weight: 700;
25
+ font-weight: 700;
27
- margin-top: 0.5rem;
26
+ margin-top: 0;
28
- margin-bottom: 0.1rem;
27
+ margin-bottom: 0.5rem;
29
- }
28
+ }
30
29
 
31
- .markdown-body p {
30
+ h3 {
32
- font-size: 1.1rem;
31
+ font-size: 1.25rem;
32
+ font-weight: 700;
33
+ margin-top: 0.5rem;
33
- padding-bottom: 1rem;
34
+ margin-bottom: 0.1rem;
34
- }
35
+ }
35
36
 
36
- .markdown-body li {
37
+ p {
37
- font-size: 1.1rem;
38
+ font-size: 1.1rem;
39
+ padding-bottom: 1rem;
38
- }
40
+ }
39
41
 
40
- .markdown-body p code,
41
- .markdown-body li code {
42
+ li {
42
- color: var(--color-code-fg) !important;
43
- background: var(--color-code-bg) !important;
44
- word-wrap: break-word;
45
- box-decoration-break: clone;
46
- padding: 2px 4px;
47
- border-radius: 0.2rem;
48
- font-weight: 400;
49
- font-size: 0.8rem;
43
+ font-size: 1.1rem;
50
- }
44
+ }
51
45
 
46
+ p code,
47
+ li code {
48
+ color: var(--color-code-fg) !important;
49
+ background: var(--color-code-bg) !important;
50
+ word-wrap: break-word;
51
+ box-decoration-break: clone;
52
+ padding: 2px 4px;
53
+ border-radius: 0.2rem;
54
+ font-weight: 400;
55
+ font-size: 0.8rem;
56
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
57
+ }
58
+
59
+ /* Fenced code blocks in a README (markdown-it output — no language-
60
+ class on the <pre> itself, unlike Prism's blog-post output, which is
61
+ how this targets only the README case) get a subtle border + corner
62
+ radius, matching the framed-code-block look. */
63
+ pre:not([class*="language-"]) {
64
+ border: 1px solid var(--color-box-bg);
65
+ border-radius: 0.4rem;
66
+ }
67
+
52
- .markdown-body blockquote {
68
+ blockquote {
53
- font-size: 1.2rem;
69
+ font-size: 1.2rem;
54
- font-weight: 600;
70
+ font-weight: 600;
55
- background: var(--color-highlight-bg);
71
+ background: var(--color-highlight-bg);
56
- padding: 0.4rem;
72
+ padding: 0.4rem;
57
- margin: 16px 0px;
73
+ margin: 16px 0px;
58
- }
59
74
 
60
- .markdown-body blockquote p {
75
+ p {
61
- padding: 0;
76
+ padding: 0;
77
+ }
78
+ }
62
79
  }
assets/css/repos.css CHANGED
@@ -6,134 +6,116 @@
6
6
 
7
7
  Font sizes on this page follow one small scale instead of one-off
8
8
  values, so nothing reads as randomly bigger/smaller than its neighbour:
9
- 0.8rem — code/monospace content (pre/code-view, file tree, blame)
10
9
  0.85rem — small badges (tags, pull-right meta, file-size hints)
11
- 0.9rem — nav links and monospace sub-headings (h3, file title)
10
+ 0.9rem — code/monospace content, nav links and sub-headings alike
11
+ (pre/code-view, file tree, blame, h3, file title)
12
12
  0.95rem — box body text (.repo, .event)
13
13
  1.6rem — page-level heading (.repo h1)
14
14
  All of them are rem-based, not em, so nesting depth can't change the
15
15
  rendered size — that's what made the old code-view/blame font sizes
16
- inconsistent (em compounds with whatever ancestor happens to wrap it). */
16
+ inconsistent (em compounds with whatever ancestor happens to wrap it).
17
+
18
+ Everything below uses plain CSS nesting rather than @scope: .repo,
19
+ .file-layout, .event, .file-explorer and table.blame are all real,
20
+ always-present classes/tags on these pages (none of them are wrapped
21
+ in a custom element anymore), so nesting under them scopes their
22
+ descendants exactly the way @scope would, without needing a scope root
23
+ selector that has to be kept in sync with whatever markup happens to
24
+ wrap it. */
17
25
 
18
26
  .repo {
19
27
  text-overflow: ellipsis;
20
28
  overflow: hidden;
21
- padding: 1rem;
29
+ padding: 0.5rem;
22
30
  background: var(--color-box-bg);
23
31
  color: var(--color-text);
24
32
  font-size: 0.95rem;
25
- border-radius: 6px;
26
- }
27
-
28
- .repo a {
29
- color: var(--color-link);
30
- }
31
-
32
- .repo .header {
33
- display: flex;
34
- flex-direction: row;
35
- align-items: baseline;
36
- }
37
33
 
38
- @media (max-width: 720px) {
39
- .repo .header {
34
+ a {
40
- flex-direction: column;
35
+ color: var(--color-link);
41
- align-items: start;
42
36
  }
43
- }
44
37
 
45
- .repo h1 {
38
+ .header {
39
+ display: flex;
46
- font-size: 1.6rem;
40
+ flex-direction: row;
47
- font-weight: 500;
48
- line-height: 1;
49
- margin-bottom: 0.1rem;
41
+ align-items: baseline;
50
- flex: 1;
51
- }
52
42
 
53
- .repo h1 a {
43
+ @media (max-width: 720px) {
54
- color: var(--color-link);
44
+ flex-direction: column;
45
+ align-items: start;
55
- }
46
+ }
56
-
57
- .repo .tags {
58
- margin-top: 0.5rem;
59
- display: flex;
60
- flex-wrap: wrap;
61
- gap: 0.35rem;
62
- }
47
+ }
63
-
64
- .repo .tag {
65
- color: var(--color-tag);
66
- background: var(--color-pre-bg);
67
- padding: 0.15rem 0.55rem;
68
- border-radius: 999px;
69
- font-size: 0.85rem;
70
- font-weight: 500;
71
- }
72
-
73
- .repo h3 {
74
- font-size: 0.9rem;
75
- font-weight: 500;
76
- margin-top: 0.75rem;
77
- font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
78
- background: var(--color-pre-bg);
79
- color: var(--color-code-fg);
80
- padding: 0.5rem 0.75rem;
81
- border-radius: 4px;
82
- overflow-x: auto;
83
- white-space: nowrap;
84
- }
85
48
 
86
- .repo .headerExtension h2 {
49
+ h1 {
87
- display: flex;
50
+ font-size: 1.6rem;
51
+ font-weight: 500;
52
+ line-height: 1;
53
+ margin-bottom: 0.1rem;
88
- flex: 1;
54
+ flex: 1;
89
- padding-top: 0.6rem;
90
- font-size: 0.95rem;
91
- font-weight: 400;
92
- }
55
+ }
93
56
 
94
- .repo hr {
57
+ .tags {
95
- margin-top: 0.75rem;
58
+ margin-top: 0.5rem;
96
- border: none;
97
- border-top: 1px solid var(--color-text);
98
- opacity: 0.15;
59
+ display: flex;
60
+ flex-wrap: wrap;
61
+ gap: 0.25rem;
99
- }
62
+ }
100
63
 
101
- .repo .nav {
64
+ .tag {
65
+ color: var(--color-tag);
66
+ padding: 0.15rem 0.5rem 0.5rem 0;
102
- display: flex;
67
+ border-radius: 4px;
103
- align-items: center;
104
- gap: 0.25rem;
105
- margin-top: 0.75rem;
68
+ font-size: 0.95rem;
69
+ font-weight: 500;
106
- }
70
+ }
107
71
 
108
- .repo .nav span {
72
+ h3 {
109
- display: none;
73
+ font-size: 1rem;
74
+ font-weight: 500;
110
- }
75
+ }
111
76
 
112
- .repo .nav a {
77
+ .headerExtension h2 {
78
+ display: flex;
79
+ flex: 1;
80
+ padding-top: 0.3rem;
113
- font-size: 0.9rem;
81
+ font-size: 0.95rem;
114
- font-weight: 500;
82
+ font-weight: 400;
115
- padding: 0.4rem 0.1rem;
116
- cursor: pointer;
117
- color: var(--color-text);
118
- border-bottom: 2px solid transparent;
119
- margin-right: 1rem;
120
- }
83
+ }
121
84
 
85
+ hr {
122
- .repo .nav a:hover {
86
+ margin-top: 0.5rem;
87
+ border: none;
123
- color: var(--color-link);
88
+ border-top: 1px solid var(--color-text);
89
+ opacity: 0.15;
124
- }
90
+ }
125
91
 
92
+ .nav {
93
+ display: flex;
94
+ align-items: center;
95
+ margin-top: 0.5rem;
96
+
97
+ span {
98
+ display: none;
99
+ }
100
+
101
+ a {
102
+ font-size: 1rem;
103
+ font-weight: 500;
126
- .repo .nav a.active-tab {
104
+ padding: 0 0.5rem 0 0;
105
+ cursor: pointer;
127
- color: var(--color-link);
106
+ color: var(--color-link);
128
- border-bottom-color: var(--color-link);
129
- text-decoration: none;
107
+ text-decoration: underline;
108
+ }
109
+ }
130
110
  }
131
111
 
132
112
  #repo-content {
133
- margin-top: 1rem;
113
+ margin-top: 0.25rem;
134
114
  }
135
115
 
136
- /* Commit list / commit detail header (Commit.astro) */
116
+ /* Commit list / commit detail header (Commit.astro) — repos/commit.njk,
117
+ commits.njk, file-history.njk. pre.commit is nested here too since
118
+ it's always .event's child, never used alone. */
137
119
  .event {
138
120
  background-color: var(--color-box-bg);
139
121
  color: var(--color-text);
@@ -142,27 +124,26 @@
142
124
  padding: 0.6rem 0.75rem;
143
125
  margin: 0.5rem 0;
144
126
  font-size: 0.95rem;
145
- border-radius: 4px;
146
- }
147
127
 
148
- .event a {
128
+ a {
149
- color: var(--color-link);
129
+ color: var(--color-link);
150
- }
130
+ }
151
131
 
152
- .event strong {
132
+ strong {
153
- font-weight: 500;
133
+ font-weight: 500;
154
- }
134
+ }
155
135
 
156
- .event .pull-right {
136
+ .pull-right {
157
- font-size: 0.85rem;
137
+ font-size: 0.85rem;
158
- font-weight: 400;
138
+ font-weight: 400;
159
- float: right;
139
+ float: right;
160
- }
140
+ }
161
141
 
162
- pre.commit {
142
+ pre.commit {
163
- font-family: inherit;
143
+ font-family: inherit;
164
- white-space: pre-wrap;
144
+ white-space: pre-wrap;
165
- margin-top: 0.35rem;
145
+ margin-top: 0.35rem;
146
+ }
166
147
  }
167
148
 
168
149
  /* Two-pane IDE-style file explorer: a persistent tree sidebar + a content
@@ -171,8 +152,9 @@ pre.commit {
171
152
  they don't also need to differ in colour. The tree lives outside
172
153
  #file-pane, so htmx swaps of #file-pane (clicking a file) or
173
154
  #file-content (Contents/History/Blame) never touch the sidebar — it
174
- just stays where it is, already expanded, no reload. */
155
+ just stays where it is, already expanded, no reload.
156
+
175
- /* Fixed-height explorer: both panes stretch to the same height and scroll
157
+ Fixed-height explorer: both panes stretch to the same height and scroll
176
158
  independently, so scrolling a long file never scrolls the tree out of
177
159
  view (or the whole page) — closer to a real IDE split view than a page
178
160
  that just keeps growing with the file's content. */
@@ -181,139 +163,167 @@ pre.commit {
181
163
  align-items: stretch;
182
164
  gap: 1.25rem;
183
165
  height: 80vh;
184
- }
185
-
186
- .file-explorer-tree {
187
- width: 260px;
188
- flex-shrink: 0;
189
- height: 100%;
190
- overflow-y: auto;
191
- padding: 0.6rem;
192
- background: var(--color-box-bg);
193
- border-radius: 6px;
194
- font-size: 0.8rem;
195
- font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
196
- }
197
-
198
- .file-explorer-main {
199
- flex: 1;
200
- min-width: 0;
201
- height: 100%;
202
- display: flex;
203
- flex-direction: column;
204
- border: 1px solid var(--color-box-bg);
205
- border-radius: 6px;
206
- overflow: hidden;
207
- }
208
-
209
- .file-explorer-main:has(.file-explorer-placeholder) {
210
- align-items: center;
211
- justify-content: center;
212
- }
213
-
214
- .file-explorer-placeholder {
215
- color: var(--color-text);
216
- opacity: 0.6;
217
- padding: 1rem 0;
218
- }
219
-
220
- @media (max-width: 720px) {
221
- .file-explorer {
222
- flex-direction: column;
223
- height: 85vh;
224
- }
225
166
 
167
+ /* File tree: flat rows (no per-item card background), a rotating
168
+ triangle for expand/collapse instead of an emoji folder glyph, and a
169
+ left accent border + tint on the active file instead of a filled
170
+ pill. */
226
171
  .file-explorer-tree {
172
+ width: 260px;
173
+ flex-shrink: 0;
174
+ height: 100%;
175
+ overflow-y: auto;
176
+ padding: 0.6rem;
177
+ background: var(--color-box-bg);
178
+ border-radius: 6px;
179
+ font-size: 0.9rem;
180
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
181
+
182
+ ul {
183
+ list-style: none;
184
+ margin: 0 0 0 1.1rem;
185
+ padding: 0;
186
+ }
187
+
188
+ > ul {
189
+ margin-left: 0;
190
+ }
191
+
192
+ li {
193
+ margin: 0;
194
+
195
+ &.active-file,
196
+ & > a.active-file {
197
+ color: var(--color-link);
198
+ }
199
+ }
200
+
201
+ summary,
202
+ li > a {
203
+ display: flex;
204
+ align-items: center;
205
+ gap: 0.4rem;
206
+ padding: 0.2rem 0.35rem;
207
+ border-radius: 4px;
208
+ border-left: 2px solid transparent;
209
+ cursor: pointer;
210
+ text-decoration: none;
211
+ color: var(--color-text);
212
+ line-height: 1.6;
213
+ }
214
+
215
+ summary {
216
+ list-style: none;
217
+
218
+ &::-webkit-details-marker {
219
+ display: none;
220
+ }
221
+
222
+ &::before {
223
+ content: "";
224
+ flex-shrink: 0;
225
+ width: 0;
226
+ height: 0;
227
+ border-style: solid;
228
+ border-width: 0.3em 0 0.3em 0.4em;
229
+ border-color: transparent transparent transparent currentColor;
230
+ opacity: 0.5;
231
+ transition: transform 120ms ease;
232
+ }
233
+ }
234
+
235
+ details[open] > summary::before {
236
+ transform: rotate(90deg);
237
+ }
238
+
239
+ summary svg,
240
+ li > a svg {
241
+ flex-shrink: 0;
227
- width: 100%;
242
+ width: 1em;
228
- height: auto;
243
+ height: 1em;
244
+ }
245
+
246
+ summary:hover,
247
+ li > a:hover {
248
+ background-color: var(--color-pre-bg);
249
+ }
250
+
251
+ li > a.active-file {
252
+ color: var(--color-link);
253
+ background-color: var(--color-highlight-bg);
254
+ border-left-color: var(--color-link);
255
+ font-weight: 500;
256
+ }
257
+
258
+ .file-size {
259
+ margin-left: auto;
229
- flex: 0 0 35%;
260
+ opacity: 0.5;
261
+ font-size: 0.85rem;
262
+ }
230
263
  }
231
264
 
232
265
  .file-explorer-main {
266
+ flex: 1;
233
- width: 100%;
267
+ min-width: 0;
234
- height: auto;
268
+ height: 100%;
269
+ display: flex;
270
+ flex-direction: column;
271
+ border: 1px solid var(--color-box-bg);
272
+ border-radius: 6px;
273
+ overflow: hidden;
274
+
275
+ &:has(.file-explorer-placeholder) {
276
+ align-items: center;
277
+ justify-content: center;
278
+ }
279
+
280
+ /* #file-content padding/background: when it holds actual code (a
281
+ plain <pre>), let the box-bg colour fill the whole pane
282
+ edge-to-edge instead of floating as a small inset card — closer to
283
+ a real code viewer's continuous code surface. box-bg (not pre-bg)
284
+ so it reads as one calm surface with the file-layout header above
285
+ it, rather than the near-black pre-bg used for one-off code
286
+ snippets elsewhere on the site. */
287
+ #file-content {
235
- flex: 1 1 auto;
288
+ flex: 1;
289
+ overflow-y: auto;
290
+ padding: 0.85rem;
291
+
292
+ &:has(> pre) {
293
+ padding: 0;
294
+ background: var(--color-box-bg);
295
+
296
+ pre {
297
+ margin: 0;
298
+ background: transparent;
299
+ border-radius: 0;
300
+ }
301
+ }
302
+ }
236
303
  }
237
- }
238
-
239
- /* File tree: flat rows (no per-item card background), a rotating
240
- triangle for expand/collapse instead of an emoji folder glyph, and a
241
- left accent border + tint on the active file instead of a filled pill. */
242
- .file-explorer-tree ul {
243
- list-style: none;
244
- margin: 0 0 0 1.1rem;
245
- padding: 0;
246
- }
247
-
248
- .file-explorer-tree > ul {
249
- margin-left: 0;
250
- }
251
304
 
252
- .file-explorer-tree li {
305
+ .file-explorer-placeholder {
306
+ color: var(--color-text);
307
+ opacity: 0.6;
253
- margin: 0;
308
+ padding: 1rem 0;
254
- }
309
+ }
255
-
256
- .file-explorer-tree summary,
257
- .file-explorer-tree li > a {
258
- display: flex;
259
- align-items: center;
260
- gap: 0.4rem;
261
- padding: 0.2rem 0.35rem;
262
- border-radius: 4px;
263
- border-left: 2px solid transparent;
264
- cursor: pointer;
265
- text-decoration: none;
266
- color: var(--color-text);
267
- line-height: 1.6;
268
- }
269
-
270
- .file-explorer-tree summary {
271
- list-style: none;
272
- }
273
-
274
- .file-explorer-tree summary::-webkit-details-marker {
275
- display: none;
276
- }
277
-
278
- .file-explorer-tree summary::before {
279
- content: "";
280
- flex-shrink: 0;
281
- width: 0;
282
- height: 0;
283
- border-style: solid;
284
- border-width: 0.3em 0 0.3em 0.4em;
285
- border-color: transparent transparent transparent currentColor;
286
- opacity: 0.5;
287
- transition: transform 120ms ease;
288
- }
289
-
290
- .file-explorer-tree details[open] > summary::before {
291
- transform: rotate(90deg);
292
- }
293
-
294
- .file-explorer-tree summary svg,
295
- .file-explorer-tree li > a svg {
296
- flex-shrink: 0;
297
- width: 1em;
298
- height: 1em;
299
- }
300
-
301
- .file-explorer-tree summary:hover,
302
- .file-explorer-tree li > a:hover {
303
- background-color: var(--color-pre-bg);
304
- }
305
310
 
306
- .file-explorer-tree li > a.active-file {
307
- color: var(--color-link);
311
+ @media (max-width: 720px) {
308
- background-color: var(--color-highlight-bg);
309
- border-left-color: var(--color-link);
312
+ flex-direction: column;
310
- font-weight: 500;
313
+ height: 85vh;
311
- }
312
314
 
313
- .file-explorer-tree .file-size {
315
+ .file-explorer-tree {
316
+ width: 100%;
314
- margin-left: auto;
317
+ height: auto;
318
+ flex: 0 0 35%;
319
+ }
320
+
321
+ .file-explorer-main {
315
- opacity: 0.5;
322
+ width: 100%;
323
+ height: auto;
316
- font-size: 0.85rem;
324
+ flex: 1 1 auto;
325
+ }
326
+ }
317
327
  }
318
328
 
319
329
  /* File view header — a compact icon+filename strip with the
@@ -323,128 +333,104 @@ pre.commit {
323
333
  display: flex;
324
334
  align-items: center;
325
335
  justify-content: flex-start;
326
- flex-wrap: wrap;
327
336
  flex-shrink: 0;
328
337
  gap: 0.5rem;
329
338
  padding: 0.6rem 0.85rem;
330
339
  background: var(--color-box-bg);
331
340
  border-bottom: 1px solid var(--color-pre-bg);
332
- }
333
-
334
- .file-layout .title {
335
- display: flex;
336
- align-items: center;
337
- gap: 0.4rem;
338
- min-width: 0;
339
- font-size: 0.8rem;
340
- font-weight: 500;
341
- font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
342
- }
343
341
 
342
+ .title {
343
+ display: flex;
344
- .file-layout .title svg {
344
+ align-items: center;
345
+ gap: 0.4rem;
346
+ /* flex:1 (not the default 0 1 auto) so it's the one that gives up
347
+ space when the row is tight — without this, a long filename could
348
+ force the row to wrap .nav onto its own line instead of shrinking,
349
+ since the wrap decision is made off each item's unshrunk
350
+ hypothetical width, not its post-shrink width. */
351
+ flex: 1 1 auto;
352
+ min-width: 0;
353
+ font-size: 0.9rem;
354
+ font-weight: 500;
355
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
356
+
357
+ svg {
345
- flex-shrink: 0;
358
+ flex-shrink: 0;
346
- width: 1em;
359
+ width: 1em;
347
- height: 1em;
360
+ height: 1em;
348
- }
361
+ }
349
-
362
+
350
- .file-layout .title span {
363
+ span {
351
- overflow: hidden;
364
+ overflow: hidden;
352
- text-overflow: ellipsis;
365
+ text-overflow: ellipsis;
353
- white-space: nowrap;
366
+ white-space: nowrap;
354
- }
367
+ }
355
-
356
- .file-layout .nav {
357
- display: flex;
358
- align-items: center;
359
- gap: 0.25rem;
360
- /* Pin to the right edge of the header row regardless of wrap — with
361
- justify-content alone on the parent, a long filename forcing a wrap
362
- would leave the tabs left-aligned on their own line. */
363
- margin-left: auto;
364
- }
368
+ }
365
-
366
- .file-layout .nav div:not(:has(a)) {
367
- display: none;
368
- }
369
-
370
- .file-layout .nav a {
371
- font-size: 0.9rem;
372
- font-weight: 500;
373
- padding: 0.3rem 0.1rem;
374
- margin-right: 0.75rem;
375
- cursor: pointer;
376
- color: var(--color-text);
377
- border-bottom: 2px solid transparent;
378
- }
379
-
380
- .file-layout .nav a:hover {
381
- color: var(--color-link);
382
- }
383
-
384
- .file-layout .nav a[aria-current="true"] {
385
- color: var(--color-link);
386
- border-bottom-color: var(--color-link);
387
- }
388
-
389
- .file-explorer-main #file-content {
390
- flex: 1;
391
- overflow-y: auto;
392
- padding: 0.85rem;
393
- }
394
-
395
- /* Code panel background: when #file-content holds actual code (a plain
396
- <pre> or the code-view custom element wrapping one), let the box-bg
397
- colour fill the whole pane edge-to-edge instead of floating as a small
398
- inset card — closer to a real code viewer's continuous code surface.
399
- box-bg (not pre-bg) so it reads as one calm surface with the header
400
- above it, rather than the near-black pre-bg used for one-off code
401
- snippets elsewhere on the site. */
402
- .file-explorer-main #file-content:has(> pre),
403
- .file-explorer-main #file-content:has(> code-view) {
404
- padding: 0;
405
- background: var(--color-box-bg);
406
- }
407
369
 
408
- .file-explorer-main #file-content:has(> pre) pre,
409
- .file-explorer-main #file-content:has(> code-view) pre {
370
+ .nav {
410
- margin: 0;
371
+ display: flex;
411
- background: transparent;
372
+ align-items: center;
373
+ gap: 0.25rem;
412
- border-radius: 0;
374
+ flex-shrink: 0;
375
+
376
+ div:not(:has(a)) {
377
+ display: none;
378
+ }
379
+
380
+ a {
381
+ font-size: 0.9rem;
382
+ font-weight: 500;
383
+ padding: 0.3rem 0.1rem;
384
+ margin-right: 0.75rem;
385
+ cursor: pointer;
386
+ color: var(--color-text);
387
+ border-bottom: 2px solid transparent;
388
+
389
+ &:hover {
390
+ color: var(--color-link);
391
+ }
392
+
393
+ &[aria-current="true"] {
394
+ color: var(--color-link);
395
+ border-bottom-color: var(--color-link);
396
+ }
397
+ }
398
+ }
413
399
  }
414
400
 
415
- /* Blame table */
401
+ /* Blame table — repos/file-blame.njk. */
416
402
  table.blame {
417
403
  width: 100%;
418
404
  border-collapse: collapse;
419
- font-size: 0.8rem;
405
+ font-size: 0.9rem;
420
406
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
421
407
  background: var(--color-pre-bg);
422
408
  border-radius: 4px;
423
409
  overflow: hidden;
424
- }
425
410
 
426
- table.blame td {
411
+ td {
427
- vertical-align: top;
412
+ vertical-align: top;
428
- padding: 0.1rem 0.5rem;
413
+ padding: 0.1rem 0.5rem;
429
- border-bottom: 1px solid var(--color-box-bg);
414
+ border-bottom: 1px solid var(--color-box-bg);
430
- }
415
+ }
431
416
 
432
- table.blame .blame-meta {
417
+ .blame-meta {
433
- white-space: nowrap;
418
+ white-space: nowrap;
434
- color: var(--color-link);
419
+ color: var(--color-link);
435
- width: 1%;
420
+ width: 1%;
436
- }
421
+ }
437
422
 
438
- table.blame .blame-line-no {
423
+ .blame-line-no {
439
- white-space: nowrap;
424
+ white-space: nowrap;
440
- opacity: 0.5;
425
+ opacity: 0.5;
441
- text-align: right;
426
+ text-align: right;
442
- width: 1%;
427
+ width: 1%;
443
- }
428
+ }
444
429
 
445
- table.blame .blame-content pre {
430
+ .blame-content pre {
446
- margin: 0;
431
+ margin: 0;
447
- white-space: pre-wrap;
432
+ white-space: pre-wrap;
433
+ }
448
434
  }
449
435
 
450
436
  /* diff2html ships its own light/dark token sets as plain CSS custom
@@ -452,7 +438,9 @@ table.blame .blame-content pre {
452
438
  applies the dark set under a `.d2h-dark-color-scheme` class we never
453
439
  add. Remapping the base tokens it actually reads to their `--d2h-dark-*`
454
440
  counterparts under our own [data-theme="dark"] gets the same result
455
- without needing that class or a second copy of every diff2html rule. */
441
+ without needing that class or a second copy of every diff2html rule.
442
+ This re-themes a third-party library's own classes rather than styling
443
+ one of our own, so it's left flat/global rather than nested. */
456
444
  :root[data-theme="dark"] {
457
445
  --d2h-bg-color: var(--d2h-dark-bg-color);
458
446
  --d2h-border-color: var(--d2h-dark-border-color);
assets/css/shared.css CHANGED
@@ -1,6 +1,6 @@
1
1
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
2
2
  html { -webkit-text-size-adjust: 100%; }
3
- body { line-height: 1.5; -webkit-font-smoothing: antialiased; }
3
+ body { line-height: 1.5; }
4
4
  img, picture, video, canvas, svg { display: block; max-width: 100%; }
5
5
  input, button, textarea, select { font: inherit; }
6
6
  p, h1, h2, h3, h4, h5, h6 { overflow-wrap: break-word; }
@@ -17,7 +17,10 @@ table { border-collapse: collapse; }
17
17
  browser handles it directly off `@view-transition { navigation: auto }`
18
18
  above — this block just replaces the default cross-fade with a slightly
19
19
  nicer slide+fade, and keeps header/footer stable instead of fading with
20
- the rest of the page. */
20
+ the rest of the page. The named ::view-transition-group(...) pair below
21
+ operates on the browser's top-layer snapshot tree, not the regular DOM,
22
+ so unlike the view-transition-name declarations themselves (nested
23
+ inside header/footer below) it can't be scoped and has to stay global. */
21
24
  ::view-transition-old(root),
22
25
  ::view-transition-new(root) {
23
26
  animation-duration: 220ms;
@@ -48,17 +51,6 @@ table { border-collapse: collapse; }
48
51
  from { transform: translateY(8px); }
49
52
  }
50
53
 
51
- /* view-transition-name needs an element with a real box, and site-header/
52
- site-footer are `display: contents` (no box of their own) — so this
53
- targets the actual <header>/<footer> element nested inside them. */
54
- site-header header {
55
- view-transition-name: site-header;
56
- }
57
-
58
- site-footer footer {
59
- view-transition-name: site-footer;
60
- }
61
-
62
54
  ::view-transition-group(site-header),
63
55
  ::view-transition-group(site-footer) {
64
56
  animation: none;
@@ -81,7 +73,7 @@ site-footer footer {
81
73
  --color-link: light-dark(hsl(211, 100%, 45%), hsl(211, 100%, 60%));
82
74
  --color-post-link: light-dark(hsl(0, 0%, 0%), hsl(211, 100%, 60%));
83
75
  --color-highlight-bg: light-dark(hsl(183, 80%, 88%), hsl(183, 80%, 18%));
84
- --color-code-fg: light-dark(hsl(348, 86%, 43%), hsl(348, 86%, 60%));
76
+ --color-code-fg: light-dark(hsl(348, 86%, 43%), hsl(348, 86%, 80%));
85
77
  --color-code-bg: light-dark(hsl(0, 0%, 95%), hsl(0, 0%, 0%));
86
78
  --color-pre-bg: light-dark(hsl(0, 0%, 95%), hsl(0, 0%, 0%));
87
79
  --color-tag: light-dark(hsl(152, 96%, 35%), hsl(152, 96%, 44%));
@@ -119,6 +111,16 @@ site-footer footer {
119
111
  }
120
112
  }
121
113
 
114
+ :root[data-theme="dark"] {
115
+ --btn-light: block;
116
+ --btn-dark: none;
117
+ }
118
+
119
+ :root[data-theme="light"] {
120
+ --btn-light: none;
121
+ --btn-dark: block;
122
+ }
123
+
122
124
  [data-tooltip] {
123
125
  position: relative;
124
126
  cursor: help;
@@ -172,135 +174,156 @@ main {
172
174
  padding-bottom: 1em;
173
175
  }
174
176
 
175
- site-header,
176
- site-footer,
177
- theme-switcher,
178
- repo-layout,
179
- file-layout,
180
- markdown-view,
181
- code-view {
177
+ .wrapper {
178
+ flex: 1;
179
+ width: 100%;
180
+ max-width: 960px;
181
+ margin-right: auto;
182
- display: contents;
182
+ margin-left: auto;
183
+ padding-left: 1em;
184
+ padding-right: 1em;
183
185
  }
184
186
 
187
+ /* site-header.njk/site-footer.njk are plain includes (no wrapping custom
188
+ element), so these are just nested under the real <header>/<footer>
189
+ tag — plain CSS nesting scopes .logo/.links etc to it same as any other
190
+ descendant selector would, no @scope needed since there's only ever
191
+ one header and one footer on the page. */
185
192
  header {
186
193
  background: #131618;
187
- }
188
-
189
- .logo {
190
- display: flex;
191
- font-size: 22px;
192
- line-height: 56px;
193
- font-weight: 400;
194
- margin-bottom: 0;
195
- float: left;
196
- color: var(--color-accent);
197
- margin-right: 8px;
198
-
199
- &.active,
200
- &:hover {
201
- color: var(--color-accent);
202
- text-decoration: underline;
194
+ view-transition-name: site-header;
203
- text-decoration-color: var(--color-accent);
204
- text-underline-offset: 8px;
205
- }
206
- }
207
-
208
- .links {
209
- display: flex;
210
- flex-direction: row;
211
- float: right;
212
- font-size: 1.4rem;
213
- line-height: 56px;
214
- color: white;
215
195
 
216
- #theme-switch {
196
+ .logo {
217
197
  display: flex;
218
- align-items: center;
219
- font-size: 1.3rem;
220
-
221
- button {
222
- padding-right: 12px;
223
- cursor: pointer;
224
- margin-bottom: -2px;
225
- }
226
- }
227
-
228
- a {
229
- display: inline-block;
230
- text-decoration: none;
231
198
  font-size: 22px;
232
199
  line-height: 56px;
233
- color: white;
234
200
  font-weight: 400;
235
- padding-left: 12px;
201
+ margin-bottom: 0;
236
- padding-right: 12px;
202
+ float: left;
237
-
238
- &:last-child {
203
+ color: var(--color-accent);
239
- padding-right: 0px;
204
+ margin-right: 8px;
240
- }
241
205
 
242
206
  &.active,
243
207
  &:hover {
244
- color: var(--color-gray-200);
208
+ color: var(--color-accent);
245
209
  text-decoration: underline;
246
210
  text-decoration-color: var(--color-accent);
247
211
  text-underline-offset: 8px;
248
212
  }
249
213
  }
214
+
215
+ .links {
216
+ display: flex;
217
+ flex-direction: row;
218
+ float: right;
219
+ font-size: 1.4rem;
220
+ line-height: 56px;
221
+ color: white;
222
+
223
+ #theme-switch {
224
+ display: flex;
225
+ align-items: center;
226
+ font-size: 1.3rem;
227
+
228
+ button {
229
+ background: none;
230
+ border: none;
231
+ padding: 0;
232
+ padding-right: 12px;
233
+ margin-bottom: -2px;
234
+ color: inherit;
235
+ cursor: pointer;
236
+
237
+ &:hover,
238
+ &:focus,
239
+ &:active {
240
+ background: none;
241
+ border: none;
242
+ color: inherit;
243
+ }
244
+ }
245
+ }
246
+
247
+ a {
248
+ display: inline-block;
249
+ text-decoration: none;
250
+ font-size: 22px;
251
+ line-height: 56px;
252
+ color: white;
253
+ font-weight: 400;
254
+ padding-left: 12px;
255
+ padding-right: 12px;
256
+
257
+ &:last-child {
258
+ padding-right: 0px;
259
+ }
260
+
261
+ &.active,
262
+ &:hover {
263
+ color: var(--color-gray-200);
264
+ text-decoration: underline;
265
+ text-decoration-color: var(--color-accent);
266
+ text-underline-offset: 8px;
267
+ }
268
+ }
269
+ }
270
+ }
271
+
272
+ /* Still a real (the only) Elena/custom element left on the site — it has
273
+ no box of its own; the real box is the <button> it renders. */
274
+ theme-switcher {
275
+ display: contents;
250
276
  }
251
277
 
252
278
  footer {
253
279
  background: #131618;
254
- }
280
+ view-transition-name: site-footer;
255
281
 
256
- .container {
282
+ .container {
257
- display: flex;
283
+ display: flex;
258
- flex-direction: row;
284
+ flex-direction: row;
259
- flex: 1;
285
+ flex: 1;
260
- padding: 16px 0px;
286
+ padding: 16px 0px;
261
- }
287
+ }
262
288
 
263
- .spacer {
289
+ .spacer {
264
- flex: 1;
290
+ flex: 1;
265
- }
291
+ }
266
292
 
267
- .copyright-container {
293
+ .copyright-container {
268
- display: flex;
294
+ display: flex;
269
- align-items: center;
295
+ align-items: center;
270
- justify-content: center;
296
+ justify-content: center;
271
- margin-left: 0.5rem;
297
+ margin-left: 0.5rem;
272
- margin-right: 0.5rem;
298
+ margin-right: 0.5rem;
273
- color: var(--color-white);
299
+ color: var(--color-white);
274
- font-size: 1rem;
300
+ font-size: 1rem;
275
- }
276
-
277
- .link {
278
- font-size: 1rem;
279
- line-height: 1.5rem;
280
- margin-left: 0.5rem;
281
- color: var(--color-white);
282
- font-weight: 500;
283
- text-underline-offset: 2px;
284
- text-decoration-thickness: 2px;
285
- text-decoration-line: underline;
286
- text-decoration-color: white;
287
-
288
- &:hover {
289
- color: var(--color-gray-200);
290
- text-decoration-color: var(--color-gray-200);
291
301
  }
292
- }
293
302
 
303
+ .link {
304
+ font-size: 1rem;
305
+ line-height: 1.5rem;
306
+ margin-left: 0.5rem;
294
- :root[data-theme="dark"] {
307
+ color: var(--color-white);
295
- --btn-light: block;
308
+ font-weight: 500;
309
+ text-underline-offset: 2px;
310
+ text-decoration-thickness: 2px;
296
- --btn-dark: none;
311
+ text-decoration-line: underline;
297
- }
312
+ text-decoration-color: white;
298
313
 
314
+ &:hover {
315
+ color: var(--color-gray-200);
299
- :root[data-theme="light"] {
316
+ text-decoration-color: var(--color-gray-200);
300
- --btn-light: none;
317
+ }
301
- --btn-dark: block;
318
+ }
302
319
  }
303
320
 
321
+ /* Syntax highlighting theme: shared by code-view's own file-viewer pre and
322
+ by fenced code blocks inside markdown-view's article content (both
323
+ Prism-highlighted blog-post code and plain markdown-it README code
324
+ fences). It's genuinely cross-cutting rather than owned by one
325
+ component — scoping it to either would mean duplicating the whole token
326
+ palette in the other — so it stays global. */
304
327
  pre[class*="language-"],
305
328
  code[class*="language-"] {
306
329
  background: var(--color-pre-bg);
@@ -314,10 +337,21 @@ pre.code-view,
314
337
  margin: 1rem 0;
315
338
  overflow-x: auto;
316
339
  border-radius: 4px;
317
- font-size: 0.8rem;
318
340
  line-height: 1.5;
319
341
  }
320
342
 
343
+ /* File-viewer code (pre.code-view) matches the rest of that layout's
344
+ 0.9rem scale (see repos.css); blog/README fenced code blocks stay at
345
+ 0.8rem — they're a different context and weren't asked to change. */
346
+ pre.code-view {
347
+ font-size: 0.9rem;
348
+ }
349
+
350
+ pre[class*="language-"],
351
+ .markdown-body pre {
352
+ font-size: 0.8rem;
353
+ }
354
+
321
355
  pre.code-view,
322
356
  .markdown-body pre {
323
357
  background: var(--color-pre-bg);
@@ -379,18 +413,3 @@ code[class*="language-"] {
379
413
  .token.property {
380
414
  color: var(--token-attr-name);
381
415
  }
382
-
383
- .active-tab {
384
- text-underline-offset: 4px;
385
- text-decoration: underline;
386
- }
387
-
388
- .wrapper {
389
- flex: 1;
390
- width: 100%;
391
- max-width: 960px;
392
- margin-right: auto;
393
- margin-left: auto;
394
- padding-left: 1em;
395
- padding-right: 1em;
396
- }
assets/css/workers/cv.css CHANGED
@@ -1,3 +1,9 @@
1
+ /* Nested under .cv-page — this ships in the site-wide bundle.css, and a
2
+ bare h1/.pdf selector would otherwise leak onto every other page's
3
+ h1/.pdf, the same mistake posts-index.css made before it got dropped
4
+ from the merge entirely (see eleventy.config.js's CSS_FILES_TO_MERGE
5
+ comment). */
6
+ .cv-page {
1
7
  h1 {
2
8
  display: flex;
3
9
  align-items: flex-start;
@@ -5,7 +11,7 @@
5
11
  font-size: 2rem;
6
12
  margin-bottom: 1rem;
7
13
 
8
- a {
14
+ a {
9
15
  margin-left: 0.4rem;
10
16
  margin-top: 0.8rem;
11
17
  color: var(--color-link);
@@ -14,4 +20,5 @@ a {
14
20
 
15
21
  .pdf {
16
22
  border: 1px solid black;
17
- }
23
+ }
24
+ }
assets/css/workers/home.css CHANGED
@@ -1,218 +1,223 @@
1
- .description {
2
- font-size: 1.1rem;
1
+ /* Homepage-only styling, nested under .pageContainer (the page's own
3
-
4
- @media (max-width: 720px) {
2
+ top-level wrapper — see src/index.njk) now that this file ships in the
5
- font-size: 1rem;
6
- }
7
- }
8
-
9
- h2 {
10
- font-size: 1.4rem;
11
- font-weight: 600;
12
-
13
- @media (max-width: 720px) {
14
- font-size: 1.15rem;
15
- }
16
- }
17
-
18
- section {
19
- display: flex;
20
- flex-direction: column;
21
- margin-top: 1.5rem;
22
- margin-bottom: 1rem;
23
-
24
- @media (max-width: 720px) {
25
- margin-top: 1.5rem;
26
- margin-bottom: 0rem;
27
- }
28
- }
29
-
30
- .rounded-md {
31
- border-radius: 0.375rem;
32
- }
33
-
34
- .repos {
35
- display: grid;
36
- grid-template-columns: auto auto;
3
+ site-wide bundle.css instead of being the only stylesheet loaded on
37
- gap: 1rem;
38
- margin-top: 0.5rem;
39
-
40
- @media (max-width: 720px) {
4
+ this page. Without this nesting, the bare `h2`/`section` selectors
41
- gap: 1rem;
42
- grid-template-columns: auto;
5
+ below would apply to every h2/section on every other page too. */
43
- }
44
- }
45
-
46
6
  .pageContainer {
47
7
  margin-left: auto;
48
8
  margin-right: auto;
49
9
  margin-bottom: 2.5rem;
50
- }
51
10
 
52
- .title {
11
+ .description {
53
- font-size: 1.875rem;
12
+ font-size: 1.1rem;
54
- line-height: 2.25rem;
55
- font-weight: 700;
56
- margin-bottom: 1rem;
57
13
 
58
- @media (max-width: 720px) {
14
+ @media (max-width: 720px) {
59
- font-size: 1.6rem;
15
+ font-size: 1rem;
16
+ }
60
17
  }
61
- }
62
18
 
63
- .firstSection {
19
+ h2 {
64
- display: flex;
65
- flex-direction: column;
20
+ font-size: 1.4rem;
66
- }
21
+ font-weight: 600;
67
22
 
68
- .firstPara {
69
- display: flex;
23
+ @media (max-width: 720px) {
70
- flex-direction: row;
24
+ font-size: 1.15rem;
71
- }
25
+ }
26
+ }
72
27
 
28
+ section {
73
- .linkUnderline {
29
+ display: flex;
74
- text-decoration-line: underline;
30
+ flex-direction: column;
75
- }
31
+ margin-top: 1.5rem;
32
+ margin-bottom: 1rem;
76
33
 
77
- .gridContainer {
34
+ @media (max-width: 720px) {
78
- display: grid;
79
- grid-template-columns: repeat(1, minmax(0, 1fr));
80
- gap: 1rem;
35
+ margin-top: 1.5rem;
36
+ margin-bottom: 0rem;
37
+ }
38
+ }
81
39
 
40
+ .rounded-md {
82
- @media (max-width: 720px) {
41
+ border-radius: 0.375rem;
83
- gap: 0rem;
84
42
  }
85
43
 
44
+ .repos {
86
- @media (min-width: 640px) {
45
+ display: grid;
87
- grid-template-columns: repeat(2, minmax(0, 1fr));
46
+ grid-template-columns: auto auto;
88
- gap: 2rem;
47
+ gap: 1rem;
48
+ margin-top: 0.5rem;
49
+
50
+ @media (max-width: 720px) {
51
+ gap: 1rem;
52
+ grid-template-columns: auto;
53
+ }
89
54
  }
90
- }
91
55
 
92
- .repo {
56
+ .title {
93
- text-overflow: ellipsis;
94
- overflow: hidden;
95
- padding: 0.5rem;
96
- background: var(--color-box-bg);
97
- color: var(--color-text);
98
- font-size: 0.95rem;
57
+ font-size: 1.875rem;
58
+ line-height: 2.25rem;
59
+ font-weight: 700;
60
+ margin-bottom: 1rem;
99
61
 
100
- a {
101
- color: var(--color-link);
62
+ @media (max-width: 720px) {
63
+ font-size: 1.6rem;
64
+ }
102
65
  }
103
66
 
104
- h4 {
105
- font-size: 1.05rem;
106
- font-weight: 500;
67
+ .firstSection {
107
- line-height: 1.2;
68
+ display: flex;
108
- border-bottom: 1px solid #495057;
109
- text-overflow: ellipsis;
69
+ flex-direction: column;
110
- white-space: nowrap;
111
- overflow: hidden;
112
- margin-bottom: 0;
113
- padding: 0;
114
70
  }
115
71
 
116
- .tags {
72
+ .firstPara {
117
- margin-top: 0.5rem;
118
73
  display: flex;
119
- flex-wrap: wrap;
74
+ flex-direction: row;
120
- gap: 0.25rem;
121
75
  }
122
76
 
123
- .tag {
124
- color: var(--color-tag);
77
+ .linkUnderline {
125
- padding: 0.15rem 0.5rem 0.5rem 0rem;
126
- border-radius: 0.25rem;
78
+ text-decoration-line: underline;
127
- font-size: 0.95rem;
128
- font-weight: 500;
129
79
  }
130
- }
131
80
 
132
- .interests {
81
+ .gridContainer {
133
- ul {
134
82
  display: grid;
83
+ grid-template-columns: repeat(1, minmax(0, 1fr));
84
+ gap: 1rem;
85
+
86
+ @media (max-width: 720px) {
135
- gap: 0.5rem;
87
+ gap: 0rem;
136
- grid-template-columns: auto auto auto;
137
- text-align: center;
138
- margin-top: 0.5rem;
139
- font-size: 1.1rem;
88
+ }
140
89
 
141
- li {
90
+ @media (min-width: 640px) {
142
- background-color: var(--color-box-bg);
91
+ grid-template-columns: repeat(2, minmax(0, 1fr));
143
- padding: 0.25rem;
92
+ gap: 2rem;
144
93
  }
145
94
  }
146
- }
147
95
 
148
- .contact {
96
+ .repo {
149
- ul {
97
+ text-overflow: ellipsis;
150
- display: grid;
98
+ overflow: hidden;
151
- gap: 0.5rem;
152
- grid-template-columns: auto;
153
- margin-top: 0.5rem;
99
+ padding: 0.5rem;
100
+ background: var(--color-box-bg);
101
+ color: var(--color-text);
102
+ font-size: 0.95rem;
154
103
 
155
- li {
104
+ a {
156
- display: flex;
157
- flex-direction: row;
158
- align-items: baseline;
159
- padding: 0.25rem;
160
- background-color: var(--color-box-bg);
105
+ color: var(--color-link);
161
- font-size: 1.1rem;
106
+ }
162
107
 
163
- @media (max-width: 720px) {
108
+ h4 {
164
- font-size: 1rem;
109
+ font-size: 1.05rem;
110
+ font-weight: 500;
111
+ line-height: 1.2;
112
+ border-bottom: 1px solid #495057;
165
- flex-direction: column;
113
+ text-overflow: ellipsis;
114
+ white-space: nowrap;
115
+ overflow: hidden;
116
+ margin-bottom: 0;
166
- }
117
+ padding: 0;
167
118
  }
168
119
 
169
- a {
120
+ .tags {
170
- color: var(--color-link);
121
+ margin-top: 0.5rem;
122
+ display: flex;
123
+ flex-wrap: wrap;
124
+ gap: 0.25rem;
171
125
  }
172
126
 
173
- strong {
127
+ .tag {
128
+ color: var(--color-tag);
129
+ padding: 0.15rem 0.5rem 0.5rem 0rem;
130
+ border-radius: 0.25rem;
131
+ font-size: 0.95rem;
174
132
  font-weight: 500;
175
- margin-left: 0.25rem;
176
- margin-right: 0.5rem;
177
133
  }
178
134
  }
179
- }
180
135
 
181
- .tools {
136
+ .interests {
182
- img {
137
+ ul {
183
- width: 36px;
138
+ display: grid;
139
+ gap: 0.5rem;
140
+ grid-template-columns: auto auto auto;
184
- height: 36px;
141
+ text-align: center;
142
+ margin-top: 0.5rem;
143
+ font-size: 1.1rem;
144
+
145
+ li {
146
+ background-color: var(--color-box-bg);
147
+ padding: 0.25rem;
148
+ }
149
+ }
185
150
  }
186
151
 
152
+ .contact {
187
- ul {
153
+ ul {
188
- display: grid;
154
+ display: grid;
189
- gap: 0.5rem;
155
+ gap: 0.5rem;
190
- grid-template-columns: auto auto auto auto;
156
+ grid-template-columns: auto;
191
- text-align: center;
192
- margin-top: 0.5rem;
157
+ margin-top: 0.5rem;
193
158
 
159
+ li {
160
+ display: flex;
161
+ flex-direction: row;
162
+ align-items: baseline;
163
+ padding: 0.25rem;
164
+ background-color: var(--color-box-bg);
165
+ font-size: 1.1rem;
166
+
194
- @media (max-width: 720px) {
167
+ @media (max-width: 720px) {
168
+ font-size: 1rem;
195
- grid-template-columns: auto auto auto;
169
+ flex-direction: column;
170
+ }
171
+ }
172
+
173
+ a {
174
+ color: var(--color-link);
175
+ }
176
+
177
+ strong {
178
+ font-weight: 500;
179
+ margin-left: 0.25rem;
180
+ margin-right: 0.5rem;
181
+ }
196
182
  }
183
+ }
197
184
 
185
+ .tools {
198
- li {
186
+ img {
199
- display: flex;
187
+ width: 36px;
200
- align-items: center;
188
+ height: 36px;
201
- background-color: var(--color-box-bg);
202
- padding: 0.25rem;
203
- font-size: 1rem;
189
+ }
204
190
 
205
- a {
191
+ ul {
192
+ display: grid;
193
+ gap: 0.5rem;
194
+ grid-template-columns: auto auto auto auto;
195
+ text-align: center;
196
+ margin-top: 0.5rem;
197
+
198
+ @media (max-width: 720px) {
199
+ grid-template-columns: auto auto auto;
200
+ }
201
+
202
+ li {
206
203
  display: flex;
207
- flex: 1;
208
- flex-direction: row;
209
204
  align-items: center;
210
- text-align: left;
211
- margin-left: 0.25rem;
205
+ background-color: var(--color-box-bg);
212
- margin-right: 0.25rem;
206
+ padding: 0.25rem;
207
+ font-size: 1rem;
213
208
 
214
- div {
209
+ a {
210
+ display: flex;
215
211
  flex: 1;
212
+ flex-direction: row;
213
+ align-items: center;
214
+ text-align: left;
215
+ margin-left: 0.25rem;
216
+ margin-right: 0.25rem;
217
+
218
+ div {
219
+ flex: 1;
220
+ }
216
221
  }
217
222
  }
218
223
  }
eleventy.config.js CHANGED
@@ -1,23 +1,54 @@
1
+ import { readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
1
3
  import syntaxHighlight from "@11ty/eleventy-plugin-syntaxhighlight";
2
4
  import pluginRss from "@11ty/eleventy-plugin-rss";
3
5
  import { register, ssr } from "@elenajs/ssr";
4
- import CurrentYear from "./src/components/current-year.js";
5
6
  import ThemeSwitcher from "./src/components/theme-switcher.js";
6
- import SiteHeader from "./src/components/site-header.js";
7
- import SiteFooter from "./src/components/site-footer.js";
8
- import RepoLayout from "./src/components/repo-layout.js";
9
- import FileLayout from "./src/components/file-layout.js";
10
- import MarkdownView from "./src/components/markdown-view.js";
11
- import CodeView from "./src/components/code-view.js";
12
7
  import { fileIcon, folderIcon } from "./src/_helpers/fileIcons.js";
13
8
 
14
- // @elenajs/ssr's expansion pass didn't recursively re-walk a component's
15
- // own render() output, so a component nesting another custom element (e.g.
9
+ // theme-switcher is the only Elena component left everything else
10
+ // (site-header, site-footer, repo-layout, file-layout, markdown-view,
11
+ // code-view, the cv page) was SSR-only markup-with-props, which plain
12
+ // Nunjucks includes/macros already do natively (see src/_includes/
16
- // site-header rendering <theme-switcher>) never got that inner tag
13
+ // site-header.njk, repo-layout.njk, file-layout.njk). Keeping those as
14
+ // Elena components meant carrying the whole elena-ssr transform, the
17
- // expanded — patched in patches/@elenajs+ssr+*.patch (applied via
15
+ // patched fork (patches/@elenajs+ssr+*.patch), @scope workarounds for
18
- // postinstall), see that patch for details. With the fix in place,
16
+ // having no shadow root, and a custom <slot> patch for the one case
17
+ // (the page layout) that needed to pass through arbitrary children —
18
+ // none of which pulls its weight once htmx is already doing the actual
19
+ // dynamic navigation. theme-switcher earns its keep because it's the one
20
+ // piece that's genuinely isomorphic: the same source SSRs the initial
21
+ // button and, loaded again client-side, drives the real click handler.
22
+ register(ThemeSwitcher);
23
+
24
+ // Every page used to pick and choose its own subset of these via `css`/
25
+ // `extraCss` frontmatter (e.g. repo pages loading repos.css + markdown.css
26
+ // + diff2html.min.css, the homepage loading just home.css). Now that
27
+ // everything below is @scope'd (see shared.css/repos.css/markdown.css/
28
+ // workers/home.css), there's no naming-collision reason to keep them
29
+ // apart, so they're concatenated into one cacheable stylesheet every page
30
+ // loads — fewer render-blocking requests than 2-4 separate <link>s.
31
+ //
19
- // site-header/site-footer can safely nest theme-switcher/current-year.
32
+ // issues-detail.css/issues-index.css/posts-index.css/only-bible-app*.css
33
+ // are deliberately left out — none of them are unscoped-safe. The
34
+ // issues-* pair is leftover from a page that never made it into this
20
- register(CurrentYear, ThemeSwitcher, SiteHeader, SiteFooter, RepoLayout, FileLayout, MarkdownView, CodeView);
35
+ // rewrite. The other three are fully superseded by inline styles on their
36
+ // own pages now (posts.njk, only-bible-app/*.njk have zero matching
37
+ // classes left) — merging them site-wide used to be harmless because
38
+ // each was the only stylesheet on its own page, but their unscoped bare-
39
+ // tag selectors (a plain `a {...}`, `h1 {...}`, etc., not `.some-class
40
+ // a`) would otherwise leak onto every other page's matching tags once
41
+ // bundled together, which is exactly what broke tools/interests links on
42
+ // the homepage — inherited from posts-index.css's `ul li a` rule matching
43
+ // any list of links, not just the post list it was meant for.
44
+ const CSS_FILES_TO_MERGE = [
45
+ "assets/css/shared.css",
46
+ "assets/css/repos.css",
47
+ "assets/css/markdown.css",
48
+ "assets/css/diff2html.min.css",
49
+ "assets/css/workers/home.css",
50
+ "assets/css/workers/cv.css",
51
+ ];
21
52
 
22
53
  export default function (eleventyConfig) {
23
54
  eleventyConfig.addPassthroughCopy({ assets: "assets" });
@@ -51,7 +82,6 @@ export default function (eleventyConfig) {
51
82
  eleventyConfig.addFilter("readableDate", (value) =>
52
83
  new Date(value).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" }),
53
84
  );
54
- eleventyConfig.addFilter("json", (value) => JSON.stringify(value));
55
85
  eleventyConfig.addFilter("fileIcon", fileIcon);
56
86
  eleventyConfig.addFilter("folderIcon", folderIcon);
57
87
  // A folder should start open only when the active file lives inside it —
@@ -68,19 +98,6 @@ export default function (eleventyConfig) {
68
98
  const value = bytes / 1024 ** i;
69
99
  return `${i === 0 ? value : value.toFixed(1)}${units[i]}`;
70
100
  });
71
- // Forces HTML-escaping on a value regardless of whether Eleventy/Nunjucks
72
- // already marked it "safe" upstream (e.g. rendered markdown/post content)
73
- // — needed to pass real HTML through a custom element attribute so it
74
- // round-trips correctly instead of breaking the tag it's embedded in.
75
- eleventyConfig.addFilter("attrEscape", (value) =>
76
- String(value)
77
- .replace(/&/g, "&amp;")
78
- .replace(/</g, "&lt;")
79
- .replace(/>/g, "&gt;")
80
- .replace(/"/g, "&quot;")
81
- .replace(/'/g, "&#39;"),
82
- );
83
-
84
101
  eleventyConfig.addTransform("elena-ssr", (content, outputPath) => {
85
102
  if (outputPath && outputPath.endsWith(".html")) {
86
103
  return ssr(content);
@@ -88,6 +105,17 @@ export default function (eleventyConfig) {
88
105
  return content;
89
106
  });
90
107
 
108
+ // Runs after passthrough copy has placed every file in CSS_FILES_TO_MERGE
109
+ // under dist/, so it reads the already-copied output rather than the
110
+ // assets/ source tree (that's also where diff2html.min.css lands, copied
111
+ // from node_modules above — there's no separate "source" copy of it).
112
+ eleventyConfig.on("eleventy.after", ({ dir }) => {
113
+ const outputDir = dir.output;
114
+ const merged = CSS_FILES_TO_MERGE.map((file) => readFileSync(path.join(outputDir, file), "utf8")).join("\n");
115
+ writeFileSync(path.join(outputDir, "assets/css/bundle.css"), merged);
116
+ for (const file of CSS_FILES_TO_MERGE) rmSync(path.join(outputDir, file));
117
+ });
118
+
91
119
  return {
92
120
  markdownTemplateEngine: false,
93
121
  dir: {
patches/@elenajs+ssr+1.0.0-alpha.11.patch CHANGED
@@ -57,10 +57,38 @@ index 2253f72..8351dc2 100644
57
57
  + return normalized.replace(RESTORE_PATTERN, (_, i) => protectedChunks[Number(i)]);
58
58
  }
59
59
  diff --git a/node_modules/@elenajs/ssr/src/index.js b/node_modules/@elenajs/ssr/src/index.js
60
- index a134eda..eccef24 100644
60
+ index a134eda..b2080a8 100644
61
61
  --- a/node_modules/@elenajs/ssr/src/index.js
62
62
  +++ b/node_modules/@elenajs/ssr/src/index.js
63
+ @@ -297,6 +297,9 @@ function serializeAttrs(attrs) {
64
+ const WS = /[\t\n\r ]+/g;
65
+ const ALL_WS = /[^\t\n\r ]/;
66
+
67
+ +/** Matches a default <slot> marker: <slot></slot>, <slot/>, or <slot />. */
68
+ +const SLOT_PATTERN = /<slot(?:\s*\/)?\s*>(?:\s*<\/slot>)?/;
69
+ +
70
+ /**
71
+ * Check whether a component class has a meaningful render() method,
72
+ * including inherited ones. Walks the prototype chain and distinguishes
63
- @@ -398,7 +398,16 @@ function walk(nodes, preserveWhitespace = false) {
73
+ @@ -395,10 +398,34 @@ function walk(nodes, preserveWhitespace = false) {
74
+ // Mark as hydrated so CSS targeting :scope:not([hydrated]) doesn’t
75
+ // double-style the host alongside the already-rendered inner element.
76
+ attrs.hydrated = "";
77
+ + // A component that wants its light-DOM children projected in place
78
+ + // (rather than only available flattened as this._text) writes a
79
+ + // literal <slot></slot> marker in its own render() output. Only
80
+ + // walk the original children (expanding any components inside
81
+ + // them too) when a component actually asked for it — most
82
+ + // components don't take children at all, so this stays free for
83
+ + // them. This is a plain string substitution done once, before the
84
+ + // re-parse below, rather than a real DOM-based <slot> element —
85
+ + // there's no shadow root here to make that meaningful, and it
86
+ + // avoids the escape/attribute/reparse round trip a "pass content
87
+ + // as a prop" approach would need for arbitrary structured HTML.
88
+ + if (SLOT_PATTERN.test(innerHTML)) {
89
+ + const slotted = walk(children, pre);
90
+ + innerHTML = innerHTML.replace(SLOT_PATTERN, slotted);
91
+ + }
64
92
  }
65
93
  }
66
94
 
src/_data/localRepos.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import MarkdownIt from "markdown-it";
2
2
  import { discoverLocalRepos } from "../_helpers/gitRepos.js";
3
+ import { highlightFenceCode } from "../_helpers/highlightCode.js";
3
4
 
4
- const md = new MarkdownIt({ html: true, linkify: true });
5
+ const md = new MarkdownIt({ html: true, linkify: true, highlight: highlightFenceCode });
5
6
 
6
7
  export default async () => {
7
8
  const repos = discoverLocalRepos();
src/_helpers/highlightCode.js CHANGED
@@ -29,23 +29,37 @@ const EXT_TO_LANG = {
29
29
  typ: null,
30
30
  };
31
31
 
32
+ // Fenced-code-block language identifiers (the "rust" in ```rust) mostly
33
+ // already match Prism's own component ids, unlike file extensions —
32
- const cache = new Map();
34
+ // except for a handful of common short forms people actually type in
33
-
35
+ // READMEs, which need the same kind of aliasing EXT_TO_LANG does above.
34
- const getLanguage = (ext) => {
36
+ const FENCE_LANG_ALIASES = {
37
+ js: "javascript",
38
+ sh: "bash",
39
+ rs: "rust",
40
+ ts: "typescript",
41
+ yml: "yaml",
42
+ html: "markup",
35
- if (cache.has(ext)) return cache.get(ext);
43
+ htm: "markup",
44
+ };
36
45
 
37
- const lang = Object.prototype.hasOwnProperty.call(EXT_TO_LANG, ext) ? EXT_TO_LANG[ext] : ext;
38
- let grammar = null;
46
+ const grammarCache = new Map();
39
- if (lang) {
40
- if (!Prism.languages[lang]) loadPrismLanguage(lang);
41
- grammar = Prism.languages[lang] ?? null;
42
- }
43
47
 
48
+ /** Load (and cache) a Prism grammar by its canonical component id. */
49
+ const loadGrammar = (lang) => {
50
+ if (grammarCache.has(lang)) return grammarCache.get(lang);
51
+ if (!Prism.languages[lang]) loadPrismLanguage(lang);
52
+ const grammar = Prism.languages[lang] ?? null;
44
53
  const result = grammar ? { lang, grammar } : null;
45
- cache.set(ext, result);
54
+ grammarCache.set(lang, result);
46
55
  return result;
47
56
  };
48
57
 
58
+ const getLanguage = (ext) => {
59
+ const lang = Object.prototype.hasOwnProperty.call(EXT_TO_LANG, ext) ? EXT_TO_LANG[ext] : ext;
60
+ return lang ? loadGrammar(lang) : null;
61
+ };
62
+
49
63
  /**
50
64
  * Highlight source text for a given file extension using Prism, at build
51
65
  * time — no client-side highlighter ships to the browser. Falls back to
@@ -62,5 +76,22 @@ export const highlightCode = (text, ext) => {
62
76
  return { html: Prism.highlight(text, found.grammar, found.lang), lang: found.lang };
63
77
  };
64
78
 
79
+ /**
80
+ * Highlight a fenced markdown code block by its declared language (the
81
+ * "rust" in ```rust), for markdown-it's `highlight` option — used for
82
+ * repo READMEs, which render through a separate MarkdownIt instance
83
+ * rather than highlightCode()'s extension-keyed lookup. markdown-it
84
+ * wraps whatever this returns in <pre><code class="language-{lang}">,
85
+ * matching the shared pre/code styling and .token.* colors in
86
+ * shared.css. Falls back to plain escaped text, same as highlightCode.
87
+ *
88
+ * @returns {string} inner HTML for markdown-it's fence renderer
89
+ */
90
+ export const highlightFenceCode = (text, lang) => {
91
+ const normalized = lang ? (FENCE_LANG_ALIASES[lang.toLowerCase()] ?? lang.toLowerCase()) : null;
92
+ const found = normalized ? loadGrammar(normalized) : null;
93
+ return found ? Prism.highlight(text, found.grammar, found.lang) : escapeHtml(text);
94
+ };
95
+
65
96
  const escapeHtml = (str) =>
66
97
  String(str).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
src/_includes/file-layout.njk ADDED
@@ -0,0 +1,13 @@
1
+ {% macro fileLayout(repo, filepath, filename, ext, tab) %}
2
+ {% set base = "/repos/" + repo + "/files/" + filepath %}
3
+ <div class="file-layout">
4
+ <div class="title">{{ filename | fileIcon(ext) | safe }}<span>{{ filename }}</span></div>
5
+ <div class="nav">
6
+ <div><a href="{{ base }}/" hx-get="{{ base }}/" hx-select="#file-pane" hx-target="#file-pane" hx-swap="outerHTML" hx-push-url="true" aria-current="{% if tab == 'Contents' %}true{% else %}false{% endif %}">Contents</a></div>
7
+ <div>|</div>
8
+ <div><a href="{{ base }}/history/" hx-get="{{ base }}/history/" hx-select="#file-pane" hx-target="#file-pane" hx-swap="outerHTML" hx-push-url="true" aria-current="{% if tab == 'History' %}true{% else %}false{% endif %}">History</a></div>
9
+ <div>|</div>
10
+ <div><a href="{{ base }}/blame/" hx-get="{{ base }}/blame/" hx-select="#file-pane" hx-target="#file-pane" hx-swap="outerHTML" hx-push-url="true" aria-current="{% if tab == 'Blame' %}true{% else %}false{% endif %}">Blame</a></div>
11
+ </div>
12
+ </div>
13
+ {% endmacro %}
src/_includes/layout.njk CHANGED
@@ -13,9 +13,7 @@
13
13
  <link rel="icon" type="image/svg+xml" href="/assets/icons/icon.svg" />
14
14
  <link rel="alternate" type="application/rss+xml" title="{{ title or site.title }}" href="/rss.xml" />
15
15
  <link rel="canonical" href="{{ site.url }}{{ page.url }}" />
16
- <link rel="stylesheet" href="/assets/css/shared.css" />
16
+ <link rel="stylesheet" href="/assets/css/bundle.css" />
17
- {% if css %}<link rel="stylesheet" href="{{ css }}" />{% endif %}
18
- {% for sheet in extraCss %}<link rel="stylesheet" href="{{ sheet }}" />{% endfor %}
19
17
  <title>{{ title or site.title }}</title>
20
18
  <meta name="title" content="{{ title or site.title }}" />
21
19
  <meta name="description" content="{{ description or site.description }}" />
@@ -30,10 +28,10 @@
30
28
  <script src="/assets/vendor/htmx.min.js"></script>
31
29
  </head>
32
30
  <body>
33
- <site-header path="{{ page.url }}"></site-header>
31
+ {% include "site-header.njk" %}
34
32
  <div class="wrapper">
35
33
  <main>{{ content | safe }}</main>
36
34
  </div>
37
- <site-footer></site-footer>
35
+ {% include "site-footer.njk" %}
38
36
  </body>
39
37
  </html>
src/_includes/post.njk CHANGED
@@ -6,5 +6,5 @@ layout: layout.njk
6
6
  <h2 style="font-size:1.1rem;font-weight:500">{{ description }}</h2>
7
7
  <time style="font-size:1.1rem;color:light-dark(hsl(from var(--color-text) h s 40%), hsl(from var(--color-text) h s 80%))">{{ pubDate | readableDate }}</time>
8
8
  <hr style="margin:0.5rem 0;border-color:var(--color-text)" />
9
- <markdown-view body="{{ content | attrEscape | safe }}"></markdown-view>
9
+ <article class="markdown-body">{{ content | safe }}</article>
10
10
  </article>
src/_includes/repo-layout.njk ADDED
@@ -0,0 +1,16 @@
1
+ {% macro repoLayout(repo, title, desc, tags, tab) %}
2
+ <div class="repo">
3
+ <div class="header">
4
+ <h1><a href="/">~repos</a>/{{ title }}</h1>
5
+ <div class="tags">{% for tag in tags %}<span class="tag">#{{ tag }}</span>{% endfor %}</div>
6
+ </div>
7
+ <h3>GIT_CONFIG_PARAMETERS="'http.version=HTTP/1.1'" git clone <a>https://git.pyrossh.dev/{{ title }}/.git</a> {{ title }}</h3>
8
+ <div class="headerExtension"><h2>{{ desc }}</h2></div>
9
+ <hr />
10
+ <div class="nav">
11
+ <div><a href="/repos/{{ repo }}/" hx-get="/repos/{{ repo }}/" hx-select="#repo-page" hx-target="#repo-page" hx-swap="outerHTML" hx-push-url="true" class="{% if tab == 'Readme' %}active-tab{% endif %}">Readme</a><span>|</span></div>
12
+ <div><a href="/repos/{{ repo }}/commits/" hx-get="/repos/{{ repo }}/commits/" hx-select="#repo-page" hx-target="#repo-page" hx-swap="outerHTML" hx-push-url="true" class="{% if tab == 'Commits' %}active-tab{% endif %}">Commits</a><span>|</span></div>
13
+ <div><a href="/repos/{{ repo }}/files/" hx-get="/repos/{{ repo }}/files/" hx-select="#repo-page" hx-target="#repo-page" hx-swap="outerHTML" hx-push-url="true" class="{% if tab == 'Files' %}active-tab{% endif %}">Files</a><span>|</span></div>
14
+ </div>
15
+ </div>
16
+ {% endmacro %}
src/_includes/site-footer.njk ADDED
@@ -0,0 +1,11 @@
1
+ <footer>
2
+ <div class="wrapper">
3
+ <div class="container">
4
+ <div class="spacer"></div>
5
+ <p class="copyright-container">
6
+ Copyright © {{ site.year }}
7
+ <a class="link" href="{{ site.url }}">{{ site.title }}</a>
8
+ </p>
9
+ </div>
10
+ </div>
11
+ </footer>
src/_includes/site-header.njk ADDED
@@ -0,0 +1,14 @@
1
+ <header>
2
+ <div class="wrapper">
3
+ <nav>
4
+ <a href="/" class="logo{% if page.url == '/' %} active{% endif %}">木 {{ site.title }}</a>
5
+ <div class="links">
6
+ <span id="theme-switch"><theme-switcher></theme-switcher></span>
7
+ <div>|</div>
8
+ {% for item in site.navItems %}
9
+ <a href="{{ item.href }}" class="{% if page.url == item.href or (item.href != '/' and page.url.startsWith(item.href)) %}active{% endif %}">{{ item.label }}</a><div>|</div>
10
+ {% endfor %}
11
+ </div>
12
+ </nav>
13
+ </div>
14
+ </header>
src/components/code-view.js DELETED
@@ -1,18 +0,0 @@
1
- import { Elena, html, unsafeHTML } from "@elenajs/core";
2
-
3
- // `body` is pre-highlighted HTML from Prism (see highlightCode.js), built
4
- // at build time — no highlighter ships to the client. Unlike a raw-text
5
- // prop this needs unsafeHTML() since it's real markup (<span
6
- // class="token...">), not text to be re-escaped for display.
7
- export default class CodeView extends Elena(HTMLElement) {
8
- static tagName = "code-view";
9
- static props = ["lang", "body"];
10
- lang = "";
11
- body = "";
12
-
13
- render() {
14
- return html`<pre class="code-view" data-lang="${this.lang}"><code class="language-${this.lang}">${unsafeHTML(this.body)}</code></pre>`;
15
- }
16
- }
17
-
18
- CodeView.define();
src/components/current-year.js DELETED
@@ -1,11 +0,0 @@
1
- import { Elena, html } from "@elenajs/core";
2
-
3
- export default class CurrentYear extends Elena(HTMLElement) {
4
- static tagName = "current-year";
5
-
6
- render() {
7
- return html`${new Date().getFullYear()}`;
8
- }
9
- }
10
-
11
- CurrentYear.define();
src/components/file-layout.js DELETED
@@ -1,42 +0,0 @@
1
- import { Elena, html, unsafeHTML } from "@elenajs/core";
2
- import { fileIcon } from "../_helpers/fileIcons.js";
3
-
4
- // Chrome-only, same reasoning as repo-layout.js: file content is a
5
- // sibling in the page template, not a slotted child of this element.
6
- // Tab links swap #file-pane (this element + #file-content together) so
7
- // this element's own aria-current state updates on htmx navigation too —
8
- // swapping only #file-content would leave the tab underline stuck.
9
- export default class FileLayout extends Elena(HTMLElement) {
10
- static tagName = "file-layout";
11
- static props = ["repo", "filepath", "filename", "tab"];
12
- repo = "";
13
- filepath = "";
14
- filename = "";
15
- tab = "Contents";
16
-
17
- render() {
18
- const base = `/repos/${this.repo}/files/${this.filepath}`;
19
- const ext = this.filename.includes(".") ? this.filename.split(".").pop().toLowerCase() : "";
20
- const tab = (label, href) => html`<div><a
21
- href="${href}"
22
- hx-get="${href}"
23
- hx-select="#file-pane"
24
- hx-target="#file-pane"
25
- hx-swap="outerHTML"
26
- hx-push-url="true"
27
- aria-current="${this.tab === label ? "true" : "false"}"
28
- >${label}</a></div>`;
29
- return html`<div class="file-layout">
30
- <div class="title">${unsafeHTML(fileIcon(this.filename, ext))}<span>${this.filename}</span></div>
31
- <div class="nav">
32
- ${tab("Contents", `${base}/`)}
33
- <div>|</div>
34
- ${tab("History", `${base}/history/`)}
35
- <div>|</div>
36
- ${tab("Blame", `${base}/blame/`)}
37
- </div>
38
- </div>`;
39
- }
40
- }
41
-
42
- FileLayout.define();
src/components/markdown-view.js DELETED
@@ -1,18 +0,0 @@
1
- import { Elena, html, unsafeHTML } from "@elenajs/core";
2
-
3
- // Content comes in via the `body` prop (already-rendered, trusted HTML —
4
- // see the `attrEscape` Nunjucks filter), not light-DOM children: this
5
- // version of @elenajs/ssr doesn't project light-DOM into <slot> during the
6
- // server render pass, it just drops it, so anything wrapping real content
7
- // has to pass it as a prop and inject it with unsafeHTML.
8
- export default class MarkdownView extends Elena(HTMLElement) {
9
- static tagName = "markdown-view";
10
- static props = ["body"];
11
- body = "";
12
-
13
- render() {
14
- return html`<article class="markdown-body">${unsafeHTML(this.body)}</article>`;
15
- }
16
- }
17
-
18
- MarkdownView.define();
src/components/repo-layout.js DELETED
@@ -1,50 +0,0 @@
1
- import { Elena, html } from "@elenajs/core";
2
-
3
- // Chrome-only: renders the repo header/tab nav from props and nothing
4
- // else. The actual tab content (readme/files/commits) is rendered as a
5
- // plain sibling in the page template, not as a child of this element —
6
- // see markdown-view.js for why light-DOM children aren't used here.
7
- //
8
- // The tab links swap #repo-page (this element + the content sibling
9
- // together, see the *.njk templates), not just the content — otherwise
10
- // this element itself never re-renders on an htmx navigation, so its own
11
- // active-tab underline would stay stuck on whichever tab the page
12
- // originally loaded on.
13
- export default class RepoLayout extends Elena(HTMLElement) {
14
- static tagName = "repo-layout";
15
- static props = ["repo", "title", "desc", "tags", "tab"];
16
- repo = "";
17
- title = "";
18
- desc = "";
19
- tags = [];
20
- tab = "Readme";
21
-
22
- render() {
23
- const tabHref = (t) => (t === "Readme" ? `/repos/${this.repo}/` : `/repos/${this.repo}/${t.toLowerCase()}/`);
24
- return html`<div class="repo">
25
- <div class="header">
26
- <h1><a href="/">~repos</a>/${this.title}</h1>
27
- <div class="tags">${this.tags.map((tag) => html`<span class="tag">#${tag}</span>`)}</div>
28
- </div>
29
- <h3>GIT_CONFIG_PARAMETERS="'http.version=HTTP/1.1'" git clone <a>https://git.pyrossh.dev/${this.title}/.git</a> ${this.title}</h3>
30
- <div class="headerExtension"><h2>${this.desc}</h2></div>
31
- <hr />
32
- <div class="nav">
33
- ${["Readme", "Commits", "Files"].map((t) => {
34
- const href = tabHref(t);
35
- return html`<div><a
36
- href="${href}"
37
- hx-get="${href}"
38
- hx-select="#repo-page"
39
- hx-target="#repo-page"
40
- hx-swap="outerHTML"
41
- hx-push-url="true"
42
- class="${this.tab === t ? "active-tab" : ""}"
43
- >${t}</a><span>|</span></div>`;
44
- })}
45
- </div>
46
- </div>`;
47
- }
48
- }
49
-
50
- RepoLayout.define();
src/components/site-footer.js DELETED
@@ -1,22 +0,0 @@
1
- import { Elena, html } from "@elenajs/core";
2
- import site from "../_data/site.js";
3
-
4
- export default class SiteFooter extends Elena(HTMLElement) {
5
- static tagName = "site-footer";
6
-
7
- render() {
8
- return html`<footer>
9
- <div class="wrapper">
10
- <div class="container">
11
- <div class="spacer"></div>
12
- <p class="copyright-container">
13
- Copyright © <current-year></current-year>
14
- <a class="link" href="${site.url}">${site.title}</a>
15
- </p>
16
- </div>
17
- </div>
18
- </footer>`;
19
- }
20
- }
21
-
22
- SiteFooter.define();
src/components/site-header.js DELETED
@@ -1,29 +0,0 @@
1
- import { Elena, html } from "@elenajs/core";
2
- import site from "../_data/site.js";
3
-
4
- export default class SiteHeader extends Elena(HTMLElement) {
5
- static tagName = "site-header";
6
- static props = ["path"];
7
- path = "/";
8
-
9
- render() {
10
- const isActive = (href) => this.path === href || (href !== "/" && this.path?.startsWith(href));
11
- return html`<header>
12
- <div class="wrapper">
13
- <nav>
14
- <a href="/" class="logo${this.path === "/" ? " active" : ""}">木 ${site.title}</a>
15
- <div class="links">
16
- <span id="theme-switch"><theme-switcher></theme-switcher></span>
17
- <div>|</div>
18
- ${site.navItems.map(
19
- (item) =>
20
- html`<a href="${item.href}" class="${isActive(item.href) ? "active" : ""}">${item.label}</a><div>|</div>`,
21
- )}
22
- </div>
23
- </nav>
24
- </div>
25
- </header>`;
26
- }
27
- }
28
-
29
- SiteHeader.define();
src/cv.njk CHANGED
@@ -3,14 +3,13 @@ layout: layout.njk
3
3
  title: Curriculum Vitae
4
4
  description: Peter John CV
5
5
  permalink: /cv/
6
- css: /assets/css/workers/cv.css
7
6
  ---
7
+ <div class="cv-page">
8
- <div>
8
+ <h1>
9
- <h1 style="display:flex;align-items:flex-start;font-weight:800;font-size:2rem;margin-bottom:1rem">
10
9
  Curriculum Vitae
11
- <a href="/assets/pdfs/resume.pdf" target="_blank" rel="noopener noreferrer" style="margin-left:0.4rem;margin-top:0.8rem;color:var(--color-link)">↗</a>
10
+ <a href="/assets/pdfs/resume.pdf" target="_blank" rel="noopener noreferrer">↗</a>
12
11
  </h1>
13
- <div style="border:1px solid black">
12
+ <div class="pdf">
14
- <img src="/assets/pdfs/resume.svg" alt="Resume" style="width:100%" />
13
+ <img src="/assets/pdfs/resume.svg" alt="Resume" />
15
14
  </div>
16
15
  </div>
src/index.njk CHANGED
@@ -3,7 +3,6 @@ layout: layout.njk
3
3
  title: pyrossh
4
4
  description: "Tech Lead from Bangalore who likes to create frameworks and programming languages."
5
5
  permalink: /
6
- css: /assets/css/workers/home.css
7
6
  ---
8
7
  <div class="pageContainer">
9
8
  <h1 class="title">Hello!</h1>
src/only-bible-app/index.njk CHANGED
@@ -3,7 +3,6 @@ layout: layout.njk
3
3
  title: Only Bible App
4
4
  description: The only bible app you will ever need
5
5
  permalink: /only-bible-app/
6
- css: /assets/css/workers/only-bible-app.css
7
6
  ---
8
7
  <div style="display:flex;flex:1;flex-direction:column;padding-bottom:16rem">
9
8
  <div style="display:flex;flex-direction:column;align-items:center">
src/only-bible-app/privacy-policy.njk CHANGED
@@ -3,7 +3,6 @@ layout: layout.njk
3
3
  title: Privacy Policy | Only Bible App
4
4
  description: Privacy Policy | Only Bible App
5
5
  permalink: /only-bible-app/privacy-policy/
6
- css: /assets/css/workers/only-bible-app-privacy.css
7
6
  ---
8
7
  <h1 style="font-size:1.875rem;line-height:2.25rem;font-weight:700">Privacy Policy</h1>
9
8
  <p>
src/only-bible-app/terms-and-conditions.njk CHANGED
@@ -3,7 +3,6 @@ layout: layout.njk
3
3
  title: Terms and Conditions | Only Bible App
4
4
  description: Terms and Conditions | Only Bible App
5
5
  permalink: /only-bible-app/terms-and-conditions/
6
- css: /assets/css/workers/only-bible-app-terms.css
7
6
  ---
8
7
  <div style="padding-bottom:2.5rem;margin-top:2.5rem">
9
8
  <h1 style="font-weight:700;font-size:1.875rem;line-height:2.25rem">Terms and Conditions</h1>
src/posts.njk CHANGED
@@ -3,7 +3,6 @@ layout: layout.njk
3
3
  title: Posts
4
4
  description: Posts
5
5
  permalink: /posts/
6
- css: /assets/css/workers/posts-index.css
7
6
  ---
8
7
  <h1 style="font-weight:800;font-size:2rem">Posts</h1>
9
8
  <ul style="display:flex;flex-direction:column;list-style:none;padding:0">
src/repos/commit.njk CHANGED
@@ -5,15 +5,12 @@ pagination:
5
5
  alias: item
6
6
  permalink: "/repos/{{ item.repoId }}/commits/{{ item.commit.hash }}/"
7
7
  layout: layout.njk
8
- css: /assets/css/repos.css
9
- extraCss:
10
- - /assets/css/markdown.css
11
- - /assets/css/diff2html.min.css
12
8
  eleventyComputed:
13
9
  title: "{{ item.commit.shortHash }} | {{ item.repo.data.title }}"
14
10
  ---
11
+ {% from "repo-layout.njk" import repoLayout %}
15
12
  <div id="repo-page">
16
- <repo-layout repo="{{ item.repo.id }}" title="{{ item.repo.data.title }}" desc="{{ item.repo.data.description }}" tags="{{ item.repo.data.tags | json }}" tab="Commits"></repo-layout>
13
+ {{ repoLayout(item.repo.id, item.repo.data.title, item.repo.data.description, item.repo.data.tags, "Commits") }}
17
14
  <div id="repo-content">
18
15
  <div class="event">
19
16
  <div>
src/repos/commits.njk CHANGED
@@ -5,15 +5,12 @@ pagination:
5
5
  alias: repo
6
6
  permalink: "/repos/{{ repo.id }}/commits/"
7
7
  layout: layout.njk
8
- css: /assets/css/repos.css
9
- extraCss:
10
- - /assets/css/markdown.css
11
- - /assets/css/diff2html.min.css
12
8
  eleventyComputed:
13
9
  title: "Commits | {{ repo.data.title }}"
14
10
  ---
11
+ {% from "repo-layout.njk" import repoLayout %}
15
12
  <div id="repo-page">
16
- <repo-layout repo="{{ repo.id }}" title="{{ repo.data.title }}" desc="{{ repo.data.description }}" tags="{{ repo.data.tags | json }}" tab="Commits"></repo-layout>
13
+ {{ repoLayout(repo.id, repo.data.title, repo.data.description, repo.data.tags, "Commits") }}
17
14
  <div id="repo-content">
18
15
  {% if repo.commits.length %}
19
16
  {% for commit in repo.commits %}
src/repos/file-blame.njk CHANGED
@@ -5,15 +5,13 @@ pagination:
5
5
  alias: item
6
6
  permalink: "/repos/{{ item.repoId }}/files/{{ item.file.path }}/blame/"
7
7
  layout: layout.njk
8
- css: /assets/css/repos.css
9
- extraCss:
10
- - /assets/css/markdown.css
11
- - /assets/css/diff2html.min.css
12
8
  eleventyComputed:
13
9
  title: "{{ item.file.name }} blame | {{ item.repo.data.title }}"
14
10
  ---
11
+ {% from "repo-layout.njk" import repoLayout %}
12
+ {% from "file-layout.njk" import fileLayout %}
15
13
  <div id="repo-page">
16
- <repo-layout repo="{{ item.repo.id }}" title="{{ item.repo.data.title }}" desc="{{ item.repo.data.description }}" tags="{{ item.repo.data.tags | json }}" tab="Files"></repo-layout>
14
+ {{ repoLayout(item.repo.id, item.repo.data.title, item.repo.data.description, item.repo.data.tags, "Files") }}
17
15
  <div id="repo-content">
18
16
  <div class="file-explorer">
19
17
  <aside class="file-explorer-tree">
@@ -21,7 +19,7 @@ eleventyComputed:
21
19
  {{ renderNodes(item.repo.tree, item.repoId, item.file.path) }}
22
20
  </aside>
23
21
  <div id="file-pane" class="file-explorer-main">
24
- <file-layout repo="{{ item.repoId }}" filepath="{{ item.file.path }}" filename="{{ item.file.name }}" tab="Blame"></file-layout>
22
+ {{ fileLayout(item.repoId, item.file.path, item.file.name, item.file.ext, "Blame") }}
25
23
  <div id="file-content">
26
24
  {% if item.blame.length %}
27
25
  <table class="blame">
src/repos/file-history.njk CHANGED
@@ -5,15 +5,13 @@ pagination:
5
5
  alias: item
6
6
  permalink: "/repos/{{ item.repoId }}/files/{{ item.file.path }}/history/"
7
7
  layout: layout.njk
8
- css: /assets/css/repos.css
9
- extraCss:
10
- - /assets/css/markdown.css
11
- - /assets/css/diff2html.min.css
12
8
  eleventyComputed:
13
9
  title: "{{ item.file.name }} history | {{ item.repo.data.title }}"
14
10
  ---
11
+ {% from "repo-layout.njk" import repoLayout %}
12
+ {% from "file-layout.njk" import fileLayout %}
15
13
  <div id="repo-page">
16
- <repo-layout repo="{{ item.repo.id }}" title="{{ item.repo.data.title }}" desc="{{ item.repo.data.description }}" tags="{{ item.repo.data.tags | json }}" tab="Files"></repo-layout>
14
+ {{ repoLayout(item.repo.id, item.repo.data.title, item.repo.data.description, item.repo.data.tags, "Files") }}
17
15
  <div id="repo-content">
18
16
  <div class="file-explorer">
19
17
  <aside class="file-explorer-tree">
@@ -21,7 +19,7 @@ eleventyComputed:
21
19
  {{ renderNodes(item.repo.tree, item.repoId, item.file.path) }}
22
20
  </aside>
23
21
  <div id="file-pane" class="file-explorer-main">
24
- <file-layout repo="{{ item.repoId }}" filepath="{{ item.file.path }}" filename="{{ item.file.name }}" tab="History"></file-layout>
22
+ {{ fileLayout(item.repoId, item.file.path, item.file.name, item.file.ext, "History") }}
25
23
  <div id="file-content">
26
24
  {% if item.history.length %}
27
25
  {% for commit in item.history %}
src/repos/file.njk CHANGED
@@ -5,15 +5,13 @@ pagination:
5
5
  alias: item
6
6
  permalink: "/repos/{{ item.repoId }}/files/{{ item.file.path }}/"
7
7
  layout: layout.njk
8
- css: /assets/css/repos.css
9
- extraCss:
10
- - /assets/css/markdown.css
11
- - /assets/css/diff2html.min.css
12
8
  eleventyComputed:
13
9
  title: "{{ item.file.name }} | {{ item.repo.data.title }}"
14
10
  ---
11
+ {% from "repo-layout.njk" import repoLayout %}
12
+ {% from "file-layout.njk" import fileLayout %}
15
13
  <div id="repo-page">
16
- <repo-layout repo="{{ item.repo.id }}" title="{{ item.repo.data.title }}" desc="{{ item.repo.data.description }}" tags="{{ item.repo.data.tags | json }}" tab="Files"></repo-layout>
14
+ {{ repoLayout(item.repo.id, item.repo.data.title, item.repo.data.description, item.repo.data.tags, "Files") }}
17
15
  <div id="repo-content">
18
16
  <div class="file-explorer">
19
17
  <aside class="file-explorer-tree">
@@ -21,7 +19,7 @@ eleventyComputed:
21
19
  {{ renderNodes(item.repo.tree, item.repoId, item.file.path) }}
22
20
  </aside>
23
21
  <div id="file-pane" class="file-explorer-main">
24
- <file-layout repo="{{ item.repoId }}" filepath="{{ item.file.path }}" filename="{{ item.file.name }}" tab="Contents"></file-layout>
22
+ {{ fileLayout(item.repoId, item.file.path, item.file.name, item.file.ext, "Contents") }}
25
23
  <div id="file-content">
26
24
  {% if item.render.kind == "binary" %}
27
25
  <p>Binary file ({{ item.file.size }} bytes).</p>
@@ -30,7 +28,7 @@ eleventyComputed:
30
28
  {% elif item.render.kind == "image" %}
31
29
  <img src="{{ item.render.src }}" alt="{{ item.file.path }}" />
32
30
  {% else %}
33
- <code-view lang="{{ item.render.lang }}" body="{{ item.render.html | attrEscape | safe }}"></code-view>
31
+ <pre class="code-view" data-lang="{{ item.render.lang }}"><code class="language-{{ item.render.lang }}">{{ item.render.html | safe }}</code></pre>
34
32
  {% endif %}
35
33
  </div>
36
34
  </div>
src/repos/files-index.njk CHANGED
@@ -5,15 +5,12 @@ pagination:
5
5
  alias: repo
6
6
  permalink: "/repos/{{ repo.id }}/files/"
7
7
  layout: layout.njk
8
- css: /assets/css/repos.css
9
- extraCss:
10
- - /assets/css/markdown.css
11
- - /assets/css/diff2html.min.css
12
8
  eleventyComputed:
13
9
  title: "Files | {{ repo.data.title }}"
14
10
  ---
11
+ {% from "repo-layout.njk" import repoLayout %}
15
12
  <div id="repo-page">
16
- <repo-layout repo="{{ repo.id }}" title="{{ repo.data.title }}" desc="{{ repo.data.description }}" tags="{{ repo.data.tags | json }}" tab="Files"></repo-layout>
13
+ {{ repoLayout(repo.id, repo.data.title, repo.data.description, repo.data.tags, "Files") }}
17
14
  <div id="repo-content">
18
15
  <div class="file-explorer">
19
16
  <aside class="file-explorer-tree">
src/repos/repo.njk CHANGED
@@ -5,19 +5,16 @@ pagination:
5
5
  alias: repo
6
6
  permalink: "/repos/{{ repo.id }}/"
7
7
  layout: layout.njk
8
- css: /assets/css/repos.css
9
- extraCss:
10
- - /assets/css/markdown.css
11
- - /assets/css/diff2html.min.css
12
8
  eleventyComputed:
13
9
  title: "{{ repo.data.title }}"
14
10
  description: "{{ repo.data.description }}"
15
11
  ---
12
+ {% from "repo-layout.njk" import repoLayout %}
16
13
  <div id="repo-page">
17
- <repo-layout repo="{{ repo.id }}" title="{{ repo.data.title }}" desc="{{ repo.data.description }}" tags="{{ repo.data.tags | json }}" tab="Readme"></repo-layout>
14
+ {{ repoLayout(repo.id, repo.data.title, repo.data.description, repo.data.tags, "Readme") }}
18
15
  <div id="repo-content">
19
16
  {% if repo.readmeHtml %}
20
- <markdown-view body="{{ repo.readmeHtml | attrEscape | safe }}"></markdown-view>
17
+ <article class="markdown-body">{{ repo.readmeHtml | safe }}</article>
21
18
  {% else %}
22
19
  <p>No README found.</p>
23
20
  {% endif %}