Skip to content

Commit 74b5a6e

Browse files
authored
Merge pull request #16 from goude/claude/backlog-work-yPBmd
Refactor header components and extract shared navigation styles
2 parents e1730bb + 8692acd commit 74b5a6e

11 files changed

Lines changed: 401 additions & 364 deletions

File tree

src/components/Header.astro

Lines changed: 8 additions & 295 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,8 @@
11
---
22
import { NAV_ITEMS, type NavItem } from "@/types/site";
3-
import rawLogoSvg from "@/assets/goude-se-logo.svg?raw";
4-
import { SWATCH_TO_CSS_VAR } from "@/styles/swatches";
5-
6-
function mapInkscapeSwatches(svg: string) {
7-
let out = svg;
8-
9-
// Hard override: currentColor is always literal currentColor at runtime
10-
const currentColorRe = new RegExp(
11-
'(inkscape:label="currentColor"[\\s\\S]*?<stop[^>]+stop-color:)[^;"]+',
12-
"g"
13-
);
14-
15-
out = out.replace(currentColorRe, "$1currentColor");
16-
17-
// All other swatches map to CSS vars
18-
for (const [label, cssVar] of Object.entries(SWATCH_TO_CSS_VAR)) {
19-
if (label === "currentColor") continue;
20-
21-
const re = new RegExp(
22-
`(inkscape:label="${label}"[\\s\\S]*?<stop[^>]+stop-color:)[^;"]+`,
23-
"g"
24-
);
25-
out = out.replace(re, `$1var(${cssVar})`);
26-
}
27-
28-
return out;
29-
}
30-
31-
const logoSvg = mapInkscapeSwatches(rawLogoSvg);
3+
import SiteLogo from "@/components/SiteLogo.astro";
4+
import ThemeToggle from "@/components/ThemeToggle.astro";
5+
import ModeToggle from "@/components/ModeToggle.astro";
326
337
interface Props {
348
current?: string | undefined;
@@ -45,15 +19,7 @@ const navRight = NAV_ITEMS.filter((item) => item.external);
4519
---
4620

4721
<header class="site-header">
48-
<!-- Logo / Home -->
49-
<a
50-
href="/"
51-
aria-label="Home"
52-
class:list={["logo", "nav-action", { selected: current === "" }]}
53-
title="Guard what you give your attention to. It quietly becomes who you are."
54-
>
55-
<span class="logo-svg" aria-hidden="true" set:html={logoSvg} />
56-
</a>
22+
<SiteLogo isSelected={current === ""} />
5723

5824
<!-- Hamburger (mobile only) -->
5925
<button
@@ -117,36 +83,8 @@ const navRight = NAV_ITEMS.filter((item) => item.external);
11783
}
11884
</nav>
11985

120-
<button
121-
class="mode-toggle nav-action icon-only"
122-
id="mode-toggle"
123-
type="button"
124-
aria-label="Toggle content detail level"
125-
>
126-
<i class="fa-solid fa-water" data-mode-icon="low" aria-hidden="true"></i>
127-
<i class="fa-solid fa-wind" data-mode-icon="medium" aria-hidden="true"
128-
></i>
129-
<i class="fa-solid fa-fire" data-mode-icon="high" aria-hidden="true"></i>
130-
<span class="tooltip" data-mode-tooltip="low">Low Detail</span>
131-
<span class="tooltip" data-mode-tooltip="medium">Medium Detail</span>
132-
<span class="tooltip" data-mode-tooltip="high">High Detail</span>
133-
</button>
134-
135-
<button
136-
class="theme-toggle nav-action icon-only tooltip-right"
137-
id="theme-toggle"
138-
type="button"
139-
aria-label="Toggle theme"
140-
>
141-
<i class="fa-regular fa-sun" id="icon-light" aria-hidden="true"></i>
142-
<i class="fa-solid fa-moon" id="icon-dark" aria-hidden="true"></i>
143-
<span class="tooltip" data-theme-tooltip="light"
144-
>The Cleonic Theme of Brother Day</span
145-
>
146-
<span class="tooltip" data-theme-tooltip="dark"
147-
>The Cleonic Theme of Brother Dusk</span
148-
>
149-
</button>
86+
<ModeToggle variant="desktop" />
87+
<ThemeToggle variant="desktop" />
15088
</div>
15189

15290
<!-- Mobile pancake menu -->
@@ -179,23 +117,8 @@ const navRight = NAV_ITEMS.filter((item) => item.external);
179117
))
180118
}
181119

182-
<button class="menu-item menu-action" id="mode-toggle-menu" type="button">
183-
<i class="fa-solid fa-water" data-mode-icon="low" aria-hidden="true"></i>
184-
<i class="fa-solid fa-wind" data-mode-icon="medium" aria-hidden="true"
185-
></i>
186-
<i class="fa-solid fa-fire" data-mode-icon="high" aria-hidden="true"></i>
187-
<span data-mode-label="low">Low Detail</span>
188-
<span data-mode-label="medium">Medium Detail</span>
189-
<span data-mode-label="high">High Detail</span>
190-
</button>
191-
192-
<button class="menu-item menu-action" id="theme-toggle-menu" type="button">
193-
<i class="fa-regular fa-sun" data-theme-icon="light" aria-hidden="true"
194-
></i>
195-
<i class="fa-solid fa-moon" data-theme-icon="dark" aria-hidden="true"></i>
196-
<span data-theme-label="light">Brother Day</span>
197-
<span data-theme-label="dark">Brother Dusk</span>
198-
</button>
120+
<ModeToggle variant="menu" />
121+
<ThemeToggle variant="menu" />
199122
</nav>
200123
</header>
201124

@@ -236,82 +159,6 @@ const navRight = NAV_ITEMS.filter((item) => item.external);
236159
opacity: 0.7;
237160
}
238161

239-
/* ---------- Unified nav item styling ---------- */
240-
.nav-action {
241-
display: inline-flex;
242-
align-items: center;
243-
gap: 0.35rem;
244-
padding: 0.25rem 0.6rem;
245-
border-radius: 999px;
246-
text-decoration: none;
247-
color: inherit;
248-
249-
font-family: var(--font-ui);
250-
font-weight: 700;
251-
letter-spacing: 0.08em;
252-
white-space: nowrap;
253-
254-
background: none;
255-
border: none;
256-
cursor: pointer;
257-
258-
transition:
259-
background-color 0.15s ease,
260-
color 0.15s ease,
261-
opacity 0.15s ease;
262-
}
263-
264-
.nav-action:hover {
265-
opacity: 0.7;
266-
}
267-
268-
.nav-action i {
269-
font-size: 1.1em;
270-
}
271-
272-
/* CSS animation for rotating icons */
273-
@keyframes icon-swap {
274-
0% {
275-
opacity: 1;
276-
}
277-
40% {
278-
opacity: 0;
279-
}
280-
60% {
281-
opacity: 0;
282-
}
283-
100% {
284-
opacity: 1;
285-
}
286-
}
287-
.nav-action i[data-icon-rotate].rotating {
288-
animation: icon-swap 0.35s ease forwards;
289-
}
290-
291-
/* Shared selected style */
292-
.selected {
293-
background: var(--fg);
294-
color: var(--bg);
295-
}
296-
297-
/* ---------- Logo ---------- */
298-
.logo {
299-
flex-shrink: 0;
300-
}
301-
302-
.logo-svg {
303-
display: flex;
304-
align-items: center;
305-
height: 1.4em;
306-
line-height: 1;
307-
}
308-
309-
.logo-svg :global(svg) {
310-
display: block;
311-
height: 100%;
312-
width: auto;
313-
}
314-
315162
/* Desktop nav containers */
316163
.main-nav {
317164
display: flex;
@@ -329,103 +176,11 @@ const navRight = NAV_ITEMS.filter((item) => item.external);
329176
gap: 0.25rem;
330177
}
331178

332-
/* ---------- Icon-only with tooltip ---------- */
333-
.icon-only {
334-
position: relative;
335-
}
336-
337-
.icon-only .tooltip {
338-
position: absolute;
339-
top: 100%;
340-
left: 50%;
341-
transform: translateX(-50%) translateY(0.25rem);
342-
padding: 0.25rem 0.5rem;
343-
border-radius: 4px;
344-
background: var(--fg);
345-
color: var(--bg);
346-
font-size: 0.75em;
347-
font-weight: 700;
348-
letter-spacing: 0.04em;
349-
white-space: nowrap;
350-
opacity: 0;
351-
pointer-events: none;
352-
transition:
353-
opacity 0.15s ease,
354-
transform 0.15s ease;
355-
z-index: 100;
356-
}
357-
358-
.icon-only:hover .tooltip,
359-
.icon-only:focus-visible .tooltip {
360-
opacity: 1;
361-
transform: translateX(-50%) translateY(0.4rem);
362-
}
363-
364-
/* Right-aligned tooltip for rightmost item to prevent cutoff */
365-
.icon-only.tooltip-right .tooltip {
366-
left: auto;
367-
right: 0;
368-
transform: translateX(0) translateY(0.25rem);
369-
}
370-
371-
.icon-only.tooltip-right:hover .tooltip,
372-
.icon-only.tooltip-right:focus-visible .tooltip {
373-
transform: translateX(0) translateY(0.4rem);
374-
}
375-
376-
.external-icon {
377-
margin-left: 0.3em;
378-
font-size: 0.85em;
379-
opacity: 0.7;
380-
}
381-
382179
/* Hamburger positioning (mobile top row) */
383180
.menu-toggle {
384181
margin-left: auto;
385182
}
386183

387-
/* Theme icons (desktop) */
388-
#icon-dark {
389-
display: none;
390-
}
391-
:global([data-theme-dark]) #icon-light {
392-
display: none;
393-
}
394-
:global([data-theme-dark]) #icon-dark {
395-
display: inline;
396-
}
397-
398-
/* Theme tooltips (desktop) */
399-
.theme-toggle [data-theme-tooltip="dark"] {
400-
display: none;
401-
}
402-
:global([data-theme-dark]) .theme-toggle [data-theme-tooltip="light"] {
403-
display: none;
404-
}
405-
:global([data-theme-dark]) .theme-toggle [data-theme-tooltip="dark"] {
406-
display: block;
407-
}
408-
409-
/* Mode icons (desktop) */
410-
.mode-toggle [data-mode-icon] {
411-
display: none;
412-
}
413-
:global([data-mode="low"]) .mode-toggle [data-mode-icon="low"],
414-
:global([data-mode="medium"]) .mode-toggle [data-mode-icon="medium"],
415-
:global([data-mode="high"]) .mode-toggle [data-mode-icon="high"] {
416-
display: inline;
417-
}
418-
419-
/* Mode tooltips (desktop) */
420-
.mode-toggle [data-mode-tooltip] {
421-
display: none;
422-
}
423-
:global([data-mode="low"]) .mode-toggle [data-mode-tooltip="low"],
424-
:global([data-mode="medium"]) .mode-toggle [data-mode-tooltip="medium"],
425-
:global([data-mode="high"]) .mode-toggle [data-mode-tooltip="high"] {
426-
display: block;
427-
}
428-
429184
/* Mobile menu defaults */
430185
.menu-toggle,
431186
.menu-panel {
@@ -500,48 +255,6 @@ const navRight = NAV_ITEMS.filter((item) => item.external);
500255
background: currentColor;
501256
margin: 0.15rem 0.6rem;
502257
}
503-
504-
/* Theme icons (mobile menu) */
505-
.menu-panel [data-theme-icon="dark"] {
506-
display: none;
507-
}
508-
:global([data-theme-dark]) .menu-panel [data-theme-icon="light"] {
509-
display: none;
510-
}
511-
:global([data-theme-dark]) .menu-panel [data-theme-icon="dark"] {
512-
display: inline;
513-
}
514-
515-
/* Theme labels (mobile menu) */
516-
.menu-panel [data-theme-label="dark"] {
517-
display: none;
518-
}
519-
:global([data-theme-dark]) .menu-panel [data-theme-label="light"] {
520-
display: none;
521-
}
522-
:global([data-theme-dark]) .menu-panel [data-theme-label="dark"] {
523-
display: inline;
524-
}
525-
526-
/* Mode icons (mobile menu) */
527-
.menu-panel [data-mode-icon] {
528-
display: none;
529-
}
530-
:global([data-mode="low"]) .menu-panel [data-mode-icon="low"],
531-
:global([data-mode="medium"]) .menu-panel [data-mode-icon="medium"],
532-
:global([data-mode="high"]) .menu-panel [data-mode-icon="high"] {
533-
display: inline;
534-
}
535-
536-
/* Mode labels (mobile menu) */
537-
.menu-panel [data-mode-label] {
538-
display: none;
539-
}
540-
:global([data-mode="low"]) .menu-panel [data-mode-label="low"],
541-
:global([data-mode="medium"]) .menu-panel [data-mode-label="medium"],
542-
:global([data-mode="high"]) .menu-panel [data-mode-label="high"] {
543-
display: inline;
544-
}
545258
}
546259
</style>
547260

src/components/Md.astro

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,37 +2,10 @@
22
// Render markdown content inline within Astro pages.
33
// Usage: <Md>## Your markdown here</Md>
44
// Supports syntax highlighting via Shiki with light/dark theme support.
5-
import { marked, type Tokens } from "marked";
6-
import { codeToHtml } from "shiki";
5+
import { renderMarkdown } from "@/utils/renderMarkdown";
76
87
const raw = await Astro.slots.render("default");
9-
10-
// Custom renderer for code blocks with Shiki highlighting
11-
const renderer = new marked.Renderer();
12-
13-
renderer.code = function ({ text, lang }: Tokens.Code) {
14-
// Placeholder that we'll replace after async processing
15-
return `<!--SHIKI:${lang || "text"}:${Buffer.from(text).toString("base64")}-->`;
16-
};
17-
18-
let html = await marked.parse(raw, { gfm: true, renderer });
19-
20-
// Process Shiki placeholders with dual themes
21-
const shikiRegex = /<!--SHIKI:([^:]*):([^-]+)-->/g;
22-
const matches = [...html.matchAll(shikiRegex)];
23-
24-
for (const match of matches) {
25-
const lang = match[1] || "text";
26-
const code = Buffer.from(match[2]!, "base64").toString("utf-8"); // ! because the match cannot exist without the group
27-
28-
const [lightHtml, darkHtml] = await Promise.all([
29-
codeToHtml(code, { lang, theme: "github-light-default" }),
30-
codeToHtml(code, { lang, theme: "github-dark-default" }),
31-
]);
32-
33-
const dual = `<div class="shiki-light">${lightHtml}</div><div class="shiki-dark">${darkHtml}</div>`;
34-
html = html.replace(match[0], dual);
35-
}
8+
const html = await renderMarkdown(raw);
369
---
3710

3811
<Fragment set:html={html} />

0 commit comments

Comments
 (0)