diff --git a/themes/tsuki/assets/css/components.css b/themes/tsuki/assets/css/components.css
index dda9d49..650412a 100644
--- a/themes/tsuki/assets/css/components.css
+++ b/themes/tsuki/assets/css/components.css
@@ -155,10 +155,12 @@
}
.pagination a,
.pagination .pagination-current {
- display: inline-block;
- min-width: 2rem;
- padding: var(--tsuki-space-1) var(--tsuki-space-3);
- text-align: center;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 2.75rem;
+ min-height: 2.75rem;
+ padding-inline: var(--tsuki-space-3);
border-radius: var(--tsuki-radius);
}
.pagination .pagination-current {
@@ -167,8 +169,8 @@
font-weight: 600;
}
.pagination .disabled {
- color: var(--tsuki-fg-subtle);
- opacity: 0.5;
+ color: var(--tsuki-fg-muted);
+ cursor: not-allowed;
}
/* code copy button */
diff --git a/themes/tsuki/assets/css/layout.css b/themes/tsuki/assets/css/layout.css
index 97eea77..b8658ed 100644
--- a/themes/tsuki/assets/css/layout.css
+++ b/themes/tsuki/assets/css/layout.css
@@ -42,12 +42,55 @@ main,
.site-nav a:hover { color: var(--tsuki-fg); }
.site-nav a[aria-current="page"] { color: var(--tsuki-fg); font-weight: 550; }
+.lang-switcher {
+ display: flex;
+ gap: var(--tsuki-space-2);
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ font-size: var(--tsuki-fs-sm);
+}
+.lang-switcher a {
+ color: var(--tsuki-fg-muted);
+ padding: var(--tsuki-space-1) var(--tsuki-space-2);
+}
+.lang-switcher a:hover { color: var(--tsuki-fg); }
+.lang-switcher a[aria-current="page"] {
+ color: var(--tsuki-fg);
+ font-weight: 550;
+ text-decoration: none;
+}
+
+.site-header-actions {
+ display: flex;
+ align-items: center;
+ gap: var(--tsuki-space-2);
+ margin-inline-start: var(--tsuki-space-3);
+}
+
+.search-button {
+ display: inline-grid;
+ place-items: center;
+ min-width: 2.5rem;
+ min-height: 2.5rem;
+ border: 1px solid var(--tsuki-border);
+ border-radius: 999px;
+ color: var(--tsuki-fg-muted);
+ transition: var(--tsuki-transition);
+}
+.search-button:hover {
+ color: var(--tsuki-fg);
+ border-color: var(--tsuki-fg-muted);
+ text-decoration: none;
+}
+.search-button svg { width: 1rem; height: 1rem; }
+
.theme-toggle {
background: transparent;
border: 1px solid var(--tsuki-border);
border-radius: 999px;
- width: 2rem;
- height: 2rem;
+ min-width: 2.5rem;
+ min-height: 2.5rem;
display: inline-grid;
place-items: center;
color: var(--tsuki-fg-muted);
@@ -79,6 +122,7 @@ body.list main { max-width: var(--tsuki-wide-width); }
gap: var(--tsuki-space-3);
}
.site-footer p { margin: 0; }
+.site-footer a { display: inline-block; padding-block: var(--tsuki-space-1); }
.list-header {
margin-block-end: var(--tsuki-space-8);
diff --git a/themes/tsuki/assets/css/search.css b/themes/tsuki/assets/css/search.css
index 1d01939..edcd2f6 100644
--- a/themes/tsuki/assets/css/search.css
+++ b/themes/tsuki/assets/css/search.css
@@ -1,30 +1,4 @@
-/* tsuki: search page — Pagefind UI overrides */
-
-.site-header-actions {
- display: flex;
- align-items: center;
- gap: var(--tsuki-space-2);
- margin-inline-start: var(--tsuki-space-3);
-}
-
-.search-button {
- display: inline-grid;
- place-items: center;
- width: 2rem;
- height: 2rem;
- border: 1px solid var(--tsuki-border);
- border-radius: 999px;
- color: var(--tsuki-fg-muted);
- transition: var(--tsuki-transition);
-}
-.search-button:hover {
- color: var(--tsuki-fg);
- border-color: var(--tsuki-fg-muted);
- text-decoration: none;
-}
-.search-button svg { width: 1rem; height: 1rem; }
-
-.site-nav { margin-inline-start: auto; }
+/* tsuki: search page — Pagefind UI container + token mapping */
.search-container { margin-block: var(--tsuki-space-6); min-block-size: 6rem; }
diff --git a/themes/tsuki/assets/css/single-extras.css b/themes/tsuki/assets/css/single-extras.css
new file mode 100644
index 0000000..406b125
--- /dev/null
+++ b/themes/tsuki/assets/css/single-extras.css
@@ -0,0 +1,47 @@
+/* tsuki: per-post extras — breadcrumbs, prev/next, details, code-copy polish, TOC narrow */
+
+.breadcrumbs {
+ margin-block-end: var(--tsuki-space-4);
+ font-size: var(--tsuki-fs-sm);
+ color: var(--tsuki-fg-muted);
+}
+.breadcrumbs ol {
+ list-style: none; padding: 0; margin: 0;
+ display: flex; flex-wrap: wrap; gap: var(--tsuki-space-2);
+}
+.breadcrumbs li + li::before { content: "›"; margin-inline-end: var(--tsuki-space-2); color: var(--tsuki-fg-subtle); }
+.breadcrumbs a { color: var(--tsuki-fg-muted); }
+.breadcrumbs a:hover { color: var(--tsuki-fg); }
+.breadcrumbs [aria-current="page"] { color: var(--tsuki-fg); }
+
+.prev-next {
+ display: grid; grid-template-columns: 1fr 1fr; gap: var(--tsuki-space-4);
+ margin-block-start: var(--tsuki-space-12);
+ padding-block-start: var(--tsuki-space-6);
+ border-block-start: 1px solid var(--tsuki-border);
+}
+.prev-next a {
+ display: flex; flex-direction: column; gap: var(--tsuki-space-1);
+ padding: var(--tsuki-space-3);
+ border: 1px solid var(--tsuki-border); border-radius: var(--tsuki-radius);
+ color: var(--tsuki-fg); text-decoration: none; min-block-size: 3rem;
+}
+.prev-next a:hover { border-color: var(--tsuki-fg-muted); }
+.prev-next-next { text-align: end; }
+.prev-next-label { font-size: var(--tsuki-fs-xs); color: var(--tsuki-fg-muted); }
+.prev-next-title { font-weight: 550; }
+@media (max-width: 32rem) {
+ .prev-next { grid-template-columns: 1fr; }
+ .prev-next-next { text-align: start; }
+}
+
+details {
+ border: 1px solid var(--tsuki-border); border-radius: var(--tsuki-radius);
+ padding: var(--tsuki-space-3) var(--tsuki-space-4);
+ margin-block: var(--tsuki-space-4);
+}
+details > summary { cursor: pointer; font-weight: 550; }
+details[open] > summary { margin-block-end: var(--tsuki-space-3); }
+
+.code-copy:hover { background: var(--tsuki-code-bg); }
+.code-copy[data-state="copied"] { opacity: 1; color: var(--tsuki-accent); border-color: var(--tsuki-accent); }
diff --git a/themes/tsuki/assets/css/tokens.css b/themes/tsuki/assets/css/tokens.css
index 4ccaf57..5611965 100644
--- a/themes/tsuki/assets/css/tokens.css
+++ b/themes/tsuki/assets/css/tokens.css
@@ -4,7 +4,7 @@
--tsuki-bg: #fbfaf7;
--tsuki-fg: #1a1a1a;
--tsuki-fg-muted: #666;
- --tsuki-fg-subtle: #888;
+ --tsuki-fg-subtle: #6b6b6b;
--tsuki-border: #e5e3dd;
--tsuki-accent: #4a6fa5;
--tsuki-accent-hover: #2c4a7a;
diff --git a/themes/tsuki/assets/js/code-copy.js b/themes/tsuki/assets/js/code-copy.js
index 9c7d20b..e7de689 100644
--- a/themes/tsuki/assets/js/code-copy.js
+++ b/themes/tsuki/assets/js/code-copy.js
@@ -1,6 +1,6 @@
-const COPY = "Sao chép";
-const COPIED = "Đã chép";
-const FAILED = "Lỗi";
+const dataset = document.documentElement.dataset;
+const COPY = dataset.copyCode || "Copy";
+const COPIED = dataset.copiedCode || "Copied";
if (navigator.clipboard) {
for (const pre of document.querySelectorAll("pre")) {
@@ -16,10 +16,14 @@ if (navigator.clipboard) {
try {
await navigator.clipboard.writeText(code.innerText);
btn.textContent = COPIED;
+ btn.setAttribute("data-state", "copied");
} catch {
- btn.textContent = FAILED;
+ btn.textContent = COPY;
}
- setTimeout(() => { btn.textContent = COPY; }, 1500);
+ setTimeout(() => {
+ btn.textContent = COPY;
+ btn.removeAttribute("data-state");
+ }, 1500);
});
}
}
diff --git a/themes/tsuki/exampleSite/hugo.yaml b/themes/tsuki/exampleSite/hugo.yaml
index 1069cb5..15027b8 100644
--- a/themes/tsuki/exampleSite/hugo.yaml
+++ b/themes/tsuki/exampleSite/hugo.yaml
@@ -16,6 +16,15 @@ taxonomies:
permalinks:
post: /:year/:month/:day/:contentbasename/
+outputFormats:
+ LlmTxt:
+ mediaType: text/plain
+ baseName: llm
+ isPlainText: true
+
+outputs:
+ home: [HTML, RSS, LlmTxt]
+
markup:
goldmark:
renderer:
@@ -47,6 +56,12 @@ params:
search:
enable: true
+ breadcrumbs:
+ enable: true
+
+ prevNextNav:
+ enable: true
+
# Giscus comments — uncomment and fill in your repo IDs to enable.
# See https://giscus.app to generate values for your repo.
# comments:
diff --git a/themes/tsuki/i18n/en.yml b/themes/tsuki/i18n/en.yml
new file mode 100644
index 0000000..3e4af32
--- /dev/null
+++ b/themes/tsuki/i18n/en.yml
@@ -0,0 +1,131 @@
+- id: readingTime
+ translation: "{{ .Count }} min read"
+
+- id: wordCount
+ translation: "{{ .Count }} words"
+
+- id: updatedOn
+ translation: "Updated"
+
+- id: search
+ translation: "Search"
+
+- id: searchPlaceholder
+ translation: "Find posts..."
+
+- id: searchNoScript
+ translation: "Search requires JavaScript."
+
+- id: searchDisabled
+ translation: "Search is currently unavailable on this page."
+
+- id: clearSearch
+ translation: "Clear"
+
+- id: loadMore
+ translation: "Load more"
+
+- id: filters
+ translation: "Filters"
+
+- id: zeroResults
+ translation: "No results for [SEARCH_TERM]"
+
+- id: oneResult
+ translation: "[COUNT] result for [SEARCH_TERM]"
+
+- id: manyResults
+ translation: "[COUNT] results for [SEARCH_TERM]"
+
+- id: searching
+ translation: "Searching [SEARCH_TERM]..."
+
+- id: searchSuggestion
+ translation: "No results for [SEARCH_TERM]. Try: [DIFFERENT_TERMS]."
+
+- id: altSearch
+ translation: "No results for [SEARCH_TERM]. Showing results for [DIFFERENT_TERM]."
+
+- id: recentPosts
+ translation: "Recent posts"
+
+- id: relatedPosts
+ translation: "Related posts"
+
+- id: featuredProjects
+ translation: "Featured projects"
+
+- id: viewAll
+ translation: "View all"
+
+- id: tableOfContents
+ translation: "Table of contents"
+
+- id: prev
+ translation: "Previous"
+
+- id: next
+ translation: "Next"
+
+- id: posts
+ translation: "posts"
+
+- id: archiveEmpty
+ translation: "No posts yet."
+
+- id: month
+ translation: "{{ .Number | printf \"Month %d\" }}"
+
+- id: comments
+ translation: "Comments"
+
+- id: toggleTheme
+ translation: "Toggle theme"
+
+- id: skipToContent
+ translation: "Skip to content"
+
+- id: poweredBy
+ translation: "Powered by"
+
+- id: pageNotFound
+ translation: "Page not found."
+
+- id: backHome
+ translation: "Back home"
+
+- id: calloutNote
+ translation: "Note"
+
+- id: calloutTip
+ translation: "Tip"
+
+- id: calloutImportant
+ translation: "Important"
+
+- id: calloutWarning
+ translation: "Warning"
+
+- id: calloutCaution
+ translation: "Caution"
+
+- id: linkToSection
+ translation: "Link to section"
+
+- id: breadcrumbHome
+ translation: "Home"
+
+- id: breadcrumb
+ translation: "Breadcrumb"
+
+- id: prevPost
+ translation: "Previous post"
+
+- id: nextPost
+ translation: "Next post"
+
+- id: copyCode
+ translation: "Copy"
+
+- id: copiedCode
+ translation: "Copied"
diff --git a/themes/tsuki/i18n/vi.yml b/themes/tsuki/i18n/vi.yml
index ddb6c00..6db9182 100644
--- a/themes/tsuki/i18n/vi.yml
+++ b/themes/tsuki/i18n/vi.yml
@@ -108,3 +108,24 @@
- id: calloutCaution
translation: "Thận trọng"
+
+- id: linkToSection
+ translation: "Liên kết đến mục"
+
+- id: breadcrumbHome
+ translation: "Trang chủ"
+
+- id: breadcrumb
+ translation: "Đường dẫn"
+
+- id: prevPost
+ translation: "Bài trước"
+
+- id: nextPost
+ translation: "Bài tiếp"
+
+- id: copyCode
+ translation: "Sao chép"
+
+- id: copiedCode
+ translation: "Đã sao chép"
diff --git a/themes/tsuki/layouts/_markup/render-heading.html b/themes/tsuki/layouts/_markup/render-heading.html
index 7fa6b56..7f48155 100644
--- a/themes/tsuki/layouts/_markup/render-heading.html
+++ b/themes/tsuki/layouts/_markup/render-heading.html
@@ -1,6 +1,6 @@
{{- $level := .Level -}}
{{- $id := .Anchor -}}