refactor(ui): redesign with warm palette, sage accent, and consistent theme vars

- Warm editorial color palette (#faf9f6 light / #141416 dark)
- Sage green accent (#5c7a5e) for nav, links, buttons, tags
- Replace all hardcoded hex colors with CSS theme utilities
- Serif font (system Georgia) only on header logo
- Better hover states: scale, brightness, color transitions
- Accent-colored focus rings on inputs
- Fix language mixing: Back to Home → 返回首页
- No external font dependencies
This commit is contained in:
xfy 2026-06-11 10:29:11 +08:00
parent fce16288b5
commit f9d23d1eed
16 changed files with 164 additions and 127 deletions

161
input.css
View File

@ -3,18 +3,20 @@
@custom-variant dark (&:where(.dark, .dark *)); @custom-variant dark (&:where(.dark, .dark *));
@theme { @theme {
/* PaperMod Light Theme */ --font-serif: 'Source Serif 4', 'Noto Serif SC', 'Source Han Serif SC', 'Songti SC', Georgia, serif;
--color-paper-theme: #ffffff;
--color-paper-theme: #faf9f6;
--color-paper-entry: #ffffff; --color-paper-entry: #ffffff;
--color-paper-primary: #1e1e1e; --color-paper-primary: #1c1917;
--color-paper-secondary: #6c6c6c; --color-paper-secondary: #78716c;
--color-paper-tertiary: #d6d6d6; --color-paper-tertiary: #d6d3d1;
--color-paper-content: #1f1f1f; --color-paper-content: #292524;
--color-paper-code-block: #1c1d21; --color-paper-code-block: #1c1917;
--color-paper-code-bg: #f5f5f5; --color-paper-code-bg: #f5f4f0;
--color-paper-border: #eeeeee; --color-paper-border: #e7e5e0;
--color-paper-accent: #5c7a5e;
/* PaperMod sizing */ --color-paper-accent-soft: #e8f0e8;
--radius-paper: 8px; --radius-paper: 8px;
--gap-paper: 24px; --gap-paper: 24px;
--content-gap-paper: 20px; --content-gap-paper: 20px;
@ -26,33 +28,36 @@
html { html {
scroll-behavior: smooth; scroll-behavior: smooth;
} }
body { body {
background-color: var(--color-paper-theme); background-color: var(--color-paper-theme);
color: var(--color-paper-primary); color: var(--color-paper-primary);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
transition: background-color 0.3s ease, color 0.3s ease; transition: background-color 0.3s ease, color 0.3s ease;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
} }
.dark { .dark {
--color-paper-theme: #1d1e20; --color-paper-theme: #141416;
--color-paper-entry: #2e2e33; --color-paper-entry: #1e1f22;
--color-paper-primary: #dadadb; --color-paper-primary: #e7e5df;
--color-paper-secondary: #9b9c9d; --color-paper-secondary: #8c8981;
--color-paper-tertiary: #414244; --color-paper-tertiary: #3a3a3d;
--color-paper-content: #c4c4c5; --color-paper-content: #d4d2cc;
--color-paper-code-block: #2e2e33; --color-paper-code-block: #1e1f22;
--color-paper-code-bg: #37383e; --color-paper-code-bg: #28292d;
--color-paper-border: #333333; --color-paper-border: #2a2b2e;
--color-paper-accent: #7da97f;
--color-paper-accent-soft: #1e2e1e;
} }
} }
@layer components { @layer components {
/* Post Single Layout */
.post-single { .post-single {
margin: 0 auto; margin: 0 auto;
} }
/* Post Header */
.post-header { .post-header {
margin: 24px auto var(--content-gap-paper) auto; margin: 24px auto var(--content-gap-paper) auto;
} }
@ -62,12 +67,14 @@
line-height: 1.2; line-height: 1.2;
color: var(--color-paper-primary); color: var(--color-paper-primary);
word-break: break-word; word-break: break-word;
letter-spacing: -0.02em;
} }
.post-description { .post-description {
margin-top: 10px; margin-top: 10px;
color: var(--color-paper-secondary); color: var(--color-paper-secondary);
font-size: 16px; font-size: 16px;
line-height: 1.7;
} }
.post-meta { .post-meta {
@ -91,7 +98,6 @@
color: var(--color-paper-primary); color: var(--color-paper-primary);
} }
/* Breadcrumbs */
.breadcrumbs { .breadcrumbs {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -112,14 +118,12 @@
text-underline-offset: 0.2rem; text-underline-offset: 0.2rem;
} }
/* Draft Badge */
.entry-hint { .entry-hint {
display: inline-flex; display: inline-flex;
margin-left: 0.5rem; margin-left: 0.5rem;
color: var(--color-paper-secondary); color: var(--color-paper-secondary);
} }
/* TOC */
details.toc { details.toc {
margin-bottom: var(--content-gap-paper); margin-bottom: var(--content-gap-paper);
background: var(--color-paper-code-bg); background: var(--color-paper-code-bg);
@ -174,7 +178,6 @@
margin-inline-start: var(--gap-paper); margin-inline-start: var(--gap-paper);
} }
/* TOC Animation */
details { details {
interpolate-size: allow-keywords; interpolate-size: allow-keywords;
} }
@ -191,7 +194,6 @@
opacity: 1; opacity: 1;
} }
/* Entry Cover */
.entry-cover { .entry-cover {
margin-bottom: var(--content-gap-paper); margin-bottom: var(--content-gap-paper);
} }
@ -202,23 +204,23 @@
height: auto; height: auto;
} }
/* Post Content */
.post-content { .post-content {
color: var(--color-paper-content); color: var(--color-paper-content);
margin: 30px 0; margin: 30px 0;
} }
/* Markdown Content */
.md-content h1 { .md-content h1 {
margin: 40px auto 32px; margin: 40px auto 32px;
font-size: 40px; font-size: 40px;
color: var(--color-paper-primary); color: var(--color-paper-primary);
letter-spacing: -0.01em;
} }
.md-content h2 { .md-content h2 {
margin: 32px auto 24px; margin: 32px auto 24px;
font-size: 32px; font-size: 32px;
color: var(--color-paper-primary); color: var(--color-paper-primary);
letter-spacing: -0.01em;
} }
.md-content h3 { .md-content h3 {
@ -248,16 +250,16 @@
.md-content a:not(.anchor) { .md-content a:not(.anchor) {
text-underline-offset: 0.3rem; text-underline-offset: 0.3rem;
text-decoration: underline; text-decoration: underline;
color: var(--color-paper-primary); color: var(--color-paper-accent);
} }
.md-content a:not(.anchor):hover { .md-content a:not(.anchor):hover {
color: var(--color-paper-secondary); color: #4a6249;
} }
.md-content p { .md-content p {
margin-bottom: var(--content-gap-paper); margin-bottom: var(--content-gap-paper);
line-height: 1.8; line-height: 1.85;
} }
.md-content p:last-child { .md-content p:last-child {
@ -325,7 +327,7 @@
.md-content-img-zoomable { .md-content-img-zoomable {
cursor: zoom-in; cursor: zoom-in;
transition: opacity 0.2s ease; transition: opacity 0.2s ease-out;
} }
.md-content-img-zoomable:hover { .md-content-img-zoomable:hover {
@ -337,7 +339,6 @@
border-radius: var(--radius-paper); border-radius: var(--radius-paper);
} }
/* Code Blocks */
.md-content code { .md-content code {
padding: 0.2rem 0.3rem; padding: 0.2rem 0.3rem;
font-size: 0.85em; font-size: 0.85em;
@ -366,12 +367,10 @@
line-height: 1.6; line-height: 1.6;
} }
/* Reset display for syntax-highlighted spans to prevent Tailwind .block conflict */
.md-content pre code span { .md-content pre code span {
display: inline !important; display: inline !important;
} }
/* Copy Code Button */
.copy-code { .copy-code {
display: none; display: none;
position: absolute; position: absolute;
@ -386,11 +385,11 @@
cursor: pointer; cursor: pointer;
border: none; border: none;
font-family: inherit; font-family: inherit;
transition: background 0.2s ease; transition: background 0.2s ease-out;
} }
.copy-code:hover { .copy-code:hover {
background: rgba(100, 100, 100, 0.9); background: rgba(92, 122, 94, 0.9);
} }
pre:hover .copy-code, pre:hover .copy-code,
@ -398,7 +397,6 @@
display: block; display: block;
} }
/* Heading Anchors */
.anchor { .anchor {
display: none; display: none;
text-decoration: none; text-decoration: none;
@ -423,12 +421,10 @@
color: var(--color-paper-primary) !important; color: var(--color-paper-primary) !important;
} }
/* Post Footer */
.post-footer { .post-footer {
margin-top: var(--content-gap-paper); margin-top: var(--content-gap-paper);
} }
/* Tags */
.post-tags { .post-tags {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -452,15 +448,15 @@
border-radius: var(--radius-paper); border-radius: var(--radius-paper);
border: 1px solid var(--color-paper-border); border: 1px solid var(--color-paper-border);
text-decoration: none; text-decoration: none;
transition: background 0.2s ease; transition: all 0.2s ease-out;
} }
.post-tags a:hover { .post-tags a:hover {
background: var(--color-paper-border); background: var(--color-paper-accent-soft);
color: var(--color-paper-primary); border-color: var(--color-paper-accent);
color: var(--color-paper-accent);
} }
/* Post Navigation */
.paginav { .paginav {
display: flex; display: flex;
border-radius: var(--radius-paper); border-radius: var(--radius-paper);
@ -477,7 +473,7 @@
padding: 0.8rem; padding: 0.8rem;
text-decoration: none; text-decoration: none;
color: var(--color-paper-primary); color: var(--color-paper-primary);
transition: background 0.2s ease; transition: background 0.2s ease-out;
} }
.paginav a:hover { .paginav a:hover {
@ -491,7 +487,6 @@
.paginav .title { .paginav .title {
letter-spacing: 1px; letter-spacing: 1px;
text-transform: uppercase;
font-size: 0.8rem; font-size: 0.8rem;
color: var(--color-paper-secondary); color: var(--color-paper-secondary);
} }
@ -501,7 +496,6 @@
font-weight: 500; font-weight: 500;
} }
/* Back to Home */
.back-to-home { .back-to-home {
margin-top: calc(var(--content-gap-paper) * 1.5); margin-top: calc(var(--content-gap-paper) * 1.5);
padding-top: var(--content-gap-paper); padding-top: var(--content-gap-paper);
@ -527,17 +521,16 @@
text-underline-offset: 0.3rem; text-underline-offset: 0.3rem;
} }
/* Image Viewer Lightbox */
.image-viewer-overlay { .image-viewer-overlay {
position: fixed; position: fixed;
inset: 0; inset: 0;
z-index: 9999; z-index: 50;
background: rgba(0, 0, 0, 0.92); background: rgba(0, 0, 0, 0.92);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 2rem; padding: 2rem;
animation: fadeIn 0.2s ease; animation: fadeIn 0.2s ease-out;
} }
.image-viewer-content { .image-viewer-content {
@ -568,24 +561,23 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
transition: background 0.2s ease; transition: background 0.2s ease-out;
} }
.image-viewer-close:hover { .image-viewer-close:hover {
background: rgba(255, 255, 255, 0.4); background: rgba(255, 255, 255, 0.4);
} }
/* Markdown Image Lightbox */
.md-image-lightbox-overlay { .md-image-lightbox-overlay {
position: fixed; position: fixed;
inset: 0; inset: 0;
z-index: 9999; z-index: 50;
background: rgba(0, 0, 0, 0.92); background: rgba(0, 0, 0, 0.92);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 2rem; padding: 2rem;
animation: fadeIn 0.2s ease; animation: fadeIn 0.2s ease-out;
} }
.md-image-lightbox-content { .md-image-lightbox-content {
@ -616,14 +608,13 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
transition: background 0.2s ease; transition: background 0.2s ease-out;
} }
.md-image-lightbox-close:hover { .md-image-lightbox-close:hover {
background: rgba(255, 255, 255, 0.4); background: rgba(255, 255, 255, 0.4);
} }
/* Post Card Cover Thumbnail */
.post-card-cover { .post-card-cover {
overflow: hidden; overflow: hidden;
border-radius: var(--radius-paper); border-radius: var(--radius-paper);
@ -633,12 +624,60 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
transition: transform 0.3s ease; transition: transform 0.3s ease-out;
} }
.post-card-cover:hover img { .post-card-cover:hover img {
transform: scale(1.05); transform: scale(1.05);
} }
a.accent-link {
color: var(--color-paper-accent);
text-decoration: none;
transition: color 0.2s ease-out;
}
a.accent-link:hover {
color: var(--color-paper-primary);
}
.btn-primary {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1.25rem;
font-size: 0.875rem;
font-weight: 500;
color: white;
background: var(--color-paper-accent);
border-radius: 9999px;
border: none;
cursor: pointer;
transition: all 0.2s ease-out;
}
.btn-primary:hover {
filter: brightness(1.1);
transform: translateY(-1px);
}
.btn-primary:active {
transform: translateY(0) scale(0.98);
}
.post-card-accent {
position: relative;
}
.post-card-accent::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 0;
height: 2px;
background: var(--color-paper-accent);
transition: width 0.3s ease-out;
}
.post-card-accent:hover::after {
width: 100%;
}
} }
@keyframes fadeIn { @keyframes fadeIn {
@ -646,7 +685,6 @@
to { opacity: 1; } to { opacity: 1; }
} }
/* Responsive */
@media screen and (max-width: 768px) { @media screen and (max-width: 768px) {
.post-title { .post-title {
font-size: 32px; font-size: 32px;
@ -686,7 +724,7 @@
@keyframes pageEnter { @keyframes pageEnter {
from { from {
opacity: 0; opacity: 0;
transform: translateY(4px); transform: translateY(6px);
} }
to { to {
opacity: 1; opacity: 1;
@ -698,7 +736,6 @@
animation: pageEnter 0.15s ease-out; animation: pageEnter 0.15s ease-out;
} }
/* Reduced Motion */
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
html { html {
scroll-behavior: auto; scroll-behavior: auto;

View File

@ -67,7 +67,7 @@ pub fn Footer() -> Element {
}); });
let btn_class = use_memo(move || { let btn_class = use_memo(move || {
let base = "fixed bottom-16 right-8 z-50 w-10 h-10 rounded-full bg-gray-100 dark:bg-[#2d2e30] shadow-md flex items-center justify-center cursor-pointer transition-all duration-300 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white"; let base = "fixed bottom-16 right-8 z-50 w-10 h-10 rounded-full bg-paper-entry border border-paper-border shadow-sm flex items-center justify-center cursor-pointer transition-all duration-300 text-paper-secondary hover:text-paper-accent";
if visible() { if visible() {
format!("{} opacity-100 translate-y-0", base) format!("{} opacity-100 translate-y-0", base)
} else { } else {
@ -76,9 +76,9 @@ pub fn Footer() -> Element {
}); });
rsx! { rsx! {
footer { class: "w-full border-t border-gray-200 dark:border-[#333] mt-auto", footer { class: "w-full border-t border-paper-border mt-auto",
div { class: "max-w-3xl mx-auto px-6 py-5 flex items-center justify-between text-sm text-gray-400 dark:text-[#9b9c9d]", div { class: "max-w-3xl mx-auto px-6 py-5 flex items-center justify-between text-sm text-paper-secondary",
span { "© 2026 Yggdrasil Blog" } span { "© 2026 Yggdrasil" }
} }
} }
a { a {

View File

@ -1,8 +1,8 @@
use dioxus::prelude::*; use dioxus::prelude::*;
pub const INPUT_CLASS: &str = "w-full px-4 py-2 border border-gray-200 dark:border-[#333] rounded-lg bg-white dark:bg-[#2e2e33] text-gray-900 dark:text-[#dadadb] focus:outline-none focus:border-gray-400 dark:focus:border-gray-600"; pub const INPUT_CLASS: &str = "w-full px-4 py-2 border border-paper-border rounded-lg bg-paper-entry text-paper-primary placeholder:text-paper-tertiary focus:outline-none focus:border-paper-accent focus:ring-1 focus:ring-paper-accent/30 transition-colors duration-200";
pub const BUTTON_PRIMARY_CLASS: &str = "w-full py-2 px-4 bg-gray-900 dark:bg-[#dadadb] text-white dark:text-gray-900 font-medium rounded-full hover:opacity-80 transition-opacity cursor-pointer"; pub const BUTTON_PRIMARY_CLASS: &str = "w-full py-2.5 px-4 bg-paper-accent text-white font-medium rounded-full hover:brightness-110 active:scale-[0.98] transition-all duration-200 cursor-pointer";
#[component] #[component]
pub fn FormInput( pub fn FormInput(
@ -31,7 +31,7 @@ pub fn FormInput(
#[component] #[component]
pub fn FormLabel(label: &'static str) -> Element { pub fn FormLabel(label: &'static str) -> Element {
rsx! { rsx! {
label { class: "block text-sm font-medium text-gray-700 dark:text-[#9b9c9d] mb-1", label { class: "block text-sm font-medium text-paper-secondary mb-1",
"{label}" "{label}"
} }
} }
@ -42,7 +42,7 @@ pub fn AlertBox(message: String, variant: &'static str) -> Element {
let (bg_class, text_class) = match variant { let (bg_class, text_class) = match variant {
"error" => ("bg-red-100 dark:bg-red-900/30", "text-red-700 dark:text-red-300"), "error" => ("bg-red-100 dark:bg-red-900/30", "text-red-700 dark:text-red-300"),
"success" => ("bg-green-100 dark:bg-green-900/30", "text-green-700 dark:text-green-300"), "success" => ("bg-green-100 dark:bg-green-900/30", "text-green-700 dark:text-green-300"),
_ => ("bg-gray-100 dark:bg-[#333]", "text-gray-700 dark:text-[#9b9c9d]"), _ => ("bg-paper-code-bg", "text-paper-secondary"),
}; };
rsx! { rsx! {
div { class: "mb-4 p-3 {bg_class} {text_class} rounded-lg text-center", div { class: "mb-4 p-3 {bg_class} {text_class} rounded-lg text-center",

View File

@ -29,7 +29,7 @@ pub fn FrontendLayout() -> Element {
let nav_items = use_nav_items(route.clone()); let nav_items = use_nav_items(route.clone());
rsx! { rsx! {
div { class: "min-h-screen flex flex-col bg-white dark:bg-[#1d1e20]", div { class: "min-h-screen flex flex-col bg-paper-theme",
Header { nav_items, right_content: rsx! { ThemeToggle {} } } Header { nav_items, right_content: rsx! { ThemeToggle {} } }
main { class: "flex-1 w-full max-w-3xl mx-auto px-6 py-6", main { class: "flex-1 w-full max-w-3xl mx-auto px-6 py-6",
SuspenseBoundary { SuspenseBoundary {

View File

@ -13,10 +13,10 @@ pub struct NavItemConfig {
#[component] #[component]
pub fn Header(nav_items: Vec<NavItemConfig>, right_content: Element) -> Element { pub fn Header(nav_items: Vec<NavItemConfig>, right_content: Element) -> Element {
rsx! { rsx! {
header { class: "sticky top-0 z-40 w-full border-b border-gray-200 dark:border-[#333] bg-white/80 dark:bg-[#1d1e20]/80 backdrop-blur-sm", header { class: "sticky top-0 z-40 w-full border-b border-paper-border bg-paper-theme/80 backdrop-blur-sm",
nav { class: "max-w-3xl mx-auto px-6 h-[60px] flex items-center justify-between", nav { class: "max-w-3xl mx-auto px-6 h-[60px] flex items-center justify-between",
Link { Link {
class: "text-2xl font-bold text-gray-900 dark:text-[#dadadb] hover:opacity-80 transition-opacity", class: "text-2xl font-bold font-serif text-paper-primary hover:text-paper-accent transition-colors duration-200",
to: Route::Home {}, to: Route::Home {},
"Yggdrasil" "Yggdrasil"
} }
@ -39,12 +39,12 @@ pub fn Header(nav_items: Vec<NavItemConfig>, right_content: Element) -> Element
#[component] #[component]
fn NavItem(route: Route, label: &'static str, is_active: bool) -> Element { fn NavItem(route: Route, label: &'static str, is_active: bool) -> Element {
let base_class = "px-3 py-1 text-base rounded-lg transition-colors"; let base_class = "px-3 py-1 text-base rounded-lg transition-all duration-200";
let class_str = if is_active { let class_str = if is_active {
format!("{} font-medium text-gray-900 dark:text-[#dadadb] underline underline-offset-[0.3rem] decoration-2 decoration-gray-900 dark:decoration-[#dadadb]", base_class) format!("{} font-medium text-paper-accent underline underline-offset-[0.3rem] decoration-2 decoration-paper-accent", base_class)
} else { } else {
format!( format!(
"{} text-gray-600 dark:text-[#9b9c9d] hover:text-gray-900 dark:hover:text-[#dadadb]", "{} text-paper-secondary hover:text-paper-primary",
base_class base_class
) )
}; };

View File

@ -34,7 +34,7 @@ pub fn PostFooter(post: Post) -> Element {
div { class: "back-to-home", div { class: "back-to-home",
Link { Link {
to: Route::Home {}, to: Route::Home {},
"Back to Home" "返回首页"
} }
} }
} }

View File

@ -13,7 +13,7 @@ pub fn PostCard(post: Post) -> Element {
rsx! { rsx! {
article { article {
class: "relative mb-6 p-6 bg-white dark:bg-[#2e2e33] rounded-lg border border-gray-200 dark:border-[#333] hover:-translate-y-0.5 hover:border-gray-300 dark:hover:border-gray-600 transition-all duration-250", class: "relative mb-6 p-6 bg-paper-entry rounded-lg border border-paper-border hover:-translate-y-0.5 hover:border-paper-accent/50 hover:shadow-sm transition-all duration-200",
Link { Link {
class: "block group", class: "block group",
to: Route::PostDetail { slug: post_slug }, to: Route::PostDetail { slug: post_slug },
@ -29,22 +29,22 @@ pub fn PostCard(post: Post) -> Element {
} }
} }
h2 { h2 {
class: "text-2xl font-bold leading-tight text-gray-900 dark:text-[#dadadb] group-hover:opacity-80 transition-opacity", class: "text-2xl font-bold leading-tight text-paper-primary group-hover:text-paper-accent transition-colors duration-200",
"{post.title}" "{post.title}"
} }
div { div {
class: "mt-2 text-sm text-gray-500 dark:text-[#9b9c9d] leading-relaxed line-clamp-2", class: "mt-2 text-sm text-paper-secondary leading-relaxed line-clamp-2",
"{post.summary.as_deref().unwrap_or_default()}" "{post.summary.as_deref().unwrap_or_default()}"
} }
div { div {
class: "mt-3 flex items-center gap-3 text-[13px] text-gray-400 dark:text-[#9b9c9d]", class: "mt-3 flex items-center gap-3 text-[13px] text-paper-secondary",
span { "{date_str}" } span { "{date_str}" }
if !post.tags.is_empty() { if !post.tags.is_empty() {
span { "·" } span { "·" }
for tag in post.tags.clone().into_iter() { for tag in post.tags.clone().into_iter() {
span { span {
Link { Link {
class: "hover:text-gray-600 dark:hover:text-[#dadadb] transition-colors", class: "hover:text-paper-accent transition-colors duration-200",
to: Route::TagDetail { tag: tag.clone() }, to: Route::TagDetail { tag: tag.clone() },
onclick: move |evt: dioxus::events::MouseEvent| evt.stop_propagation(), onclick: move |evt: dioxus::events::MouseEvent| evt.stop_propagation(),
"{tag}" "{tag}"

View File

@ -4,7 +4,7 @@ use dioxus::prelude::*;
pub fn SkeletonBox(class: &'static str, style: Option<&'static str>) -> Element { pub fn SkeletonBox(class: &'static str, style: Option<&'static str>) -> Element {
rsx! { rsx! {
div { div {
class: "bg-gray-200 dark:bg-[#2a2a2a] animate-pulse {class}", class: "bg-paper-tertiary/30 animate-pulse {class}",
style: style.unwrap_or(""), style: style.unwrap_or(""),
} }
} }

View File

@ -4,14 +4,14 @@ use dioxus::prelude::*;
pub fn About() -> Element { pub fn About() -> Element {
rsx! { rsx! {
header { class: "page-header mb-6", header { class: "page-header mb-6",
h1 { class: "text-[34px] font-bold text-gray-900 dark:text-[#dadadb]", h1 { class: "text-4xl font-bold text-paper-primary tracking-tight",
"关于" "关于"
} }
} }
article { class: "prose dark:prose-invert max-w-none text-gray-800 dark:text-[#c9cacc] leading-relaxed", article { class: "prose dark:prose-invert max-w-none text-paper-content leading-relaxed",
p { "Yggdrasil 是一个以文字为主的简约博客系统。" } p { "Yggdrasil 是一个以文字为主的简约博客系统。" }
p { "它使用 Rust + Dioxus 构建,采用 PostgreSQL 作为数据库,支持 Markdown 写作、标签管理和暗色模式。" } p { "它使用 Rust + Dioxus 构建,采用 PostgreSQL 作为数据库,支持 Markdown 写作、标签管理和暗色模式。" }
h2 { class: "text-xl font-bold text-gray-900 dark:text-[#dadadb] mt-8 mb-4", "技术栈" } h2 { class: "text-xl font-bold text-paper-primary mt-8 mb-4", "技术栈" }
ul { class: "list-disc pl-5 space-y-1", ul { class: "list-disc pl-5 space-y-1",
li { "Rust + Dioxus 0.7 (全栈 Web 框架)" } li { "Rust + Dioxus 0.7 (全栈 Web 框架)" }
li { "PostgreSQL + tokio-postgres (数据库)" } li { "PostgreSQL + tokio-postgres (数据库)" }
@ -19,7 +19,7 @@ pub fn About() -> Element {
li { "Tiptap Editor (富文本编辑器)" } li { "Tiptap Editor (富文本编辑器)" }
li { "pulldown-cmark (Markdown 渲染)" } li { "pulldown-cmark (Markdown 渲染)" }
} }
h2 { class: "text-xl font-bold text-gray-900 dark:text-[#dadadb] mt-8 mb-4", "特性" } h2 { class: "text-xl font-bold text-paper-primary mt-8 mb-4", "特性" }
ul { class: "list-disc pl-5 space-y-1", ul { class: "list-disc pl-5 space-y-1",
li { "Markdown 写作与实时预览" } li { "Markdown 写作与实时预览" }
li { "文章标签与归档" } li { "文章标签与归档" }

View File

@ -81,7 +81,7 @@ fn group_posts(posts: &[Post]) -> Vec<YearGroup> {
pub fn Archives() -> Element { pub fn Archives() -> Element {
rsx! { rsx! {
header { class: "page-header mb-6", header { class: "page-header mb-6",
h1 { class: "text-[34px] font-bold text-gray-900 dark:text-[#dadadb]", h1 { class: "text-4xl font-bold text-paper-primary tracking-tight",
"归档" "归档"
} }
} }
@ -98,9 +98,9 @@ fn ArchivesContent() -> Element {
Some(Ok(PostListResponse { posts, total })) => { Some(Ok(PostListResponse { posts, total })) => {
let grouped = group_posts(posts); let grouped = group_posts(posts);
rsx! { rsx! {
div { class: "mt-2 text-base text-gray-500 dark:text-[#9b9c9d]", div { class: "mt-2 text-base text-paper-secondary",
"" ""
span { class: "font-medium text-gray-700 dark:text-[#dadadb]", "{total}" } span { class: "font-medium text-paper-primary", "{total}" }
" 篇文章" " 篇文章"
} }
for year_group in grouped.iter() { for year_group in grouped.iter() {
@ -134,14 +134,14 @@ fn YearSection(year_group: YearGroup) -> Element {
rsx! { rsx! {
div { class: "archive-year mt-10", div { class: "archive-year mt-10",
h2 { h2 {
class: "archive-year-header text-2xl font-bold text-gray-900 dark:text-[#dadadb] mb-4", class: "archive-year-header text-2xl font-bold text-paper-primary mb-4",
id: "{year_group.year}", id: "{year_group.year}",
a { a {
class: "archive-header-link hover:opacity-80 transition-opacity", class: "archive-header-link hover:opacity-80 transition-opacity",
href: "#{year_group.year}", href: "#{year_group.year}",
"{year_group.year}" "{year_group.year}"
} }
sup { class: "archive-count text-sm text-gray-400 dark:text-[#9b9c9d] ml-1", "{total}" } sup { class: "archive-count text-sm text-paper-secondary ml-1", "{total}" }
} }
for month_group in year_group.months.iter() { for month_group in year_group.months.iter() {
MonthSection { month_group: month_group.clone(), year: year_group.year.clone() } MonthSection { month_group: month_group.clone(), year: year_group.year.clone() }
@ -155,16 +155,16 @@ fn MonthSection(month_group: MonthGroup, year: String) -> Element {
let count = month_group.posts.len(); let count = month_group.posts.len();
rsx! { rsx! {
div { class: "archive-month flex flex-col md:flex-row md:items-start py-2.5 border-b border-gray-100 dark:border-[#333]/50", div { class: "archive-month flex flex-col md:flex-row md:items-start py-2.5 border-b border-paper-border/50",
h3 { h3 {
class: "archive-month-header text-lg font-medium text-gray-700 dark:text-[#9b9c9d] md:w-[200px] shrink-0 mt-0 mb-0 py-1.5", class: "archive-month-header text-lg font-medium text-paper-secondary md:w-[200px] shrink-0 mt-0 mb-0 py-1.5",
id: "{year}-{month_group.month_en}", id: "{year}-{month_group.month_en}",
a { a {
class: "archive-header-link hover:opacity-80 transition-opacity", class: "archive-header-link hover:opacity-80 transition-opacity",
href: "#{year}-{month_group.month_en}", href: "#{year}-{month_group.month_en}",
"{month_group.month}" "{month_group.month}"
} }
sup { class: "archive-count text-sm text-gray-400 dark:text-[#9b9c9d] ml-1", "{count}" } sup { class: "archive-count text-sm text-paper-secondary ml-1", "{count}" }
} }
div { class: "archive-posts flex-1", div { class: "archive-posts flex-1",
for post in month_group.posts.iter() { for post in month_group.posts.iter() {
@ -181,10 +181,10 @@ fn ArchiveEntry(post: Post) -> Element {
rsx! { rsx! {
div { class: "archive-entry relative py-1.5 my-2.5 group", div { class: "archive-entry relative py-1.5 my-2.5 group",
h3 { class: "archive-entry-title text-base font-normal text-gray-900 dark:text-[#dadadb] m-0", h3 { class: "archive-entry-title text-base font-normal text-paper-primary m-0",
"{post.title}" "{post.title}"
} }
div { class: "archive-meta text-sm text-gray-400 dark:text-[#9b9c9d] mt-1", div { class: "archive-meta text-sm text-paper-secondary mt-1",
"{date_str}" "{date_str}"
} }
Link { Link {

View File

@ -40,7 +40,7 @@ fn HomePosts(current_page: i32) -> Element {
PostCard { post: post.clone() } PostCard { post: post.clone() }
} }
if posts.is_empty() { if posts.is_empty() {
div { class: "text-center text-gray-500 dark:text-[#9b9c9d] py-20", div { class: "text-center text-paper-secondary py-20",
"暂无文章" "暂无文章"
} }
} }
@ -66,10 +66,10 @@ fn HomePosts(current_page: i32) -> Element {
fn HomeInfo() -> Element { fn HomeInfo() -> Element {
rsx! { rsx! {
div { class: "mb-10 text-center", div { class: "mb-10 text-center",
h1 { class: "text-[34px] font-bold leading-tight text-gray-900 dark:text-[#dadadb]", h1 { class: "text-4xl font-bold leading-tight text-paper-primary tracking-tight",
"Yggdrasil" "Yggdrasil"
} }
p { class: "mt-3 text-base text-gray-500 dark:text-[#9b9c9d] leading-relaxed", p { class: "mt-3 text-base text-paper-secondary leading-relaxed",
"以文字为主的简约博客系统" "以文字为主的简约博客系统"
} }
} }
@ -92,7 +92,7 @@ fn Pagination(current_page: i32, total: i64) -> Element {
nav { class: "flex mt-10 mb-6 justify-between", nav { class: "flex mt-10 mb-6 justify-between",
if has_prev { if has_prev {
Link { Link {
class: "inline-flex items-center px-4 py-2 text-sm text-white bg-gray-900 dark:bg-[#dadadb] dark:text-gray-900 rounded-full hover:opacity-80 transition-opacity cursor-pointer", class: "inline-flex items-center px-4 py-2 text-sm text-white bg-paper-accent rounded-full hover:brightness-110 active:scale-[0.98] transition-all duration-200 cursor-pointer",
to: prev_route, to: prev_route,
span { class: "mr-1", "«" } span { class: "mr-1", "«" }
"上一页" "上一页"
@ -100,7 +100,7 @@ fn Pagination(current_page: i32, total: i64) -> Element {
} }
if has_next { if has_next {
Link { Link {
class: "ml-auto inline-flex items-center px-4 py-2 text-sm text-white bg-gray-900 dark:bg-[#dadadb] dark:text-gray-900 rounded-full hover:opacity-80 transition-opacity cursor-pointer", class: "ml-auto inline-flex items-center px-4 py-2 text-sm text-white bg-paper-accent rounded-full hover:brightness-110 active:scale-[0.98] transition-all duration-200 cursor-pointer",
to: Route::HomePage { page: current_page + 1 }, to: Route::HomePage { page: current_page + 1 },
"下一页" "下一页"
span { class: "ml-1", "»" } span { class: "ml-1", "»" }

View File

@ -51,9 +51,9 @@ pub fn Login() -> Element {
}); });
rsx! { rsx! {
div { class: "min-h-screen flex items-center justify-center bg-white dark:bg-[#1d1e20]", div { class: "min-h-screen flex items-center justify-center bg-paper-theme",
div { class: "w-full max-w-md p-8 bg-white dark:bg-[#2e2e33] rounded-2xl border border-gray-200 dark:border-[#333]", div { class: "w-full max-w-md p-8 bg-paper-entry rounded-2xl border border-paper-border shadow-sm",
h1 { class: "text-2xl font-bold text-center text-gray-900 dark:text-[#dadadb] mb-6", h1 { class: "text-2xl font-bold text-center text-paper-primary mb-6",
"登录" "登录"
} }
@ -88,7 +88,7 @@ pub fn Login() -> Element {
"登录" "登录"
} }
Link { Link {
class: "block w-full py-2 px-4 text-center text-gray-500 dark:text-[#9b9c9d] hover:text-gray-700 dark:hover:text-[#dadadb] font-medium rounded-lg transition-colors cursor-pointer", class: "block w-full py-2 px-4 text-center text-paper-secondary hover:text-paper-accent font-medium rounded-lg transition-all duration-200 cursor-pointer",
to: Route::Register {}, to: Route::Register {},
"还没有账号?去注册" "还没有账号?去注册"
} }

View File

@ -51,19 +51,19 @@ pub fn Register() -> Element {
}; };
rsx! { rsx! {
div { class: "min-h-screen flex items-center justify-center bg-white dark:bg-[#1d1e20]", div { class: "min-h-screen flex items-center justify-center bg-paper-theme",
div { class: "w-full max-w-md p-8 bg-white dark:bg-[#2e2e33] rounded-2xl border border-gray-200 dark:border-[#333]", div { class: "w-full max-w-md p-8 bg-paper-entry rounded-2xl border border-paper-border shadow-sm",
h1 { class: "text-2xl font-bold text-center text-gray-900 dark:text-[#dadadb] mb-2", h1 { class: "text-2xl font-bold text-center text-paper-primary mb-2",
"注册" "注册"
} }
p { class: "text-sm text-center text-gray-500 dark:text-[#9b9c9d] mb-6", p { class: "text-sm text-center text-paper-secondary mb-6",
"首个注册账号将自动成为管理员" "首个注册账号将自动成为管理员"
} }
if success() { if success() {
div { class: "mb-4 p-3 bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 rounded-lg text-center", div { class: "mb-4 p-3 bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 rounded-lg text-center",
"注册成功!" "注册成功!"
Link { class: "block mt-2 text-gray-700 dark:text-[#dadadb] hover:underline cursor-pointer", Link { class: "block mt-2 text-paper-accent hover:underline cursor-pointer",
to: Route::Login {}, to: Route::Login {},
"去登录" "去登录"
} }
@ -121,9 +121,9 @@ pub fn Register() -> Element {
"注册" "注册"
} }
} }
p { class: "mt-4 text-center text-sm text-gray-500 dark:text-[#9b9c9d]", p { class: "mt-4 text-center text-sm text-paper-secondary",
"已有账号?" "已有账号?"
Link { class: "text-gray-700 dark:text-[#dadadb] hover:underline cursor-pointer", Link { class: "text-paper-accent hover:underline cursor-pointer",
to: Route::Login {}, to: Route::Login {},
"去登录" "去登录"
} }

View File

@ -26,14 +26,14 @@ pub fn Search() -> Element {
rsx! { rsx! {
header { class: "page-header mb-6", header { class: "page-header mb-6",
h1 { class: "text-[34px] font-bold text-gray-900 dark:text-[#dadadb]", h1 { class: "text-4xl font-bold text-paper-primary tracking-tight",
"搜索" "搜索"
} }
} }
div { class: "mb-8", div { class: "mb-8",
div { class: "flex gap-2", div { class: "flex gap-2",
input { input {
class: "flex-1 px-4 py-2 border border-gray-200 dark:border-[#333] rounded-lg bg-white dark:bg-[#2e2e33] text-gray-900 dark:text-[#dadadb] focus:outline-none focus:border-gray-400 dark:focus:border-gray-600", class: "flex-1 px-4 py-2 border border-paper-border rounded-lg bg-paper-entry text-paper-primary placeholder:text-paper-tertiary focus:outline-none focus:border-paper-accent focus:ring-1 focus:ring-paper-accent/30",
r#type: "text", r#type: "text",
placeholder: "输入关键词搜索文章...", placeholder: "输入关键词搜索文章...",
value: query(), value: query(),
@ -41,7 +41,7 @@ pub fn Search() -> Element {
onkeydown: move |e| if e.key() == Key::Enter { on_search() }, onkeydown: move |e| if e.key() == Key::Enter { on_search() },
} }
button { button {
class: "px-6 py-2 bg-gray-900 dark:bg-[#dadadb] text-white dark:text-gray-900 rounded-full font-medium hover:opacity-80 transition-opacity", class: "px-6 py-2 bg-paper-accent text-white rounded-full font-medium hover:brightness-110 active:scale-[0.98] transition-all duration-200",
onclick: move |_| on_search(), onclick: move |_| on_search(),
"搜索" "搜索"
} }
@ -51,7 +51,7 @@ pub fn Search() -> Element {
DelayedSkeleton { SearchSkeleton {} } DelayedSkeleton { SearchSkeleton {} }
} else if let Some(Ok(PostListResponse { posts, total: _ })) = search_res() { } else if let Some(Ok(PostListResponse { posts, total: _ })) = search_res() {
if posts.is_empty() { if posts.is_empty() {
div { class: "text-center text-gray-500 dark:text-[#9b9c9d] py-20", div { class: "text-center text-paper-secondary py-20",
"未找到相关文章" "未找到相关文章"
} }
} else { } else {

View File

@ -11,7 +11,7 @@ use crate::router::Route;
pub fn Tags() -> Element { pub fn Tags() -> Element {
rsx! { rsx! {
header { class: "page-header mb-6", header { class: "page-header mb-6",
h1 { class: "text-[34px] font-bold text-gray-900 dark:text-[#dadadb]", h1 { class: "text-4xl font-bold text-paper-primary tracking-tight",
"标签" "标签"
} }
} }
@ -32,21 +32,21 @@ fn TagsContent() -> Element {
Some(Ok(tags)) => { Some(Ok(tags)) => {
let total = tags.iter().map(|t| t.post_count).sum::<i64>(); let total = tags.iter().map(|t| t.post_count).sum::<i64>();
rsx! { rsx! {
div { class: "mt-2 text-base text-gray-500 dark:text-[#9b9c9d]", div { class: "mt-2 text-base text-paper-secondary",
"" ""
span { class: "font-medium text-gray-700 dark:text-[#dadadb]", "{tags.len()}" } span { class: "font-medium text-paper-primary", "{tags.len()}" }
" 个标签," " 个标签,"
span { class: "font-medium text-gray-700 dark:text-[#dadadb]", "{total}" } span { class: "font-medium text-paper-primary", "{total}" }
" 篇文章" " 篇文章"
} }
ul { class: "flex flex-wrap gap-4 mt-6", ul { class: "flex flex-wrap gap-4 mt-6",
for tag in tags { for tag in tags {
li { li {
Link { Link {
class: "inline-flex items-center px-3 py-1.5 text-base font-medium bg-gray-100 dark:bg-[#2e2e33] text-gray-700 dark:text-[#9b9c9d] rounded-lg hover:bg-gray-200 dark:hover:bg-[#333] transition-colors", class: "inline-flex items-center px-3 py-1.5 text-base font-medium bg-paper-accent-soft text-paper-accent rounded-lg hover:bg-paper-accent hover:text-white transition-all duration-200",
to: Route::TagDetail { tag: tag.name.clone() }, to: Route::TagDetail { tag: tag.name.clone() },
"{tag.name}" "{tag.name}"
sup { class: "ml-1 text-sm text-gray-500 dark:text-[#9b9c9d]", "{tag.post_count}" } sup { class: "ml-1 text-sm text-paper-secondary", "{tag.post_count}" }
} }
} }
} }
@ -72,7 +72,7 @@ fn TagsContent() -> Element {
pub fn TagDetail(tag: String) -> Element { pub fn TagDetail(tag: String) -> Element {
rsx! { rsx! {
header { class: "page-header mb-6", header { class: "page-header mb-6",
h1 { class: "text-[34px] font-bold text-gray-900 dark:text-[#dadadb]", h1 { class: "text-4xl font-bold text-paper-primary tracking-tight",
"{tag}" "{tag}"
} }
} }
@ -92,9 +92,9 @@ fn TagDetailContent(tag: String) -> Element {
match posts_data { match posts_data {
Some(Ok((posts, total))) => { Some(Ok((posts, total))) => {
rsx! { rsx! {
div { class: "mt-2 text-base text-gray-500 dark:text-[#9b9c9d]", div { class: "mt-2 text-base text-paper-secondary",
"" ""
span { class: "font-medium text-gray-700 dark:text-[#dadadb]", "{total}" } span { class: "font-medium text-paper-primary", "{total}" }
" 篇文章" " 篇文章"
} }
for post in posts.iter() { for post in posts.iter() {

View File

@ -135,7 +135,7 @@ pub fn ThemeToggle() -> Element {
rsx! { rsx! {
button { button {
class: "theme-toggle p-2 rounded-full cursor-pointer hover:opacity-80 transition-opacity text-gray-600 dark:text-gray-300", class: "theme-toggle p-2 rounded-full cursor-pointer hover:text-paper-accent transition-colors duration-200 text-paper-secondary",
onclick: move |_| theme.set(theme().toggle()), onclick: move |_| theme.set(theme().toggle()),
if mounted() && theme() == Theme::Dark { if mounted() && theme() == Theme::Dark {
svg { svg {