From 2c00bd2a9fa8cb39466efd475a9479da4dad467f Mon Sep 17 00:00:00 2001 From: lucasnevespereira Date: Sun, 26 Apr 2026 22:04:22 +0200 Subject: [PATCH] feat: add bio theme --- .github/workflows/ci.yml | 3 + README.md | 2 +- examples/bio/ink.yaml | 31 +++ examples/bio/pages/index/content.md | 4 + inkssg.go | 28 ++- themes/bio/layout.html | 88 ++++++++ themes/bio/script.js | 46 ++++ themes/bio/styles.css | 326 ++++++++++++++++++++++++++++ 8 files changed, 518 insertions(+), 10 deletions(-) create mode 100644 examples/bio/ink.yaml create mode 100644 examples/bio/pages/index/content.md create mode 100644 themes/bio/layout.html create mode 100644 themes/bio/script.js create mode 100644 themes/bio/styles.css diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e4e65d7..b60a350 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,9 @@ jobs: - name: Build example - multi-page run: go run ./cmd/inkssg build ./examples/multi-page + - name: Build example - bio + run: go run ./cmd/inkssg build ./examples/bio + - name: Build example - library working-directory: examples/library run: | diff --git a/README.md b/README.md index d09f61e..a4dc1e8 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ Build output goes to `public/`. ## Themes -inkssg ships with two themes: `minimal` and `devtool`. Pick one in `ink.yaml`: +inkssg ships with three themes: `minimal`, `devtool`, and `bio`. Pick one in `ink.yaml`: ```yaml default_theme: devtool diff --git a/examples/bio/ink.yaml b/examples/bio/ink.yaml new file mode 100644 index 0000000..a761ecb --- /dev/null +++ b/examples/bio/ink.yaml @@ -0,0 +1,31 @@ +name: snowz.ai +avatar: /assets/img/avatar.png +bio: Open Source Engineer · AI workflows and tools +status: currently building vikusha +default_theme: bio +output_dir: public + +meta: + lang: en + title: snowz.ai + description: AI workflows and developer tools. + siteUrl: https://snowz.ai + +projects: + - name: vikusha + description: framework for AI assistants + url: https://github.com/snowztech/vikusha + badge: go · wip + - name: nevinho + description: my personal AI assistant + url: https://github.com/lucasnevespereira/nevinho + badge: go + +contact: + socials: + - name: github + url: https://github.com/snowztech + - name: youtube + url: https://www.youtube.com/c/lucaasnp + - name: tiktok + url: https://www.tiktok.com/@snowz.ai diff --git a/examples/bio/pages/index/content.md b/examples/bio/pages/index/content.md new file mode 100644 index 0000000..9c3fcd8 --- /dev/null +++ b/examples/bio/pages/index/content.md @@ -0,0 +1,4 @@ +--- +title: snowz.ai +description: AI workflows and developer tools +--- diff --git a/inkssg.go b/inkssg.go index b2e6061..a5afaf7 100644 --- a/inkssg.go +++ b/inkssg.go @@ -44,15 +44,25 @@ type Page struct { } type SiteConfig struct { - Name string `yaml:"name"` - Avatar string `yaml:"avatar"` - Bio string `yaml:"bio"` - Install string `yaml:"install"` - DefaultTheme string `yaml:"default_theme"` - OutputDir string `yaml:"output_dir"` - Links []Link `yaml:"links"` - Meta Meta `yaml:"meta"` - Contact Contact `yaml:"contact"` + Name string `yaml:"name"` + Avatar string `yaml:"avatar"` + Bio string `yaml:"bio"` + Status string `yaml:"status"` + Install string `yaml:"install"` + DefaultTheme string `yaml:"default_theme"` + OutputDir string `yaml:"output_dir"` + Links []Link `yaml:"links"` + Projects []Project `yaml:"projects"` + Meta Meta `yaml:"meta"` + Contact Contact `yaml:"contact"` +} + +type Project struct { + Name string `yaml:"name"` + Description string `yaml:"description"` + URL string `yaml:"url"` + Image string `yaml:"image"` + Badge string `yaml:"badge"` } type Link struct { diff --git a/themes/bio/layout.html b/themes/bio/layout.html new file mode 100644 index 0000000..b2d6c46 --- /dev/null +++ b/themes/bio/layout.html @@ -0,0 +1,88 @@ + + + + + + + {{if .Site.Meta.Title}}{{.Site.Meta.Title}}{{else}}{{.Page.Title}}{{end}} + {{if .Site.Meta.Author}}{{end}} + {{if .Site.Meta.SiteUrl}}{{end}} + + + + + + + + + + {{if .Site.Meta.Title}} + + + {{end}} + {{if .Site.Meta.Description}}{{end}} + {{if .Site.Meta.Lang}}{{end}} + {{if .Site.Avatar}} + + + + {{end}} + + +
+
+ {{if .Site.Avatar}} + {{.Site.Name}} + {{end}} +
+ +
+
+ +
+ {{if .Site.Name}}
{{.Site.Name}}
{{end}} + {{if .Site.Bio}}
{{.Site.Bio}}
{{end}} + {{if .Site.Status}} +
+ + {{.Site.Status}} +
+ {{end}} +
+ + {{if .Content}}
{{.Content}}
{{end}} + + {{if .Site.Projects}} +
+ +
    + {{range .Site.Projects}} +
  • + {{if .Image}}{{end}} +
    + {{if .URL}}{{.Name}}{{else}}{{.Name}}{{end}} + {{if .Description}}{{.Description}}{{end}} +
    + {{if .Badge}}{{.Badge}}{{end}} +
  • + {{end}} +
+
+ {{end}} + +
+ + © +
+
+ + + diff --git a/themes/bio/script.js b/themes/bio/script.js new file mode 100644 index 0000000..133c1a6 --- /dev/null +++ b/themes/bio/script.js @@ -0,0 +1,46 @@ +(function () { + const toggle = document.getElementById("theme-toggle"); + const icon = toggle?.querySelector(".theme-icon"); + const body = document.body; + + let savedTheme; + try { + savedTheme = localStorage.getItem("theme"); + } catch (err) {} + + const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; + const currentTheme = savedTheme || (prefersDark ? "dark" : "light"); + + if (currentTheme === "dark") { + body.classList.add("dark"); + document.documentElement.classList.remove("light"); + if (icon) icon.textContent = "●"; + } else { + body.classList.remove("dark"); + document.documentElement.classList.add("light"); + if (icon) icon.textContent = "○"; + } + + if (toggle) { + const handleToggle = (e) => { + e.preventDefault(); + e.stopPropagation(); + const isDark = body.classList.toggle("dark"); + if (isDark) { + document.documentElement.classList.remove("light"); + } else { + document.documentElement.classList.add("light"); + } + try { + localStorage.setItem("theme", isDark ? "dark" : "light"); + } catch (err) {} + if (icon) icon.textContent = isDark ? "●" : "○"; + }; + + toggle.addEventListener("click", handleToggle); + toggle.addEventListener("touchend", handleToggle, { passive: false }); + } + + const yearEl = document.querySelector(".year"); + if (yearEl) yearEl.textContent = new Date().getFullYear(); +})(); diff --git a/themes/bio/styles.css b/themes/bio/styles.css new file mode 100644 index 0000000..706ac6e --- /dev/null +++ b/themes/bio/styles.css @@ -0,0 +1,326 @@ +/* Bio Theme — centered profile card with project list */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; + --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + + --bg-light: #ffffff; + --text-light: #1a1a1a; + --text-muted-light: #666666; + --link-light: #0066cc; + --border-light: #e0e0e0; + + --bg-dark: #0d0d0d; + --text-dark: #e6e6e6; + --text-muted-dark: #888888; + --link-dark: #66b3ff; + --border-dark: #333333; + + --bg: var(--bg-light); + --text: var(--text-light); + --text-muted: var(--text-muted-light); + --link: var(--link-light); + --border: var(--border-light); +} + +body.dark { + --bg: var(--bg-dark); + --text: var(--text-dark); + --text-muted: var(--text-muted-dark); + --link: var(--link-dark); + --border: var(--border-dark); +} + +:root.light { + --bg: var(--bg-light); + --text: var(--text-light); + --text-muted: var(--text-muted-light); + --link: var(--link-light); + --border: var(--border-light); +} + +@media (prefers-color-scheme: dark) { + :root:not(.light) { + --bg: var(--bg-dark); + --text: var(--text-dark); + --text-muted: var(--text-muted-dark); + --link: var(--link-dark); + --border: var(--border-dark); + } +} + +html { + font-family: var(--font-mono); + font-size: 15px; + line-height: 1.6; +} + +body { + background-color: var(--bg); + color: var(--text); + display: flex; + align-items: center; + justify-content: center; + min-height: 100vh; + padding: 2rem 1.5rem; +} + +main { + width: 100%; + max-width: 440px; + position: relative; +} + +header { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 1.25rem; + gap: 1rem; +} + +.avatar { + width: 88px; + height: 88px; + border-radius: 50%; + object-fit: cover; +} + +.header-actions { + position: absolute; + top: 0; + right: 0; +} + +.theme-toggle { + background: none; + border: none; + cursor: pointer; + font-family: inherit; + font-size: 18px; + color: var(--text-muted); + padding: 0.75rem; + margin: -0.5rem; + transition: color 0.2s ease; + outline: none; +} + +.theme-toggle:hover { + color: var(--text); +} + +.hero { + text-align: center; + margin-bottom: 1.75rem; +} + +.hero-name { + font-weight: 600; + font-size: 17px; + margin-bottom: 0.35rem; +} + +.hero-tagline { + color: var(--text-muted); + font-size: 13px; +} + +.status { + display: inline-flex; + align-items: center; + gap: 0.5rem; + font-size: 12px; + color: var(--text-muted); + margin-top: 0.75rem; +} + +.status-dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: #4ade80; + box-shadow: 0 0 0 0 rgba(74, 222, 128, 0.7); + animation: pulse 2s infinite; +} + +@keyframes pulse { + 0% { box-shadow: 0 0 0 0 rgba(74, 222, 128, 0.5); } + 70% { box-shadow: 0 0 0 6px rgba(74, 222, 128, 0); } + 100% { box-shadow: 0 0 0 0 rgba(74, 222, 128, 0); } +} + +.content { + text-align: center; + margin-bottom: 1.75rem; + font-size: 14px; + color: var(--text-muted); +} + +.content a { + color: var(--link); + text-decoration: none; + border-bottom: 1px solid transparent; + transition: border-color 0.2s ease; +} + +.content a:hover { + border-bottom-color: var(--link); +} + +.projects { + margin-bottom: 1.75rem; +} + +.section-label { + font-size: 11px; + color: var(--text-muted); + text-transform: lowercase; + letter-spacing: 0.08em; + margin: 0 0 0.75rem; + opacity: 0.6; +} + +.projects ul { + list-style: none; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.project { + position: relative; + padding: 0.85rem 1rem; + display: flex; + align-items: center; + gap: 0.85rem; + border: 1px solid var(--border); + border-radius: 10px; + background: rgba(255, 255, 255, 0.02); + transition: border-color 0.2s ease, background 0.2s ease, transform 0.15s ease; +} + +.project:hover { + border-color: var(--text-muted); + background: rgba(255, 255, 255, 0.04); + transform: translateY(-1px); +} + +body:not(.dark) .project { + background: rgba(0, 0, 0, 0.02); +} + +body:not(.dark) .project:hover { + background: rgba(0, 0, 0, 0.04); +} + +.project a { + text-decoration: none; + color: var(--text); +} + +.project a::after { + content: ""; + position: absolute; + inset: 0; +} + +.project-icon { + width: 38px; + height: 38px; + border-radius: 50%; + object-fit: cover; + flex-shrink: 0; +} + +.project-body { + display: flex; + flex-direction: column; + gap: 0.15rem; + min-width: 0; +} + +.project-name { + font-weight: 600; + font-size: 14px; +} + +.project-name a { + color: var(--link); +} + +.project-desc { + font-size: 12px; + color: var(--text-muted); +} + +.project-meta { + margin-left: auto; + font-size: 10px; + color: var(--text-muted); + opacity: 0.7; + letter-spacing: 0.05em; + padding-left: 0.5rem; + flex-shrink: 0; +} + +footer { + display: flex; + flex-direction: column; + gap: 1rem; + align-items: center; + border-top: 1px solid var(--border); + padding-top: 1.25rem; + font-size: 14px; + color: var(--text-muted); +} + +.links { + display: flex; + gap: 1.25rem; + justify-content: center; + flex-wrap: wrap; +} + +.links a { + color: var(--text-muted); + text-decoration: none; + transition: color 0.2s ease; +} + +.links a:hover { + color: var(--text); +} + +.copyright { + font-size: 11px; + opacity: 0.6; +} + +@media (max-width: 480px) { + body { + padding: 1.5rem 1rem; + align-items: flex-start; + } + .avatar { + width: 72px; + height: 72px; + } + .project { + padding: 0.75rem 0.85rem; + } + .project-icon { + width: 34px; + height: 34px; + } +} + +::selection { + background-color: var(--link); + color: var(--bg); +}